New portal feature apps/portal/src/app/features/exam-attempt/, following
the repo's existing folder convention (pages/<Page>/index.tsx, components/,
types/, hooks/ — matches the payments feature's hooks/ precedent):
- types/exam-attempt.ts — response/local-state shapes
- hooks/useExamAttempt.ts — all API orchestration: load (registration +
attempt), start/resume, per-question autosave (immediate on MCQ select,
debounced+flushed-on-navigation for essay text), local countdown seeded
from the server's serverTime/remainingSeconds, submit, and resync from
the server whenever a write is refused as expired/already-submitted
- components/ExamInstructions, ExamTimer, ExamQuestionNav,
ExamQuestionDisplay, ExamCompletion — one concern per file, not a single
page dump
- pages/ExamAttemptPage — thin view layer over the hook
Flow: /exams (existing) gets a 'Take exam' action on eligible registration
rows → /exams/:examId/take, which shows instructions before an attempt
exists, the live exam screen while IN_PROGRESS, and a no-score completion
screen once SUBMITTED/EXPIRED (never fabricates a result — grading
doesn't exist yet).
Security note: RequirePermission on the route and disabled inputs after
local timeout are UI conveniences only. Every save/submit is independently
re-checked by the backend's ownership + applyExpiry() on each call: a
client that skipped the UI entirely and hit the API directly would be
bound by exactly the same rules.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>