commit 35fd3d6adc6ebf9b5a556fc1edd2ce77c5f1466a Author: Bert Hausmans Date: Wed May 20 20:08:09 2026 +0200 docs: add flashcard app design spec Initial design for a single-user local flashcard webapp with hierarchical lessons, Leitner-based spaced repetition, Excel import/export, and animated practice UI. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acffffa --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +build/ +.DS_Store +.env +.env.local +data/*.db +data/*.db-* +coverage/ +.vite/ +*.log diff --git a/docs/superpowers/specs/2026-05-20-flashcard-app-design.md b/docs/superpowers/specs/2026-05-20-flashcard-app-design.md new file mode 100644 index 0000000..7f176e4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-flashcard-app-design.md @@ -0,0 +1,362 @@ +# Flashcard Webapplicatie — Design Spec + +**Datum:** 2026-05-20 +**Status:** Draft, ready for review +**Scope:** v1, single-user lokale webapp + +--- + +## 1. Doel + +Een moderne, snelle webapplicatie waarin een student flashcards kan oefenen, georganiseerd in een hiërarchische lessenstructuur. De applicatie houdt voortgang en statistieken bij en gebruikt een spaced-repetition algoritme om kaarten die nog niet beheerst worden vaker te tonen. + +## 2. Uitgangspunten & non-goals + +**Single-user, lokaal.** Geen authenticatie, geen multi-tenant. De student is ook de admin. + +**v1 is text-only.** Geen audio, afbeeldingen of formules op kaarten (toekomstwerk). + +**Geen cloud sync.** Data leeft lokaal in SQLite. Back-up via Excel-export. + +**Modern en snel.** Animaties, dark mode, responsive. Geen native mobiele app. + +## 3. Functionele eisen + +### 3.1 Lessenstructuur + +- Lessen vormen een boom (n-diep, geen vaste limiet). Elke les heeft een `parent_id` (nullable voor wortels). +- Lessen hebben naam, optionele beschrijving, en een `position` voor handmatige sortering binnen hun parent. +- Lessen hebben een `bidirectional` flag (default `false`): als `true` worden kaarten in deze les óók in omgekeerde richting (antwoord → vraag) geoefend. +- Een les kan verplaatst, hernoemd en verwijderd worden. Bij verwijdering: cascade naar onderliggende lessen en kaarten (na bevestiging). + +### 3.2 Flashcards + +- Een kaart hoort bij precies één les. +- Velden: `question`, `answer`, optionele `hint`, `position`. +- Kaarten kunnen handmatig worden aangemaakt, bewerkt, verwijderd en herordend. +- Bulk import via Excel; bulk export naar Excel. + +### 3.3 Oefensessie + +- De student start een sessie vanaf een les. De sessie bevat alle kaarten van die les **én alle onderliggende lessen** (recursief). +- Sessie-instellingen vooraf: + - **Max. aantal kaarten** (default 20, of "alle") + - **Shuffle** (default aan) + - **Richting** (alleen relevant als één of meer betrokken lessen `bidirectional=true`): voorwaarts / achterwaarts / beide +- Per kaart: + 1. Vraag wordt getoond (groot, gecentreerd). + 2. "Hint" knop indien aanwezig. + 3. "Toon antwoord" knop. Klik triggert een 3D card-flip animatie. + 4. Antwoord verschijnt; gelijktijdig verschijnen **Goed** (groen) en **Fout** (rood) knoppen. + 5. Klik → resultaat opgeslagen, volgende kaart verschijnt met slide-animatie. +- Na de laatste kaart: sessie-overzicht met confetti bij score ≥80%. + +### 3.4 Spaced repetition — Leitner met intra-sessie correctie + +**Tussen sessies — Leitner (5 dozen):** +- Nieuwe kaart start in doos 1. +- **Goed** → kaart naar volgende doos (max 5). +- **Fout** → kaart terug naar doos 1. +- Volgende-due-datum per doos: + - Doos 1: direct (volgende sessie) + - Doos 2: +1 dag + - Doos 3: +3 dagen + - Doos 4: +7 dagen + - Doos 5: +14 dagen + +**Volgorde binnen een sessie:** +- Selecteer eerst alle "due" kaarten (next_due_at ≤ nu) uit de scope; vul aan met laagste-doos kaarten tot het sessiemaximum. +- Volgorde: shuffle, maar gewogen — kaarten uit lagere dozen krijgen meer prioriteit aan het begin. +- **Intra-sessie regel:** bij fout antwoord komt de kaart terug in de wachtrij ~3 posities later in dezelfde sessie. Een kaart kan in één sessie meerdere keren voorkomen, maar telt voor Leitner-progressie alleen op het eindresultaat (laatste poging). + +### 3.5 Statistieken + +**Per kaart:** +- Totaal getoond (aantal pogingen) +- Goed / fout (aantal en %) +- Huidige doos (1-5) +- Laatst getoond, eerstvolgende due-datum +- Geschiedenis: lijst pogingen met datum/tijd en resultaat + +**Per les (inclusief onderliggende lessen, optioneel toe te tonen):** +- Score: gewogen gemiddelde van kaart-scores. Een kaart-score = `correct / (correct + incorrect)`, mits ≥3 pogingen; anders telt de kaart niet mee. +- Aantal kaarten / aantal beheerst (in doos ≥4) +- Aantal sessies gespeeld +- Totale oefenduur + +**Per sessie:** +- Start- en eindtijd, totale duur +- Aantal kaarten behandeld, goed/fout-verdeling +- Gemiddelde reactietijd per kaart + +**Globaal (dashboard):** +- Daily streak (aaneengesloten dagen met ≥1 sessie) +- Totale oefenduur, totale aantal sessies +- Heatmap van laatste 12 weken +- Recente sessies + +### 3.6 Admin-interface + +- Boomweergave van lessen met inline rename, drag-drop herordening en "+ subles" knoppen. +- Klik op een les → kaartenbeheer rechts (of nieuwe pagina): tabel met question/answer/hint, inline edit, snel toevoegen, sortering. +- **Excel import:** + - Knop "Importeer Excel" → upload `.xlsx` + - Verwachte kolommen (header rij verplicht): `question`, `answer`, `hint` (optioneel), `lesson_path` (optioneel; bv. `Spaans/Werkwoorden/Onregelmatig`. Indien leeg: kaart komt in de huidige les. Indien pad niet bestaat: lessen worden aangemaakt na bevestiging.) + - Preview met aantal rijen, fouten en duplicaten (op question binnen dezelfde les) + - "Bevestig import" knop + - Idempotent: bestaande kaart (zelfde question in zelfde les) wordt geüpdatet, niet gedupliceerd. Optie: "alleen toevoegen, niet updaten". +- **Excel export:** + - Per les (inclusief sublessen optioneel) → `.xlsx` met dezelfde kolommen. + +### 3.7 Sessie hervatten + +Als de gebruiker een sessie verlaat zonder hem af te ronden, blijft de sessie "open". Op de volgende bezoek toont de app: "Je hebt een open sessie — hervatten of afsluiten?". Bij hervatten wordt de wachtrij hersteld. + +### 3.8 Daily streak + +Een dag telt als de gebruiker minstens één voltooide sessie heeft. Bij twee opeenvolgende dagen zonder sessie: streak reset naar 0. + +## 4. Tech stack + +| Laag | Keuze | Reden | +|---|---|---| +| Monorepo | npm workspaces + concurrently | Eenvoudig, geen extra tooling | +| Frontend | React 18 + TS + Vite 7 + Tailwind 3 + React Router 6 + Zustand + Framer Motion | Modern, snel; Framer Motion voor de gevraagde animaties | +| Backend | Node.js + Express 4 + TS (ESM, tsx in dev) | Pragmatisch, brede ervaring | +| Database | SQLite (better-sqlite3) | Single-user lokaal; geen Postgres-overhead. Eén bestand, simpele back-up. | +| ORM | Drizzle ORM | TS-first, lichtgewicht, eenvoudige migratie naar Postgres later | +| Validatie | Zod | Hergebruikbaar tussen API en Excel-import | +| Excel | SheetJS (`xlsx`) | De-facto standaard, server-side | +| Test | Vitest (unit) + Playwright (smoke) | Past bij Vite-stack | +| Lint/format | ESLint + Prettier | Standaard | + +## 5. Architectuur + +### 5.1 Mappenstructuur + +``` +flashcard/ +├── packages/ +│ ├── backend/ +│ │ ├── src/ +│ │ │ ├── db/ Drizzle schema + migraties +│ │ │ ├── routes/ Express routers per resource +│ │ │ ├── services/ Business logic (sessions, leitner, stats, import) +│ │ │ ├── lib/ Helpers (excel, errors) +│ │ │ └── index.ts Server entry +│ │ └── package.json +│ ├── frontend/ +│ │ ├── src/ +│ │ │ ├── pages/ Route components +│ │ │ ├── components/ UI atoms/molecules +│ │ │ ├── stores/ Zustand stores +│ │ │ ├── api/ Fetch client +│ │ │ ├── lib/ Helpers +│ │ │ └── main.tsx +│ │ └── package.json +│ └── shared/ +│ ├── types.ts Domain types +│ └── schemas.ts Zod schemas (gedeeld FE/BE) +├── data/ +│ ├── flashcard.db SQLite (gitignored) +│ └── templates/import.xlsx +└── package.json workspaces + scripts +``` + +### 5.2 Datamodel (Drizzle / SQLite) + +```ts +lessons { + id: integer pk + parent_id: integer fk → lessons.id, nullable + name: text not null + description: text + position: integer not null default 0 + bidirectional: integer (boolean) not null default 0 + created_at: integer (timestamp) not null + updated_at: integer (timestamp) not null +} + +cards { + id: integer pk + lesson_id: integer fk → lessons.id, not null, on delete cascade + question: text not null + answer: text not null + hint: text + position: integer not null default 0 + created_at: integer not null + updated_at: integer not null +} + +card_progress { + card_id: integer fk → cards.id pk, on delete cascade + direction: text ('forward'|'backward') pk -- bidirectional → twee rijen per kaart + box: integer not null default 1 -- 1..5 + correct_count: integer not null default 0 + incorrect_count: integer not null default 0 + last_shown_at: integer + next_due_at: integer not null -- direct due bij aanmaak +} + +sessions { + id: integer pk + lesson_id: integer fk → lessons.id, not null + started_at: integer not null + ended_at: integer + duration_seconds: integer + cards_shown: integer not null default 0 + cards_correct: integer not null default 0 + cards_incorrect: integer not null default 0 + status: text ('active'|'completed'|'abandoned') not null + queue_snapshot: text -- JSON: voor hervatten +} + +attempts { + id: integer pk + session_id: integer fk → sessions.id, on delete cascade + card_id: integer fk → cards.id, on delete cascade + direction: text ('forward'|'backward') + shown_at: integer not null + result: text ('correct'|'incorrect') not null + time_to_answer_ms: integer +} +``` + +Indexen op `cards.lesson_id`, `attempts.session_id`, `attempts.card_id`, `card_progress.next_due_at`. + +### 5.3 API endpoints + +``` +# Lessen +GET /api/lessons/tree +POST /api/lessons +PATCH /api/lessons/:id +DELETE /api/lessons/:id +POST /api/lessons/:id/move { parent_id, position } + +# Kaarten +GET /api/lessons/:id/cards +POST /api/lessons/:id/cards +PATCH /api/cards/:id +DELETE /api/cards/:id +POST /api/lessons/:id/cards/import (multipart: file) +GET /api/lessons/:id/cards/export (?include_descendants=true) + +# Sessies +POST /api/sessions { lesson_id, max_cards?, shuffle?, direction? } +GET /api/sessions/active -- nog niet afgesloten sessie +GET /api/sessions/:id/next -- volgende kaart of {done:true} +POST /api/sessions/:id/attempts { card_id, direction, result, time_to_answer_ms } +POST /api/sessions/:id/end +POST /api/sessions/:id/abandon + +# Statistieken +GET /api/stats/overview +GET /api/stats/lessons/:id +GET /api/stats/cards/:id +GET /api/stats/heatmap (?weeks=12) +``` + +### 5.4 Frontend routes + +``` +/ Dashboard +/practice/:lessonId Oefensessie +/practice/:lessonId/setup Pre-sessie instellingen +/practice/:lessonId/done Sessie-overzicht +/admin Lessenboom + (rechts) selectie +/admin/lessons/:id Kaartenbeheer + import/export +/stats Overzicht / heatmap +/stats/lessons/:id Les-detail +/stats/cards/:id Kaart-detail +/settings Dark mode toggle, max kaarten default, e.d. +``` + +### 5.5 State management (Zustand) + +- `sessionStore` — actieve sessie + huidige kaart + wachtrij +- `lessonsStore` — boom-cache met optimistic updates +- `settingsStore` — UI prefs (dark mode, defaults) +- `statsStore` — gecachede stats per les/kaart + +### 5.6 Animaties (Framer Motion) + +- Card flip: `rotateY` 0→180 met `perspective` (300ms ease-out) +- Kaart-overgang: outgoing slide-left + fade, incoming slide-right + fade (200ms) +- Goed/Fout: korte border-glow puls (groen/rood, 400ms) +- Sessie-einde: confetti bij score ≥80% (`canvas-confetti`) +- Streak counter: count-up animatie +- Progress bar: tween op `width` + +### 5.7 Sessie-engine — gedetailleerd + +Bij `POST /api/sessions`: +1. Resolveer alle kaarten in de les + descendants. +2. Bouw per kaart een lijst van te oefenen "directions" (forward; backward indien de les `bidirectional`). +3. Filter: alleen items met `card_progress.next_due_at ≤ now`. Indien minder dan `max_cards`: vul aan met laagste-doos items, dan willekeurig. +4. Sorteer: doos 1 eerst, dan oplopend; shuffle binnen elke doos. +5. Trim tot `max_cards`. +6. Sla op als `queue_snapshot` JSON. + +Bij elke `attempts` POST: +1. Update `card_progress`: bij correct → box+1 (max 5), `next_due_at` op interval; bij incorrect → box=1, `next_due_at` direct beschikbaar. +2. Update `attempts` rij. +3. Update sessie tellers. +4. Intra-sessie: bij incorrect, item terug in queue op positie `current_index + 3` (of einde indien minder posities resteren). +5. Sla nieuwe queue_snapshot op (voor hervatten). + +Bij `GET /sessions/:id/next`: pop volgende uit queue snapshot of return `{done:true}`. + +## 6. Foutafhandeling + +- API: alle endpoints retourneren `{ error: { code, message, details? } }` bij fout. Codes: `VALIDATION_ERROR`, `NOT_FOUND`, `CONFLICT`, `INTERNAL`. +- Validatie via Zod, gedeelde schemas in `packages/shared`. +- Frontend: globale `` + toasts voor API-fouten. +- Import: rijen die falen worden gerapporteerd, geldige rijen worden geïmporteerd (transactie per rij, niet per file). + +## 7. Test-strategie + +- **Backend unit (Vitest):** Leitner-progressie, queue-bouwer, score-berekening, import-parser. Lessen/kaart CRUD via integratietest met in-memory SQLite. +- **Frontend unit (Vitest + RTL):** kaartcomponent, statistiek-componenten, store-gedrag. +- **E2E smoke (Playwright):** kaart aanmaken → sessie starten → goed/fout-flow → stats zichtbaar. Excel-import flow. +- **TDD voor sessie-engine en Leitner-logica** — dat is het hart van de app. + +## 8. Deployment / scripts + +``` +# root package.json +"scripts": { + "dev": "concurrently -k -n be,fe \"npm:dev:be\" \"npm:dev:fe\"", + "dev:be": "npm -w backend run dev", + "dev:fe": "npm -w frontend run dev", + "build": "npm -w backend run build && npm -w frontend run build", + "start": "node packages/backend/dist/index.js", + "db:migrate": "npm -w backend run db:migrate", + "db:seed": "npm -w backend run db:seed" +} +``` + +In production: backend serveert ook de gebuilde frontend (statische assets) → één proces, één poort. Default `http://localhost:5173` (dev) / `http://localhost:3000` (prod). + +## 9. Volgorde van implementatie (hoog over) + +1. Monorepo + tooling + shared types +2. DB-schema + migraties + seed +3. Lessen CRUD (BE + FE admin) +4. Kaarten CRUD (BE + FE admin) +5. Excel import/export +6. Sessie-engine + Leitner (TDD) +7. Oefen-UI (kaart-flip, animaties) +8. Statistieken (BE aggregaties + FE pagina's) +9. Dashboard + streak + heatmap +10. Polish: dark mode, responsive, sessie-hervat, confetti +11. E2E smoke tests + +Concrete taken volgen in het implementation plan. + +## 10. Open punten (laag risico) + +- Drag-drop in admin: gebruik `@dnd-kit/sortable`. +- Confetti library: `canvas-confetti` (klein, geen React deps). +- Of de admin een aparte route blijft of een tabblad binnen het dashboard — voorkeur: aparte route voor duidelijkheid. +- Excel-template downloadbaar in admin import-scherm.