feat(frontend): lesson stats panel + sublesson list + recent sessions list

This commit is contained in:
2026-05-21 07:07:46 +02:00
parent 2a6d048b65
commit d9b913aab7
3 changed files with 93 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import type { LessonStats } from '../api/stats.js';
function relativeTime(unixSec: number | null): string {
if (!unixSec) return 'nooit';
const diff = Math.floor(Date.now() / 1000) - unixSec;
if (diff < 3600) return `${Math.max(1, 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 LessonStatsPanel({ stats, lastSessionAt }: { stats: LessonStats; lastSessionAt?: number | null }) {
const masteredFrac = stats.totalCards === 0 ? 0 : stats.mastered / stats.totalCards;
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Card label="Kaarten" value={String(stats.totalCards)} />
<Card label="Beheerst" value={`${stats.mastered}/${stats.totalCards}`} sub={`${Math.round(masteredFrac * 100)}%`} />
<Card label="Score" value={`${Math.round(stats.score * 100)}%`} />
<Card label="Laatst geoefend" value={relativeTime(lastSessionAt ?? null)} />
</div>
);
}
function Card({ label, value, sub }: { label: string; value: string; sub?: string }) {
return (
<div className="surface p-4">
<div className="text-[10px] font-semibold uppercase tracking-wider text-slate-500">{label}</div>
<div className="mt-1 font-display text-2xl font-bold">{value}</div>
{sub && <div className="text-xs text-slate-500">{sub}</div>}
</div>
);
}

View File

@@ -0,0 +1,41 @@
function relativeTime(unixSec: number): string {
const diff = Math.floor(Date.now() / 1000) - unixSec;
if (diff < 3600) return `${Math.max(1, Math.floor(diff / 60))}m geleden`;
if (diff < 86400) return `${Math.floor(diff / 3600)}u geleden`;
return `${Math.floor(diff / 86400)}d geleden`;
}
function fmtDuration(s: number): string {
if (s < 60) return `${s}s`;
return `${Math.floor(s / 60)}m ${s % 60}s`;
}
export interface RecentSessionRow {
id: number;
startedAt: number;
durationSeconds: number | null;
cardsShown: number;
cardsCorrect: number;
}
export function RecentSessionsList({ rows }: { rows: RecentSessionRow[] }) {
if (rows.length === 0) {
return <p className="text-sm text-slate-500">Nog geen sessies op deze les.</p>;
}
return (
<ul className="surface divide-y divide-brand-100/60 dark:divide-slate-800">
{rows.map((s) => {
const pct = s.cardsShown > 0 ? Math.round((s.cardsCorrect / s.cardsShown) * 100) : 0;
return (
<li key={s.id} className="flex items-center justify-between p-3 text-sm">
<span className="text-slate-600 dark:text-slate-300">{relativeTime(s.startedAt)}</span>
<span className="flex items-center gap-3 text-xs">
<span className="rounded-full bg-brand-100 px-2 py-0.5 font-semibold text-brand-700 dark:bg-brand-900/30 dark:text-brand-200">{pct}%</span>
<span className="text-slate-500">{s.cardsCorrect}/{s.cardsShown} · {fmtDuration(s.durationSeconds ?? 0)}</span>
</span>
</li>
);
})}
</ul>
);
}

View File

@@ -0,0 +1,21 @@
import { Link } from 'react-router-dom';
import type { LessonTreeNode } from '@flashcard/shared';
export function SublessonList({ items }: { items: LessonTreeNode[] }) {
if (items.length === 0) return null;
return (
<div>
<h2 className="mb-3 font-display text-xl font-bold">Sublessen</h2>
<ul className="grid gap-2 sm:grid-cols-2">
{items.map((c) => (
<li key={c.id} className="surface flex items-center justify-between p-3">
<Link to={`/lessons/${c.id}`} className="flex-1 truncate font-medium">{c.name}</Link>
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-xs font-semibold text-brand-700 dark:bg-brand-900/30 dark:text-brand-200">
{c.cardCount}
</span>
</li>
))}
</ul>
</div>
);
}