Files
flashcards/docs/superpowers/specs/2026-05-21-ux-extensions-design.md

13 KiB
Raw Permalink Blame History

Sub-project C — UX Extensions

Datum: 2026-05-21 Status: Draft, ready for review Parent app: Flashcard webapplicatie Voorgaande specs: 2026-05-20-auth-and-roles-design.md (A, opgeleverd), 2026-05-20-ownership-and-sharing-design.md (B, opgeleverd) Scope: Derde en laatste van drie subprojecten in de multi-user uitbreiding (A→B→C).


1. Doel

De gebruikerservaring van de flashcard-app op een professioneel niveau brengen door vier samenhangende UX-uitbreidingen:

  1. Les-detailpagina — één rijke pagina die overzicht, statistiek, sublessen en kaarten samenbrengt
  2. App-brede search — een ⌘K-stijl command-palette die zowel lessen als kaarten doorzoekt
  3. Statistieken-pagina — heatmap + per-les voortgang + reviews-due overzicht
  4. Admin-lessen polish — drag-and-drop herordening + inline filter in de lessen-boom

Geen nieuwe data-modellen. Wel drie nieuwe backend endpoints (search, lessons-progress, due-overview).

2. Uitgangspunten

  • Routes restructureren: /admin/lessons, /admin/lessons/:id/lessons/:id. Oude routes worden client-side <Navigate replace> redirects.
  • Geen extra charts-library: heatmap en progressbars met Tailwind + simpele SVG.
  • Drag-and-drop library: @dnd-kit/core + @dnd-kit/sortable.
  • Search-strategie: SQLite LIKE %q% zonder FTS5. Bovengrens: 30 lessen + 30 kaarten per query. Debounce 200ms in UI.
  • Sublessen op detail page: alleen directe children; diepere navigatie via doorklikken.
  • Card preview op detail page: client-side limit 30; "Toon alle X" expandt.

3. Functionele eisen

3.1 Les-detailpagina (/lessons/:id)

Vervangt AdminLessonPage. Eén pagina, in vier secties + header.

Header

  • Breadcrumb-pad (parents → huidige les)
  • Titel + beschrijving inline-editable voor eigenaar (klik op tekst → input → blur saves)
  • Badges: 🔒 Privé / 🌍 Gedeeld / Curated / 📥 Geabonneerd
  • Primary CTA: "Start oefenen →" (groot, gradient)
  • Secundaire acties (rechts uitgelijnd):
    • Eigenaar: Visibility-toggle, Curated-toggle (sysadmin op shared lesson), Importeer Excel, Exporteer Excel, Verwijder les
    • Niet-eigenaar geabonneerd: Fork, Abonnement opzeggen
    • Niet-eigenaar niet-geabonneerd (curated): Fork, Abonneer

Sectie 1: Stats summary — 4 horizontale stat-cards (compact):

  • Kaarten — totaal recursief over subtree
  • Beheerst — count(box≥4) / totaal, met percentage
  • Score — gewogen gemiddelde van card scores
  • Laatst geoefend — relatieve tijd (bv. "3 dagen geleden")

Sectie 2: Sublessen (verbergen indien leeg)

  • Lijst met: naam, # kaarten (recursief), score%, klikbaar → /lessons/:childId
  • Voor eigenaar: "+ Subles" knop opent inline form

Sectie 3: Kaarten

  • Voor eigenaar: bestaande CardTable (editable), default 30 zichtbaar, "Toon alle X" knop expandt
  • Voor niet-eigenaar: read-only weergave van dezelfde tabel
  • Empty state: "Nog geen kaarten — voeg er hieronder een toe" (eigenaar) of "Deze les heeft nog geen kaarten" (niet-eigenaar)

Sectie 4: Recente sessies op deze les

  • Laatste 5 voltooide sessies voor deze user op deze les
  • Per rij: datum, score%, duur

Backend endpoint: GET /api/search?q=<term>&limit=30

  • Authenticatie vereist
  • Min query length: 2 chars; lege query → { lessons: [], cards: [] }
  • Doorzoekt:
    • Lessen: alle waar canReadLesson(currentUser, lesson) of (visibility='shared' en owner ≠ currentUser) — d.w.z. eigen library + marketplace
    • Kaarten: alleen kaarten in lessen die de user mag lezen (geen marketplace cards — die staan niet in de "library")
  • Match op LIKE %q% (lowercase) op lessons.name, lessons.description, cards.question, cards.answer, cards.hint
  • Response:
    {
      lessons: Array<{
        id: number;
        name: string;
        ownerDisplayName: string;
        location: 'library' | 'marketplace';
        totalCards: number;
        isCurated: boolean;
      }>;
      cards: Array<{
        id: number;
        lessonId: number;
        lessonName: string;
        question: string;
        snippet: string; // first 80 chars of question or answer with match
      }>;
    }
    
  • Sort: lessen library eerst, dan marketplace; binnen elke groep alfabetisch
  • Kaarten: alfabetisch op lesson name + question

