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.
This commit is contained in:
LaurensBartelds committed 2026-04-21 22:19:06 +02:00
1 parent 8420e6cae0
commit be69db04a5
21 files changed
+6872

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
/* 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 />);