From 1c0aedfcad5c032507478033b44a3b278850a374 Mon Sep 17 00:00:00 2001 From: ChristopheCode <135648470+ChristopheCode@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:38:37 +0200 Subject: [PATCH 1/4] Add flashcard progress storage helper --- flashcard-progress.js | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 flashcard-progress.js diff --git a/flashcard-progress.js b/flashcard-progress.js new file mode 100644 index 0000000..b9841b2 --- /dev/null +++ b/flashcard-progress.js @@ -0,0 +1,104 @@ +/* + * Flashcard progress helper. + * Stores local learning progress for irregular verbs without sending data anywhere. + */ +(function () { + const STORAGE_KEY = "casualEnglishFlashcardProgress"; + const REVIEW_DELAYS_DAYS = [0, 1, 3, 7, 14, 30]; + + const readProgress = () => { + try { + const raw = typeof safeGet === "function" + ? safeGet(localStorage, STORAGE_KEY) + : localStorage[STORAGE_KEY]; + const parsed = JSON.parse(raw || "{}"); + + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } + }; + + const writeProgress = progress => { + try { + const value = JSON.stringify(progress); + + if (typeof safeSet === "function") { + safeSet(localStorage, STORAGE_KEY, value); + } else { + localStorage[STORAGE_KEY] = value; + } + } catch {} + }; + + const keyForVerb = verb => [verb.base, verb.past, verb.pp] + .map(value => String(value || "").trim().toLowerCase()) + .join("|"); + + const addDays = (date, days) => { + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + days); + return nextDate; + }; + + const emptyProgress = () => ({ + views: 0, + correct: 0, + wrong: 0, + level: 0, + lastSeen: null, + nextReview: null, + }); + + const recordAnswer = (verb, isCorrect) => { + if (!verb || typeof verb !== "object") return null; + + const progress = readProgress(); + const key = keyForVerb(verb); + const current = { ...emptyProgress(), ...progress[key] }; + const now = new Date(); + + current.views += 1; + current.correct += isCorrect ? 1 : 0; + current.wrong += isCorrect ? 0 : 1; + current.level = isCorrect + ? Math.min(current.level + 1, REVIEW_DELAYS_DAYS.length - 1) + : 0; + current.lastSeen = now.toISOString(); + current.nextReview = addDays(now, REVIEW_DELAYS_DAYS[current.level]).toISOString(); + + progress[key] = current; + writeProgress(progress); + + return current; + }; + + const recordView = verb => { + if (!verb || typeof verb !== "object") return null; + + const progress = readProgress(); + const key = keyForVerb(verb); + const current = { ...emptyProgress(), ...progress[key] }; + + current.views += 1; + current.lastSeen = new Date().toISOString(); + + progress[key] = current; + writeProgress(progress); + + return current; + }; + + const getProgress = verb => { + if (!verb || typeof verb !== "object") return emptyProgress(); + + return { ...emptyProgress(), ...readProgress()[keyForVerb(verb)] }; + }; + + window.FlashcardProgress = { + storageKey: STORAGE_KEY, + getProgress, + recordAnswer, + recordView, + }; +}()); From d9b193f1b4b68ca065fd9628bb62107badac0dee Mon Sep 17 00:00:00 2001 From: ChristopheCode <135648470+ChristopheCode@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:40:31 +0200 Subject: [PATCH 2/4] Load flashcard progress on exercises page --- exercises.html | 1 + 1 file changed, 1 insertion(+) diff --git a/exercises.html b/exercises.html index 2047519..0004241 100644 --- a/exercises.html +++ b/exercises.html @@ -113,6 +113,7 @@

How to use

+ From 2f8f208c75a8135c52e065d021b32b4eb5df44a1 Mon Sep 17 00:00:00 2001 From: ChristopheCode <135648470+ChristopheCode@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:40:57 +0200 Subject: [PATCH 3/4] Record exercise answers in flashcard progress --- exercises.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/exercises.js b/exercises.js index d7b3557..f08a6c3 100644 --- a/exercises.js +++ b/exercises.js @@ -48,12 +48,12 @@ function renderExercise() { button.type = "button"; button.className = "exercise-choice"; button.textContent = option; - button.addEventListener("click", () => onPick(option, button, answer)); + button.addEventListener("click", () => onPick(option, button, answer, verb)); choicesEl.appendChild(button); }); } -function onPick(value, button, answer) { +function onPick(value, button, answer, verb) { if (locked) return; locked = true; @@ -61,7 +61,10 @@ function onPick(value, button, answer) { choice.disabled = true; }); - if (value === answer) { + const isCorrect = value === answer; + window.FlashcardProgress?.recordAnswer(verb, isCorrect); + + if (isCorrect) { button.classList.add("correct"); feedbackEl.textContent = "Correct!"; feedbackEl.classList.add("ok"); From fe55a469306eac4f6e235e49552bc0b2e81e3092 Mon Sep 17 00:00:00 2001 From: ChristopheCode <135648470+ChristopheCode@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:41:33 +0200 Subject: [PATCH 4/4] Test exercise flashcard progress storage --- tests/smoke.spec.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/smoke.spec.js b/tests/smoke.spec.js index db42d94..cc39445 100644 --- a/tests/smoke.spec.js +++ b/tests/smoke.spec.js @@ -69,7 +69,7 @@ test('Learn loads verbs and Next stays usable', async ({ page }) => { expect(errors).toEqual([]); }); -test('Exercises can answer and move to next question', async ({ page }) => { +test('Exercises can answer, save progress, and move to next question', async ({ page }) => { const errors = trackPageErrors(page); await page.goto(pageUrl('exercises.html')); @@ -80,6 +80,14 @@ test('Exercises can answer and move to next question', async ({ page }) => { await page.locator('.exercise-choice').first().click(); await expect(page.locator('#feedback')).not.toHaveText(''); + const progressRaw = await page.evaluate(() => window.localStorage.casualEnglishFlashcardProgress || null); + expect(progressRaw).not.toBeNull(); + + const progress = JSON.parse(progressRaw); + const firstEntry = Object.values(progress)[0]; + expect(firstEntry.views).toBe(1); + expect(firstEntry.correct + firstEntry.wrong).toBe(1); + await page.locator('#nextExercise').click(); await expect(page.locator('.exercise-choice')).toHaveCount(3); expect(errors).toEqual([]);