Frontend

  • Trigger: ⌘K / Ctrl+K (global key listener) of klik op zoekicoon in header
  • Modal: full-screen overlay met centered command-palette panel
  • Input: autofocus, debounced 200ms request
  • Sections: "Lessen — Jouw bibliotheek", "Lessen — Marketplace", "Kaarten"
  • Keyboard nav: ↑/↓ door results, Enter selecteert, Esc sluit
  • Click/Enter:
    • Lesson result → /lessons/:id
    • Card result → /lessons/:lessonId#card-:id (anchor scrollt + flash highlight)
  • Empty/error states: "Begin met typen om te zoeken", "Geen resultaten gevonden", error message

3.3 Statistieken-pagina (/stats)

Vervangt huidige StatsPage. Drie blokken in deze volgorde.

A. Activiteit-heatmap (12 maanden)

  • Bron: GET /api/stats/heatmap?weeks=52
  • Component: Heatmap
  • Layout: 53 kolommen × 7 rijen (per dag een cell). Kleur intensiteit op basis van attempts:
    • 0 → gray-100
    • 14 → success-200
    • 514 → success-400
    • 1549 → success-500
    • 50+ → success-700
  • Maand-labels boven, weekday-labels links (Ma, Wo, Vr)
  • Tooltip op hover: "X mei: Y pogingen, Z sessies"

B. Voortgang per les

  • Backend: GET /api/stats/lessons-progress
    {
      rows: Array<{
        lessonId: number;
        name: string;
        totalCards: number;
        masteredCards: number; // box >= 4
        scorePct: number;       // 0..100, computed like getLessonStats
        lastSessionAt: number | null;
      }>;
    }
    
    Alleen root-lessen van de user's tree (lessen waar canRead + parent ofwel null of niet zichtbaar). Sublessen aggregeren in de root.
  • Frontend: gesorteerde lijst (default: hoogste score eerst; sortable op naam/score/laatst). Per rij:
    • Lesnaam (link → /lessons/:id)
    • Progressbar: mastered/total fractie, achtergrond brand-100, vulling success-500
    • Score% rechts
    • Laatst geoefend relative time

C. Reviews-due overzicht

  • Backend: GET /api/stats/due
    {
      overdue: number;     // next_due_at < now()
      today: number;       // due tussen now en eindigend at midnight UTC
      tomorrow: number;    // morgen
      thisWeek: number;    // 7 dagen incl. vandaag
    }
    
    Berekend over alle card_progress rijen van de user op cards in lessen die hij kan lezen.
  • Frontend: 4 grote "badges" met aantallen en kleuren (overdue=red, today=brand, tomorrow=success, thisWeek=slate)
  • CTA-knop onder: "Start review (X kaarten)" start een sessie op een speciale "due-only" mode. Voor v1 implementatie: navigate naar /practice/__due__/setup met een query ?mode=due die de session start filtert op enkel due cards over de hele library.
    • Dit vereist een uitbreiding: POST /api/sessions accepteert { lessonId: 'due', mode: 'due' } of een nieuwe alias-route POST /api/sessions/due die een sessie maakt over alle due cards van de user.
    • Eenvoudiger compromis: een nieuw endpoint POST /api/sessions/due dat dit verzorgt. Spec kiest deze variant.

3.4 Admin polish — /lessons (lessen-tree)

Inline filter

  • Tekstveld bovenin de tree (zelfde rij als "Nieuwe wortel-les...")
  • Live filter (geen debounce nodig, alles client-side)
  • Case-insensitive substring match op name
  • Bij hit: het node + alle voorouders blijven zichtbaar
  • Bij geen-hit: voorouders gedimd, geen-hit-nodes verborgen
  • Bij geen filter: alles normaal

Drag-and-drop

  • Library: @dnd-kit/core + @dnd-kit/sortable
  • Alleen lessen die de user bezit zijn sleepbaar (isOwner)
  • Drop targets: een andere positie binnen dezelfde parent (herordenen) of in een andere les (re-parent)
  • Bij drop: client roept POST /api/lessons/:id/move aan met nieuwe parentId en position
  • Visual feedback: drop-zone hint highlight, snap-back op invalid drop
  • Keyboard support: dnd-kit's built-in (pijltoetsen + space om op te pakken)
  • Restrictions enforced client-side: niet eigenaar → geen drag handle; restricties op backend (cycle check) blijven gelden

4. Datamodel

Geen wijzigingen.

5. API-overzicht

Nieuw

GET  /api/search?q=<term>&limit=30           # globale zoek
GET  /api/stats/lessons-progress             # voortgang per root-les
GET  /api/stats/due                          # due counts
POST /api/sessions/due                       # speciale sessie over alle due cards

