// Dyktando — ortografia u/ó, rz/ż, h/ch ;(function () { // Pairs: key = lowercase match → [choice1, choice2] (shown to user) const PAIRS = { ch: ['ch', 'h'], rz: ['rz', 'ż'], ż: ['rz', 'ż'], ó: ['u', 'ó'], u: ['u', 'ó'], h: ['h', 'ch'], } // Order matters: digraphs (ch, rz) must be tried before single chars (h) const BLANK_RE = /ch|rz|[uóżh]/gi let tokens = [] // {type:'text',content} | {type:'blank',...} let blanks = [] // subset where type==='blank' let current = 0 // index of active blank let origText = '' // ── DOM refs ───────────────────────────────────────────────────────────── const listWrap = document.getElementById('list-wrap') const playWrap = document.getElementById('play-wrap') const textList = document.getElementById('text-list') const customInput = document.getElementById('custom-input') const customStartBtn = document.getElementById('custom-start-btn') const playBackBtn = document.getElementById('play-back-btn') const playTitle = document.getElementById('play-title') const textDisplay = document.getElementById('text-display') const choicesEl = document.getElementById('choices') const progressEl = document.getElementById('blank-progress') const summaryWrap = document.getElementById('summary') const summaryText = document.getElementById('summary-text') const summaryScore = document.getElementById('summary-score') const summaryBackBtn = document.getElementById('summary-back-btn') const dykScroll = document.getElementById('dyk-scroll') const progressBar = document.getElementById('dyk-progress-bar-inner') // ── Load texts ──────────────────────────────────────────────────────────── fetch('dyktanda.json') .then(r => r.json()) .then(data => { data.forEach(item => { const btn = document.createElement('button') btn.className = 'list-item-btn' btn.textContent = item.name btn.addEventListener('click', () => startGame(item.name, item.text)) textList.appendChild(btn) }) }) .catch(() => { textList.innerHTML = '

Nie udało się wczytać tekstów.

