From a10d02cbaf8bfaff70aaae4832cda1d7716062b5 Mon Sep 17 00:00:00 2001 From: Bert Hausmans Date: Thu, 21 May 2026 07:04:34 +0200 Subject: [PATCH] feat(frontend): lesson progress list with sorting --- .../src/components/LessonProgressList.tsx | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 packages/frontend/src/components/LessonProgressList.tsx diff --git a/packages/frontend/src/components/LessonProgressList.tsx b/packages/frontend/src/components/LessonProgressList.tsx new file mode 100644 index 0000000..2eebb7f --- /dev/null +++ b/packages/frontend/src/components/LessonProgressList.tsx @@ -0,0 +1,79 @@ +import { useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; + +export interface LessonProgressRow { + lessonId: number; + name: string; + totalCards: number; + masteredCards: number; + scorePct: number; + lastSessionAt: number | null; +} + +type SortKey = 'name' | 'score' | 'last'; + +function relativeTime(unixSec: number | null): string { + if (!unixSec) return 'nooit'; + const diff = Math.floor(Date.now() / 1000) - unixSec; + if (diff < 60) return 'zojuist'; + if (diff < 3600) return `${Math.floor(diff / 60)}m geleden`; + if (diff < 86400) return `${Math.floor(diff / 3600)}u geleden`; + return `${Math.floor(diff / 86400)}d geleden`; +} + +export function LessonProgressList({ rows }: { rows: LessonProgressRow[] }) { + const [sortBy, setSortBy] = useState('score'); + + const sorted = useMemo(() => { + const copy = [...rows]; + copy.sort((a, b) => { + if (sortBy === 'name') return a.name.localeCompare(b.name); + if (sortBy === 'last') return (b.lastSessionAt ?? 0) - (a.lastSessionAt ?? 0); + return b.scorePct - a.scorePct; + }); + return copy; + }, [rows, sortBy]); + + if (rows.length === 0) { + return

Nog geen lessen.

; + } + + return ( +
+
+ Sorteer: + {(['score', 'name', 'last'] as SortKey[]).map((k) => ( + + ))} +
+
    + {sorted.map((r) => { + const masteredFrac = r.totalCards === 0 ? 0 : r.masteredCards / r.totalCards; + return ( +
  • + + {r.name} + +
    +
    +
    + {r.masteredCards}/{r.totalCards} + {r.scorePct}% + {relativeTime(r.lastSessionAt)} +
  • + ); + })} +
+
+ ); +}