Public Access
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:
1 parent
8420e6cae0
commit
be69db04a5
21 files changed
+6872
No files matched your search
@@ -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 />);
|
||||
Reference in new issue
Block a user