diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 50a6df4..6829d44 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,106 @@ +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() { - return
BrainWave
+ 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)} />} +
+ ) } diff --git a/frontend/src/components/TweaksPanel.tsx b/frontend/src/components/TweaksPanel.tsx new file mode 100644 index 0000000..626cdfb --- /dev/null +++ b/frontend/src/components/TweaksPanel.tsx @@ -0,0 +1,57 @@ +import type { Tweaks, ThemeMode, FontSize, FeedbackTweak } from '../types' + +interface Props { + tweaks: Tweaks + update: (key: K, value: Tweaks[K]) => void + onClose: () => void +} + +function Seg({ tkey, options, tweaks, update }: { + tkey: keyof Tweaks + options: { value: string; label: string }[] + tweaks: Tweaks + update: Props['update'] +}) { + return ( +
+ {options.map((opt) => ( + + ))} +
+ ) +} + +export default function TweaksPanel({ tweaks, update, onClose }: Props) { + return ( +
+
+ Tweaks + +
+
+
+
Theme
+ +
+
+
Text size
+ +
+
+
Feedback
+ +
+
+
+ ) +}