├── .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 |
39 |
40 |

41 |
42 |
43 |

44 |
45 |
46 |

47 |
48 |
49 | )} 50 | {number === 4 && ( 51 |
52 |
53 |

54 |

55 |
56 |
57 |

58 |

59 |
60 |
61 | )} 62 | {number === 5 && ( 63 |
64 |
65 |

66 |

67 |
68 |
69 |

70 |
71 |
72 |

73 |

74 |
75 |
76 | )} 77 | {number === 6 && ( 78 |
79 |
80 |

81 |

82 |

83 |
84 |
85 |

86 |

87 |

88 |
89 |
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 |
35 | setAmount(e.target.value)} 41 | /> 42 | 43 | 51 | 52 | 53 |
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 |
15 |
16 | 17 | setFrom(e.target.value)} 20 | className="bg-gray-300 text-black px-2 rounded" 21 | name="from" 22 | /> 23 | 24 | 25 | setTo(e.target.value)} 28 | className="bg-gray-300 text-black px-2 rounded" 29 | name="to" 30 | /> 31 | 32 | 33 | 46 |
47 | 48 |
54 |
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 |
36 | setInput(e.target.value)} 40 | /> 41 | 42 |
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 |
72 |
73 | setPasswordLength(e.target.value)} 80 | /> 81 | setPasswordLength(e.target.value)} 87 | /> 88 |
89 | 90 |
91 | 95 | 96 | 104 | 105 | 113 | 114 | 122 |
123 |
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 |
51 |
54 |
57 |
60 |
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 | --------------------------------------------------------------------------------