Public Access
feat: add notes functionality to exam state and update UI for note-taking
This commit is contained in:
1 parent
d0b1ac0145
commit
c4c1e1a45a
5 files changed
+90
-31
No files matched your search
@@ -14,10 +14,11 @@ interface Props {
|
||||
onExit: () => void
|
||||
}
|
||||
|
||||
export default function ExamRunner({ exam, state, setState, onSubmit, onExit }: Props) {
|
||||
export default function ExamRunner({ exam, state, setState, onSubmit, onExit }: Readonly<Props>) {
|
||||
const { currentIdx, answers, flags, startedAt, feedbackMode } = state
|
||||
const q = exam.questions[currentIdx]
|
||||
const total = exam.questions.length
|
||||
const notes = state.notes ?? ''
|
||||
|
||||
const [revealed, setRevealed] = useState(() => !!state.revealed?.[q.id])
|
||||
useEffect(() => { setRevealed(!!state.revealed?.[q.id]) }, [currentIdx, q.id, state.revealed])
|
||||
@@ -31,8 +32,14 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }:
|
||||
const toggleFlag = useCallback(() =>
|
||||
setState((s) => ({ ...s, flags: { ...s.flags, [q.id]: !s.flags[q.id] } })), [q.id])
|
||||
|
||||
const updateNotes = (value: string) =>
|
||||
setState((s) => ({ ...s, notes: value.slice(0, 1000) }))
|
||||
|
||||
const checkAnswer = () => {
|
||||
setState((s) => ({ ...s, revealed: { ...(s.revealed ?? {}), [q.id]: true } }))
|
||||
setState((s) => ({
|
||||
...s,
|
||||
revealed: s.revealed ? { ...s.revealed, [q.id]: true } : { [q.id]: true },
|
||||
}))
|
||||
setRevealed(true)
|
||||
}
|
||||
|
||||
@@ -58,13 +65,13 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }:
|
||||
if (e.key === 'ArrowLeft' || e.key === 'k') goTo(currentIdx - 1)
|
||||
if (e.key === 'f') toggleFlag()
|
||||
if (/^[1-9]$/.test(e.key) && q.type === 'multiple-choice' && q.choices) {
|
||||
const ci = parseInt(e.key, 10) - 1
|
||||
const ci = Number.parseInt(e.key, 10) - 1
|
||||
const choices = q.choices
|
||||
if (choices[ci]) setState((s) => ({ ...s, answers: { ...s.answers, [q.id]: choices[ci].id } }))
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
globalThis.addEventListener('keydown', handler)
|
||||
return () => globalThis.removeEventListener('keydown', handler)
|
||||
}, [currentIdx, q, goTo, toggleFlag, setState])
|
||||
|
||||
const graded = feedbackMode === 'immediate' && revealed
|
||||
@@ -90,6 +97,12 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }:
|
||||
}).length
|
||||
|
||||
const totalPossible = exam.questions.reduce((acc, qq) => acc + (qq.points ?? 1), 0)
|
||||
let answerText = ''
|
||||
if (!Array.isArray(q.answer) && typeof q.answer === 'string') {
|
||||
answerText = q.choices?.find((c) => c.id === q.answer)?.text ?? q.answer
|
||||
} else if (Array.isArray(q.answer)) {
|
||||
answerText = q.answer.map((id) => q.choices?.find((c) => c.id === id)?.text ?? id).join(', ')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="exam-shell">
|
||||
@@ -145,11 +158,7 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }:
|
||||
{!result.correct && q.answer && q.type !== 'match' && q.type !== 'order' && (
|
||||
<p>
|
||||
<strong>Answer:</strong>{' '}
|
||||
<Tex>
|
||||
{Array.isArray(q.answer)
|
||||
? (q.answer as string[]).map((id) => q.choices?.find((c) => c.id === id)?.text ?? id).join(', ')
|
||||
: (q.choices?.find((c) => c.id === q.answer)?.text ?? String(q.answer))}
|
||||
</Tex>
|
||||
<Tex>{answerText}</Tex>
|
||||
</p>
|
||||
)}
|
||||
{q.explanation && <p><Tex>{q.explanation}</Tex></p>}
|
||||
@@ -201,6 +210,18 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }:
|
||||
{feedbackMode === 'immediate' ? 'Immediate feedback' : 'Grade at end'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="meta-block notes-block">
|
||||
<div className="meta-label">Notes</div>
|
||||
<textarea
|
||||
className="textarea notes-pad"
|
||||
value={notes}
|
||||
onChange={(e) => updateNotes(e.target.value)}
|
||||
placeholder="Add short notes while you work..."
|
||||
maxLength={1000}
|
||||
aria-label="Exam notes"
|
||||
/>
|
||||
<div className="notes-count mono">{notes.length}/1000</div>
|
||||
</div>
|
||||
{exam.author && (
|
||||
<div className="meta-block">
|
||||
<div className="meta-label">Author</div>
|
||||
|
||||
Reference in new issue
Block a user