From 24d47e1830110a3807a9fb850c38eb3d0053e9cd Mon Sep 17 00:00:00 2001 From: LaurensBartelds Date: Tue, 21 Apr 2026 21:48:06 +0200 Subject: [PATCH] feat: add Landing screen --- frontend/src/components/Landing.tsx | 153 ++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 frontend/src/components/Landing.tsx diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx new file mode 100644 index 0000000..9fde90d --- /dev/null +++ b/frontend/src/components/Landing.tsx @@ -0,0 +1,153 @@ +import { useState, useRef } from 'react' +import type { Exam, HistoryEntry } from '../types' +import { validateExam, parseExam } from '../utils/validation' +import { loadState } from '../utils/storage' +import { formatDate, formatTime } from '../utils/formatting' + +interface Props { + onLoadExam: (exam: Exam) => void + history: HistoryEntry[] + onResume: () => void + onClearHistory: () => void + onReviewAttempt: (entry: HistoryEntry) => void +} + +const SAMPLES = [ + { path: '/samples/calculus-101.json', title: 'Calculus I — Midterm Practice', meta: '7 questions · 45 min · LaTeX math', mark: '∫' }, + { path: '/samples/spanish-a2.json', title: 'Spanish A2 — Vocabulary & Grammar', meta: '6 questions · 20 min · Translation', mark: 'ñ' }, + { path: '/samples/mixed-demo.json', title: 'Mixed Demo — Every Question Type', meta: '4 questions · Untimed · Media', mark: '◇' }, +] + +export default function Landing({ onLoadExam, history, onResume, onClearHistory, onReviewAttempt }: Props) { + const [err, setErr] = useState(null) + const [dragOver, setDragOver] = useState(false) + const fileRef = useRef(null) + + const handleFile = async (file: File) => { + setErr(null) + try { + const text = await file.text() + const data = JSON.parse(text) as unknown + const msg = validateExam(data) + if (msg) throw new Error(msg) + onLoadExam(parseExam(data)) + } catch (e) { + setErr(`Could not load: ${(e as Error).message}`) + } + } + + const loadSample = async (path: string) => { + setErr(null) + try { + const r = await fetch(path) + const data = await r.json() as unknown + onLoadExam(parseExam(data)) + } catch (e) { + setErr(`Could not load sample: ${(e as Error).message}`) + } + } + + const resumable = loadState() + + return ( +
+
BrainWave · practice exam platform
+

Upload an exam. Practice anything.

+

+ Drop in a JSON file to run a practice exam — math with full LaTeX, language translation, multiple choice, matching, ordering, essays, and more. Progress saves automatically. +

+ +
+
+
fileRef.current?.click()} + onDragOver={(e) => { e.preventDefault(); setDragOver(true) }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { + e.preventDefault(); setDragOver(false) + const f = e.dataTransfer.files?.[0] + if (f) void handleFile(f) + }} + > +
+

Drop your exam JSON here

+

or click to browse · max 5 MB

+ { const f = e.target.files?.[0]; if (f) void handleFile(f) }} + /> +
+ + {err && ( +
+ {err} +
+ )} + + {resumable && ( +
+
+
Resume in-progress exam
+
+ {resumable.exam.title} · {Object.keys(resumable.state.answers ?? {}).length}/{resumable.exam.questions.length} answered +
+
+ +
+ )} + +
+
Supported types
+ multiple-choice · multiple-select · short-answer · translation · math-input (LaTeX) · essay · match · order +
+
+ +
+

Try a sample

+ {SAMPLES.map((s) => ( +
void loadSample(s.path)}> +
{s.mark}
+
+
{s.title}
+
{s.meta}
+
+
→
+
+ ))} +
+
+ + {history.length > 0 && ( +
+
+

+ Past attempts +

+ +
+ {history.map((h) => { + const pct = h.totalPoints ? Math.round((h.scorePoints / h.totalPoints) * 100) : 0 + const passed = pct >= (h.passingScore ?? 60) + return ( +
+
+
{h.title}
+
+ {formatDate(h.completedAt)} · {h.correctCount}/{h.gradedCount} correct +
+
+
{formatTime(h.elapsedMs)}
+
{pct}%
+ +
+ ) + })} +
+ )} +
+ ) +}