From 7f6268d6e57229971b51a6089c6f2c415766a610 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:43:24 -0400 Subject: [PATCH] Add word list preview, young learner mode, and practice improvements - Show full word list before practice starts so students can study words first - Add per-student young learner mode (set in admin): word stays visible while typing - Center all practice card content (speak button, prompt, feedback, check button) - Remove 10-word-per-session cap so large lists (25-30 words) practice all words - Double the misspelled-word study countdown from 4s to 8s - Fix word list preview numbering for 10+ items (inline numbers, no clipping) - Add young_learner column to students table with admin toggle Co-Authored-By: Claude Sonnet 4.6 --- db.js | 1 + public/admin.js | 18 +++++++++++++++++- public/app.js | 48 +++++++++++++++++++++++++++++++++++++++++++++-- public/index.html | 11 +++++++++++ public/styles.css | 33 +++++++++++++++++++++++++++++++- server.js | 11 ++++++++--- 6 files changed, 115 insertions(+), 7 deletions(-) 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 @@ + + +