feat(frontend): practice setup, session and done flow

This commit is contained in:
2026-05-20 21:20:30 +02:00
parent 16a7cc6ad6
commit 2444e2400f
6 changed files with 190 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import type { Card } from '@flashcard/shared';
import { cardsApi } from '../api/cards.js';
import { useSession } from '../stores/sessionStore.js';
import { Flashcard } from '../components/Flashcard.js';
export function PracticePage() {
const { lessonId } = useParams();
const { session, current, done, showAnswer, reveal, answer, end } = useSession();
const navigate = useNavigate();
const [card, setCard] = useState<Card | null>(null);
useEffect(() => {
if (!session) { navigate(`/practice/${lessonId}/setup`); return; }
}, [session, lessonId, navigate]);
useEffect(() => {
let cancel = false;
(async () => {
if (!current) { setCard(null); return; }
const c = await cardsApi.get(current.cardId);
if (!cancel) setCard(c);
})();
return () => { cancel = true; };
}, [current]);
useEffect(() => {
if (done && session) { (async () => { await end(); navigate(`/practice/${lessonId}/done`); })(); }
}, [done, session, end, navigate, lessonId]);
if (!current || !card) return <div className="p-6 text-center">Laden...</div>;
const isReverse = current.direction === 'backward';
return (
<div className="flex h-full flex-col">
<div className="mx-auto w-full max-w-2xl p-6">
<div className="mb-2 text-xs text-slate-500">{session?.cardsShown ?? 0} kaarten behandeld</div>
<Flashcard
question={isReverse ? card.answer : card.question}
answer={isReverse ? card.question : card.answer}
hint={card.hint}
showAnswer={showAnswer}
onReveal={reveal}
onAnswer={(r) => answer(r)}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Link, useParams } from 'react-router-dom';
import { useSession } from '../stores/sessionStore.js';
import { Confetti } from '../components/Confetti.js';
export function PracticeDonePage() {
const { lessonId } = useParams();
const session = useSession((s) => s.session);
const reset = useSession((s) => s.reset);
if (!session) return <div className="p-6">Geen sessie gegevens. <Link to="/" className="text-blue-600">Terug</Link></div>;
const total = session.cardsShown || 1;
const pct = Math.round((session.cardsCorrect / total) * 100);
return (
<div className="mx-auto max-w-md p-6 text-center">
<Confetti trigger={pct >= 80} />
<h1 className="text-3xl font-semibold">Sessie klaar!</h1>
<p className="mt-4 text-5xl font-bold">{pct}%</p>
<p className="mt-2 text-slate-500">{session.cardsCorrect} goed, {session.cardsIncorrect} fout van {session.cardsShown}</p>
<p className="mt-1 text-sm text-slate-500">Duur: {session.durationSeconds ?? 0}s</p>
<div className="mt-6 flex justify-center gap-3">
<Link to={`/practice/${lessonId}/setup`} className="rounded bg-green-600 px-4 py-2 text-white" onClick={reset}>Opnieuw</Link>
<Link to="/" className="rounded bg-slate-200 px-4 py-2 dark:bg-slate-800" onClick={reset}>Dashboard</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useSettings } from '../stores/settingsStore.js';
import { useSession } from '../stores/sessionStore.js';
export function PracticeSetupPage() {
const { lessonId } = useParams();
const id = Number(lessonId);
const defaultMax = useSettings((s) => s.defaultMaxCards);
const start = useSession((s) => s.start);
const navigate = useNavigate();
const [maxCards, setMaxCards] = useState<number | 'all'>(defaultMax);
const [shuffle, setShuffle] = useState(true);
const [direction, setDirection] = useState<'forward' | 'backward' | 'both'>('forward');
const [busy, setBusy] = useState(false);
async function begin() {
setBusy(true);
await start({ lessonId: id, maxCards: maxCards === 'all' ? null : maxCards, shuffle, direction });
navigate(`/practice/${id}`);
}
return (
<div className="mx-auto max-w-md p-6">
<h1 className="mb-4 text-2xl font-semibold">Sessie starten</h1>
<label className="mb-2 block text-sm">Max. aantal kaarten
<select className="ml-2 rounded border px-2 py-1 dark:bg-slate-900" value={String(maxCards)} onChange={(e) => setMaxCards(e.target.value === 'all' ? 'all' : Number(e.target.value))}>
{[10, 20, 30, 50].map((n) => <option key={n} value={n}>{n}</option>)}
<option value="all">Alle</option>
</select>
</label>
<label className="mb-2 flex items-center gap-2 text-sm"><input type="checkbox" checked={shuffle} onChange={(e) => setShuffle(e.target.checked)} /> Shuffle</label>
<label className="mb-4 block text-sm">Richting
<select className="ml-2 rounded border px-2 py-1 dark:bg-slate-900" value={direction} onChange={(e) => setDirection(e.target.value as typeof direction)}>
<option value="forward">Vraag antwoord</option>
<option value="backward">Antwoord vraag</option>
<option value="both">Beide</option>
</select>
</label>
<button className="w-full rounded bg-green-600 py-3 font-medium text-white" onClick={begin} disabled={busy}>
Start
</button>
</div>
);
}