diff --git a/db.js b/db.js index 934914d..543681a 100644 --- a/db.js +++ b/db.js @@ -229,6 +229,7 @@ tryAlter(`ALTER TABLE students ADD COLUMN streak_date TEXT`); tryAlter(`ALTER TABLE students ADD COLUMN streak_count INTEGER NOT NULL DEFAULT 0`); tryAlter(`ALTER TABLE students ADD COLUMN best_streak INTEGER NOT NULL DEFAULT 0`); tryAlter(`ALTER TABLE students ADD COLUMN bg_pattern TEXT NOT NULL DEFAULT 'none'`); +tryAlter(`ALTER TABLE students ADD COLUMN young_learner INTEGER NOT NULL DEFAULT 0`); tryAlter(`ALTER TABLE lists ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); tryAlter(`ALTER TABLE decks ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); diff --git a/public/admin.js b/public/admin.js index f8b4d26..9ec7d46 100644 --- a/public/admin.js +++ b/public/admin.js @@ -128,11 +128,18 @@ async function loadKids() { .map(([t, c]) => ``) .join(''); const streak = s.streak_count >= 2 ? `🔥 ${s.streak_count} day streak` : ''; - return `
+ const youngLabel = s.young_learner + ? `👶 Young` + : ''; + return `
${esc(s.emoji)} ${esc(s.name)} ${streak} + ${youngLabel}
${swatches}
+
`; }) @@ -151,6 +158,15 @@ async function loadKids() { }) ); + document.querySelectorAll('[data-young-learner]').forEach((chk) => + chk.addEventListener('change', async () => { + await api(`/api/students/${chk.dataset.youngLearner}/young-learner`, { + method: 'PATCH', + body: { young_learner: chk.checked }, + }); + }) + ); + document.querySelectorAll('[data-del-student]').forEach((btn) => btn.addEventListener('click', async () => { if (!confirm('Remove this kid and all their progress? This cannot be undone.')) return; diff --git a/public/app.js b/public/app.js index 3c0f33a..6ea240c 100644 --- a/public/app.js +++ b/public/app.js @@ -316,6 +316,7 @@ async function openKid(id, tab = 'today', welcome = false) { api(`/api/students/${id}/tests`), ]); currentStudent = state.student; + youngLearnerMode = !!currentStudent.young_learner; applyTheme(currentStudent.theme || 'blue'); applyBgPattern(currentStudent.bg_pattern || 'none'); updateThemeDots(currentStudent.theme || 'blue'); @@ -1959,7 +1960,35 @@ async function finishFlashcards(nothingDue) { const practice = { words: [], i: 0, missed: [], missedWords: [], firstTryCorrect: 0, awaitingRetype: false, itemId: null, streak: 0 }; +// Young learner mode: word stays visible while typing (set per-student by parent in admin) +let youngLearnerMode = false; + +// Pending practice args stored while the word-list preview is showing +let pendingPractice = null; + async function startPractice({ listId, itemId } = {}) { + const previewListId = listId || currentSpellingListId; + if (previewListId) { + pendingPractice = { listId, itemId }; + const list = await api(`/api/lists/${previewListId}`); + $('#word-preview-title').textContent = list.name; + $('#word-preview-list').innerHTML = list.words + .map((w, i) => `
  • ${i + 1}.${esc(w.word)}${w.sentence ? `${esc(w.sentence)}` : ''}
  • `) + .join(''); + show('word-preview'); + return; + } + await startPracticeNow({ listId, itemId }); +} + +$('#word-preview-start').addEventListener('click', () => { + if (!pendingPractice) return; + const args = pendingPractice; + pendingPractice = null; + startPracticeNow(args); +}); + +async function startPracticeNow({ listId, itemId } = {}) { const url = listId ? `/api/session/${currentStudent.id}?listId=${listId}` : `/api/session/${currentStudent.id}`; const { words } = await api(url); if (words.length === 0) { @@ -1982,10 +2011,25 @@ function presentPracticeWord() { practice.awaitingRetype = false; $('#practice-progress').textContent = `Word ${practice.i + 1} of ${practice.words.length}`; $('#practice-bar').style.width = `${(practice.i / practice.words.length) * 100}%`; - $('#practice-prompt').textContent = 'Listen, then type the word:'; $('#feedback').innerHTML = ''; const input = $('#practice-input'); input.value = ''; + + if (youngLearnerMode) { + showWordFirst(w); + } else { + $('#practice-prompt').textContent = 'Listen, then type the word:'; + input.disabled = false; + input.focus(); + speakWord(w); + } +} + +function showWordFirst(w) { + const input = $('#practice-input'); + $('#practice-prompt').textContent = 'Look at the word and type it:'; + $('#feedback').innerHTML = `
    ${esc(w.word)}
    `; + input.value = ''; input.disabled = false; input.focus(); speakWord(w); @@ -2043,7 +2087,7 @@ function showStudyThenRetype(typed, w) { input.disabled = true; const d = diffLetters(typed, w.word); const fb = $('#feedback'); - let secs = 4; + let secs = 8; const hint = `Hint: starts with ${esc(w.word[0].toUpperCase())}`; const render = () => { diff --git a/public/index.html b/public/index.html index 38f9128..db1ea32 100644 --- a/public/index.html +++ b/public/index.html @@ -195,6 +195,17 @@ + + +