Skip to content

Commit 1e5b157

Browse files
dsshimelclaude
andcommitted
add URL-based tab routing for all tabbed views and fold auth simulator into SPA
Every tab now has its own URL (/instructor/:tab, /student/:tab, /simulations/:kind/:flowId), enabling bookmarking, refresh persistence, and browser back/forward navigation. The auth simulator is no longer a separate Vite MPA entry point — it's rendered inside the main React Router app behind the auth wall. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ba5b89c commit 1e5b157

File tree

11 files changed

+84
-101
lines changed

11 files changed

+84
-101
lines changed

attendabot/backend/src/api/index.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,10 @@ if (process.env.NODE_ENV === "production") {
7272

7373
app.use(express.static(frontendPath));
7474

75-
// Fallback: serve the correct index.html based on path
75+
// Fallback: serve index.html for all non-API routes (SPA routing)
7676
app.get("*", (req, res) => {
7777
if (req.path.startsWith("/api")) return;
78-
if (req.path.startsWith("/simulations/auth")) {
79-
res.sendFile(path.join(frontendPath, "simulations/auth/index.html"));
80-
} else if (req.path.startsWith("/simulations")) {
81-
res.sendFile(path.join(frontendPath, "simulations/auth/index.html"));
82-
} else {
83-
res.sendFile(path.join(frontendPath, "index.html"));
84-
}
78+
res.sendFile(path.join(frontendPath, "index.html"));
8579
});
8680
}
8781

attendabot/frontend/simulations/auth/index.html

Lines changed: 0 additions & 13 deletions
This file was deleted.

attendabot/frontend/src/components/SimulationsHub.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
* @fileoverview Displays available simulations as cards with links.
33
*/
44

5+
import { useNavigate } from "react-router";
6+
57
const simulations = [
68
{
79
id: "auth",
810
title: "Auth Flow Simulator",
911
description: "Interactive animations of common authentication systems",
10-
url: "/simulations/auth/",
12+
url: "/simulations/auth",
1113
},
1214
];
1315

