feat: replace screen state machine with React Router v7 URL routing
Release Pipeline / build-and-release (push) Successful in 5m24s

- Install react-router-dom
- Add slugify utility with full TDD test suite (7 tests)
- Wrap app in BrowserRouter in main.tsx
- Add optional onNavigate prop to ExamRunner.goTo
- Create ExamRoute component (deep-link restore + back/forward sync)
- Migrate App.tsx: replace screen useState with <Routes>, all setScreen()
  calls replaced with navigate(), add activateExam for deep-link activation,
  use useLocation for verify/reset-password special layouts
- Remove unused AppScreen type from types.ts
This commit is contained in:
LaurensBartelds committed 2026-04-24 23:55:52 +02:00
1 parent e97d90e316
commit 4eeacc38c5
10 files changed
+796 -367

No files matched your search

+82 -83
View File
@@ -1,125 +1,124 @@
// src/types.ts
export type QuestionType =
| 'multiple-choice'
| 'multiple-select'
| 'short-answer'
| 'translation'
| 'math-input'
| 'essay'
| 'match'
| 'order'
| "multiple-choice"
| "multiple-select"
| "short-answer"
| "translation"
| "math-input"
| "essay"
| "match"
| "order";
export interface Choice {
id: string
text: string
id: string;
text: string;
}
export interface MatchItem {
id: string
text: string
id: string;
text: string;
}
export interface QuestionImage {
src: string
alt?: string
caption?: string
src: string;
alt?: string;
caption?: string;
}
export interface QuestionAudio {
src: string
src: string;
}
export interface QuestionCode {
language: string
content: string
language: string;
content: string;
}
export interface QuestionTable {
headers: string[]
rows: string[][]
headers: string[];
rows: string[][];
}
export interface Question {
id: string
type: QuestionType
category?: string
points?: number
prompt: string
explanation?: string
image?: QuestionImage
audio?: QuestionAudio
code?: QuestionCode
table?: QuestionTable
svg?: string
answer?: string | string[] | Record<string, string>
acceptedForms?: string[]
caseSensitive?: boolean
ignorePunctuation?: boolean
ignoreAccents?: boolean
sourceLanguage?: string
targetLanguage?: string
choices?: Choice[]
minWords?: number
rubric?: string[]
left?: MatchItem[]
right?: MatchItem[]
items?: MatchItem[]
id: string;
type: QuestionType;
category?: string;
points?: number;
prompt: string;
explanation?: string;
image?: QuestionImage;
audio?: QuestionAudio;
code?: QuestionCode;
table?: QuestionTable;
svg?: string;
answer?: string | string[] | Record<string, string>;
acceptedForms?: string[];
caseSensitive?: boolean;
ignorePunctuation?: boolean;
ignoreAccents?: boolean;
sourceLanguage?: string;
targetLanguage?: string;
choices?: Choice[];
minWords?: number;
rubric?: string[];
left?: MatchItem[];
right?: MatchItem[];
items?: MatchItem[];
}
export interface Exam {
title: string
subtitle?: string
author?: string
timeLimitMinutes?: number | null
feedbackMode?: 'end' | 'immediate'
passingScore?: number
categories?: string[]
questions: Question[]
title: string;
subtitle?: string;
author?: string;
timeLimitMinutes?: number | null;
feedbackMode?: "end" | "immediate";
passingScore?: number;
categories?: string[];
questions: Question[];
}
export type FeedbackMode = 'immediate' | 'end'
export type FeedbackMode = "immediate" | "end";
export interface ExamState {
currentIdx: number
answers: Record<string, unknown>
flags: Record<string, boolean>
notes: string
revealed?: Record<string, boolean>
startedAt: number
completedAt?: number
feedbackMode: FeedbackMode
currentIdx: number;
answers: Record<string, unknown>;
flags: Record<string, boolean>;
notes: string;
revealed?: Record<string, boolean>;
startedAt: number;
completedAt?: number;
feedbackMode: FeedbackMode;
}
export interface HistoryEntry {
id: string
title: string
completedAt: number
elapsedMs: number
gradedCount: number
correctCount: number
totalPoints: number
scorePoints: number
passingScore: number
exam: Exam
state: ExamState
id: string;
title: string;
completedAt: number;
elapsedMs: number;
gradedCount: number;
correctCount: number;
totalPoints: number;
scorePoints: number;
passingScore: number;
exam: Exam;
state: ExamState;
}
export type GradeStatus = 'correct' | 'wrong' | 'ungraded' | 'unanswered'
export type GradeStatus = "correct" | "wrong" | "ungraded" | "unanswered";
export interface GradeResult {
status: GradeStatus
correct: boolean
points: number
status: GradeStatus;
correct: boolean;
points: number;
}
export type AppScreen = 'landing' | 'exam' | 'results' | 'review' | 'verify' | 'reset-password' | 'admin'
export type ThemeMode = 'light' | 'dark'
export type FontSize = 'small' | 'medium' | 'large'
export type FeedbackTweak = 'auto' | 'immediate' | 'end'
export type ThemeMode = "light" | "dark";
export type FontSize = "small" | "medium" | "large";
export type FeedbackTweak = "auto" | "immediate" | "end";
export interface Tweaks {
theme: ThemeMode
fontSize: FontSize
feedbackMode: FeedbackTweak
theme: ThemeMode;
fontSize: FontSize;
feedbackMode: FeedbackTweak;
}