Skip to content

Commit 0d6958e

Browse files
georgeglarsonclaude
andcommitted
Add Overview landing page; reorder tabs by increasing complexity
The app now opens to an Overview panel that explains the polyglot philosophy and lists all 6 exercises with language rationale and test counts. Exercise names link directly to their panels. Tab order follows complexity: Overview -> Cash Register -> Missing Number -> Morse Code -> On-Screen Keyboard -> Gilded Rose -> Restaurant Reviews. Restaurant Reviews is no longer the default — it's the most complex exercise, positioned last. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 261111a commit 0d6958e

7 files changed

Lines changed: 318 additions & 40 deletions

File tree

RestaurantReviews/src/client/App.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import React, { useState } from "react";
22
import type { TabId } from "./types/index.js";
33
import { TabNav } from "./components/TabNav.js";
4+
import { OverviewPanel } from "./components/OverviewPanel.js";
45
import { ReviewsPanel } from "./components/reviews/ReviewsPanel.js";
56
import { CashRegisterPanel } from "./components/exercises/CashRegisterPanel.js";
67
import { MissingNumberPanel } from "./components/exercises/MissingNumberPanel.js";
78
import { MorseCodePanel } from "./components/exercises/MorseCodePanel.js";
89
import { OnScreenKeyboardPanel } from "./components/exercises/OnScreenKeyboardPanel.js";
910
import { GildedRosePanel } from "./components/exercises/GildedRosePanel.js";
1011

