├── .gitignore
├── .prettierignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── app.config.ts
├── app
├── api.ts
├── client.tsx
├── components
│ ├── DefaultCatchBoundary.tsx
│ └── NotFound.tsx
├── routeTree.gen.ts
├── router.tsx
├── routes
│ ├── __root.tsx
│ ├── calculator.tsx
│ ├── connect-four.tsx
│ ├── dice.tsx
│ ├── expense.tsx
│ ├── gradient.tsx
│ ├── hangman.tsx
│ ├── hanoi.tsx
│ ├── histogram.tsx
│ ├── index.tsx
│ ├── memory.tsx
│ ├── password.tsx
│ ├── quiz.tsx
│ ├── quote.tsx
│ ├── rock-paper-scissors.tsx
│ ├── simon.tsx
│ ├── speed.tsx
│ ├── split.tsx
│ ├── stopwatch.tsx
│ ├── tic-tac-toe.tsx
│ ├── traffic-light.tsx
│ ├── tree.tsx
│ └── whack-a-mole.tsx
├── ssr.tsx
├── styles
│ └── app.css
└── utils
│ ├── loggingMiddleware.tsx
│ ├── posts.tsx
│ ├── seo.ts
│ └── users.tsx
├── package.json
├── postcss.config.mjs
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── favicon.png
└── site.webmanifest
├── tailwind.config.mjs
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | yarn.lock
4 |
5 | .DS_Store
6 | .cache
7 | .env
8 | .vercel
9 | .output
10 | .vinxi
11 |
12 | /build/
13 | /api/
14 | /server/build
15 | /public/build
16 | .vinxi
17 | # Sentry Config File
18 | .env.sentry-build-plugin
19 | /test-results/
20 | /playwright-report/
21 | /blob-report/
22 | /playwright/.cache/
23 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/build
2 | **/public
3 | pnpm-lock.yaml
4 | routeTree.gen.ts
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.watcherExclude": {
3 | "**/routeTree.gen.ts": true
4 | },
5 | "search.exclude": {
6 | "**/routeTree.gen.ts": true
7 | },
8 | "files.readonlyInclude": {
9 | "**/routeTree.gen.ts": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Web Dev Cody
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ready to Challenge Yourself?
2 |
3 | Welcome to the codebase for the interactive examples! This code base has 20 different React examples you can use to practice your react skills for personal learning or interview practice. Most of these challenges are geared towards beginners and should be easy to solve within 15-30 minutes. Some of more intermediate and might take longer to solve. If you enjoyed checking out the code for these challenges, checkout the paid course I created with over 6 hours of walkthrough videos explaining how to solve these challenges:
4 |
5 | [https://webdevcody.gumroad.com/l/beginner-react-challenges-collection](https://webdevcody.gumroad.com/l/beginner-react-challenges-collection)
6 |
7 | # Challenges
8 |
9 | - [Calculator](app/routes/calculator.tsx) 🔢 - Build a functional calculator with basic operations
10 | - [Connect Four](app/routes/connect-four.tsx) 🔴 - Create the classic two-player connection game
11 | - [Dice](app/routes/dice.tsx) 🎲 - Build a simple dice rolling simulator
12 | - [Expense Tracker](app/routes/expense.tsx) 💰 - Create a basic expense tracking application
13 | - [Gradient Generator](app/routes/gradient.tsx) 🎨 - Build a customizable gradient generator
14 | - [Hangman](app/routes/hangman.tsx) 🎯 - Implement the classic word guessing game
15 | - [Tower of Hanoi](app/routes/hanoi.tsx) 🗼 - Implement the classic Tower of Hanoi puzzle game
16 | - [Histogram](app/routes/histogram.tsx) 📊 - Create a dynamic bar chart visualization
17 | - [Memory Game](app/routes/memory.tsx) 🎴 - Build a card-matching memory game
18 | - [Quiz](app/routes/quiz.tsx) ❓ - Build an interactive quiz with scoring
19 | - [Quote Generator](app/routes/quote.tsx) 💭 - Create a random quote generator
20 | - [Rock Paper Scissors](app/routes/rock-paper-scissors.tsx) ✂️ - Build the classic hand game
21 | - [Simon Says](app/routes/simon.tsx) 🎮 - Create the classic memory skill game
22 | - [Speed Typing](app/routes/speed.tsx) ⌨️ - Build a typing speed test application
23 | - [Split View](app/routes/split.tsx) ↔️ - Build a resizable split pane interface
24 | - [Stopwatch](app/routes/stopwatch.tsx) ⏱️ - Create a timer with start, stop, and reset
25 | - [Tic Tac Toe](app/routes/tic-tac-toe.tsx) ⭕ - Implement the classic game with win detection
26 | - [Traffic Light](app/routes/traffic-light.tsx) 🚦 - Create an animated traffic light simulator
27 | - [Tree Visualization](app/routes/tree.tsx) 🌳 - Render a hierarchical tree structure
28 | - [Whack-a-Mole](app/routes/whack-a-mole.tsx) 🔨 - Build the classic arcade game
29 |
30 | # How to Run
31 |
32 | These examples are built using [TanStack Router](https://tanstack.com/router) and React, providing a modern, type-safe routing solution. Running the examples locally allows you to:
33 |
34 | - Explore the code implementation of each challenge
35 | - Experiment with modifications and improvements
36 | - Debug and understand React patterns in practice
37 | - Test your solutions against working examples
38 |
39 | To run the examples locally, first clone this repository, then follow these steps:
40 |
41 | 1. `npm i`
42 | 2. `npm run dev`
43 | 3. `open http://localhost:3000`
44 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@tanstack/start/config'
2 | import tsConfigPaths from 'vite-tsconfig-paths'
3 |
4 | export default defineConfig({
5 | vite: {
6 | plugins: [
7 | tsConfigPaths({
8 | projects: ['./tsconfig.json'],
9 | }),
10 | ],
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/app/api.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createStartAPIHandler,
3 | defaultAPIFileRouteHandler,
4 | } from '@tanstack/start/api'
5 |
6 | export default createStartAPIHandler(defaultAPIFileRouteHandler)
7 |
--------------------------------------------------------------------------------
/app/client.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import { hydrateRoot } from 'react-dom/client'
3 | import { StartClient } from '@tanstack/start'
4 | import { createRouter } from './router'
5 |
6 | const router = createRouter()
7 |
8 | hydrateRoot(document, )
9 |
--------------------------------------------------------------------------------
/app/components/DefaultCatchBoundary.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ErrorComponent,
3 | Link,
4 | rootRouteId,
5 | useMatch,
6 | useRouter,
7 | } from '@tanstack/react-router'
8 | import type { ErrorComponentProps } from '@tanstack/react-router'
9 |
10 | export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
11 | const router = useRouter()
12 | const isRoot = useMatch({
13 | strict: false,
14 | select: (state) => state.id === rootRouteId,
15 | })
16 |
17 | console.error('DefaultCatchBoundary Error:', error)
18 |
19 | return (
20 |
21 |
22 |
23 |
31 | {isRoot ? (
32 |
36 | Home
37 |
38 | ) : (
39 | {
43 | e.preventDefault()
44 | window.history.back()
45 | }}
46 | >
47 | Go Back
48 |
49 | )}
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@tanstack/react-router'
2 |
3 | export function NotFound({ children }: { children?: any }) {
4 | return (
5 |
6 |
7 | {children ||
The page you are looking for does not exist.
}
8 |
9 |
10 |
16 |
20 | Start Over
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/app/routeTree.gen.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // @ts-nocheck
4 |
5 | // noinspection JSUnusedGlobalSymbols
6 |
7 | // This file was automatically generated by TanStack Router.
8 | // You should NOT make any changes in this file as it will be overwritten.
9 | // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10 |
11 | // Import Routes
12 |
13 | import { Route as rootRoute } from './routes/__root'
14 | import { Route as WhackAMoleImport } from './routes/whack-a-mole'
15 | import { Route as TreeImport } from './routes/tree'
16 | import { Route as TrafficLightImport } from './routes/traffic-light'
17 | import { Route as TicTacToeImport } from './routes/tic-tac-toe'
18 | import { Route as StopwatchImport } from './routes/stopwatch'
19 | import { Route as SplitImport } from './routes/split'
20 | import { Route as SpeedImport } from './routes/speed'
21 | import { Route as SimonImport } from './routes/simon'
22 | import { Route as RockPaperScissorsImport } from './routes/rock-paper-scissors'
23 | import { Route as QuoteImport } from './routes/quote'
24 | import { Route as QuizImport } from './routes/quiz'
25 | import { Route as PasswordImport } from './routes/password'
26 | import { Route as MemoryImport } from './routes/memory'
27 | import { Route as HistogramImport } from './routes/histogram'
28 | import { Route as HanoiImport } from './routes/hanoi'
29 | import { Route as HangmanImport } from './routes/hangman'
30 | import { Route as GradientImport } from './routes/gradient'
31 | import { Route as ExpenseImport } from './routes/expense'
32 | import { Route as DiceImport } from './routes/dice'
33 | import { Route as ConnectFourImport } from './routes/connect-four'
34 | import { Route as CalculatorImport } from './routes/calculator'
35 | import { Route as IndexImport } from './routes/index'
36 |
37 | // Create/Update Routes
38 |
39 | const WhackAMoleRoute = WhackAMoleImport.update({
40 | id: '/whack-a-mole',
41 | path: '/whack-a-mole',
42 | getParentRoute: () => rootRoute,
43 | } as any)
44 |
45 | const TreeRoute = TreeImport.update({
46 | id: '/tree',
47 | path: '/tree',
48 | getParentRoute: () => rootRoute,
49 | } as any)
50 |
51 | const TrafficLightRoute = TrafficLightImport.update({
52 | id: '/traffic-light',
53 | path: '/traffic-light',
54 | getParentRoute: () => rootRoute,
55 | } as any)
56 |
57 | const TicTacToeRoute = TicTacToeImport.update({
58 | id: '/tic-tac-toe',
59 | path: '/tic-tac-toe',
60 | getParentRoute: () => rootRoute,
61 | } as any)
62 |
63 | const StopwatchRoute = StopwatchImport.update({
64 | id: '/stopwatch',
65 | path: '/stopwatch',
66 | getParentRoute: () => rootRoute,
67 | } as any)
68 |
69 | const SplitRoute = SplitImport.update({
70 | id: '/split',
71 | path: '/split',
72 | getParentRoute: () => rootRoute,
73 | } as any)
74 |
75 | const SpeedRoute = SpeedImport.update({
76 | id: '/speed',
77 | path: '/speed',
78 | getParentRoute: () => rootRoute,
79 | } as any)
80 |
81 | const SimonRoute = SimonImport.update({
82 | id: '/simon',
83 | path: '/simon',
84 | getParentRoute: () => rootRoute,
85 | } as any)
86 |
87 | const RockPaperScissorsRoute = RockPaperScissorsImport.update({
88 | id: '/rock-paper-scissors',
89 | path: '/rock-paper-scissors',
90 | getParentRoute: () => rootRoute,
91 | } as any)
92 |
93 | const QuoteRoute = QuoteImport.update({
94 | id: '/quote',
95 | path: '/quote',
96 | getParentRoute: () => rootRoute,
97 | } as any)
98 |
99 | const QuizRoute = QuizImport.update({
100 | id: '/quiz',
101 | path: '/quiz',
102 | getParentRoute: () => rootRoute,
103 | } as any)
104 |
105 | const PasswordRoute = PasswordImport.update({
106 | id: '/password',
107 | path: '/password',
108 | getParentRoute: () => rootRoute,
109 | } as any)
110 |
111 | const MemoryRoute = MemoryImport.update({
112 | id: '/memory',
113 | path: '/memory',
114 | getParentRoute: () => rootRoute,
115 | } as any)
116 |
117 | const HistogramRoute = HistogramImport.update({
118 | id: '/histogram',
119 | path: '/histogram',
120 | getParentRoute: () => rootRoute,
121 | } as any)
122 |
123 | const HanoiRoute = HanoiImport.update({
124 | id: '/hanoi',
125 | path: '/hanoi',
126 | getParentRoute: () => rootRoute,
127 | } as any)
128 |
129 | const HangmanRoute = HangmanImport.update({
130 | id: '/hangman',
131 | path: '/hangman',
132 | getParentRoute: () => rootRoute,
133 | } as any)
134 |
135 | const GradientRoute = GradientImport.update({
136 | id: '/gradient',
137 | path: '/gradient',
138 | getParentRoute: () => rootRoute,
139 | } as any)
140 |
141 | const ExpenseRoute = ExpenseImport.update({
142 | id: '/expense',
143 | path: '/expense',
144 | getParentRoute: () => rootRoute,
145 | } as any)
146 |
147 | const DiceRoute = DiceImport.update({
148 | id: '/dice',
149 | path: '/dice',
150 | getParentRoute: () => rootRoute,
151 | } as any)
152 |
153 | const ConnectFourRoute = ConnectFourImport.update({
154 | id: '/connect-four',
155 | path: '/connect-four',
156 | getParentRoute: () => rootRoute,
157 | } as any)
158 |
159 | const CalculatorRoute = CalculatorImport.update({
160 | id: '/calculator',
161 | path: '/calculator',
162 | getParentRoute: () => rootRoute,
163 | } as any)
164 |
165 | const IndexRoute = IndexImport.update({
166 | id: '/',
167 | path: '/',
168 | getParentRoute: () => rootRoute,
169 | } as any)
170 |
171 | // Populate the FileRoutesByPath interface
172 |
173 | declare module '@tanstack/react-router' {
174 | interface FileRoutesByPath {
175 | '/': {
176 | id: '/'
177 | path: '/'
178 | fullPath: '/'
179 | preLoaderRoute: typeof IndexImport
180 | parentRoute: typeof rootRoute
181 | }
182 | '/calculator': {
183 | id: '/calculator'
184 | path: '/calculator'
185 | fullPath: '/calculator'
186 | preLoaderRoute: typeof CalculatorImport
187 | parentRoute: typeof rootRoute
188 | }
189 | '/connect-four': {
190 | id: '/connect-four'
191 | path: '/connect-four'
192 | fullPath: '/connect-four'
193 | preLoaderRoute: typeof ConnectFourImport
194 | parentRoute: typeof rootRoute
195 | }
196 | '/dice': {
197 | id: '/dice'
198 | path: '/dice'
199 | fullPath: '/dice'
200 | preLoaderRoute: typeof DiceImport
201 | parentRoute: typeof rootRoute
202 | }
203 | '/expense': {
204 | id: '/expense'
205 | path: '/expense'
206 | fullPath: '/expense'
207 | preLoaderRoute: typeof ExpenseImport
208 | parentRoute: typeof rootRoute
209 | }
210 | '/gradient': {
211 | id: '/gradient'
212 | path: '/gradient'
213 | fullPath: '/gradient'
214 | preLoaderRoute: typeof GradientImport
215 | parentRoute: typeof rootRoute
216 | }
217 | '/hangman': {
218 | id: '/hangman'
219 | path: '/hangman'
220 | fullPath: '/hangman'
221 | preLoaderRoute: typeof HangmanImport
222 | parentRoute: typeof rootRoute
223 | }
224 | '/hanoi': {
225 | id: '/hanoi'
226 | path: '/hanoi'
227 | fullPath: '/hanoi'
228 | preLoaderRoute: typeof HanoiImport
229 | parentRoute: typeof rootRoute
230 | }
231 | '/histogram': {
232 | id: '/histogram'
233 | path: '/histogram'
234 | fullPath: '/histogram'
235 | preLoaderRoute: typeof HistogramImport
236 | parentRoute: typeof rootRoute
237 | }
238 | '/memory': {
239 | id: '/memory'
240 | path: '/memory'
241 | fullPath: '/memory'
242 | preLoaderRoute: typeof MemoryImport
243 | parentRoute: typeof rootRoute
244 | }
245 | '/password': {
246 | id: '/password'
247 | path: '/password'
248 | fullPath: '/password'
249 | preLoaderRoute: typeof PasswordImport
250 | parentRoute: typeof rootRoute
251 | }
252 | '/quiz': {
253 | id: '/quiz'
254 | path: '/quiz'
255 | fullPath: '/quiz'
256 | preLoaderRoute: typeof QuizImport
257 | parentRoute: typeof rootRoute
258 | }
259 | '/quote': {
260 | id: '/quote'
261 | path: '/quote'
262 | fullPath: '/quote'
263 | preLoaderRoute: typeof QuoteImport
264 | parentRoute: typeof rootRoute
265 | }
266 | '/rock-paper-scissors': {
267 | id: '/rock-paper-scissors'
268 | path: '/rock-paper-scissors'
269 | fullPath: '/rock-paper-scissors'
270 | preLoaderRoute: typeof RockPaperScissorsImport
271 | parentRoute: typeof rootRoute
272 | }
273 | '/simon': {
274 | id: '/simon'
275 | path: '/simon'
276 | fullPath: '/simon'
277 | preLoaderRoute: typeof SimonImport
278 | parentRoute: typeof rootRoute
279 | }
280 | '/speed': {
281 | id: '/speed'
282 | path: '/speed'
283 | fullPath: '/speed'
284 | preLoaderRoute: typeof SpeedImport
285 | parentRoute: typeof rootRoute
286 | }
287 | '/split': {
288 | id: '/split'
289 | path: '/split'
290 | fullPath: '/split'
291 | preLoaderRoute: typeof SplitImport
292 | parentRoute: typeof rootRoute
293 | }
294 | '/stopwatch': {
295 | id: '/stopwatch'
296 | path: '/stopwatch'
297 | fullPath: '/stopwatch'
298 | preLoaderRoute: typeof StopwatchImport
299 | parentRoute: typeof rootRoute
300 | }
301 | '/tic-tac-toe': {
302 | id: '/tic-tac-toe'
303 | path: '/tic-tac-toe'
304 | fullPath: '/tic-tac-toe'
305 | preLoaderRoute: typeof TicTacToeImport
306 | parentRoute: typeof rootRoute
307 | }
308 | '/traffic-light': {
309 | id: '/traffic-light'
310 | path: '/traffic-light'
311 | fullPath: '/traffic-light'
312 | preLoaderRoute: typeof TrafficLightImport
313 | parentRoute: typeof rootRoute
314 | }
315 | '/tree': {
316 | id: '/tree'
317 | path: '/tree'
318 | fullPath: '/tree'
319 | preLoaderRoute: typeof TreeImport
320 | parentRoute: typeof rootRoute
321 | }
322 | '/whack-a-mole': {
323 | id: '/whack-a-mole'
324 | path: '/whack-a-mole'
325 | fullPath: '/whack-a-mole'
326 | preLoaderRoute: typeof WhackAMoleImport
327 | parentRoute: typeof rootRoute
328 | }
329 | }
330 | }
331 |
332 | // Create and export the route tree
333 |
334 | export interface FileRoutesByFullPath {
335 | '/': typeof IndexRoute
336 | '/calculator': typeof CalculatorRoute
337 | '/connect-four': typeof ConnectFourRoute
338 | '/dice': typeof DiceRoute
339 | '/expense': typeof ExpenseRoute
340 | '/gradient': typeof GradientRoute
341 | '/hangman': typeof HangmanRoute
342 | '/hanoi': typeof HanoiRoute
343 | '/histogram': typeof HistogramRoute
344 | '/memory': typeof MemoryRoute
345 | '/password': typeof PasswordRoute
346 | '/quiz': typeof QuizRoute
347 | '/quote': typeof QuoteRoute
348 | '/rock-paper-scissors': typeof RockPaperScissorsRoute
349 | '/simon': typeof SimonRoute
350 | '/speed': typeof SpeedRoute
351 | '/split': typeof SplitRoute
352 | '/stopwatch': typeof StopwatchRoute
353 | '/tic-tac-toe': typeof TicTacToeRoute
354 | '/traffic-light': typeof TrafficLightRoute
355 | '/tree': typeof TreeRoute
356 | '/whack-a-mole': typeof WhackAMoleRoute
357 | }
358 |
359 | export interface FileRoutesByTo {
360 | '/': typeof IndexRoute
361 | '/calculator': typeof CalculatorRoute
362 | '/connect-four': typeof ConnectFourRoute
363 | '/dice': typeof DiceRoute
364 | '/expense': typeof ExpenseRoute
365 | '/gradient': typeof GradientRoute
366 | '/hangman': typeof HangmanRoute
367 | '/hanoi': typeof HanoiRoute
368 | '/histogram': typeof HistogramRoute
369 | '/memory': typeof MemoryRoute
370 | '/password': typeof PasswordRoute
371 | '/quiz': typeof QuizRoute
372 | '/quote': typeof QuoteRoute
373 | '/rock-paper-scissors': typeof RockPaperScissorsRoute
374 | '/simon': typeof SimonRoute
375 | '/speed': typeof SpeedRoute
376 | '/split': typeof SplitRoute
377 | '/stopwatch': typeof StopwatchRoute
378 | '/tic-tac-toe': typeof TicTacToeRoute
379 | '/traffic-light': typeof TrafficLightRoute
380 | '/tree': typeof TreeRoute
381 | '/whack-a-mole': typeof WhackAMoleRoute
382 | }
383 |
384 | export interface FileRoutesById {
385 | __root__: typeof rootRoute
386 | '/': typeof IndexRoute
387 | '/calculator': typeof CalculatorRoute
388 | '/connect-four': typeof ConnectFourRoute
389 | '/dice': typeof DiceRoute
390 | '/expense': typeof ExpenseRoute
391 | '/gradient': typeof GradientRoute
392 | '/hangman': typeof HangmanRoute
393 | '/hanoi': typeof HanoiRoute
394 | '/histogram': typeof HistogramRoute
395 | '/memory': typeof MemoryRoute
396 | '/password': typeof PasswordRoute
397 | '/quiz': typeof QuizRoute
398 | '/quote': typeof QuoteRoute
399 | '/rock-paper-scissors': typeof RockPaperScissorsRoute
400 | '/simon': typeof SimonRoute
401 | '/speed': typeof SpeedRoute
402 | '/split': typeof SplitRoute
403 | '/stopwatch': typeof StopwatchRoute
404 | '/tic-tac-toe': typeof TicTacToeRoute
405 | '/traffic-light': typeof TrafficLightRoute
406 | '/tree': typeof TreeRoute
407 | '/whack-a-mole': typeof WhackAMoleRoute
408 | }
409 |
410 | export interface FileRouteTypes {
411 | fileRoutesByFullPath: FileRoutesByFullPath
412 | fullPaths:
413 | | '/'
414 | | '/calculator'
415 | | '/connect-four'
416 | | '/dice'
417 | | '/expense'
418 | | '/gradient'
419 | | '/hangman'
420 | | '/hanoi'
421 | | '/histogram'
422 | | '/memory'
423 | | '/password'
424 | | '/quiz'
425 | | '/quote'
426 | | '/rock-paper-scissors'
427 | | '/simon'
428 | | '/speed'
429 | | '/split'
430 | | '/stopwatch'
431 | | '/tic-tac-toe'
432 | | '/traffic-light'
433 | | '/tree'
434 | | '/whack-a-mole'
435 | fileRoutesByTo: FileRoutesByTo
436 | to:
437 | | '/'
438 | | '/calculator'
439 | | '/connect-four'
440 | | '/dice'
441 | | '/expense'
442 | | '/gradient'
443 | | '/hangman'
444 | | '/hanoi'
445 | | '/histogram'
446 | | '/memory'
447 | | '/password'
448 | | '/quiz'
449 | | '/quote'
450 | | '/rock-paper-scissors'
451 | | '/simon'
452 | | '/speed'
453 | | '/split'
454 | | '/stopwatch'
455 | | '/tic-tac-toe'
456 | | '/traffic-light'
457 | | '/tree'
458 | | '/whack-a-mole'
459 | id:
460 | | '__root__'
461 | | '/'
462 | | '/calculator'
463 | | '/connect-four'
464 | | '/dice'
465 | | '/expense'
466 | | '/gradient'
467 | | '/hangman'
468 | | '/hanoi'
469 | | '/histogram'
470 | | '/memory'
471 | | '/password'
472 | | '/quiz'
473 | | '/quote'
474 | | '/rock-paper-scissors'
475 | | '/simon'
476 | | '/speed'
477 | | '/split'
478 | | '/stopwatch'
479 | | '/tic-tac-toe'
480 | | '/traffic-light'
481 | | '/tree'
482 | | '/whack-a-mole'
483 | fileRoutesById: FileRoutesById
484 | }
485 |
486 | export interface RootRouteChildren {
487 | IndexRoute: typeof IndexRoute
488 | CalculatorRoute: typeof CalculatorRoute
489 | ConnectFourRoute: typeof ConnectFourRoute
490 | DiceRoute: typeof DiceRoute
491 | ExpenseRoute: typeof ExpenseRoute
492 | GradientRoute: typeof GradientRoute
493 | HangmanRoute: typeof HangmanRoute
494 | HanoiRoute: typeof HanoiRoute
495 | HistogramRoute: typeof HistogramRoute
496 | MemoryRoute: typeof MemoryRoute
497 | PasswordRoute: typeof PasswordRoute
498 | QuizRoute: typeof QuizRoute
499 | QuoteRoute: typeof QuoteRoute
500 | RockPaperScissorsRoute: typeof RockPaperScissorsRoute
501 | SimonRoute: typeof SimonRoute
502 | SpeedRoute: typeof SpeedRoute
503 | SplitRoute: typeof SplitRoute
504 | StopwatchRoute: typeof StopwatchRoute
505 | TicTacToeRoute: typeof TicTacToeRoute
506 | TrafficLightRoute: typeof TrafficLightRoute
507 | TreeRoute: typeof TreeRoute
508 | WhackAMoleRoute: typeof WhackAMoleRoute
509 | }
510 |
511 | const rootRouteChildren: RootRouteChildren = {
512 | IndexRoute: IndexRoute,
513 | CalculatorRoute: CalculatorRoute,
514 | ConnectFourRoute: ConnectFourRoute,
515 | DiceRoute: DiceRoute,
516 | ExpenseRoute: ExpenseRoute,
517 | GradientRoute: GradientRoute,
518 | HangmanRoute: HangmanRoute,
519 | HanoiRoute: HanoiRoute,
520 | HistogramRoute: HistogramRoute,
521 | MemoryRoute: MemoryRoute,
522 | PasswordRoute: PasswordRoute,
523 | QuizRoute: QuizRoute,
524 | QuoteRoute: QuoteRoute,
525 | RockPaperScissorsRoute: RockPaperScissorsRoute,
526 | SimonRoute: SimonRoute,
527 | SpeedRoute: SpeedRoute,
528 | SplitRoute: SplitRoute,
529 | StopwatchRoute: StopwatchRoute,
530 | TicTacToeRoute: TicTacToeRoute,
531 | TrafficLightRoute: TrafficLightRoute,
532 | TreeRoute: TreeRoute,
533 | WhackAMoleRoute: WhackAMoleRoute,
534 | }
535 |
536 | export const routeTree = rootRoute
537 | ._addFileChildren(rootRouteChildren)
538 | ._addFileTypes()
539 |
540 | /* ROUTE_MANIFEST_START
541 | {
542 | "routes": {
543 | "__root__": {
544 | "filePath": "__root.tsx",
545 | "children": [
546 | "/",
547 | "/calculator",
548 | "/connect-four",
549 | "/dice",
550 | "/expense",
551 | "/gradient",
552 | "/hangman",
553 | "/hanoi",
554 | "/histogram",
555 | "/memory",
556 | "/password",
557 | "/quiz",
558 | "/quote",
559 | "/rock-paper-scissors",
560 | "/simon",
561 | "/speed",
562 | "/split",
563 | "/stopwatch",
564 | "/tic-tac-toe",
565 | "/traffic-light",
566 | "/tree",
567 | "/whack-a-mole"
568 | ]
569 | },
570 | "/": {
571 | "filePath": "index.tsx"
572 | },
573 | "/calculator": {
574 | "filePath": "calculator.tsx"
575 | },
576 | "/connect-four": {
577 | "filePath": "connect-four.tsx"
578 | },
579 | "/dice": {
580 | "filePath": "dice.tsx"
581 | },
582 | "/expense": {
583 | "filePath": "expense.tsx"
584 | },
585 | "/gradient": {
586 | "filePath": "gradient.tsx"
587 | },
588 | "/hangman": {
589 | "filePath": "hangman.tsx"
590 | },
591 | "/hanoi": {
592 | "filePath": "hanoi.tsx"
593 | },
594 | "/histogram": {
595 | "filePath": "histogram.tsx"
596 | },
597 | "/memory": {
598 | "filePath": "memory.tsx"
599 | },
600 | "/password": {
601 | "filePath": "password.tsx"
602 | },
603 | "/quiz": {
604 | "filePath": "quiz.tsx"
605 | },
606 | "/quote": {
607 | "filePath": "quote.tsx"
608 | },
609 | "/rock-paper-scissors": {
610 | "filePath": "rock-paper-scissors.tsx"
611 | },
612 | "/simon": {
613 | "filePath": "simon.tsx"
614 | },
615 | "/speed": {
616 | "filePath": "speed.tsx"
617 | },
618 | "/split": {
619 | "filePath": "split.tsx"
620 | },
621 | "/stopwatch": {
622 | "filePath": "stopwatch.tsx"
623 | },
624 | "/tic-tac-toe": {
625 | "filePath": "tic-tac-toe.tsx"
626 | },
627 | "/traffic-light": {
628 | "filePath": "traffic-light.tsx"
629 | },
630 | "/tree": {
631 | "filePath": "tree.tsx"
632 | },
633 | "/whack-a-mole": {
634 | "filePath": "whack-a-mole.tsx"
635 | }
636 | }
637 | }
638 | ROUTE_MANIFEST_END */
639 |
--------------------------------------------------------------------------------
/app/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter as createTanStackRouter } from '@tanstack/react-router'
2 | import { routeTree } from './routeTree.gen'
3 | import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
4 | import { NotFound } from './components/NotFound'
5 |
6 | export function createRouter() {
7 | const router = createTanStackRouter({
8 | routeTree,
9 | defaultPreload: 'intent',
10 | defaultErrorComponent: DefaultCatchBoundary,
11 | defaultNotFoundComponent: () => ,
12 | scrollRestoration: true,
13 | })
14 |
15 | return router
16 | }
17 |
18 | declare module '@tanstack/react-router' {
19 | interface Register {
20 | router: ReturnType
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | HeadContent,
3 | Link,
4 | Outlet,
5 | Scripts,
6 | createRootRoute,
7 | } from "@tanstack/react-router";
8 | import { TanStackRouterDevtools } from "@tanstack/router-devtools";
9 | import * as React from "react";
10 | import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
11 | import { NotFound } from "~/components/NotFound";
12 | import appCss from "~/styles/app.css?url";
13 | import { seo } from "~/utils/seo";
14 | import {
15 | useQuery,
16 | useMutation,
17 | useQueryClient,
18 | QueryClient,
19 | QueryClientProvider,
20 | } from "@tanstack/react-query";
21 |
22 | export const Route = createRootRoute({
23 | head: () => ({
24 | meta: [
25 | {
26 | charSet: "utf-8",
27 | },
28 | {
29 | name: "viewport",
30 | content: "width=device-width, initial-scale=1",
31 | },
32 | ...seo({
33 | title:
34 | "TanStack Start | Type-Safe, Client-First, Full-Stack React Framework",
35 | description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
36 | }),
37 | ],
38 | links: [
39 | { rel: "stylesheet", href: appCss },
40 | {
41 | rel: "apple-touch-icon",
42 | sizes: "180x180",
43 | href: "/apple-touch-icon.png",
44 | },
45 | {
46 | rel: "icon",
47 | type: "image/png",
48 | sizes: "32x32",
49 | href: "/favicon-32x32.png",
50 | },
51 | {
52 | rel: "icon",
53 | type: "image/png",
54 | sizes: "16x16",
55 | href: "/favicon-16x16.png",
56 | },
57 | { rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
58 | { rel: "icon", href: "/favicon.ico" },
59 | ],
60 | }),
61 | errorComponent: (props) => {
62 | return (
63 |
64 |
65 |
66 | );
67 | },
68 | notFoundComponent: () => ,
69 | component: RootComponent,
70 | });
71 |
72 | function RootComponent() {
73 | return (
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | const queryClient = new QueryClient();
81 |
82 | function RootDocument({ children }: { children: React.ReactNode }) {
83 | return (
84 |
85 |
86 |
87 |
88 |
89 |
90 | {children}
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/app/routes/calculator.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useRef, useState } from "react";
3 |
4 | export const Route = createFileRoute("/calculator")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Operator = "+" | "-" | "/" | "*" | "=";
9 | type Input = number | Operator;
10 | function RouteComponent() {
11 | const [input, setInput] = useState("");
12 | const inputsRef = useRef([]);
13 |
14 | function appendNumber(char: string) {
15 | setInput(input + char);
16 | }
17 |
18 | function clear() {
19 | setInput("");
20 | inputsRef.current = [];
21 | }
22 |
23 | function handleOperator(operator: Operator) {
24 | inputsRef.current.push(parseFloat(input));
25 |
26 | if (operator !== "=") {
27 | inputsRef.current.push(operator);
28 | }
29 |
30 | if (inputsRef.current.length === 3) {
31 | const [a, op, b] = inputsRef.current.splice(0, 3) as [
32 | number,
33 | Operator,
34 | number,
35 | ];
36 | let result = 0;
37 | if (op === "+") {
38 | result = a + b;
39 | } else if (op === "-") {
40 | result = a - b;
41 | } else if (op === "/") {
42 | result = a / b;
43 | } else if (op === "*") {
44 | result = a * b;
45 | }
46 |
47 | setInput(result.toString());
48 | } else {
49 | setInput("");
50 | }
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 | {input}
58 |
59 |
65 |
71 |
77 |
83 |
84 |
85 |
91 |
97 |
103 |
104 |
110 |
116 |
122 |
123 |
129 |
135 |
141 |
142 |
148 |
154 |
157 |
158 |
164 |
165 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------
/app/routes/connect-four.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/connect-four")({
5 | component: RouteComponent,
6 | });
7 |
8 | const RED = 1;
9 | const BLUE = 2;
10 | const EMPTY = undefined;
11 |
12 | type Piece = typeof RED | typeof BLUE | typeof EMPTY;
13 |
14 | const ROWS = 6;
15 | const COLUMNS = 7;
16 |
17 | function RouteComponent() {
18 | const [isRedTurn, setIsRedTurn] = useState(true);
19 | const [board, setBoard] = useState(
20 | new Array(ROWS).fill(new Array(COLUMNS).fill(EMPTY))
21 | );
22 |
23 | function handleCellClicked(columnIndex: number) {
24 | for (let rowIndex = ROWS - 1; rowIndex >= 0; rowIndex--) {
25 | const cell = board[rowIndex][columnIndex];
26 | if (cell === EMPTY) {
27 | const newBoard = [...board.map((row) => [...row])];
28 | newBoard[rowIndex][columnIndex] = isRedTurn ? RED : BLUE;
29 | setIsRedTurn(!isRedTurn);
30 | setBoard(newBoard);
31 | checkForWin(newBoard, rowIndex, columnIndex);
32 | break;
33 | }
34 | }
35 | }
36 |
37 | function checkForWin(currentBoard: Piece[][], row: number, column: number) {
38 | if (
39 | checkForDirectionalWin(
40 | currentBoard,
41 | (r, c) => [
42 | [r, c + 1],
43 | [r, c - 1],
44 | ],
45 | row,
46 | column
47 | ) >= 4 ||
48 | checkForDirectionalWin(
49 | currentBoard,
50 | (r, c) => [
51 | [r + 1, c],
52 | [r - 1, c],
53 | ],
54 | row,
55 | column
56 | ) >= 4 ||
57 | checkForDirectionalWin(
58 | currentBoard,
59 | (r, c) => [
60 | [r + 1, c + 1],
61 | [r - 1, c - 1],
62 | ],
63 | row,
64 | column
65 | ) >= 4 ||
66 | checkForDirectionalWin(
67 | currentBoard,
68 | (r, c) => [
69 | [r - 1, c + 1],
70 | [r + 1, c - 1],
71 | ],
72 | row,
73 | column
74 | ) >= 4
75 | ) {
76 | setTimeout(() => {
77 | alert(isRedTurn ? "Red wins" : "Blue wins");
78 | reset();
79 | }, 1);
80 | }
81 | }
82 |
83 | function reset() {
84 | setBoard(new Array(ROWS).fill(new Array(COLUMNS).fill(EMPTY)));
85 | setIsRedTurn(true);
86 | }
87 |
88 | function checkForDirectionalWin(
89 | currentBoard: Piece[][],
90 | getDirections: (
91 | row: number,
92 | column: number
93 | ) => [[number, number], [number, number]],
94 | row: number,
95 | column: number,
96 | seen: Set = new Set()
97 | ) {
98 | let sum = 0;
99 |
100 | if (row < 0 || row >= ROWS || column < 0 || column >= COLUMNS) {
101 | return 0;
102 | }
103 |
104 | if (seen.has(`${row} ${column}`)) {
105 | return 0;
106 | }
107 |
108 | seen.add(`${row} ${column}`);
109 |
110 | if (currentBoard[row][column] === (isRedTurn ? RED : BLUE)) {
111 | sum++;
112 | }
113 |
114 | const directions = getDirections(row, column);
115 |
116 | sum +=
117 | checkForDirectionalWin(
118 | currentBoard,
119 | getDirections,
120 | directions[0][0],
121 | directions[0][1],
122 | seen
123 | ) +
124 | checkForDirectionalWin(
125 | currentBoard,
126 | getDirections,
127 | directions[1][0],
128 | directions[1][1],
129 | seen
130 | );
131 |
132 | return sum;
133 | }
134 |
135 | return (
136 |
137 |
Connect Four
138 |
139 |
140 | {board.map((row, rowIndex) => (
141 |
142 | {row.map((cell, columnIndex) => (
143 |
156 | ))}
157 |
158 | ))}
159 |
160 |
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/app/routes/dice.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/dice")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | const [number, setNumber] = useState();
10 |
11 | return (
12 |
13 |
22 | {number !== undefined && }
23 |
24 | );
25 | }
26 |
27 | function Dice({ number }: { number: number }) {
28 | return (
29 |
30 | {number === 1 &&
}
31 | {number === 2 && (
32 | <>
33 |
34 |
35 | >
36 | )}
37 | {number === 3 && (
38 |
49 | )}
50 | {number === 4 && (
51 |
61 | )}
62 | {number === 5 && (
63 |
76 | )}
77 | {number === 6 && (
78 |
90 | )}
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/app/routes/expense.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/expense")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Entry = {
9 | amount: number;
10 | type: "expense" | "revenue";
11 | };
12 |
13 | function RouteComponent() {
14 | const [entries, setEntries] = useState([]);
15 | const [amount, setAmount] = useState("");
16 | const [type, setType] = useState<"expense" | "revenue">("expense");
17 |
18 | function handleAddEntry(e: React.FormEvent) {
19 | e.preventDefault();
20 | setEntries([...entries, { amount: Number(amount), type }]);
21 | setAmount("");
22 | }
23 |
24 | const total = entries.reduce((acc, entry) => {
25 | return acc + (entry.type === "expense" ? -entry.amount : entry.amount);
26 | }, 0);
27 |
28 | function handleRemoveEntry(index: number) {
29 | setEntries((prevEntries) => prevEntries.filter((_, i) => i !== index));
30 | }
31 |
32 | return (
33 |
34 |
54 |
55 |
56 | {entries.map((entry, index) => (
57 |
63 |
64 | {entry.type === "expense" ? "-" : "+"}${entry.amount} {entry.type}
65 |
66 |
72 |
73 | ))}
74 |
75 |
76 |
77 | ${total} {total > 0 ? "profit" : "loss"}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/routes/gradient.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/gradient")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | const [from, setFrom] = useState("#000000");
10 | const [to, setTo] = useState("#ffffff");
11 | const [direction, setDirection] = useState("left");
12 |
13 | return (
14 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/app/routes/hangman.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/hangman")({
5 | component: RouteComponent,
6 | });
7 |
8 | const words = ["fast", "slow", "happy", "sad", "big", "small", "tall", "short"];
9 |
10 | function RouteComponent() {
11 | const [word, setWord] = useState(getRandomWord());
12 | const [guessedLetters, setGuessedLetters] = useState(
13 | new Array(word.length).fill("")
14 | );
15 | const [letter, setLetter] = useState("");
16 | const [guessesRemaining, setGuessesRemaining] = useState(5);
17 |
18 | function getRandomWord() {
19 | return words[Math.floor(Math.random() * words.length)];
20 | }
21 |
22 | function restartGame() {
23 | const randomWord = getRandomWord();
24 | setGuessedLetters(new Array(randomWord.length).fill(""));
25 | setGuessesRemaining(5);
26 | setLetter("");
27 | setWord(randomWord);
28 | }
29 |
30 | function handleGuess() {
31 | const newGuessesRemaining = guessesRemaining - 1;
32 | setGuessesRemaining(newGuessesRemaining);
33 | const newGuessedLetters = [...guessedLetters];
34 | for (let i = 0; i < word.length; i++) {
35 | if (word[i] === letter) {
36 | newGuessedLetters[i] = letter;
37 | }
38 | }
39 | setGuessedLetters(newGuessedLetters);
40 | setLetter("");
41 |
42 | if (newGuessedLetters.every((letter) => letter !== "")) {
43 | alert("You win!");
44 | restartGame();
45 | }
46 |
47 | if (newGuessesRemaining === 0) {
48 | alert("You lose!");
49 | restartGame();
50 | }
51 | }
52 |
53 | return (
54 |
55 |
56 | {guessedLetters.map((letter, index) => (
57 |
61 | {letter}
62 |
63 | ))}
64 |
65 |
Guesses Remaining: {guessesRemaining}
66 |
67 | setLetter(e.target.value)}
70 | className="border border-gray-300 rounded p-2"
71 | />
72 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/routes/hanoi.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/hanoi")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | const [towers, setTowers] = useState([[3, 2, 1], [], []]);
10 | const [selectedTower, setSelectedTower] = useState(
11 | undefined
12 | );
13 |
14 | function handleClickTower(clickedTowerIndex: number) {
15 | if (selectedTower !== undefined) {
16 | const newTowers = [...towers.map((tower) => [...tower])];
17 | const fromTower = newTowers[selectedTower];
18 | const toTower = newTowers[clickedTowerIndex];
19 | const lastTower = newTowers[2];
20 |
21 | const fromValue = fromTower[fromTower.length - 1];
22 | const toValue = toTower[toTower.length - 1];
23 |
24 | if (toValue === undefined || fromValue < toValue) {
25 | fromTower.pop();
26 | toTower.push(fromValue);
27 | setTowers(newTowers);
28 | setSelectedTower(undefined);
29 | if (lastTower.length === 3) {
30 | alert("You win!");
31 | }
32 | }
33 | } else {
34 | setSelectedTower(clickedTowerIndex);
35 | }
36 | }
37 |
38 | return (
39 |
40 | {towers.map((tower, towerIndex) => (
41 |
63 | ))}
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/app/routes/histogram.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/histogram")({
5 | component: RouteComponent,
6 | });
7 |
8 | const allLettersInAlphabet = "abcdefghijklmnopqrstuvwxyz";
9 |
10 | type Histogram = { [key: string]: number };
11 |
12 | function getBlankHistogram(): Histogram {
13 | return allLettersInAlphabet.split("").reduce((obj, char) => {
14 | obj[char] = 0;
15 | return obj;
16 | }, {} as Histogram);
17 | }
18 |
19 | function RouteComponent() {
20 | const [input, setInput] = useState("");
21 | const [histogram, setHistogram] = useState(getBlankHistogram());
22 |
23 | function calculateHistogram(e: React.FormEvent) {
24 | e.preventDefault();
25 | const chars = input.split("").filter((char) => char !== " ");
26 | const newHistogram = getBlankHistogram();
27 | for (let char of chars) {
28 | newHistogram[char]++;
29 | }
30 | setHistogram(newHistogram);
31 | }
32 |
33 | return (
34 |
35 |
43 |
44 |
51 | {allLettersInAlphabet.split("").map((char) => (
52 |
53 |
59 | {histogram[char] > 0 && histogram[char]}
60 |
61 | {char}
62 |
63 | ))}
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute, Link } from "@tanstack/react-router";
2 |
3 | export const Route = createFileRoute("/")({
4 | component: Home,
5 | });
6 |
7 | function Home() {
8 | const examples = [
9 | { path: "/calculator", title: "Calculator" },
10 | { path: "/connect-four", title: "Connect Four" },
11 | { path: "/dice", title: "Dice" },
12 | { path: "/expense", title: "Expense Tracker" },
13 | { path: "/gradient", title: "Gradient" },
14 | { path: "/hangman", title: "Hangman" },
15 | { path: "/hanoi", title: "Tower of Hanoi" },
16 | { path: "/histogram", title: "Histogram" },
17 | { path: "/memory", title: "Memory Game" },
18 | { path: "/quiz", title: "Quiz" },
19 | { path: "/quote", title: "Quote Generator" },
20 | { path: "/rock-paper-scissors", title: "Rock Paper Scissors" },
21 | { path: "/simon", title: "Simon Says" },
22 | { path: "/speed", title: "Speed Test" },
23 | { path: "/split", title: "Split View" },
24 | { path: "/stopwatch", title: "Stopwatch" },
25 | { path: "/tic-tac-toe", title: "Tic Tac Toe" },
26 | { path: "/traffic-light", title: "Traffic Light" },
27 | { path: "/tree", title: "Tree Visualization" },
28 | { path: "/whack-a-mole", title: "Whack-a-Mole" },
29 | { path: "/password", title: "Password Generator" },
30 | ].sort((a, b) => a.title.localeCompare(b.title));
31 |
32 | return (
33 |
34 |
35 | Interactive Examples
36 |
37 |
38 | {examples.map((example) => (
39 |
44 |
45 | {example.title}
46 |
47 |
48 | ))}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/routes/memory.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useState } from "react";
3 |
4 | export const Route = createFileRoute("/memory")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Emoji = {
9 | emoji: string;
10 | isVisible: boolean;
11 | };
12 |
13 | const GRID_SIZE = 4;
14 | const TOTAL_PAIRS = (GRID_SIZE * GRID_SIZE) / 2;
15 |
16 | const EMOJIS = ["🐶", "🌽", "🐱", "🍎", "🍌", "🍓", "🍇", "🍉"];
17 |
18 | function RouteComponent() {
19 | const [grid, setGrid] = useState([]);
20 | const [lastCellIndexClicked, setLastCellIndexClicked] = useState<
21 | number | null
22 | >(null);
23 | const [preventClick, setPreventClick] = useState(false);
24 | const [pairsFound, setPairsFound] = useState(0);
25 |
26 | function shuffle(array: any[]) {
27 | return array.sort(() => Math.random() - 0.5);
28 | }
29 |
30 | function restartGame() {
31 | setPairsFound(0);
32 | setPreventClick(false);
33 | setLastCellIndexClicked(null);
34 | initializeGrid();
35 | }
36 |
37 | function initializeGrid() {
38 | const emojis = [];
39 | const totalEmojis = GRID_SIZE * GRID_SIZE;
40 | let emojiIndex = 0;
41 | for (let i = 0; i < totalEmojis; i += 2) {
42 | const emoji = EMOJIS[emojiIndex];
43 | emojis.push({
44 | emoji,
45 | isVisible: false,
46 | });
47 | emojis.push({
48 | emoji,
49 | isVisible: false,
50 | });
51 | emojiIndex++;
52 | }
53 |
54 | shuffle(emojis);
55 | setGrid(emojis);
56 | }
57 |
58 | useEffect(() => {
59 | initializeGrid();
60 | }, []);
61 |
62 | function handleEmojiClicked(index: number) {
63 | if (preventClick) return;
64 | if (grid[index].isVisible) return;
65 |
66 | const newGrid = [...grid];
67 | newGrid[index].isVisible = true;
68 |
69 | const hasPreviouslyClicked = lastCellIndexClicked !== null;
70 |
71 | if (hasPreviouslyClicked) {
72 | const lastCell = newGrid[lastCellIndexClicked];
73 | const currentCell = newGrid[index];
74 | const isMatchingPair = lastCell.emoji === currentCell.emoji;
75 |
76 | if (isMatchingPair) {
77 | setPairsFound(pairsFound + 1);
78 | } else {
79 | setPreventClick(true);
80 | setTimeout(() => {
81 | newGrid[lastCellIndexClicked].isVisible = false;
82 | newGrid[index].isVisible = false;
83 | setGrid([...newGrid]);
84 | setPreventClick(false);
85 | }, 1000);
86 | }
87 |
88 | setLastCellIndexClicked(null);
89 | } else {
90 | setLastCellIndexClicked(index);
91 | }
92 | setGrid(newGrid);
93 | }
94 |
95 | const isGameOver = pairsFound === TOTAL_PAIRS;
96 |
97 | return (
98 |
99 |
106 | {grid.map((emoji, index) => (
107 |
113 | ))}
114 |
115 |
116 | {pairsFound} / {TOTAL_PAIRS}
117 |
118 | {isGameOver && (
119 |
120 | You Won!
121 |
127 |
128 | )}
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/app/routes/password.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { Copy } from "lucide-react";
3 | import { useState } from "react";
4 |
5 | export const Route = createFileRoute("/password")({
6 | component: RouteComponent,
7 | });
8 |
9 | const MIN_PASSWORD_LENGTH = 8;
10 | const MAX_PASSWORD_LENGTH = 32;
11 |
12 | function generatePassword(
13 | length: number,
14 | isUppercase: boolean,
15 | isLowercase: boolean,
16 | isNumbers: boolean,
17 | isSpecial: boolean
18 | ) {
19 | const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
20 | const lowercase = "abcdefghijklmnopqrstuvwxyz";
21 | const numbers = "0123456789";
22 | const special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
23 |
24 | let combined = "";
25 |
26 | if (isUppercase) {
27 | combined += uppercase;
28 | }
29 |
30 | if (isLowercase) {
31 | combined += lowercase;
32 | }
33 |
34 | if (isNumbers) {
35 | combined += numbers;
36 | }
37 |
38 | if (isSpecial) {
39 | combined += special;
40 | }
41 |
42 | const password = new Array(length)
43 | .fill("")
44 | .map(() => {
45 | const randomCharacterIndex = Math.floor(Math.random() * combined.length);
46 | const randomCharacter = combined[randomCharacterIndex];
47 | return randomCharacter;
48 | })
49 | .join("");
50 |
51 | return password;
52 | }
53 |
54 | function RouteComponent() {
55 | const [passwordLength, setPasswordLength] = useState("12");
56 | const [isUppercase, setIsUppercase] = useState(true);
57 | const [isLowercase, setIsLowercase] = useState(true);
58 | const [isNumbers, setIsNumbers] = useState(true);
59 | const [isSpecial, setIsSpecial] = useState(true);
60 |
61 | const password = generatePassword(
62 | parseInt(passwordLength),
63 | isUppercase,
64 | isLowercase,
65 | isNumbers,
66 | isSpecial
67 | );
68 |
69 | return (
70 |
71 |
124 |
125 |
126 |
130 |
138 |
139 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/app/routes/quiz.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/quiz")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Answer = {
9 | answer: string;
10 | isCorrect: boolean;
11 | };
12 |
13 | type Question = {
14 | question: string;
15 | answers: Answer[];
16 | };
17 |
18 | const QUESTIONS: Question[] = [
19 | {
20 | question: "What is the capital of France?",
21 | answers: [
22 | { answer: "Paris", isCorrect: true },
23 | { answer: "London", isCorrect: false },
24 | { answer: "Berlin", isCorrect: false },
25 | { answer: "Madrid", isCorrect: false },
26 | ],
27 | },
28 | {
29 | question: "What is the capital of Germany?",
30 | answers: [
31 | { answer: "Berlin", isCorrect: true },
32 | { answer: "Paris", isCorrect: false },
33 | { answer: "Madrid", isCorrect: false },
34 | { answer: "London", isCorrect: false },
35 | ],
36 | },
37 | {
38 | question: "What is the capital of Italy?",
39 | answers: [
40 | { answer: "Rome", isCorrect: true },
41 | { answer: "Paris", isCorrect: false },
42 | { answer: "Madrid", isCorrect: false },
43 | { answer: "London", isCorrect: false },
44 | ],
45 | },
46 | ];
47 |
48 | function RouteComponent() {
49 | const [score, setScore] = useState(0);
50 | const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
51 | const [selectedAnswerIndex, setSelectedAnswerIndex] = useState(
52 | null
53 | );
54 | const currentQuestion = QUESTIONS[currentQuestionIndex];
55 | const isQuizFinished = currentQuestionIndex === QUESTIONS.length;
56 |
57 | function handleAnswerClicked(answerIndex: number) {
58 | setSelectedAnswerIndex(answerIndex);
59 | }
60 |
61 | function handleSubmit() {
62 | const answer = currentQuestion.answers[selectedAnswerIndex!];
63 | if (answer.isCorrect) {
64 | setScore(score + 1);
65 | }
66 |
67 | setSelectedAnswerIndex(null);
68 | setCurrentQuestionIndex(currentQuestionIndex + 1);
69 | }
70 |
71 | function restart() {
72 | setScore(0);
73 | setCurrentQuestionIndex(0);
74 | setSelectedAnswerIndex(null);
75 | }
76 |
77 | function Quiz() {
78 | return (
79 | <>
80 |
81 | Question {currentQuestionIndex + 1} of {QUESTIONS.length}
82 |
83 |
84 | {currentQuestion.question}
85 |
86 |
87 | {currentQuestion.answers.map((answer, answerIndex) => (
88 |
95 | ))}
96 |
97 |
98 |
105 | >
106 | );
107 | }
108 |
109 | function QuizFinished() {
110 | return (
111 | <>
112 |
113 | You scored {score} out of {QUESTIONS.length}
114 |
115 |
121 | >
122 | );
123 | }
124 |
125 | return (
126 |
127 | {isQuizFinished ? : }
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/app/routes/quote.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useQuery } from "@tanstack/react-query";
3 |
4 | export const Route = createFileRoute("/quote")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Quote = {
9 | sentence: string;
10 | character: {
11 | name: string;
12 | slug: string;
13 | house: {
14 | name: string;
15 | slug: string;
16 | };
17 | };
18 | };
19 |
20 | function getQuote() {
21 | return fetch("https://api.gameofthronesquotes.xyz/v1/random").then(
22 | (response) => response.json() as Promise
23 | );
24 | }
25 |
26 | function RouteComponent() {
27 | const {
28 | data: quote,
29 | isFetching,
30 | refetch,
31 | } = useQuery({
32 | queryKey: ["quote"],
33 | queryFn: () => getQuote(),
34 | });
35 |
36 | return (
37 |
38 |
39 | {isFetching ?
Loading...
:
"{quote?.sentence}"
}
40 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/app/routes/rock-paper-scissors.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import {
3 | Scissors,
4 | BrickWall,
5 | Scroll,
6 | Skull,
7 | PartyPopper,
8 | Handshake,
9 | } from "lucide-react";
10 | import { useState } from "react";
11 |
12 | export const Route = createFileRoute("/rock-paper-scissors")({
13 | component: RouteComponent,
14 | });
15 |
16 | type Weapon = "rock" | "paper" | "scissors";
17 | const weapons = ["rock", "paper", "scissors"] as const;
18 |
19 | function RouteComponent() {
20 | const [userChoice, setUserChoice] = useState(null);
21 | const [aiChoice, setAiChoice] = useState(null);
22 | const [gameState, setGameState] = useState<"playing" | "done">("playing");
23 |
24 | function handlerUserChoice(weapon: Weapon) {
25 | setUserChoice(weapon);
26 | const randomIndex = Math.floor(Math.random() * weapons.length);
27 | const randomWeapon = weapons[randomIndex];
28 | setAiChoice(randomWeapon);
29 | setGameState("done");
30 | }
31 |
32 | function getResult() {
33 | let message = <>>;
34 |
35 | if (userChoice === aiChoice) {
36 | message = (
37 | <>
38 |
39 | Draw
40 | >
41 | );
42 | } else if (
43 | (userChoice === "rock" && aiChoice === "scissors") ||
44 | (userChoice === "paper" && aiChoice === "rock") ||
45 | (userChoice === "scissors" && aiChoice === "paper")
46 | ) {
47 | message = (
48 | <>
49 |
50 | You Win
51 | >
52 | );
53 | } else {
54 | message = (
55 | <>
56 | You lose
57 | >
58 | );
59 | }
60 |
61 | return (
62 |
63 | {message}
64 |
65 | );
66 | }
67 |
68 | function restartGame() {
69 | setUserChoice(null);
70 | setAiChoice(null);
71 | setGameState("playing");
72 | }
73 |
74 | return (
75 |
76 | {gameState === "playing" ? (
77 | <>
78 |
Pick your Weapon
79 |
80 |
89 |
98 |
107 |
108 | >
109 | ) : (
110 | <>
111 |
112 | You chose {userChoice}, AI chose {aiChoice}
113 |
114 |
{getResult()}
115 |
121 | >
122 | )}
123 |
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/app/routes/simon.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useRef, useState } from "react";
3 |
4 | export const Route = createFileRoute("/simon")({
5 | component: RouteComponent,
6 | });
7 |
8 | const GREEN = 0;
9 | const RED = 1;
10 | const YELLOW = 2;
11 | const BLUE = 3;
12 |
13 | const FLASH_TIME = 700;
14 | const FLASH_TIME_BEFORE = 500;
15 |
16 | function RouteComponent() {
17 | const sequenceRef = useRef([getRandomSegmentIndex()]);
18 | const playerIndexRef = useRef(0);
19 | const [flashIndex, setFlashIndex] = useState(undefined);
20 | const [isPlayerTurn, setIsPlayerTurn] = useState(false);
21 |
22 | function getRandomSegmentIndex() {
23 | return Math.floor(Math.random() * 4);
24 | }
25 |
26 | function flashSequence(index: number) {
27 | if (index >= sequenceRef.current.length) {
28 | setFlashIndex(undefined);
29 | setIsPlayerTurn(true);
30 | return;
31 | }
32 | const value = sequenceRef.current[index];
33 | setFlashIndex(value);
34 |
35 | setTimeout(() => {
36 | setFlashIndex(undefined);
37 | }, FLASH_TIME_BEFORE);
38 |
39 | setTimeout(() => {
40 | flashSequence(index + 1);
41 | }, FLASH_TIME);
42 | }
43 |
44 | useEffect(() => {
45 | flashSequence(0);
46 | }, []);
47 |
48 | function handleSegmentClicked(clickedValue: number) {
49 | const sequenceValue = sequenceRef.current[playerIndexRef.current];
50 |
51 | if (clickedValue !== sequenceValue) {
52 | alert("you lost");
53 | setIsPlayerTurn(false);
54 | sequenceRef.current = [getRandomSegmentIndex()];
55 | playerIndexRef.current = 0;
56 | flashSequence(0);
57 | return;
58 | }
59 | playerIndexRef.current++;
60 |
61 | if (playerIndexRef.current === sequenceRef.current.length) {
62 | sequenceRef.current.push(getRandomSegmentIndex());
63 | playerIndexRef.current = 0;
64 | setIsPlayerTurn(false);
65 | flashSequence(0);
66 | }
67 | }
68 |
69 | return (
70 |
71 |
72 |
80 |
88 |
96 |
104 |
105 |
106 | {isPlayerTurn &&
Your turn
}
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/app/routes/speed.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useState } from "react";
3 |
4 | export const Route = createFileRoute("/speed")({
5 | component: RouteComponent,
6 | });
7 |
8 | const randomPromptsAboutHistory = [
9 | "The first president of the United States was George Washington",
10 | "The American Civil War lasted from 1861 to 1865",
11 | "The Declaration of Independence was signed in 1776",
12 | "Ancient Egypt built the Great Pyramids around 2500 BC",
13 | "The Roman Empire fell in 476 AD when its last emperor was deposed",
14 | ];
15 |
16 | function getRandomPrompt() {
17 | return randomPromptsAboutHistory[
18 | Math.floor(Math.random() * randomPromptsAboutHistory.length)
19 | ];
20 | }
21 |
22 | function RouteComponent() {
23 | const [prompt, setPrompt] = useState(randomPromptsAboutHistory[0]);
24 | const [currentIndex, setCurrentIndex] = useState(0);
25 | const [startTime, setStartTime] = useState(0);
26 | const [endTime, setEndTime] = useState(0);
27 | const [numberTyped, setNumberTyped] = useState(0);
28 |
29 | const promptArray = prompt.split("");
30 | if (currentIndex < promptArray.length) {
31 | promptArray[currentIndex] =
32 | `${promptArray[currentIndex]}`;
33 | }
34 | const promptString = promptArray.join("");
35 |
36 | useEffect(() => {
37 | const handleKeyDown = (e: KeyboardEvent) => {
38 | if (e.key === "Shift") {
39 | return;
40 | }
41 |
42 | if (endTime > 0) {
43 | return;
44 | }
45 |
46 | setNumberTyped((prevTyped) => prevTyped + 1);
47 |
48 | if (currentIndex === 0) {
49 | setStartTime(Date.now());
50 | }
51 |
52 | if (e.key === prompt[currentIndex]) {
53 | setCurrentIndex(currentIndex + 1);
54 |
55 | if (currentIndex + 1 === prompt.length) {
56 | setEndTime(Date.now());
57 | return;
58 | }
59 | }
60 | };
61 | window.addEventListener("keydown", handleKeyDown);
62 |
63 | return () => {
64 | window.removeEventListener("keydown", handleKeyDown);
65 | };
66 | }, [currentIndex]);
67 |
68 | const elaspedTimeInMs = endTime - startTime;
69 |
70 | function restartGame(e: React.MouseEvent) {
71 | setCurrentIndex(0);
72 | setStartTime(0);
73 | setEndTime(0);
74 | setNumberTyped(0);
75 | setPrompt(getRandomPrompt());
76 | e.currentTarget.blur();
77 | }
78 |
79 | const numberOfWords = prompt.split(" ").length;
80 |
81 | const accuracy = Math.round((prompt.length / numberTyped) * 100);
82 | const isGameOver = elaspedTimeInMs > 0;
83 | const charactersPerMinute = Math.round(
84 | (prompt.length / elaspedTimeInMs) * 60 * 1000
85 | );
86 | const elaspedTimeInSeconds = elaspedTimeInMs / 1000;
87 | const wordsPerMinute = Math.round(
88 | (numberOfWords / elaspedTimeInSeconds) * 60
89 | );
90 |
91 | return (
92 |
93 |
99 | {isGameOver && (
100 | <>
101 |
{elaspedTimeInMs / 1000}s elasped time
102 |
{accuracy}% accuracy
103 |
{charactersPerMinute} characters per minute
104 |
{wordsPerMinute} words per minute
105 |
108 | >
109 | )}
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/app/routes/split.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/split")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | return (
10 |
11 | |
12 |
13 | );
14 | }
15 |
16 | function getRandomHex() {
17 | return "#" + Math.floor(Math.random() * 16777215).toString(16);
18 | }
19 |
20 | function getRandomId() {
21 | return (
22 | Math.random().toString(36).substring(2, 15) +
23 | Math.random().toString(36).substring(2, 15)
24 | );
25 | }
26 |
27 | function Cell({ isVertical }: { isVertical: boolean }) {
28 | const [cells, setCells] = useState([]);
29 | const [color, setColor] = useState(getRandomHex());
30 |
31 | function split() {
32 | setCells([...cells, getRandomId(), getRandomId()]);
33 | }
34 |
35 | return (
36 |
39 | {cells.length === 0 && (
40 |
49 | )}
50 | {cells.map((cell) => (
51 | |
52 | ))}
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/app/routes/stopwatch.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useRef, useState } from "react";
3 |
4 | export const Route = createFileRoute("/stopwatch")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | const [elasped, setElapsed] = useState(0);
10 | const [state, setState] = useState<"initial" | "running" | "paused">(
11 | "initial"
12 | );
13 | const intervalRef = useRef(null);
14 |
15 | function start() {
16 | setState("running");
17 | intervalRef.current = setInterval(() => {
18 | setElapsed((prev) => prev + 1);
19 | }, 1000);
20 | }
21 |
22 | function clear() {
23 | if (intervalRef.current) {
24 | clearInterval(intervalRef.current);
25 | intervalRef.current = null;
26 | }
27 | }
28 |
29 | function pause() {
30 | setState("paused");
31 | clear();
32 | }
33 |
34 | function reset() {
35 | setState("initial");
36 | setElapsed(0);
37 | clear();
38 | }
39 |
40 | return (
41 |
42 |
43 |
{elasped} seconds elapsed
44 |
45 | {state === "initial" && (
46 |
52 | )}
53 |
54 | {state === "running" && (
55 |
61 | )}
62 |
63 | {state === "paused" && (
64 |
65 |
71 |
77 |
78 | )}
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/app/routes/tic-tac-toe.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useState } from "react";
3 |
4 | export const Route = createFileRoute("/tic-tac-toe")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Token = "X" | "O" | "";
9 |
10 | function RouteComponent() {
11 | const [grid, setGrid] = useState([
12 | ["", "", ""],
13 | ["", "", ""],
14 | ["", "", ""],
15 | ]);
16 | const [currentPlayer, setCurrentPlayer] = useState<"X" | "O">("X");
17 | const [winner, setWinner] = useState<"X" | "O" | "Tie" | "None">("None");
18 |
19 | useEffect(() => {
20 | const winner = checkWin();
21 | if (winner !== "None") {
22 | setWinner(winner);
23 | }
24 | }, [grid]);
25 |
26 | function checkWin(): "X" | "O" | "Tie" | "None" {
27 | // check the rows for winners
28 | for (let i = 0; i < 3; i++) {
29 | const row = grid[i];
30 | if (row[0] === row[1] && row[1] === row[2] && row[0] !== "") {
31 | return row[0];
32 | }
33 | }
34 |
35 | // check the columns for winners
36 | for (let i = 0; i < 3; i++) {
37 | const col = grid.map((row) => row[i]);
38 | if (col[0] === col[1] && col[1] === col[2] && col[0] !== "") {
39 | return col[0];
40 | }
41 | }
42 |
43 | // check the top left to bottom right diagonal
44 | if (
45 | grid[0][0] === grid[1][1] &&
46 | grid[1][1] === grid[2][2] &&
47 | grid[0][0] !== ""
48 | ) {
49 | return grid[0][0];
50 | }
51 |
52 | // check the bottom left to top right diagonal
53 | if (
54 | grid[2][0] === grid[1][1] &&
55 | grid[1][1] === grid[0][2] &&
56 | grid[2][0] !== ""
57 | ) {
58 | return grid[2][0];
59 | }
60 |
61 | const isTie = grid.every((row) => row.every((cell) => cell !== ""));
62 | if (isTie) {
63 | return "Tie";
64 | }
65 |
66 | return "None";
67 | }
68 |
69 | function resetGame() {
70 | setGrid([
71 | ["", "", ""],
72 | ["", "", ""],
73 | ["", "", ""],
74 | ]);
75 | setCurrentPlayer("X");
76 | setWinner("None");
77 | }
78 |
79 | function handleCellClicked(rowIndex: number, colIndex: number) {
80 | setGrid((prevGrid) => {
81 | const newGrid = [...prevGrid.map((row) => [...row])];
82 | newGrid[rowIndex][colIndex] = currentPlayer;
83 | return newGrid;
84 | });
85 | setCurrentPlayer((prevPlayer) => (prevPlayer === "X" ? "O" : "X"));
86 | }
87 |
88 | return (
89 |
90 | {winner !== "None" && (
91 |
92 |
{winner} wins!
93 |
99 |
100 | )}
101 |
102 |
103 | {grid.map((row, rowIndex) => (
104 |
105 | {row.map((piece, colIndex) => (
106 |
113 | ))}
114 |
115 | ))}
116 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/app/routes/traffic-light.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useState } from "react";
3 |
4 | export const Route = createFileRoute("/traffic-light")({
5 | component: RouteComponent,
6 | });
7 |
8 | const RED = "red";
9 | const YELLOW = "yellow";
10 | const GREEN = "green";
11 |
12 | const RED_DELAY = 3000;
13 | const YELLOW_DELAY = 1000;
14 | const GREEN_DELAY = 3000;
15 |
16 | function RouteComponent() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | function TrafficLight({ initialColor }: { initialColor: string }) {
27 | const [enabled, setEnabled] = useState(initialColor);
28 |
29 | useEffect(() => {
30 | if (enabled === RED) {
31 | setTimeout(() => {
32 | setEnabled(GREEN);
33 | }, RED_DELAY);
34 | } else if (enabled === GREEN) {
35 | setTimeout(() => {
36 | setEnabled(YELLOW);
37 | }, GREEN_DELAY);
38 | } else if (enabled === YELLOW) {
39 | setTimeout(() => {
40 | setEnabled(RED);
41 | }, YELLOW_DELAY);
42 | }
43 | }, [enabled]);
44 |
45 | function isActive(color: string) {
46 | return enabled === color;
47 | }
48 |
49 | return (
50 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/app/routes/tree.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useState } from "react";
3 |
4 | export const Route = createFileRoute("/tree")({
5 | component: RouteComponent,
6 | });
7 |
8 | type Node = {
9 | children: Node[];
10 | text: string;
11 | type: "file" | "folder";
12 | };
13 |
14 | function RouteComponent() {
15 | const [root, setRoot] = useState({
16 | children: [
17 | {
18 | children: [
19 | {
20 | children: [
21 | {
22 | children: [],
23 | text: "file1-1-1.txt",
24 | type: "file",
25 | },
26 | {
27 | children: [],
28 | text: "file1-1-2.txt",
29 | type: "file",
30 | },
31 | ],
32 | text: "folder1-1",
33 | type: "folder",
34 | },
35 | {
36 | children: [],
37 | text: "file1-2.txt",
38 | type: "file",
39 | },
40 | ],
41 | text: "folder1",
42 | type: "folder",
43 | },
44 | {
45 | children: [
46 | {
47 | children: [
48 | {
49 | children: [],
50 | text: "file2-1-1",
51 | type: "file",
52 | },
53 | ],
54 | text: "folder2-1",
55 | type: "folder",
56 | },
57 | ],
58 | text: "folder2",
59 | type: "folder",
60 | },
61 | ],
62 | text: "root",
63 | type: "folder",
64 | });
65 |
66 | return (
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | function NodeComponent({ node }: { node: Node }) {
74 | const [isExpanded, setIsExpanded] = useState(false);
75 |
76 | return (
77 | <>
78 | {node.type === "folder" ? (
79 |
85 | ) : (
86 | {node.text}
87 | )}
88 | {isExpanded && (
89 |
90 | {node.children.map((child) => (
91 |
92 | ))}
93 |
94 | )}
95 | >
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/app/routes/whack-a-mole.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from "@tanstack/react-router";
2 | import { useEffect, useState } from "react";
3 |
4 | export const Route = createFileRoute("/whack-a-mole")({
5 | component: RouteComponent,
6 | });
7 |
8 | const MOLE_INTERVAL = 1000;
9 | const MOLE_HIDE_TIMEOUT = 700;
10 | const GRID_SIZE = 4;
11 |
12 | function RouteComponent() {
13 | const [holes, setHoles] = useState(
14 | new Array(GRID_SIZE).fill(new Array(GRID_SIZE).fill(false))
15 | );
16 | const [score, setScore] = useState(0);
17 |
18 | function toggleMole(rowIndex: number, colIndex: number, value: boolean) {
19 | setHoles((prevHoles) => {
20 | const newHoles = [...prevHoles.map((row) => [...row])];
21 | newHoles[rowIndex][colIndex] = value;
22 | return newHoles;
23 | });
24 | }
25 |
26 | function handleCellClicked(rowIndex: number, colIndex: number) {
27 | if (holes[rowIndex][colIndex]) {
28 | setScore(score + 1);
29 | toggleMole(rowIndex, colIndex, false);
30 | }
31 | }
32 |
33 | useEffect(() => {
34 | setInterval(() => {
35 | const randomRowIndex = Math.floor(Math.random() * holes.length);
36 | const randomColIndex = Math.floor(
37 | Math.random() * holes[randomRowIndex].length
38 | );
39 | toggleMole(randomRowIndex, randomColIndex, true);
40 | setTimeout(() => {
41 | toggleMole(randomRowIndex, randomColIndex, false);
42 | }, MOLE_HIDE_TIMEOUT);
43 | }, MOLE_INTERVAL);
44 | }, []);
45 |
46 | return (
47 |
48 |
Score: {score}
49 |
50 | {holes.map((row, rowIndex) => (
51 |
52 | {row.map((col, colIndex) => (
53 |
60 | ))}
61 |
62 | ))}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/app/ssr.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import {
3 | createStartHandler,
4 | defaultStreamHandler,
5 | } from '@tanstack/start/server'
6 | import { getRouterManifest } from '@tanstack/start/router-manifest'
7 |
8 | import { createRouter } from './router'
9 |
10 | export default createStartHandler({
11 | createRouter,
12 | getRouterManifest,
13 | })(defaultStreamHandler)
14 |
--------------------------------------------------------------------------------
/app/styles/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | html {
7 | color-scheme: light dark;
8 | }
9 |
10 | * {
11 | @apply border-gray-200 dark:border-gray-800;
12 | }
13 |
14 | html,
15 | body {
16 | @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
17 | }
18 |
19 | .using-mouse * {
20 | outline: none !important;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/utils/loggingMiddleware.tsx:
--------------------------------------------------------------------------------
1 | import { createMiddleware } from '@tanstack/start'
2 |
3 | export const logMiddleware = createMiddleware()
4 | .client(async (ctx) => {
5 | const clientTime = new Date()
6 |
7 | return ctx.next({
8 | context: {
9 | clientTime,
10 | },
11 | sendContext: {
12 | clientTime,
13 | },
14 | })
15 | })
16 | .server(async (ctx) => {
17 | const serverTime = new Date()
18 |
19 | return ctx.next({
20 | sendContext: {
21 | serverTime,
22 | durationToServer:
23 | serverTime.getTime() - ctx.context.clientTime.getTime(),
24 | },
25 | })
26 | })
27 | .clientAfter(async (ctx) => {
28 | const now = new Date()
29 |
30 | console.log('Client Req/Res:', {
31 | duration: ctx.context.clientTime.getTime() - now.getTime(),
32 | durationToServer: ctx.context.durationToServer,
33 | durationFromServer: now.getTime() - ctx.context.serverTime.getTime(),
34 | })
35 |
36 | return ctx.next()
37 | })
38 |
--------------------------------------------------------------------------------
/app/utils/posts.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from '@tanstack/react-router'
2 | import { createServerFn } from '@tanstack/start'
3 | import axios from 'redaxios'
4 |
5 | export type PostType = {
6 | id: string
7 | title: string
8 | body: string
9 | }
10 |
11 | export const fetchPost = createServerFn({ method: 'GET' })
12 | .validator((d: string) => d)
13 | .handler(async ({ data }) => {
14 | console.info(`Fetching post with id ${data}...`)
15 | const post = await axios
16 | .get(`https://jsonplaceholder.typicode.com/posts/${data}`)
17 | .then((r) => r.data)
18 | .catch((err) => {
19 | console.error(err)
20 | if (err.status === 404) {
21 | throw notFound()
22 | }
23 | throw err
24 | })
25 |
26 | return post
27 | })
28 |
29 | export const fetchPosts = createServerFn({ method: 'GET' }).handler(
30 | async () => {
31 | console.info('Fetching posts...')
32 | return axios
33 | .get>('https://jsonplaceholder.typicode.com/posts')
34 | .then((r) => r.data.slice(0, 10))
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/app/utils/seo.ts:
--------------------------------------------------------------------------------
1 | export const seo = ({
2 | title,
3 | description,
4 | keywords,
5 | image,
6 | }: {
7 | title: string
8 | description?: string
9 | image?: string
10 | keywords?: string
11 | }) => {
12 | const tags = [
13 | { title },
14 | { name: 'description', content: description },
15 | { name: 'keywords', content: keywords },
16 | { name: 'twitter:title', content: title },
17 | { name: 'twitter:description', content: description },
18 | { name: 'twitter:creator', content: '@tannerlinsley' },
19 | { name: 'twitter:site', content: '@tannerlinsley' },
20 | { name: 'og:type', content: 'website' },
21 | { name: 'og:title', content: title },
22 | { name: 'og:description', content: description },
23 | ...(image
24 | ? [
25 | { name: 'twitter:image', content: image },
26 | { name: 'twitter:card', content: 'summary_large_image' },
27 | { name: 'og:image', content: image },
28 | ]
29 | : []),
30 | ]
31 |
32 | return tags
33 | }
34 |
--------------------------------------------------------------------------------
/app/utils/users.tsx:
--------------------------------------------------------------------------------
1 | export type User = {
2 | id: number
3 | name: string
4 | email: string
5 | }
6 |
7 | export const DEPLOY_URL = 'http://localhost:3000'
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tanstack-start-example-basic",
3 | "private": true,
4 | "sideEffects": false,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vinxi dev",
8 | "build": "vinxi build",
9 | "start": "vinxi start"
10 | },
11 | "dependencies": {
12 | "@tanstack/react-query": "^5.66.7",
13 | "@tanstack/react-router": "^1.105.0",
14 | "@tanstack/router-devtools": "^1.105.0",
15 | "@tanstack/start": "^1.105.4",
16 | "lodash": "^4.17.21",
17 | "lucide-react": "^0.475.0",
18 | "react": "^19.0.0",
19 | "react-dom": "^19.0.0",
20 | "redaxios": "^0.5.1",
21 | "tailwind-merge": "^2.6.0",
22 | "vinxi": "0.5.3"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^22.5.4",
26 | "@types/react": "^19.0.8",
27 | "@types/react-dom": "^19.0.3",
28 | "autoprefixer": "^10.4.20",
29 | "postcss": "^8.5.1",
30 | "tailwindcss": "^3.4.17",
31 | "typescript": "^5.7.2",
32 | "vite-tsconfig-paths": "^5.1.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webdevcody/react-challenges-course/e5f897d35c7e39455d59072ba233868579bd36a3/public/favicon.png
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./app/**/*.{js,jsx,ts,tsx}'],
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "jsx": "react-jsx",
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
10 | "isolatedModules": true,
11 | "resolveJsonModule": true,
12 | "skipLibCheck": true,
13 | "target": "ES2022",
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "~/*": ["./app/*"]
19 | },
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------