From 1c13f4849aa9e933df8e4961539761cab2cea48d Mon Sep 17 00:00:00 2001 From: LaurensBartelds Date: Fri, 24 Apr 2026 23:39:06 +0200 Subject: [PATCH] docs: add React Router URL routing design spec Co-Authored-By: Claude Sonnet 4.6 --- ...6-04-24-react-router-url-routing-design.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md diff --git a/docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md b/docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md new file mode 100644 index 0000000..79185b8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md @@ -0,0 +1,112 @@ +# 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)