Files
brainwave/docs/design_handoff_brainwave/app/app.jsx
T
LaurensBartelds be69db04a5 Add SVG icons and TypeScript configuration files
- Introduced a new favicon.svg file for the application.
- Added icons.svg containing various SVG symbols for UI components.
- Created tsconfig.app.json for frontend TypeScript configuration with ES2023 target and bundler mode.
- Established tsconfig.json to reference app and node configurations.
- Implemented tsconfig.node.json for Node.js specific TypeScript settings.
2026-04-21 22:19:06 +02:00

229 lines
7.0 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global React, ReactDOM */
// ==============================================================
// app.jsx — main controller + Tweaks
// ==============================================================
var { useState, useEffect, useCallback } = React;
const TWEAKS = /*EDITMODE-BEGIN*/{
"theme": "light",
"fontSize": "medium",
"questionLayout": "standard",
"feedbackMode": "auto"
}/*EDITMODE-END*/;
const App = () => {
const [screen, setScreen] = useState('landing'); // landing | exam | results | review
const [exam, setExam] = useState(null);
const [state, setState] = useState(null);
const [history, setHistory] = useState(() => window.loadHistory());
const [tweaks, setTweaks] = useState(TWEAKS);
const [tweaksOpen, setTweaksOpen] = useState(false);
// Apply theme/font tweaks
useEffect(() => {
document.documentElement.dataset.theme = tweaks.theme;
const map = { small: '14px', medium: '15px', large: '17px' };
document.body.style.fontSize = map[tweaks.fontSize] || '15px';
document.documentElement.dataset.layout = tweaks.questionLayout;
}, [tweaks]);
// Persist state on change during exam
useEffect(() => {
if (screen === 'exam' && exam && state) {
window.saveState({ exam, state });
}
}, [exam, state, screen]);
// Edit mode protocol
useEffect(() => {
const handler = (e) => {
if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
};
window.addEventListener('message', handler);
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
return () => window.removeEventListener('message', handler);
}, []);
const updateTweak = (key, value) => {
setTweaks((t) => {
const next = { ...t, [key]: value };
window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: value } }, '*');
return next;
});
};
const loadExam = (data) => {
const finalFeedbackMode = tweaks.feedbackMode === 'auto' ? (data.feedbackMode || 'end') : tweaks.feedbackMode;
const initialState = {
currentIdx: 0,
answers: {},
flags: {},
revealed: {},
startedAt: Date.now(),
feedbackMode: finalFeedbackMode,
};
setExam(data);
setState(initialState);
setScreen('exam');
};
const resume = () => {
const saved = window.loadState();
if (saved?.exam && saved?.state) {
setExam(saved.exam);
setState(saved.state);
setScreen('exam');
}
};
const submit = useCallback(() => {
if (!exam || !state) return;
const completedAt = Date.now();
const elapsedMs = completedAt - state.startedAt;
const graded = exam.questions.map((q) => window.gradeQuestion(q, state.answers[q.id]));
const gradedCount = graded.filter((r) => r.status !== 'ungraded').length;
const correctCount = graded.filter((r) => r.correct).length;
const totalPoints = exam.questions.reduce((a, q) => a + (q.points || 1), 0);
const scorePoints = graded.reduce((a, r) => a + r.points, 0);
const historyEntry = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
title: exam.title,
completedAt,
elapsedMs,
gradedCount,
correctCount,
totalPoints,
scorePoints,
passingScore: exam.passingScore || 60,
exam, // full snapshot so we can re-review
state: { ...state, completedAt },
};
window.appendHistory(historyEntry);
setHistory(window.loadHistory());
window.clearState();
setState((s) => ({ ...s, completedAt }));
setScreen('results');
}, [exam, state]);
const exit = () => {
setScreen('landing');
setExam(null);
setState(null);
};
const restart = () => {
if (!exam) return;
loadExam(exam);
};
// Replay a past attempt
window.__replayAttempt = (entry) => {
setExam(entry.exam);
setState(entry.state);
setScreen('review');
};
const clearAllHistory = () => {
if (confirm('Clear all past attempts?')) {
window.clearHistory();
setHistory([]);
}
};
return (
<div className="shell">
<header className="topbar">
<div className="brand">
<span className="brand-mark" />
<span>BrainWave</span>
<span className="faint mono" style={{ fontSize: 11, marginLeft: 4 }}>/ practice</span>
</div>
<div className="topbar-meta">
{screen === 'exam' && exam && (
<>
<span className="mono" style={{ fontSize: 12 }}>{exam.title}</span>
<button className="btn btn-ghost btn-sm" onClick={exit}>Exit</button>
</>
)}
</div>
</header>
{screen === 'landing' && (
<Landing
onLoadExam={loadExam}
history={history}
onResume={resume}
onClearHistory={clearAllHistory}
/>
)}
{screen === 'exam' && exam && state && (
<ExamRunner
exam={exam}
state={state}
setState={setState}
onSubmit={submit}
onExit={exit}
/>
)}
{(screen === 'results' || screen === 'review') && exam && state && (
<Results
exam={exam}
state={state}
onRestart={restart}
onExit={exit}
reviewOnly={screen === 'review'}
/>
)}
{tweaksOpen && <TweaksPanel tweaks={tweaks} update={updateTweak} onClose={() => setTweaksOpen(false)} />}
</div>
);
};
// ---------- Tweaks Panel ----------
const TweaksPanel = ({ tweaks, update, onClose }) => {
const Seg = ({ tkey, options }) => (
<div className="tweak-control">
{options.map((opt) => (
<button
key={opt.value}
className={`tweak-seg ${tweaks[tkey] === opt.value ? 'active' : ''}`}
onClick={() => update(tkey, opt.value)}
>
{opt.label}
</button>
))}
</div>
);
return (
<div className="tweaks-panel">
<div className="tweaks-header">
<span>Tweaks</span>
<button className="btn btn-ghost btn-sm" onClick={onClose} style={{ padding: '2px 6px' }}>×</button>
</div>
<div className="tweaks-body">
<div className="tweak-row">
<div className="tweak-label">Theme</div>
<Seg tkey="theme" options={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }]} />
</div>
<div className="tweak-row">
<div className="tweak-label">Text size</div>
<Seg tkey="fontSize" options={[{ value: 'small', label: 'S' }, { value: 'medium', label: 'M' }, { value: 'large', label: 'L' }]} />
</div>
<div className="tweak-row">
<div className="tweak-label">Feedback</div>
<Seg tkey="feedbackMode" options={[
{ value: 'auto', label: 'Auto' },
{ value: 'immediate', label: 'Immediate' },
{ value: 'end', label: 'End' },
]} />
</div>
</div>
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);