"use client"; import { useMemo, useState, useSyncExternalStore } from "react"; import { useRouter } from "next/navigation"; import { SessionFeed, type SessionItem } from "@/components/SessionFeed"; import { isReported } from "@/lib/question-reports"; import { Button } from "@/components/ui"; import { haptic } from "@/lib/haptics"; import { recordAnswer } from "@/lib/history"; import { useI18n } from "@/lib/i18n"; import { deckStore } from "@/lib/storage"; import { cn } from "@/lib/utils"; const COUNTS = [5, 10, 15, 20]; const TIMERS = [0, 300, 600, 900]; /** * Exam mode: pick one or more decks, a question count and an optional timer, * then answer without hints. Results show the score plus a per-deck and * per-topic breakdown; every wrong answer lands in the mistakes set (the * normal history does that automatically). */ export function ExamScreen() { const { t } = useI18n(); const router = useRouter(); const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot); const [selected, setSelected] = useState>(new Set()); const [count, setCount] = useState(10); const [timer, setTimer] = useState(0); const [running, setRunning] = useState(null); const pool = useMemo( () => decks .filter((deck) => selected.has(deck.id)) .flatMap((deck) => deck.activities.map((activity) => ({ deck, activity }))) // Reported questions are out of rotation (see lib/question-reports). .filter(({ deck, activity }) => !isReported(deck.id, activity.id)), [decks, selected], ); const effectiveCount = Math.min(count, pool.length); const toggle = (id: string) => { haptic("toggle"); setSelected((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const start = () => { const shuffled = [...pool]; for (let index = shuffled.length - 1; index > 0; index--) { const swap = Math.floor(Math.random() * (index + 1)); [shuffled[index], shuffled[swap]] = [shuffled[swap], shuffled[index]]; } setRunning(shuffled.slice(0, effectiveCount)); }; if (running) { return ( setRunning(null)} onAnswered={(item, correct) => { haptic(correct ? "success" : "error"); recordAnswer({ deckId: item.deck.id, deckTitle: item.deck.title, activityId: item.activity.id, type: item.activity.type, topic: item.activity.topic, correct, }); }} renderResults={({ results, items, correct, total, score }) => { const byDeck = new Map(); const byTopic = new Map(); for (const item of items) { const result = results[item.activity.id]; if (!result) continue; const deck = byDeck.get(item.deck.id) ?? { title: item.deck.title, answered: 0, correct: 0 }; deck.answered += 1; if (result.correct) deck.correct += 1; byDeck.set(item.deck.id, deck); if (item.activity.topic) { const topic = byTopic.get(item.activity.topic) ?? { answered: 0, correct: 0 }; topic.answered += 1; if (result.correct) topic.correct += 1; byTopic.set(item.activity.topic, topic); } } return (

{t("exam.results")}

{score}%

{t("results.gotOf", { correct, total, retry: "" })}

{t("exam.byDeck")}

{[...byDeck.entries()].map(([id, entry]) => (
{entry.title} {entry.correct}/{entry.answered}
))}
{byTopic.size > 0 ? (

{t("exam.byTopic")}

{[...byTopic.entries()].map(([topic, entry]) => (
{topic} {entry.correct}/{entry.answered}
))}
) : null}
{correct < total ? ( ) : null}
); }} /> ); } return (

{t("exam.title")}

{t("exam.subtitle")}

{t("exam.pickDecks")}

{decks.length === 0 ? (

{t("exam.noDecks")}

) : (
    {decks.map((deck) => { const active = selected.has(deck.id); return (
  • ); })}
)}

{t("exam.questions")}

{COUNTS.map((option) => ( ))}

{t("exam.timer")}

{TIMERS.map((option) => ( ))}
); }