Aangepast

GET /api/stats/heatmap                       # default weeks 12 → 52

6. Routes-restructure (frontend)

Oud Nieuw Migratie
/admin /lessons <Navigate to="/lessons" replace />
/admin/lessons/:id /lessons/:id <Navigate to="/lessons/${id}" replace />
/admin/users /admin/users (ongewijzigd; alleen sysadmin)

Nav-link "Lessen" in Layout wijst naar /lessons.

7. UI-componenten

Nieuwe

Bestand Verantwoordelijkheid
pages/LessonDetail.tsx Detail page met header + stats + sublessen + kaarten + recente sessies
pages/Lessons.tsx Vervangt Admin.tsx; tree-pagina met filter + drag-drop
components/SearchPalette.tsx Cmd-K modal met grouped results
components/LessonStatsPanel.tsx 4 mini stat-cards voor detail page header
components/SublessonList.tsx Lijst sublessen met progress
components/RecentSessionsList.tsx Laatste 5 sessies op les
components/Heatmap.tsx 12-maands grid
components/LessonProgressList.tsx Sortable lijst met progressbars
components/DueOverviewCard.tsx 4-badge widget met "Start review" CTA
api/search.ts Client voor /api/search

Aanpassingen

Bestand Wat
components/Layout.tsx + Search icoon + Cmd-K listener; "Lessen" link → /lessons
components/LessonTree.tsx + filter prop, + drag-drop (dnd-kit)
pages/Stats.tsx Volledig herschrijven naar drie nieuwe componenten
router.tsx Routes restructuren + redirects
services/sessions.ts + startDueSession(userId)
routes/sessions.ts + POST /due
services/stats.ts + getLessonsProgress(userId), getDueOverview(userId)
routes/stats.ts + /lessons-progress, /due
services/search.ts Nieuw service module
routes/search.ts Nieuwe router
app.ts Mount nieuwe routers

Te verwijderen

  • pages/AdminLesson.tsx → vervangen door pages/LessonDetail.tsx
  • pages/Admin.tsx → vervangen door pages/Lessons.tsx

8. Tests

Backend unit

  • search.test.ts: filter op canRead, marketplace inclusion, card snippet generation, empty query, special chars
  • stats.test.ts (extend): getLessonsProgress per-root grouping, getDueOverview bucket boundaries

Backend integration

  • /api/search: user A's private niet in user B's results; curated zichtbaar voor B; marketplace lessons; cards niet uit marketplace
  • /api/stats/lessons-progress: alleen roots; correct aggregeren over subtree
  • /api/stats/due: edge cases (geen kaarten, alles future, alles overdue)
  • /api/sessions/due: start sessie met alleen due cards

Frontend unit (Vitest + RTL)

  • SearchPalette: opent op ⌘K, debouncing, keyboard navigation, click navigation
  • LessonStatsPanel: rendering met null/zero waarden
  • Heatmap: kleurintensiteit-buckets, tooltip
  • LessonTree: filter-functie, drag-drop call op move

E2E (Playwright)

  • Detail-page flow: open lesson, zie sublessen, start practice
  • Search flow: ⌘K, type query, klik result → navigatie naar detail
  • Stats page laadt en toont alle drie blokken
  • Drag-drop reorder (smoke; kan flaky zijn — accept een minimum-versie)

9. Performance / a11y

  • Search backend: response time-budget 200ms voor q<=10 cards/lessons (typische tree). Met LIKE %q% zonder index is dit OK voor verwachte schaal (~1000 lessen, ~10000 kaarten). Indien problematisch later: FTS5 index.
  • Heatmap rendert als statisch SVG/grid (max 371 cells × kleine div). Geen virtualization nodig.
  • Cmd-K modal: focus management (autofocus input, Tab cycle binnen modal, Esc sluit, return focus naar trigger)
  • Drag-drop: prefers-reduced-motion respect via dnd-kit's MeasuringStrategy.Always + animated reorder kan uit
  • All interactive elements ARIA-labeled

10. Migratie

Geen DB-wijzigingen. Frontend route-redirects voor bookmarks naar oude URLs.

11. Out of scope (YAGNI)

  • Card-level detail-pagina re-design (/stats/cards/:id blijft staan)
  • Bulk-operaties op kaarten (multi-select, batch delete/move)
  • Duplicate-lesson knop (fork dekt het)
  • Charts-library (recharts, echarts) — voor 3 blokken niet nodig
  • Search saved queries / search history
  • Box-distributie chart, cumulative study curve, streak milestones (afgewezen tijdens brainstorm)
  • FTS5 full-text search index
  • Pagineren in search results boven de 30
  • Editing van een card via search-modal (alleen navigatie)
  • Bulk move bij drag-drop (één les per keer)
  • Drag-drop reorder van cards binnen een les