Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.9 KiB
React Router URL Routing — Design Spec
Date: 2026-04-24 Status: Approved
Overview
Add React Router v7 to the Brainwave frontend so the URL reflects the active exam and current question number. A user on question 3 of "My Exam" will see /exam/my-exam/q3 in the address bar. Back/forward browser navigation works naturally, and deep links to a specific question are supported.
Route Structure
| URL | Screen |
|---|---|
/ |
Landing |
/exam/:slug/q:n |
ExamRunner — question n (1-indexed) of exam identified by slug |
/results |
Results (post-submission) |
/review/:slug |
Results in review-only mode |
/verify |
Email verification |
/reset-password |
Password reset |
/admin |
Admin dashboard (admin users only) |
:slug is the exam title converted to lowercase kebab-case (e.g. "My Exam 2024" → "my-exam-2024"). :n is the 1-indexed question number.
Architecture
New dependency
react-router-dom v7 added to frontend/package.json dependencies.
New utility — frontend/src/utils/slug.ts
Pure function slugify(title: string): string:
- Lowercase
- Replace non-alphanumeric characters with hyphens
- Collapse consecutive hyphens
- Trim leading/trailing hyphens
main.tsx
Wrap <App> in <BrowserRouter>.
App.tsx
- Remove
screenstate andAppScreentype usage. - Replace the URL-on-mount
useEffectwith React Router's declarative routing. - Use
<Routes>/<Route>to render screen components. - Replace all
setScreen()calls withuseNavigate():loadExam()→navigate('/exam/${slug}/q1')submit()→navigate('/results')exit()→navigate('/')reviewAttempt()→navigate('/review/${slug}')- Admin menu item →
navigate('/admin') handleAuthFlowDone()→navigate('/')
examandexamStateremain in React state — no change to storage, grading, or submission logic.
ExamRunner.tsx
- Accept an
onNavigate: (i: number) => voidprop, called fromgoTo()after updatingexamState. App.tsxpasses a handler that callsnavigate('/exam/${slug}/q${i+1}').- ExamRunner has no direct knowledge of React Router — it remains router-agnostic.
Exam route handler in App.tsx
- Reads
:nand:slugfromuseParams(). - On mount and whenever
:nchanges (back/forward), callssetExamStateto setcurrentIdx = n - 1. - Handles the deep-link case: if no active exam in state, checks localStorage for a matching slug; resumes or redirects to
/.
Deep linking
When the app loads on /exam/:slug/q:n:
- Check localStorage for a saved exam state (
brainwave.exam.state). - If the saved exam's slugified title matches
:slug, resume withcurrentIdx = n - 1. - If no match, redirect to
/.
Vite config
Add server.historyApiFallback: true to vite.config.ts so the dev server serves index.html for all non-asset paths. The production backend already handles this (it serves index.html for non-API routes).
Data Flow
User clicks question 5 in sidebar
→ goTo(4) called in ExamRunner
→ setState updates examState.currentIdx = 4
→ onNavigate(4) called
→ navigate('/exam/my-exam/q5') in App
→ URL bar updates
→ :n param changes
→ useEffect in App exam route calls setExamState [no-op, already 4]
User presses browser Back
→ URL changes to /exam/my-exam/q4
→ :n param changes to '4'
→ useEffect in App exam route calls setExamState({ currentIdx: 3 })
→ ExamRunner re-renders at question 4
What Does Not Change
Results.tsx,Landing.tsx, all question componentsAuthContext, storage utils, grading utils, API utils- Backend — no changes required
ExamStateshape and localStorage persistenceAppScreentype removed fromtypes.ts
Out of Scope
- Animated route transitions
- Scroll restoration between questions
- Sharing/bookmarking exam links to others (exam data is client-side only)