# 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 `` in ``. ### `App.tsx` - Remove `screen` state and `AppScreen` type usage. - Replace the URL-on-mount `useEffect` with React Router's declarative routing. - Use `` / `` to render screen components. - Replace all `setScreen()` calls with `useNavigate()`: - `loadExam()` → `navigate('/exam/${slug}/q1')` - `submit()` → `navigate('/results')` - `exit()` → `navigate('/')` - `reviewAttempt()` → `navigate('/review/${slug}')` - Admin menu item → `navigate('/admin')` - `handleAuthFlowDone()` → `navigate('/')` - `exam` and `examState` remain in React state — no change to storage, grading, or submission logic. ### `ExamRunner.tsx` - Accept an `onNavigate: (i: number) => void` prop, called from `goTo()` after updating `examState`. - `App.tsx` passes a handler that calls `navigate('/exam/${slug}/q${i+1}')`. - ExamRunner has no direct knowledge of React Router — it remains router-agnostic. ### Exam route handler in `App.tsx` - Reads `:n` and `:slug` from `useParams()`. - On mount and whenever `:n` changes (back/forward), calls `setExamState` to set `currentIdx = 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 with `currentIdx = 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 ```text 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 components - `AuthContext`, storage utils, grading utils, API utils - Backend — no changes required - `ExamState` shape and localStorage persistence - `AppScreen` type removed from `types.ts` ## Out of Scope - Animated route transitions - Scroll restoration between questions - Sharing/bookmarking exam links to others (exam data is client-side only)