11-
const PANELS: Record<TabId, React.ComponentType> = {
12-
reviews: ReviewsPanel,
12+
const PANELS: Record<Exclude<TabId, "overview">, React.ComponentType> = {
1313
"cash-register": CashRegisterPanel,
1414
"missing-number": MissingNumberPanel,
1515
"morse-code": MorseCodePanel,
1616
"on-screen-keyboard": OnScreenKeyboardPanel,
1717
"gilded-rose": GildedRosePanel,
18+
reviews: ReviewsPanel,
1819
};
1920

2021
export function App() {
21-
const [activeTab, setActiveTab] = useState<TabId>("reviews");
22-
const Panel = PANELS[activeTab];
22+
const [activeTab, setActiveTab] = useState<TabId>("overview");
2323

2424
return (
2525
<div style={styles.shell}>
@@ -43,7 +43,11 @@ export function App() {
4343
</header>
4444
<TabNav activeTab={activeTab} onTabChange={setActiveTab} />
4545
<main style={styles.main}>
46-
<Panel />
46+
{activeTab === "overview" ? (
47+
<OverviewPanel onNavigate={setActiveTab} />
48+
) : (
49+
React.createElement(PANELS[activeTab])
50+
)}
4751
</main>
4852
</div>
4953
);

RestaurantReviews/src/client/__tests__/App.test.tsx

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,52 +54,53 @@ describe("App", () => {
5454
it("renders tab navigation", () => {
5555
mockInitialLoads();
5656
render(<App />);
57-
expect(screen.getByText("Restaurant Reviews", { selector: "button" })).toBeInTheDocument();
58-
expect(screen.getByText("Cash Register", { selector: "button" })).toBeInTheDocument();
59-
expect(screen.getByText("Missing Number", { selector: "button" })).toBeInTheDocument();
60-
expect(screen.getByText("Morse Code", { selector: "button" })).toBeInTheDocument();
61-
expect(screen.getByText("On-Screen Keyboard", { selector: "button" })).toBeInTheDocument();
62-
expect(screen.getByText("Gilded Rose", { selector: "button" })).toBeInTheDocument();
57+
expect(screen.getByText("Overview", { selector: "button[role='tab']" })).toBeInTheDocument();
58+
expect(screen.getByText("Cash Register", { selector: "button[role='tab']" })).toBeInTheDocument();
59+
expect(screen.getByText("Missing Number", { selector: "button[role='tab']" })).toBeInTheDocument();
60+
expect(screen.getByText("Morse Code", { selector: "button[role='tab']" })).toBeInTheDocument();
61+
expect(screen.getByText("On-Screen Keyboard", { selector: "button[role='tab']" })).toBeInTheDocument();
62+
expect(screen.getByText("Gilded Rose", { selector: "button[role='tab']" })).toBeInTheDocument();
63+
expect(screen.getByText("Restaurant Reviews", { selector: "button[role='tab']" })).toBeInTheDocument();
6364
});
6465

65-
it("shows Restaurant Reviews panel by default", () => {
66+
it("shows Overview panel by default", () => {
6667
mockInitialLoads();
6768
render(<App />);
68-
expect(screen.getByText("Restaurant Reviews", { selector: "h2" })).toBeInTheDocument();
69+
expect(screen.getByText("Overview", { selector: "h2" })).toBeInTheDocument();
6970
});
7071

7172
it("switches to Cash Register panel", async () => {
7273
mockInitialLoads();
7374
render(<App />);
74-
fireEvent.click(screen.getByText("Cash Register", { selector: "button" }));
75+
fireEvent.click(screen.getByText("Cash Register", { selector: "button[role='tab']" }));
7576
expect(screen.getByText("Cash Register", { selector: "h2" })).toBeInTheDocument();
7677
});
7778

7879
it("switches to Missing Number panel", async () => {
7980
mockInitialLoads();
8081
render(<App />);
81-
fireEvent.click(screen.getByText("Missing Number", { selector: "button" }));
82+
fireEvent.click(screen.getByText("Missing Number", { selector: "button[role='tab']" }));
8283
expect(screen.getByText("Missing Number", { selector: "h2" })).toBeInTheDocument();
8384
});
8485

8586
it("switches to Morse Code panel", async () => {
8687
mockInitialLoads();
8788
render(<App />);
88-
fireEvent.click(screen.getByText("Morse Code", { selector: "button" }));
89+
fireEvent.click(screen.getByText("Morse Code", { selector: "button[role='tab']" }));
8990
expect(screen.getByText("Morse Code", { selector: "h2" })).toBeInTheDocument();
9091
});
9192

9293
it("switches to On-Screen Keyboard panel", async () => {
9394
mockInitialLoads();
9495
render(<App />);
95-
fireEvent.click(screen.getByText("On-Screen Keyboard", { selector: "button" }));
96+
fireEvent.click(screen.getByText("On-Screen Keyboard", { selector: "button[role='tab']" }));
9697
expect(screen.getByText("On-Screen Keyboard", { selector: "h2" })).toBeInTheDocument();
9798
});
9899

99100
it("switches to Gilded Rose panel", async () => {
100101
mockInitialLoads();
101102
render(<App />);
102-
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button" }));
103+
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button[role='tab']" }));
103104
expect(screen.getByText("Gilded Rose", { selector: "h2" })).toBeInTheDocument();
104105
});
105106
});
@@ -110,7 +111,7 @@ describe("CashRegisterPanel", () => {
110111
it("renders structured inputs and Make Change button", () => {
111112
mockInitialLoads();
112113
render(<App />);
113-
fireEvent.click(screen.getByText("Cash Register", { selector: "button" }));
114+
fireEvent.click(screen.getByText("Cash Register", { selector: "button[role='tab']" }));
114115
// Has dollar amount inputs and the action button
115116
expect(screen.getByText("Make Change")).toBeInTheDocument();
116117
expect(screen.getByText("+ Add")).toBeInTheDocument();
@@ -121,7 +122,7 @@ describe("CashRegisterPanel", () => {
121122
it("displays parsed change output on successful run", async () => {
122123
mockInitialLoads();
123124
render(<App />);
124-
fireEvent.click(screen.getByText("Cash Register", { selector: "button" }));
125+
fireEvent.click(screen.getByText("Cash Register", { selector: "button[role='tab']" }));
125126
mockFetch.mockReturnValueOnce(
126127
jsonResponse({
127128
output: "3 quarters,1 dime,3 pennies\n3 pennies\n1 dollar,1 quarter,4 dimes,2 pennies",
@@ -139,7 +140,7 @@ describe("CashRegisterPanel", () => {
139140
it("displays error on failure", async () => {
140141
mockInitialLoads();
141142
render(<App />);
142-
fireEvent.click(screen.getByText("Cash Register", { selector: "button" }));
143+
fireEvent.click(screen.getByText("Cash Register", { selector: "button[role='tab']" }));
143144
mockFetch.mockReturnValueOnce(jsonResponse({ error: "exercise failed" }, 422));
144145

145146
fireEvent.click(screen.getByText("Make Change"));
@@ -154,7 +155,7 @@ describe("MissingNumberPanel", () => {
154155
it("renders and runs", async () => {
155156
mockInitialLoads();
156157
render(<App />);
157-
fireEvent.click(screen.getByText("Missing Number", { selector: "button" }));
158+
fireEvent.click(screen.getByText("Missing Number", { selector: "button[role='tab']" }));
158159
mockFetch.mockReturnValueOnce(jsonResponse({ output: "3" }));
159160

160161
fireEvent.click(screen.getByText("Find It"));
@@ -169,15 +170,15 @@ describe("MorseCodePanel", () => {
169170
it("has Text to Morse / Morse to Text toggle", () => {
170171
mockInitialLoads();
171172
render(<App />);
172-
fireEvent.click(screen.getByText("Morse Code", { selector: "button" }));
173+
fireEvent.click(screen.getByText("Morse Code", { selector: "button[role='tab']" }));
173174
expect(screen.getByText("Text to Morse")).toBeInTheDocument();
174175
expect(screen.getByText("Morse to Text")).toBeInTheDocument();
175176
});
176177

177178
it("converts text to morse", async () => {
178179
mockInitialLoads();
179180
render(<App />);
180-
fireEvent.click(screen.getByText("Morse Code", { selector: "button" }));
181+
fireEvent.click(screen.getByText("Morse Code", { selector: "button[role='tab']" }));
181182
mockFetch.mockReturnValueOnce(jsonResponse({ output: ".... . .-.. .-.. ---" }));
182183

183184
const textarea = screen.getByPlaceholderText("HELLO WORLD");
@@ -192,7 +193,7 @@ describe("MorseCodePanel", () => {
192193
it("switches to decode mode", async () => {
193194
mockInitialLoads();
194195
render(<App />);
195-
fireEvent.click(screen.getByText("Morse Code", { selector: "button" }));
196+
fireEvent.click(screen.getByText("Morse Code", { selector: "button[role='tab']" }));
196197
fireEvent.click(screen.getByText("Morse to Text"));
197198
expect(screen.getByPlaceholderText(/\.\.\.\./)).toBeInTheDocument();
198199
});
@@ -202,7 +203,7 @@ describe("OnScreenKeyboardPanel", () => {
202203
it("renders and runs", async () => {
203204
mockInitialLoads();
204205
render(<App />);
205-
fireEvent.click(screen.getByText("On-Screen Keyboard", { selector: "button" }));
206+
fireEvent.click(screen.getByText("On-Screen Keyboard", { selector: "button[role='tab']" }));
206207
mockFetch.mockReturnValueOnce(jsonResponse({ output: "D,R,R,R,S,U,L,L,L,S" }));
207208

208209
fireEvent.click(screen.getByText("Spell It"));
@@ -217,14 +218,14 @@ describe("GildedRosePanel", () => {
217218
it("renders start simulation form", () => {
218219
mockInitialLoads();
219220
render(<App />);
220-
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button" }));
221+
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button[role='tab']" }));
221222
expect(screen.getByText("Start Simulation")).toBeInTheDocument();
222223
});
223224

224225
it("starts a session and shows day controls", async () => {
225226
mockInitialLoads();
226227
render(<App />);
227-
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button" }));
228+
fireEvent.click(screen.getByText("Gilded Rose", { selector: "button[role='tab']" }));
228229
mockFetch
229230
.mockReturnValueOnce(jsonResponse({ sessionId: "test-123", day: 0 }))
230231
.mockReturnValueOnce(
@@ -245,10 +246,16 @@ describe("GildedRosePanel", () => {
245246

246247
// ── Reviews Panel ────────────────────────────────────────────────────
247248

249+
/** Navigate from Overview to Restaurant Reviews tab */
250+
function goToReviews() {
251+
fireEvent.click(screen.getByText("Restaurant Reviews", { selector: "button[role='tab']" }));
252+
}
253+
248254
describe("ReviewsPanel", () => {
249255
it("renders user creation form", () => {
250256
mockInitialLoads();
251257
render(<App />);
258+
goToReviews();
252259
expect(screen.getAllByPlaceholderText("Name").length).toBeGreaterThanOrEqual(1);
253260
expect(screen.getByPlaceholderText("Email")).toBeInTheDocument();
254261
expect(screen.getByText("Create User")).toBeInTheDocument();
@@ -257,6 +264,7 @@ describe("ReviewsPanel", () => {
257264
it("renders restaurant creation form", () => {
258265
mockInitialLoads();
259266
render(<App />);
267+
goToReviews();
260268
expect(screen.getByPlaceholderText("City")).toBeInTheDocument();
261269
expect(screen.getByPlaceholderText("Cuisine (optional)")).toBeInTheDocument();
262270
});
@@ -271,6 +279,7 @@ describe("ReviewsPanel", () => {
271279
};
272280
mockInitialLoads();
273281
render(<App />);
282+
goToReviews();
274283

275284
// Wait for initial load
276285
await waitFor(() => {
@@ -298,6 +307,7 @@ describe("ReviewsPanel", () => {
298307
it("displays error when user creation fails", async () => {
299308
mockInitialLoads();
300309
render(<App />);
310+
goToReviews();
301311

302312
await waitFor(() => {
303313
expect(mockFetch).toHaveBeenCalled();
@@ -330,6 +340,7 @@ describe("ReviewsPanel", () => {
330340
};
331341
mockInitialLoads();
332342
render(<App />);
343+
goToReviews();
333344

334345
await waitFor(() => {
335346
expect(mockFetch).toHaveBeenCalled();
@@ -393,6 +404,7 @@ describe("ReviewsPanel", () => {
393404
]
394405
);
395406
render(<App />);
407+
goToReviews();
396408

397409
await waitFor(() => {
398410
// Alice appears in user table + review table
@@ -421,6 +433,7 @@ describe("ReviewsPanel", () => {
421433
]
422434
);
423435
render(<App />);
436+
goToReviews();
424437

425438
await waitFor(() => {
426439
expect(screen.getByText("Bob")).toBeInTheDocument();
@@ -449,6 +462,7 @@ describe("ReviewsPanel", () => {
449462
]
450463
);
451464
render(<App />);
465+
goToReviews();
452466

453467
await waitFor(() => {
454468
expect(screen.getByText("Place A")).toBeInTheDocument();

0 commit comments

Comments
 (0)