1416
/** Renders a grid of available simulation cards. */
1517
export function SimulationsHub() {
18+
const navigate = useNavigate();
19+
1620
return (
1721
<div className="panel" style={{ gridColumn: "span 2" }}>
1822
<h2>Simulations</h2>
@@ -45,7 +49,7 @@ export function SimulationsHub() {
4549
</p>
4650
<button
4751
className="primary-btn"
48-
onClick={() => window.open(sim.url, "_blank")}
52+
onClick={() => navigate(sim.url)}
4953
>
5054
Open
5155
</button>

attendabot/frontend/src/components/StudentPortal.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
*/
55

66
import { useState } from "react";
7+
import { useLocation, useNavigate } from "react-router";
78
import { MyEods } from "./MyEods";
89
import { StudentStats } from "./StudentStats";
910
import { SimulationsHub } from "./SimulationsHub";
1011

1112
type StudentTab = "stats" | "eods" | "simulations";
13+
const VALID_TABS: StudentTab[] = ["stats", "eods", "simulations"];
1214

1315
interface StudentPortalProps {
1416
username: string | null;
@@ -21,8 +23,21 @@ interface StudentPortalProps {
2123

2224
/** Student portal with tabs for Stats, My EODs, and Simulations. */
2325
export function StudentPortal({ username, sessionInvalid, onLogout, studentDiscordId, cohortStartDate, cohortEndDate }: StudentPortalProps) {
24-
const [activeTab, setActiveTab] = useState<StudentTab>("stats");
2526
const embedded = !!studentDiscordId;
27+
const location = useLocation();
28+
const navigate = useNavigate();
29+
30+
// When embedded (instructor impersonation), use local state since URL is /instructor/*
31+
const [embeddedTab, setEmbeddedTab] = useState<StudentTab>("stats");
32+
33+
// When standalone, derive tab from URL
34+
const segment = location.pathname.split("/")[2] ?? "";
35+
const urlTab: StudentTab = VALID_TABS.includes(segment as StudentTab) ? (segment as StudentTab) : "stats";
36+
37+
const activeTab = embedded ? embeddedTab : urlTab;
38+
const setActiveTab = embedded
39+
? setEmbeddedTab
40+
: (tab: StudentTab) => navigate(`/student/${tab}`);
2641

2742
const content = (
2843
<>

attendabot/frontend/src/main.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RootLayout } from './components/RootLayout'
88
import { LoginPage } from './pages/LoginPage'
99
import { StudentPage } from './pages/StudentPage'
1010
import { InstructorPage } from './pages/InstructorPage'
11+
import { SimulatorPage } from './pages/SimulatorPage'
1112

1213
createRoot(document.getElementById('root')!).render(
1314
<StrictMode>
@@ -16,8 +17,9 @@ createRoot(document.getElementById('root')!).render(
1617
<Routes>
1718
<Route element={<RootLayout />}>
1819
<Route path="/" element={<LoginPage />} />
19-
<Route path="/student" element={<StudentPage />} />
20-
<Route path="/instructor" element={<InstructorPage />} />
20+
<Route path="/student/*" element={<StudentPage />} />
21+
<Route path="/instructor/*" element={<InstructorPage />} />
22+
<Route path="/simulations/:kind/:flowId?" element={<SimulatorPage />} />
2123
</Route>
2224
</Routes>
2325
</AuthProvider>

attendabot/frontend/src/pages/InstructorPage.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { useState, useEffect } from "react";
7+
import { useLocation, useNavigate } from "react-router";
78
import { getCohorts, getStudentsByCohort } from "../api/client";
89
import type { Student, Cohort } from "../api/client";
910
import { MessageFeed } from "../components/MessageFeed";
@@ -17,11 +18,16 @@ import { SimulationsHub } from "../components/SimulationsHub";
1718
import { useAuth } from "../hooks/useAuth";
1819

1920
type Tab = "students" | "simulations" | "observers" | "messages" | "testing" | "diagnostics";
21+
const VALID_TABS: Tab[] = ["students", "simulations", "observers", "messages", "testing", "diagnostics"];
2022

2123
/** Instructor admin dashboard with tabs and impersonation. */
2224
export function InstructorPage() {
2325
const { username, sessionInvalid, logout } = useAuth();
24-
const [activeTab, setActiveTab] = useState<Tab>("students");
26+
const location = useLocation();
27+
const navigate = useNavigate();
28+
29+
const segment = location.pathname.split("/")[2] ?? "";
30+
const activeTab: Tab = VALID_TABS.includes(segment as Tab) ? (segment as Tab) : "students";
2531
const [impersonating, setImpersonating] = useState<{ discordId: string; name: string; cohortId: number } | null>(null);
2632
const [impersonateStudents, setImpersonateStudents] = useState<{ discordId: string; name: string; cohortId: number }[]>([]);
2733
const [cohorts, setCohorts] = useState<Cohort[]>([]);
@@ -115,37 +121,37 @@ export function InstructorPage() {
115121
<nav className="tab-navigation">
116122
<button
117123
className={`tab-btn ${activeTab === "students" ? "active" : ""}`}
118-
onClick={() => setActiveTab("students")}
124+
onClick={() => navigate("/instructor/students")}
119125
>
120126
Students
121127
</button>
122128
<button
123129
className={`tab-btn ${activeTab === "simulations" ? "active" : ""}`}
124-
onClick={() => setActiveTab("simulations")}
130+
onClick={() => navigate("/instructor/simulations")}
125131
>
126132
Simulations
127133
</button>
128134
<button
129135
className={`tab-btn ${activeTab === "observers" ? "active" : ""}`}
130-
onClick={() => setActiveTab("observers")}
136+
onClick={() => navigate("/instructor/observers")}
131137
>
132138
Observers
133139
</button>
134140
<button
135141
className={`tab-btn ${activeTab === "messages" ? "active" : ""}`}
136-
onClick={() => setActiveTab("messages")}
142+
onClick={() => navigate("/instructor/messages")}
137143
>
138144
Messages
139145
</button>
140146
<button
141147
className={`tab-btn ${activeTab === "testing" ? "active" : ""}`}
142-
onClick={() => setActiveTab("testing")}
148+
onClick={() => navigate("/instructor/testing")}
143149
>
144150
Testing
145151
</button>
146152
<button
147153
className={`tab-btn ${activeTab === "diagnostics" ? "active" : ""}`}
148-
onClick={() => setActiveTab("diagnostics")}
154+
onClick={() => navigate("/instructor/diagnostics")}
149155
>
150156
Configuration
151157
</button>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @fileoverview Simulator page route component.
3+
* Reads URL params to determine which simulation and flow to display.
4+
*/
5+
6+
import { useParams, useNavigate } from "react-router";
7+
import SimulatorApp from "../simulations/SimulatorApp";
8+
import { flows } from "../simulations/flowData";
9+
10+
/** Thin wrapper that maps URL params to SimulatorApp props. */
11+
export function SimulatorPage() {
12+
const { kind, flowId } = useParams();
13+
const navigate = useNavigate();
14+
15+
if (kind === "auth") {
16+
const flowIndex = flowId ? flows.findIndex((f) => f.id === flowId) : 0;
17+
const activeTab = flowIndex >= 0 ? flowIndex : 0;
18+
19+
return (
20+
<div className="app">
21+
<SimulatorApp
22+
activeTab={activeTab}
23+
onTabChange={(i) => navigate(`/simulations/auth/${flows[i].id}`)}
24+
/>
25+
</div>
26+
);
27+
}
28+
29+
return (
30+
<div className="app">
31+
<p>Unknown simulation: {kind}</p>
32+
</div>
33+
);
34+
}

attendabot/frontend/src/simulations/SimulatorApp.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
import { useState } from "react";
21
import FlowDiagram from "./FlowDiagram";
32
import { flows } from "./flowData";
3+
import "./simulations.css";
44

5-
export default function SimulatorApp() {
6-
const [activeTab, setActiveTab] = useState(0);
5+
interface SimulatorAppProps {
6+
activeTab: number;
7+
onTabChange: (index: number) => void;
8+
}
79

10+
export default function SimulatorApp({ activeTab, onTabChange }: SimulatorAppProps) {
811
return (
912
<>
1013
<h1 className="app-title">Auth Flow Simulator</h1>
1114
<p className="app-subtitle">
1215
Interactive animations of common authentication systems
1316
</p>
1417

15-
<div className="tab-bar">
18+
<nav className="tab-navigation">
1619
{flows.map((flow, i) => (
1720
<button
1821
key={flow.id}
1922
className={`tab-btn ${i === activeTab ? "active" : ""}`}
20-
onClick={() => setActiveTab(i)}
23+
onClick={() => onTabChange(i)}
2124
>
2225
{flow.title}
2326
</button>
2427
))}
25-
</div>
28+
</nav>
2629

2730
<FlowDiagram flow={flows[activeTab]} />
2831
</>

attendabot/frontend/src/simulations/main.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

attendabot/frontend/src/simulations/simulations.css

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
/* ── Simulations page – Fractal design system ── */
22

3-
* {
4-
margin: 0;
5-
padding: 0;
6-
box-sizing: border-box;
7-
}
8-
9-
body {
10-
min-height: 100vh;
11-
}
12-
13-
#root {
14-
max-width: 1400px;
15-
margin: 0 auto;
16-
padding: var(--space-5) var(--space-6);
17-
}
18-
193
/* ── Header ── */
204
.app-title {
215
font-size: var(--text-2xl);
@@ -30,40 +14,6 @@ body {
3014
margin-bottom: var(--space-5);
3115
}
3216

33-
/* ── Tabs ── */
34-
.tab-bar {
35-
display: flex;
36-
gap: 0;
37-
margin-bottom: var(--space-6);
38-
border: 2px solid var(--color-charcoal);
39-
}
40-
.tab-btn {
41-
background: var(--color-white);
42-
border: none;
43-
border-right: 2px solid var(--color-charcoal);
44-
box-shadow: none;
45-
color: var(--color-slate);
46-
padding: var(--space-3) var(--space-5);
47-
cursor: pointer;
48-
font-size: var(--text-sm);
49-
font-weight: 700;
50-
text-transform: uppercase;
51-
letter-spacing: 0.05em;
52-
font-family: inherit;
53-
transition: background 0.15s, color 0.15s;
54-
}
55-
.tab-btn:last-child {
56-
border-right: none;
57-
}
58-
.tab-btn:hover {
59-
color: var(--color-charcoal);
60-
background: var(--color-platinum);
61-
}
62-
.tab-btn.active {
63-
color: var(--color-white);
64-
background: var(--color-charcoal);
65-
}
66-
6717
/* ── Flow Diagram ── */
6818
.flow-diagram {
6919
max-width: 100%;

0 commit comments

Comments
 (0)