Skip to content

Commit f681356

Browse files
Track flashcard progress from exercises (#142)
* Add flashcard progress storage helper * Load flashcard progress on exercises page * Record exercise answers in flashcard progress * Test exercise flashcard progress storage
1 parent b0c290d commit f681356

4 files changed

Lines changed: 120 additions & 4 deletions

File tree

exercises.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ <h3 class="help-title">How to use</h3>
113113
<script src="./app-config.js"></script>
114114
<script src="./data/irregular-verbs.js"></script>
115115
<script src="./verb-utils.js"></script>
116+
<script src="./flashcard-progress.js"></script>
116117
<script src="./exercises.js"></script>
117118

118119
</body>

exercises.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,23 @@ function renderExercise() {
4848
button.type = "button";
4949
button.className = "exercise-choice";
5050
button.textContent = option;
51-
button.addEventListener("click", () => onPick(option, button, answer));
51+
button.addEventListener("click", () => onPick(option, button, answer, verb));
5252
choicesEl.appendChild(button);
5353
});
5454
}
5555

56-
function onPick(value, button, answer) {
56+
function onPick(value, button, answer, verb) {
5757
if (locked) return;
5858
locked = true;
5959

6060
choicesEl.querySelectorAll("button").forEach(choice => {
6161
choice.disabled = true;
6262
});
6363

64-
if (value === answer) {
64+
const isCorrect = value === answer;
65+
window.FlashcardProgress?.recordAnswer(verb, isCorrect);
66+
67+
if (isCorrect) {
6568
button.classList.add("correct");
6669
feedbackEl.textContent = "Correct!";
6770
feedbackEl.classList.add("ok");

flashcard-progress.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Flashcard progress helper.
3+
* Stores local learning progress for irregular verbs without sending data anywhere.
4+
*/
5+
(function () {
6+
const STORAGE_KEY = "casualEnglishFlashcardProgress";
7+
const REVIEW_DELAYS_DAYS = [0, 1, 3, 7, 14, 30];
8+
9+
const readProgress = () => {
10+
try {
11+
const raw = typeof safeGet === "function"
12+
? safeGet(localStorage, STORAGE_KEY)
13+
: localStorage[STORAGE_KEY];
14+
const parsed = JSON.parse(raw || "{}");
15+
16+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
17+
} catch {
18+
return {};
19+
}
20+
};
21+
22+
const writeProgress = progress => {
23+
try {
24+
const value = JSON.stringify(progress);
25+
26+
if (typeof safeSet === "function") {
27+
safeSet(localStorage, STORAGE_KEY, value);
28+
} else {
29+
localStorage[STORAGE_KEY] = value;
30+
}
31+
} catch {}
32+
};
33+
34+
const keyForVerb = verb => [verb.base, verb.past, verb.pp]
35+
.map(value => String(value || "").trim().toLowerCase())
36+
.join("|");
37+
38+
const addDays = (date, days) => {
39+
const nextDate = new Date(date);
40+
nextDate.setDate(nextDate.getDate() + days);
41+
return nextDate;
42+
};
43+
44+
const emptyProgress = () => ({
45+
views: 0,
46+
correct: 0,
47+
wrong: 0,
48+
level: 0,
49+
lastSeen: null,
50+
nextReview: null,
51+
});
52+
53+
const recordAnswer = (verb, isCorrect) => {
54+
if (!verb || typeof verb !== "object") return null;
55+
56+
const progress = readProgress();
57+
const key = keyForVerb(verb);
58+
const current = { ...emptyProgress(), ...progress[key] };
59+
const now = new Date();
60+
61+
current.views += 1;
62+
current.correct += isCorrect ? 1 : 0;
63+
current.wrong += isCorrect ? 0 : 1;
64+
current.level = isCorrect
65+
? Math.min(current.level + 1, REVIEW_DELAYS_DAYS.length - 1)
66+
: 0;
67+
current.lastSeen = now.toISOString();
68+
current.nextReview = addDays(now, REVIEW_DELAYS_DAYS[current.level]).toISOString();
69+
70+
progress[key] = current;
71+
writeProgress(progress);
72+
73+
return current;
74+
};
75+
76+
const recordView = verb => {
77+
if (!verb || typeof verb !== "object") return null;
78+
79+
const progress = readProgress();
80+
const key = keyForVerb(verb);
81+
const current = { ...emptyProgress(), ...progress[key] };
82+
83+
current.views += 1;
84+
current.lastSeen = new Date().toISOString();
85+
86+
progress[key] = current;
87+
writeProgress(progress);
88+
89+
return current;
90+
};
91+
92+
const getProgress = verb => {
93+
if (!verb || typeof verb !== "object") return emptyProgress();
94+
95+
return { ...emptyProgress(), ...readProgress()[keyForVerb(verb)] };
96+
};
97+
98+
window.FlashcardProgress = {
99+
storageKey: STORAGE_KEY,
100+
getProgress,
101+
recordAnswer,
102+
recordView,
103+
};
104+
}());

tests/smoke.spec.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ test('Learn loads verbs and Next stays usable', async ({ page }) => {
6969
expect(errors).toEqual([]);
7070
});
7171

72-
test('Exercises can answer and move to next question', async ({ page }) => {
72+
test('Exercises can answer, save progress, and move to next question', async ({ page }) => {
7373
const errors = trackPageErrors(page);
7474

7575
await page.goto(pageUrl('exercises.html'));
@@ -80,6 +80,14 @@ test('Exercises can answer and move to next question', async ({ page }) => {
8080
await page.locator('.exercise-choice').first().click();
8181
await expect(page.locator('#feedback')).not.toHaveText('');
8282

83+
const progressRaw = await page.evaluate(() => window.localStorage.casualEnglishFlashcardProgress || null);
84+
expect(progressRaw).not.toBeNull();
85+
86+
const progress = JSON.parse(progressRaw);
87+
const firstEntry = Object.values(progress)[0];
88+
expect(firstEntry.views).toBe(1);
89+
expect(firstEntry.correct + firstEntry.wrong).toBe(1);
90+
8391
await page.locator('#nextExercise').click();
8492
await expect(page.locator('.exercise-choice')).toHaveCount(3);
8593
expect(errors).toEqual([]);

0 commit comments

Comments
 (0)