Files
brainwave/docs/superpowers/specs/2026-04-24-react-router-url-routing-design.md
T
2026-04-24 23:39:06 +02:00

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 screen state and AppScreen type usage.
  • Replace the URL-on-mount useEffect with React Router's declarative routing.
  • Use <Routes> / <Route> 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

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)