import { useState, useEffect, useCallback } from 'react' import type { AppScreen, Exam, ExamState, HistoryEntry, Tweaks } from './types' import { gradeQuestion } from './utils/grading' import { loadState, saveState, clearState, loadHistory, appendHistory, clearHistory } from './utils/storage' import Landing from './components/Landing' import ExamRunner from './components/ExamRunner' import Results from './components/Results' import TweaksPanel from './components/TweaksPanel' const DEFAULT_TWEAKS: Tweaks = { theme: 'light', fontSize: 'medium', feedbackMode: 'auto' } export default function App() { const [screen, setScreen] = useState('landing') const [exam, setExam] = useState(null) const [examState, setExamState] = useState(null) const [history, setHistory] = useState(() => loadHistory()) const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS) const [tweaksOpen, setTweaksOpen] = useState(false) useEffect(() => { document.documentElement.dataset['theme'] = tweaks.theme const sizeMap: Record = { small: '14px', medium: '15px', large: '17px' } document.body.style.fontSize = sizeMap[tweaks.fontSize] ?? '15px' }, [tweaks]) useEffect(() => { if (screen === 'exam' && exam && examState) saveState({ exam, state: examState }) }, [exam, examState, screen]) const updateTweak = (key: K, value: Tweaks[K]) => setTweaks((t) => ({ ...t, [key]: value })) const loadExam = (data: Exam) => { const fm = tweaks.feedbackMode === 'auto' ? (data.feedbackMode ?? 'end') : tweaks.feedbackMode setExam(data) setExamState({ currentIdx: 0, answers: {}, flags: {}, revealed: {}, startedAt: Date.now(), feedbackMode: fm }) setScreen('exam') } const resume = () => { const saved = loadState() if (saved?.exam && saved?.state) { setExam(saved.exam); setExamState(saved.state); setScreen('exam') } } const submit = useCallback(() => { if (!exam || !examState) return const completedAt = Date.now() const elapsedMs = completedAt - examState.startedAt const graded = exam.questions.map((q) => gradeQuestion(q, examState.answers[q.id])) const entry: HistoryEntry = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, title: exam.title, completedAt, elapsedMs, gradedCount: graded.filter((r) => r.status !== 'ungraded').length, correctCount: graded.filter((r) => r.correct).length, totalPoints: exam.questions.reduce((a, q) => a + (q.points ?? 1), 0), scorePoints: graded.reduce((a, r) => a + r.points, 0), passingScore: exam.passingScore ?? 60, exam, state: { ...examState, completedAt }, } appendHistory(entry) setHistory(loadHistory()) clearState() setExamState((s) => s ? { ...s, completedAt } : s) setScreen('results') }, [exam, examState]) const exit = () => { setScreen('landing'); setExam(null); setExamState(null) } const restart = () => { if (exam) loadExam(exam) } const reviewAttempt = (entry: HistoryEntry) => { setExam(entry.exam); setExamState(entry.state); setScreen('review') } const clearAllHistory = () => { if (confirm('Clear all past attempts?')) { clearHistory(); setHistory([]) } } return (
BrainWave / practice
{screen === 'exam' && exam && ( <> {exam.title} )}
{screen === 'landing' && ( )} {screen === 'exam' && exam && examState && ( )} {(screen === 'results' || screen === 'review') && exam && examState && ( )} {tweaksOpen && setTweaksOpen(false)} />}
) }