' }) customStartBtn.addEventListener('click', () => { const txt = customInput.value.trim() if (!txt) return startGame('Własny', txt) }) // ── Start game ──────────────────────────────────────────────────────────── function startGame(title, text) { origText = text tokens = [] blanks = [] current = 0 // Tokenize: split text into plain segments and blanks BLANK_RE.lastIndex = 0 let lastIdx = 0 let m while ((m = BLANK_RE.exec(text)) !== null) { if (m.index > lastIdx) { tokens.push({ type: 'text', content: text.slice(lastIdx, m.index) }) } const blank = { type: 'blank', id: blanks.length, answer: m[0], // original (preserves case) choices: PAIRS[m[0].toLowerCase()], // pair to show start: m.index, // position in origText end: m.index + m[0].length, userAnswer: null, correct: null, } tokens.push(blank) blanks.push(blank) lastIdx = m.index + m[0].length } if (lastIdx < text.length) { tokens.push({ type: 'text', content: text.slice(lastIdx) }) } // Reset UI playTitle.textContent = title summaryWrap.classList.add('hidden') textDisplay.classList.remove('hidden') choicesEl.classList.remove('hidden') progressEl.textContent = '' progressBar.style.width = '0%' listWrap.classList.add('hidden') playWrap.classList.remove('hidden') if (blanks.length === 0) { renderText() showSummary() } else { renderText() activateBlank(0) } } // ── Render the text with blanks ─────────────────────────────────────────── function renderText() { textDisplay.innerHTML = '' tokens.forEach(tok => { if (tok.type === 'text') { textDisplay.appendChild(document.createTextNode(tok.content)) } else { const span = document.createElement('span') span.id = `blank-${tok.id}` if (tok.correct !== null) { // Already answered if (tok.correct) { span.className = 'dyk-blank dyk-blank--ok' span.textContent = tok.answer } else { span.className = 'dyk-blank dyk-blank--err' span.innerHTML = `${esc(tok.userAnswer)}${esc(tok.answer)}` } } else if (tok.id === current) { span.className = 'dyk-blank dyk-blank--active' span.textContent = '__' } else { span.className = 'dyk-blank dyk-blank--pending' span.textContent = '__' } textDisplay.appendChild(span) } }) } // ── Activate a blank ────────────────────────────────────────────────────── function activateBlank(idx) { current = idx renderText() const blank = blanks[idx] // Shuffle choices so correct answer isn't always in same position const choices = Math.random() < 0.5 ? [...blank.choices] : [...blank.choices].reverse() choicesEl.innerHTML = '' choices.forEach(choice => { const btn = document.createElement('button') btn.className = 'choice-btn' btn.textContent = choice btn.addEventListener('click', () => handleAnswer(blank, choice)) choicesEl.appendChild(btn) }) progressEl.textContent = `${idx + 1} / ${blanks.length}` // Scroll active blank into view requestAnimationFrame(() => { const el = document.getElementById(`blank-${idx}`) if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }) }) updateProgressBar() } // ── Handle answer ───────────────────────────────────────────────────────── function handleAnswer(blank, choice) { blank.correct = choice.toLowerCase() === blank.answer.toLowerCase() blank.userAnswer = choice const nextIdx = blank.id + 1 if (nextIdx >= blanks.length) { // Last blank answered: show final state briefly, then summary current = blanks.length choicesEl.innerHTML = '' progressEl.textContent = '' renderText() setTimeout(showSummary, 500) } else { activateBlank(nextIdx) } updateProgressBar() } // ── Summary ─────────────────────────────────────────────────────────────── function showSummary() { choicesEl.classList.add('hidden') textDisplay.classList.add('hidden') summaryWrap.classList.remove('hidden') const total = blanks.length const correct = blanks.filter(b => b.correct).length if (total === 0) { summaryScore.textContent = 'Brak liter do uzupełnienia w tym tekście.' summaryText.innerHTML = esc(origText) updateProgressBar() } else { summaryScore.textContent = `Poprawnie: ${correct} z ${total}` summaryText.innerHTML = buildSummaryHTML() } dykScroll.scrollTop = 0 } // Build word-coloured summary HTML function buildSummaryHTML() { // Find word spans (non-whitespace sequences) in original text const wordRe = /\S+/g const words = [] let wm while ((wm = wordRe.exec(origText)) !== null) { words.push({ start: wm.index, end: wm.index + wm[0].length }) } // Assign each word its blanks and overall status words.forEach(w => { w.blanks = blanks.filter(b => b.start >= w.start && b.end <= w.end) w.status = w.blanks.length === 0 ? 'none' : w.blanks.every(b => b.correct) ? 'ok' : 'err' }) let html = '' let pos = 0 words.forEach(word => { // Whitespace / punctuation before this word if (word.start > pos) html += esc(origText.slice(pos, word.start)) // Build inner word HTML (blanks replaced by correct answers, coloured) let wPos = word.start let inner = '' word.blanks.forEach(blank => { if (blank.start > wPos) inner += esc(origText.slice(wPos, blank.start)) if (blank.correct) { inner += `${esc(blank.answer)}` } else { inner += `${esc(blank.userAnswer)}${esc(blank.answer)}` } wPos = blank.end }) inner += esc(origText.slice(wPos, word.end)) html += word.status !== 'none' ? `${inner}` : inner pos = word.end }) if (pos < origText.length) html += esc(origText.slice(pos)) return html } function updateProgressBar() { const total = blanks.length if (total === 0) { progressBar.style.width = '100%' return } // Use `current` which is the index of the *next* blank to be filled. // When the game ends, `current` becomes `blanks.length`. const progress = (current / total) * 100 progressBar.style.width = `${progress}%` } function esc(s) { return (s || '').replace(/&/g, '&').replace(//g, '>') } // ── Navigation ──────────────────────────────────────────────────────────── playBackBtn.addEventListener('click', () => { const inProgress = blanks.length > 0 && current < blanks.length if (inProgress && !confirm('Przerwać dyktando i wrócić do listy?')) return goToList() }) summaryBackBtn.addEventListener('click', goToList) function goToList() { tokens = [] blanks = [] current = 0 textDisplay.innerHTML = '' textDisplay.classList.remove('hidden') summaryWrap.classList.add('hidden') choicesEl.innerHTML = '' choicesEl.classList.remove('hidden') playWrap.classList.add('hidden') listWrap.classList.remove('hidden') } })()