├── src ├── vite-env.d.ts ├── form │ ├── shared.ts │ ├── page.tsx │ ├── use-state.tsx │ ├── use-reducer.tsx │ └── machine.tsx ├── form-complete │ ├── shared.ts │ ├── use-state.tsx │ ├── use-reducer.tsx │ └── machine.tsx ├── toggle │ ├── use-state.tsx │ ├── page.tsx │ ├── use-reducer.tsx │ ├── actor.tsx │ ├── machine.tsx │ └── machine-context.tsx ├── mount │ ├── shared.ts │ ├── page.tsx │ ├── use-state.tsx │ ├── use-reducer.tsx │ └── machine.tsx ├── timer │ ├── machine │ │ ├── page.tsx │ │ └── machine.ts │ └── use-state │ │ └── page.tsx ├── npc │ ├── page.tsx │ └── machine.ts ├── notifications │ ├── machine │ │ ├── page.tsx │ │ └── machine.ts │ └── use-state │ │ └── page.tsx ├── movie-search │ ├── page.tsx │ └── machine.ts ├── modal │ ├── page.tsx │ └── machine.ts ├── main.tsx └── tic-tac-toe │ ├── page.tsx │ └── machine.ts ├── tsconfig.json ├── vite.config.ts ├── index.html ├── .gitignore ├── tsconfig.node.json ├── package.json ├── tsconfig.app.json ├── README.md └── pnpm-lock.yaml /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/form/shared.ts: -------------------------------------------------------------------------------- 1 | export interface Context { 2 | username: string; 3 | } 4 | export const initialContext: Context = { username: "" }; 5 | 6 | export const postRequest = (context: Context) => 7 | new Promise((resolve) => 8 | setTimeout(() => { 9 | resolve(context); 10 | }, 1000) 11 | ); 12 | -------------------------------------------------------------------------------- /src/form-complete/shared.ts: -------------------------------------------------------------------------------- 1 | export interface Context { 2 | username: string; 3 | } 4 | export const initialContext: Context = { username: "" }; 5 | 6 | export const postRequest = (context: Context) => 7 | new Promise((resolve) => 8 | setTimeout(() => { 9 | resolve(context); 10 | }, 1000) 11 | ); 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | XState | Typeonce 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/toggle/use-state.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | type Context = boolean; 4 | const initialContext = false; 5 | 6 | export default function UseState() { 7 | const [context, setContext] = useState(initialContext); 8 | return ( 9 | setContext(!context)} 13 | /> 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/toggle/page.tsx: -------------------------------------------------------------------------------- 1 | import Actor from "./actor"; 2 | import Machine from "./machine"; 3 | import MachineContext from "./machine-context"; 4 | import UseReducer from "./use-reducer"; 5 | import UseState from "./use-state"; 6 | 7 | export default function Page() { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/mount/shared.ts: -------------------------------------------------------------------------------- 1 | export type Post = { 2 | userId: number; 3 | id: number; 4 | title: string; 5 | body: string; 6 | }; 7 | 8 | export type Context = { query: string; posts: Post[] }; 9 | export const initialContext = { query: "", posts: [] }; 10 | 11 | export const searchRequest = async (query: string): Promise => 12 | fetch(`https://jsonplaceholder.typicode.com/posts?title_like=${query}`).then( 13 | (response) => response.json() 14 | ); 15 | -------------------------------------------------------------------------------- /src/form/page.tsx: -------------------------------------------------------------------------------- 1 | import Machine from "./machine"; 2 | import UseReducer from "./use-reducer"; 3 | import UseState from "./use-state"; 4 | 5 | export default function Page() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/mount/page.tsx: -------------------------------------------------------------------------------- 1 | import Machine from "./machine"; 2 | import UseReducer from "./use-reducer"; 3 | import UseState from "./use-state"; 4 | 5 | export default function Page() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/toggle/use-reducer.tsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | type Event = { type: "toggle" }; 4 | type Context = boolean; 5 | const initialContext = false; 6 | 7 | const reducer = (context: Context, event: Event): Context => { 8 | if (event.type === "toggle") { 9 | return !context; 10 | } 11 | return context; 12 | }; 13 | 14 | export default function UseReducer() { 15 | const [context, dispatch] = useReducer(reducer, initialContext); 16 | return ( 17 | dispatch({ type: "toggle" })} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xstate-complete-getting-started-guide", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "tsc": "tsc -b" 9 | }, 10 | "dependencies": { 11 | "@xstate/react": "^4.1.1", 12 | "react": "^18.3.1", 13 | "react-dom": "^18.3.1", 14 | "react-router-dom": "^6.25.1", 15 | "xstate": "^5.16.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.3.3", 19 | "@types/react-dom": "^18.3.0", 20 | "@vitejs/plugin-react": "^4.3.1", 21 | "typescript": "^5.5.4", 22 | "vite": "^5.3.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/toggle/actor.tsx: -------------------------------------------------------------------------------- 1 | import { useActor } from "@xstate/react"; 2 | import { fromTransition } from "xstate"; 3 | 4 | type Event = { type: "toggle" }; 5 | type Context = boolean; 6 | const initialContext = false; 7 | 8 | const actor = fromTransition((context: Context, event: Event) => { 9 | if (event.type === "toggle") { 10 | return !context; 11 | } 12 | 13 | return context; 14 | }, initialContext); 15 | 16 | export default function Actor() { 17 | const [snapshot, send] = useActor(actor); 18 | return ( 19 | send({ type: "toggle" })} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/timer/machine/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { machine } from "./machine"; 3 | 4 | export default function Page() { 5 | const [snapshot, send] = useMachine(machine); 6 | return ( 7 |
8 |

Time left: {snapshot.context.time} seconds

9 | {snapshot.matches("Completed") ? ( 10 |

Done!

11 | ) : snapshot.matches("Paused") ? ( 12 | 13 | ) : ( 14 | 15 | )} 16 | 17 | {snapshot.matches("Paused") &&

Paused

} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/toggle/machine.tsx: -------------------------------------------------------------------------------- 1 | import { useActor } from "@xstate/react"; 2 | import { setup } from "xstate"; 3 | 4 | type Event = { type: "toggle" }; 5 | 6 | const machine = setup({ 7 | types: { 8 | events: {} as Event, 9 | }, 10 | }).createMachine({ 11 | initial: "Off", 12 | states: { 13 | Off: { 14 | on: { 15 | toggle: { target: "On" }, 16 | }, 17 | }, 18 | On: { 19 | on: { 20 | toggle: { target: "Off" }, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | export default function Machine() { 27 | const [snapshot, send] = useActor(machine); 28 | return ( 29 | send({ type: "toggle" })} 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/npc/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { machine } from "./machine"; 3 | 4 | export default function Page() { 5 | const [snapshot] = useMachine(machine); 6 | return ( 7 |
8 |

9 | {snapshot.context.mousePosition[0]},{snapshot.context.mousePosition[1]} 10 |

11 |

{snapshot.value}

12 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedIndexedAccess": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /src/notifications/machine/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { machine } from "./machine"; 3 | 4 | export default function Page() { 5 | const [snapshot, send] = useMachine(machine); 6 | return ( 7 |
8 | {snapshot.matches("Loading") ? ( 9 | Loading... 10 | ) : snapshot.matches("Error") ? ( 11 | <> 12 | {snapshot.context.errorMessage} 13 | {snapshot.can({ type: "fetch" }) && ( 14 | 15 | )} 16 | 17 | ) : ( 18 | <> 19 | {snapshot.context.notifications.map((notification) => ( 20 |

{notification.message}

21 | ))} 22 | 23 | )} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/form/use-state.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { initialContext, postRequest, type Context } from "./shared"; 3 | 4 | export default function UseState() { 5 | const [context, setContext] = useState(initialContext); 6 | const [loading, setLoading] = useState(false); 7 | 8 | const onUpdateUsername = (value: string) => { 9 | setContext({ username: value }); 10 | }; 11 | 12 | const onSubmit = async (event: React.FormEvent) => { 13 | event.preventDefault(); 14 | if (!loading) { 15 | setLoading(true); 16 | await postRequest(context); 17 | setLoading(false); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 | onUpdateUsername(e.target.value)} 27 | /> 28 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/toggle/machine-context.tsx: -------------------------------------------------------------------------------- 1 | import { useActor } from "@xstate/react"; 2 | import { assign, setup } from "xstate"; 3 | 4 | type Event = { type: "toggle" }; 5 | type Context = { toggleValue: boolean }; 6 | const initialContext: Context = { toggleValue: false }; 7 | 8 | const machine = setup({ 9 | types: { 10 | events: {} as Event, 11 | context: {} as Context, 12 | }, 13 | }).createMachine({ 14 | context: initialContext, 15 | initial: "Idle", 16 | states: { 17 | Idle: { 18 | on: { 19 | toggle: { 20 | target: "Idle", 21 | actions: assign(({ context }) => ({ 22 | toggleValue: !context.toggleValue, 23 | })), 24 | }, 25 | }, 26 | }, 27 | }, 28 | }); 29 | 30 | export default function MachineContext() { 31 | const [snapshot, send] = useActor(machine); 32 | return ( 33 | send({ type: "toggle" })} 37 | /> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/mount/use-state.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { initialContext, searchRequest, type Context } from "./shared"; 3 | 4 | export default function UseState() { 5 | const [context, setContext] = useState(initialContext); 6 | 7 | const submitSearch = async () => { 8 | const newPosts = await searchRequest(context.query); 9 | setContext({ ...context, posts: newPosts }); 10 | }; 11 | 12 | useEffect(() => { 13 | submitSearch(); 14 | }, []); 15 | 16 | return ( 17 |
18 |
19 | setContext({ ...context, query: e.target.value })} 23 | /> 24 | 27 |
28 | 29 | {context.posts.map((post) => ( 30 |
31 |

{post.title}

32 |

{post.body}

33 |
34 | ))} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/movie-search/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { machine } from "./machine"; 3 | 4 | export default function Page() { 5 | const [snapshot, send] = useMachine(machine); 6 | return ( 7 |
8 |
9 | 13 | send({ type: "updated-search-text", value: e.target.value }) 14 | } 15 | /> 16 |
17 | 18 | {snapshot.matches("Searching") && Loading...} 19 | {snapshot.matches("SearchError") && ( 20 | {snapshot.context.searchError} 21 | )} 22 | 23 | {snapshot.matches("OpenedMovie") ? ( 24 |
25 |

{snapshot.context.openedMovie?.name}

26 | 29 |
30 | ) : ( 31 | snapshot.context.movies.map((movie) => ( 32 | 39 | )) 40 | )} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/form-complete/use-state.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { initialContext, postRequest, type Context } from "./shared"; 3 | 4 | type State = "Editing" | "Loading" | "Error" | "Complete"; 5 | 6 | export default function UseState() { 7 | const [context, setContext] = useState(initialContext); 8 | const [state, setState] = useState("Editing"); 9 | 10 | const onUpdateUsername = (value: string) => { 11 | setContext({ username: value }); 12 | }; 13 | 14 | const onSubmit = async (event: React.FormEvent) => { 15 | event.preventDefault(); 16 | if (state !== "Loading" && state !== "Complete") { 17 | setState("Loading"); 18 | try { 19 | await postRequest(context); 20 | setState("Complete"); 21 | } catch (_) { 22 | setState("Error"); 23 | } 24 | } 25 | }; 26 | 27 | if (state === "Complete") { 28 | return

Done

; 29 | } 30 | 31 | return ( 32 |
33 | onUpdateUsername(e.target.value)} 37 | /> 38 | 41 | {state === "Error" &&

Error occurred

} 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/modal/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine, useSelector } from "@xstate/react"; 2 | import type { ActorRefFrom } from "xstate"; 3 | import { modalMachine, type modalContentMachine } from "./machine"; 4 | 5 | const ModalContent = ({ 6 | actor, 7 | }: { 8 | actor: ActorRefFrom; 9 | }) => { 10 | const context = useSelector(actor, (snapshot) => snapshot.context); 11 | return ( 12 |
13 | 17 | actor.send({ type: "update-message", value: e.target.value }) 18 | } 19 | /> 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default function Page() { 27 | const [snapshot, send] = useMachine(modalMachine); 28 | return ( 29 |
30 | 31 | 32 | {snapshot.context.receivedMessage !== null && ( 33 |

Received message: {snapshot.context.receivedMessage}

34 | )} 35 | 36 | 37 | {snapshot.children.modalContent !== undefined && ( 38 | 39 | )} 40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/form/use-reducer.tsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | import { initialContext, postRequest, type Context } from "./shared"; 3 | 4 | type Event = 5 | | { type: "update-username"; value: string } 6 | | { type: "update-loading"; value: boolean }; 7 | 8 | type ReducerContext = Context & { 9 | loading: boolean; 10 | }; 11 | 12 | const reducer = (context: ReducerContext, event: Event): ReducerContext => { 13 | if (event.type === "update-username") { 14 | return { ...context, username: event.value }; 15 | } else if (event.type === "update-loading") { 16 | return { ...context, loading: event.value }; 17 | } 18 | return context; 19 | }; 20 | 21 | export default function UseReducer() { 22 | const [context, dispatch] = useReducer(reducer, { 23 | ...initialContext, 24 | loading: false, 25 | }); 26 | 27 | const onSubmit = async (event: React.FormEvent) => { 28 | event.preventDefault(); 29 | if (!context.loading) { 30 | dispatch({ type: "update-loading", value: true }); 31 | await postRequest(context); 32 | dispatch({ type: "update-loading", value: false }); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 42 | dispatch({ type: "update-username", value: e.target.value }) 43 | } 44 | /> 45 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/mount/use-reducer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from "react"; 2 | import { 3 | initialContext, 4 | searchRequest, 5 | type Context, 6 | type Post, 7 | } from "./shared"; 8 | 9 | type Event = 10 | | { type: "update-query"; value: string } 11 | | { type: "update-posts"; newPosts: Post[] }; 12 | 13 | const reducer = (context: Context, event: Event): Context => { 14 | if (event.type === "update-query") { 15 | return { ...context, query: event.value }; 16 | } else if (event.type === "update-posts") { 17 | return { ...context, posts: event.newPosts }; 18 | } 19 | 20 | return context; 21 | }; 22 | 23 | export default function UseReducer() { 24 | const [context, dispatch] = useReducer(reducer, initialContext); 25 | 26 | const submitSearch = async () => { 27 | const newPosts = await searchRequest(context.query); 28 | dispatch({ type: "update-posts", newPosts }); 29 | }; 30 | 31 | useEffect(() => { 32 | submitSearch(); 33 | }, []); 34 | 35 | return ( 36 |
37 |
38 | 42 | dispatch({ type: "update-query", value: e.target.value }) 43 | } 44 | /> 45 | 48 |
49 | 50 | {context.posts.map((post) => ( 51 |
52 |

{post.title}

53 |

{post.body}

54 |
55 | ))} 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/timer/use-state/page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | export default function Page() { 4 | const interval = useRef(null); 5 | const [time, setTime] = useState(10); 6 | const [isPaused, setIsPaused] = useState(false); 7 | const [isCompleted, setIsCompleted] = useState(false); 8 | useEffect(() => { 9 | if (interval.current !== null) { 10 | clearInterval(interval.current); 11 | } 12 | 13 | interval.current = setInterval(() => { 14 | setTime((t) => t - 1); 15 | }, 1000); 16 | }, []); 17 | 18 | useEffect(() => { 19 | if (time <= 0) { 20 | if (interval.current !== null) { 21 | clearInterval(interval.current); 22 | } 23 | 24 | if (isPaused) { 25 | setIsPaused(false); 26 | } 27 | 28 | setIsCompleted(true); 29 | } 30 | }, [time]); 31 | 32 | const onPause = () => { 33 | if (interval.current !== null && !isPaused && !isCompleted) { 34 | clearInterval(interval.current); 35 | setIsPaused(true); 36 | } 37 | }; 38 | 39 | const onRestart = () => { 40 | setIsPaused(false); 41 | interval.current = setInterval(() => { 42 | setTime((t) => t - 1); 43 | }, 1000); 44 | }; 45 | 46 | return ( 47 |
48 |

Time left: {time} seconds

49 | {isCompleted ? ( 50 | <> 51 | ) : isPaused ? ( 52 | 53 | ) : ( 54 | 55 | )} 56 | {isPaused && !isCompleted &&

Paused

} 57 | {isCompleted &&

Done!

} 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XState: Complete getting started guide 2 | This repository contains all the code for the course [XState: Complete Getting Started Guide](https://www.typeonce.dev/course/xstate-complete-getting-started-guide). 3 | 4 | The app is implemented using typescript. You can get started by forking/cloning the repository and installing the dependencies: 5 | 6 | ```sh 7 | pnpm install 8 | ``` 9 | 10 | *** 11 | 12 | ## Course content 13 | 14 | [`xstate`](https://xstate.js.org/) is the last state management library you will ever need. 15 | 16 | `xstate` leverages [state charts](https://stately.ai/docs/state-machines-and-statecharts) to model UI state: 17 | - json-like object to describe states and transitions 18 | - Define functions to execute between transitions (`actions`) 19 | - Orchestrate async effects with the [actor model](https://stately.ai/docs/actor-model) (`actors`) 20 | - Powerful typescript support 21 | - Framework agnostic 22 | 23 | ## Course content 24 | 25 | The course starts from the classic way of handling state and effects using `useState` and `useEffect`. We learn why this model is flawed and what problem it introduces. 26 | 27 | We then refactor the same logic using `xstate`. This allows to understand the state chart model and how to refactor plain react code to use `xstate`. 28 | 29 | The rest of the course will present different example of UI state implemented using `xstate`. Each module will introduce new features of `xstate` to show how we can build even complex use cases with ease. 30 | 31 | 32 | ### How the course is organized 33 | 34 | The course is organized in small self-contained lessons. Each lesson introduces 1 single new concept. 35 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 4 | import FormPage from "./form/page"; 5 | import ModalPage from "./modal/page"; 6 | import MountPage from "./mount/page"; 7 | import MoveSearchPage from "./movie-search/page"; 8 | import NotificationsMachinePage from "./notifications/machine/page"; 9 | import NotificationsUseStatePage from "./notifications/use-state/page"; 10 | import NpcPage from "./npc/page"; 11 | import TicTacToePage from "./tic-tac-toe/page"; 12 | import TimerMachinePage from "./timer/machine/page"; 13 | import TimerUseStatePage from "./timer/use-state/page"; 14 | import TogglePage from "./toggle/page"; 15 | 16 | const router = createBrowserRouter([ 17 | { 18 | path: "/timer/use-state", 19 | element: , 20 | }, 21 | { 22 | path: "/timer/machine", 23 | element: , 24 | }, 25 | { 26 | path: "/movie-search", 27 | element: , 28 | }, 29 | { 30 | path: "/tic-tac-toe", 31 | element: , 32 | }, 33 | { 34 | path: "/notifications/use-state", 35 | element: , 36 | }, 37 | { 38 | path: "/notifications/machine", 39 | element: , 40 | }, 41 | { 42 | path: "/modal", 43 | element: , 44 | }, 45 | { 46 | path: "/npc", 47 | element: , 48 | }, 49 | { 50 | path: "/form", 51 | element: , 52 | }, 53 | { 54 | path: "/toggle", 55 | element: , 56 | }, 57 | { 58 | path: "/mount", 59 | element: , 60 | }, 61 | ]); 62 | 63 | ReactDOM.createRoot(document.getElementById("root")!).render( 64 | 65 | 66 | 67 | ); 68 | -------------------------------------------------------------------------------- /src/timer/machine/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, fromCallback, sendTo, setup } from "xstate"; 2 | 3 | type TickTimeEvent = Readonly<{ type: "tick-time" }>; 4 | type PauseEvent = Readonly<{ type: "pause" }>; 5 | 6 | type CallbackEvent = PauseEvent; 7 | 8 | export const machine = setup({ 9 | types: { 10 | events: {} as Readonly<{ type: "restart" }> | TickTimeEvent | PauseEvent, 11 | context: {} as { 12 | time: number; 13 | interval: number | null; 14 | }, 15 | }, 16 | guards: { 17 | isCompleted: ({ context }) => context.time <= 0, 18 | }, 19 | actors: { 20 | interval: fromCallback(({ sendBack, receive }) => { 21 | const interval = setInterval(() => { 22 | sendBack({ type: "tick-time" } satisfies TickTimeEvent); 23 | }, 1000); 24 | 25 | const clear = () => { 26 | clearInterval(interval); 27 | }; 28 | 29 | receive((event) => { 30 | if (event.type) { 31 | clear(); 32 | } 33 | }); 34 | 35 | return clear; 36 | }), 37 | }, 38 | actions: { 39 | onTickTime: assign(({ context }) => ({ time: context.time - 1 })), 40 | onPauseTimer: sendTo("interval", { type: "pause" } satisfies CallbackEvent), 41 | }, 42 | }).createMachine({ 43 | id: "use-state-machine", 44 | context: { time: 10, interval: null }, 45 | initial: "Running", 46 | states: { 47 | Running: { 48 | invoke: { 49 | id: "interval", 50 | src: "interval", 51 | }, 52 | always: { 53 | target: "Completed", 54 | guard: "isCompleted", 55 | }, 56 | on: { 57 | pause: { target: "Paused" }, 58 | "tick-time": { actions: { type: "onTickTime" } }, 59 | }, 60 | }, 61 | Paused: { 62 | on: { 63 | restart: { target: "Running" }, 64 | }, 65 | }, 66 | Completed: { 67 | type: "final", 68 | }, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /src/form/machine.tsx: -------------------------------------------------------------------------------- 1 | import { useActor } from "@xstate/react"; 2 | import { assertEvent, assign, fromPromise, setup } from "xstate"; 3 | import { initialContext, postRequest, type Context } from "./shared"; 4 | 5 | type Event = 6 | | { type: "update-username"; username: string } 7 | | { type: "submit"; event: React.FormEvent }; 8 | 9 | const submitActor = fromPromise( 10 | async ({ 11 | input, 12 | }: { 13 | input: { event: React.FormEvent; context: Context }; 14 | }) => { 15 | input.event.preventDefault(); 16 | await postRequest(input.context); 17 | } 18 | ); 19 | 20 | const machine = setup({ 21 | types: { 22 | context: {} as Context, 23 | events: {} as Event, 24 | }, 25 | actors: { submitActor }, 26 | }).createMachine({ 27 | context: initialContext, 28 | initial: "Editing", 29 | states: { 30 | Editing: { 31 | on: { 32 | "update-username": { 33 | actions: assign(({ event }) => ({ 34 | username: event.username, 35 | })), 36 | }, 37 | submit: { target: "Loading" }, 38 | }, 39 | }, 40 | Loading: { 41 | invoke: { 42 | src: "submitActor", 43 | input: ({ event, context }) => { 44 | assertEvent(event, "submit"); 45 | return { event: event.event, context }; 46 | }, 47 | onDone: { target: "Complete" }, 48 | }, 49 | }, 50 | Complete: {}, 51 | }, 52 | }); 53 | 54 | export default function Machine() { 55 | const [snapshot, send] = useActor(machine); 56 | return ( 57 |
send({ type: "submit", event })}> 58 | 62 | send({ type: "update-username", username: e.target.value }) 63 | } 64 | /> 65 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/mount/machine.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { assign, fromPromise, setup } from "xstate"; 3 | import { 4 | initialContext, 5 | searchRequest, 6 | type Context, 7 | type Post, 8 | } from "./shared"; 9 | 10 | type Event = 11 | | { type: "update-query"; value: string } 12 | | { type: "submit-search" }; 13 | 14 | const searchingActor = fromPromise( 15 | async ({ input }: { input: { query: string } }): Promise => 16 | searchRequest(input.query) 17 | ); 18 | 19 | const machine = setup({ 20 | types: { 21 | events: {} as Event, 22 | context: {} as Context, 23 | }, 24 | actors: { searchingActor }, 25 | }).createMachine({ 26 | context: initialContext, 27 | initial: "Searching", 28 | states: { 29 | Searching: { 30 | invoke: { 31 | src: "searchingActor", 32 | input: ({ context }) => ({ query: context.query }), 33 | onDone: { 34 | target: "Idle", 35 | actions: assign(({ event }) => ({ 36 | posts: event.output, 37 | })), 38 | }, 39 | }, 40 | }, 41 | Idle: { 42 | on: { 43 | "update-query": { 44 | actions: assign(({ event }) => ({ 45 | query: event.value, 46 | })), 47 | }, 48 | "submit-search": { 49 | target: "Searching", 50 | }, 51 | }, 52 | }, 53 | }, 54 | }); 55 | 56 | export default function Machine() { 57 | const [snapshot, send] = useMachine(machine); 58 | return ( 59 |
60 |
61 | 65 | send({ type: "update-query", value: e.target.value }) 66 | } 67 | /> 68 | 71 |
72 | 73 | {snapshot.context.posts.map((post) => ( 74 |
75 |

{post.title}

76 |

{post.body}

77 |
78 | ))} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/form-complete/use-reducer.tsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | import { initialContext, postRequest, type Context } from "./shared"; 3 | 4 | type State = "Editing" | "Loading" | "Error" | "Complete"; 5 | 6 | type Event = 7 | | { type: "update-username"; value: string } 8 | | { type: "request-start" } 9 | | { type: "request-complete" } 10 | | { type: "request-fail" }; 11 | 12 | type ReducerContext = Context & { 13 | state: State; 14 | }; 15 | 16 | const reducer = (context: ReducerContext, event: Event): ReducerContext => { 17 | if (event.type === "update-username") { 18 | return { ...context, username: event.value }; 19 | } else if (event.type === "request-start") { 20 | return { ...context, state: "Loading" }; 21 | } else if (event.type === "request-complete") { 22 | return { ...context, state: "Complete" }; 23 | } else if (event.type === "request-fail") { 24 | return { ...context, state: "Error" }; 25 | } 26 | return context; 27 | }; 28 | 29 | export default function UseReducer() { 30 | const [context, dispatch] = useReducer(reducer, { 31 | ...initialContext, 32 | state: "Editing", 33 | }); 34 | 35 | const onSubmit = async (event: React.FormEvent) => { 36 | event.preventDefault(); 37 | if (context.state !== "Loading" && context.state !== "Complete") { 38 | dispatch({ type: "request-start" }); 39 | try { 40 | await postRequest(context); 41 | dispatch({ type: "request-complete" }); 42 | } catch (_) { 43 | dispatch({ type: "request-fail" }); 44 | } 45 | } 46 | }; 47 | 48 | if (context.state === "Complete") { 49 | return

Done

; 50 | } 51 | 52 | return ( 53 |
54 | 58 | dispatch({ type: "update-username", value: e.target.value }) 59 | } 60 | /> 61 | 64 | {context.state === "Error" &&

Error occurred

} 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/notifications/use-state/page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | class ApiError { 4 | status: number; 5 | constructor(status: number) { 6 | this.status = status; 7 | } 8 | } 9 | 10 | interface Notification { 11 | id: string; 12 | message: string; 13 | } 14 | 15 | export default function Page() { 16 | const [notifications, setNotifications] = useState([]); 17 | const [open, setOpen] = useState(false); 18 | const [loading, setLoading] = useState(false); 19 | const [error, setError] = useState(""); 20 | const [retry, setRetry] = useState(true); 21 | 22 | const onLoadNotification = async () => { 23 | if (!loading) { 24 | try { 25 | setError(""); 26 | setLoading(true); 27 | const response = await new Promise((resolve) => { 28 | setTimeout(() => { 29 | resolve([ 30 | { id: "1", message: "It's time to work" }, 31 | { id: "2", message: "Up for a meeting?" }, 32 | ]); 33 | }, 1200); 34 | }); 35 | 36 | setOpen(true); 37 | setNotifications(response); 38 | } catch (e) { 39 | if (e instanceof ApiError && e.status >= 500) { 40 | setRetry(false); 41 | setError("Error while loading notifications"); 42 | } else { 43 | setError("Error while loading notifications, please retry"); 44 | } 45 | } finally { 46 | setLoading(false); 47 | } 48 | } 49 | }; 50 | 51 | useEffect(() => { 52 | onLoadNotification(); 53 | }, []); 54 | 55 | return ( 56 |
57 | {!open ? ( 58 | <> 59 | {loading && Loading...} 60 | {error.length > 0 && ( 61 | <> 62 | {error} 63 | {retry && } 64 | 65 | )} 66 | 67 | ) : ( 68 | <> 69 | {notifications.map((notification) => ( 70 |

{notification.message}

71 | ))} 72 | 73 | )} 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/notifications/machine/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, fromPromise, setup } from "xstate"; 2 | 3 | class ApiError { 4 | status: number; 5 | constructor(status: number) { 6 | this.status = status; 7 | } 8 | } 9 | 10 | interface Notification { 11 | id: string; 12 | message: string; 13 | } 14 | 15 | const getNotifications = fromPromise( 16 | () => 17 | new Promise((resolve) => { 18 | setTimeout(() => { 19 | resolve([ 20 | { id: "1", message: "It's time to work" }, 21 | { id: "2", message: "Up for a meeting?" }, 22 | ]); 23 | }, 1200); 24 | }) 25 | ); 26 | 27 | export const machine = setup({ 28 | types: { 29 | events: {} as { type: "fetch" }, 30 | context: {} as { 31 | notifications: Notification[]; 32 | errorMessage: string; 33 | status: number | null; 34 | }, 35 | }, 36 | guards: { 37 | canReload: ({ context }) => context.status === null || context.status < 500, 38 | }, 39 | actors: { getNotifications }, 40 | actions: { 41 | onUpdateErrorMessage: assign((_, { error }: { error: unknown }) => ({ 42 | status: error instanceof ApiError ? error.status : null, 43 | errorMessage: "Error while loading notifications", 44 | })), 45 | onUpdateNotifications: assign( 46 | (_, { notifications }: { notifications: Notification[] }) => ({ 47 | notifications, 48 | }) 49 | ), 50 | }, 51 | }).createMachine({ 52 | context: { 53 | status: null, 54 | errorMessage: "", 55 | notifications: [ 56 | { id: "1", message: "It's time to work" }, 57 | { id: "2", message: "Up for a meeting?" }, 58 | ], 59 | }, 60 | initial: "Loading", 61 | states: { 62 | Loading: { 63 | invoke: { 64 | src: "getNotifications", 65 | onError: { 66 | target: "Error", 67 | actions: { 68 | type: "onUpdateErrorMessage", 69 | params: ({ event }) => ({ error: event.error }), 70 | }, 71 | }, 72 | onDone: { 73 | target: "Opened", 74 | actions: { 75 | type: "onUpdateNotifications", 76 | params: ({ event }) => ({ notifications: event.output }), 77 | }, 78 | }, 79 | }, 80 | }, 81 | Opened: {}, 82 | Error: { 83 | on: { 84 | fetch: { guard: "canReload", target: "Loading" }, 85 | }, 86 | }, 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /src/form-complete/machine.tsx: -------------------------------------------------------------------------------- 1 | import { useActor } from "@xstate/react"; 2 | import { assertEvent, assign, fromPromise, setup } from "xstate"; 3 | import { initialContext, postRequest, type Context } from "./shared"; 4 | 5 | type Event = 6 | | { type: "update-username"; username: string } 7 | | { type: "submit"; event: React.FormEvent }; 8 | 9 | const submitActor = fromPromise( 10 | async ({ 11 | input, 12 | }: { 13 | input: { event: React.FormEvent; context: Context }; 14 | }) => { 15 | input.event.preventDefault(); 16 | await postRequest(input.context); 17 | } 18 | ); 19 | 20 | const machine = setup({ 21 | types: { 22 | context: {} as Context, 23 | events: {} as Event, 24 | }, 25 | actors: { submitActor }, 26 | }).createMachine({ 27 | context: initialContext, 28 | initial: "Editing", 29 | states: { 30 | Editing: { 31 | on: { 32 | "update-username": { 33 | actions: assign(({ event }) => ({ 34 | username: event.username, 35 | })), 36 | }, 37 | submit: { target: "Loading" }, 38 | }, 39 | }, 40 | Loading: { 41 | invoke: { 42 | src: "submitActor", 43 | input: ({ event, context }) => { 44 | assertEvent(event, "submit"); 45 | return { event: event.event, context }; 46 | }, 47 | onError: { target: "Error" }, 48 | onDone: { target: "Complete" }, 49 | }, 50 | }, 51 | Error: { 52 | on: { 53 | "update-username": { 54 | actions: assign(({ event }) => ({ 55 | username: event.username, 56 | })), 57 | }, 58 | submit: { target: "Loading" }, 59 | }, 60 | }, 61 | Complete: {}, 62 | }, 63 | }); 64 | 65 | export default function Machine() { 66 | const [snapshot, send] = useActor(machine); 67 | 68 | if (snapshot.matches("Complete")) { 69 | return

Done

; 70 | } 71 | 72 | return ( 73 |
send({ type: "submit", event })}> 74 | 78 | send({ type: "update-username", username: e.target.value }) 79 | } 80 | /> 81 | 84 | {snapshot.matches("Error") &&

Error occurred

} 85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/modal/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, sendParent, setup } from "xstate"; 2 | 3 | type SendMessageEvent = Readonly<{ type: "send-message"; message: string }>; 4 | type CloseEvent = Readonly<{ type: "close" }>; 5 | 6 | export const modalContentMachine = setup({ 7 | types: { 8 | events: {} as 9 | | Readonly<{ type: "update-message"; value: string }> 10 | | CloseEvent 11 | | Readonly<{ type: "send" }>, 12 | context: {} as Readonly<{ 13 | message: string; 14 | }>, 15 | }, 16 | actions: { 17 | onUpdateMessage: assign((_, { value }: { value: string }) => ({ 18 | message: value, 19 | })), 20 | onSendMessage: sendParent( 21 | ({ context }) => 22 | ({ 23 | type: "send-message", 24 | message: context.message, 25 | } satisfies SendMessageEvent) 26 | ), 27 | onClose: sendParent({ 28 | type: "close", 29 | } satisfies CloseEvent), 30 | }, 31 | }).createMachine({ 32 | id: "modal-content-machine", 33 | context: { message: "" }, 34 | initial: "Idle", 35 | states: { 36 | Idle: { 37 | on: { 38 | "update-message": { 39 | actions: { type: "onUpdateMessage", params: ({ event }) => event }, 40 | }, 41 | send: { 42 | actions: { type: "onSendMessage" }, 43 | }, 44 | close: { 45 | actions: { type: "onClose" }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | export const modalMachine = setup({ 53 | types: { 54 | events: {} as Readonly<{ type: "open" }> | SendMessageEvent | CloseEvent, 55 | context: {} as { receivedMessage: string | null }, 56 | children: {} as { modalContent: "modalContent" }, 57 | }, 58 | actors: { 59 | modalContent: modalContentMachine, 60 | }, 61 | actions: { 62 | onGetMessage: assign((_, { message }: { message: string }) => ({ 63 | receivedMessage: message, 64 | })), 65 | }, 66 | }).createMachine({ 67 | id: "modal-machine", 68 | context: { receivedMessage: null }, 69 | initial: "Closed", 70 | states: { 71 | Closed: { 72 | on: { 73 | open: { target: "Opened" }, 74 | }, 75 | }, 76 | Opened: { 77 | invoke: { 78 | id: "modalContent", 79 | src: "modalContent", 80 | }, 81 | on: { 82 | close: { target: "Closed" }, 83 | "send-message": { 84 | target: "Closed", 85 | actions: { type: "onGetMessage", params: ({ event }) => event }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /src/npc/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, fromCallback, not, setup } from "xstate"; 2 | 3 | type MouseMoveEvent = Readonly<{ 4 | type: "mouse-move"; 5 | position: [number, number]; 6 | }>; 7 | 8 | type MouseNpcEvent = Readonly<{ type: "npc-move" }>; 9 | 10 | const interpolate = ( 11 | from: [number, number], 12 | to: [number, number], 13 | frac: number 14 | ): [number, number] => { 15 | const nx = from[0] + (to[0] - from[0]) * frac; 16 | const ny = from[1] + (to[1] - from[1]) * frac; 17 | return [nx, ny]; 18 | }; 19 | 20 | const pointDistance = ( 21 | from: [number, number], 22 | to: [number, number] 23 | ): number => { 24 | const x = from[0] - to[0]; 25 | const y = from[1] - to[1]; 26 | return Math.sqrt(x * x + y * y); 27 | }; 28 | 29 | export const machine = setup({ 30 | types: { 31 | events: {} as MouseMoveEvent | MouseNpcEvent, 32 | context: {} as { 33 | position: [number, number]; 34 | mousePosition: [number, number]; 35 | }, 36 | }, 37 | actors: { 38 | moveMouse: fromCallback(({ sendBack }) => { 39 | const event = (e: MouseEvent) => 40 | sendBack({ 41 | type: "mouse-move", 42 | position: [e.clientX, e.clientY], 43 | } satisfies MouseMoveEvent); 44 | 45 | document.addEventListener("mousemove", event); 46 | return () => { 47 | document.removeEventListener("mousemove", event); 48 | }; 49 | }), 50 | moveNcp: fromCallback(({ sendBack }) => { 51 | const interval = setInterval(() => { 52 | sendBack({ type: "npc-move" } satisfies MouseNpcEvent); 53 | }, 100); 54 | 55 | return () => { 56 | clearInterval(interval); 57 | }; 58 | }), 59 | }, 60 | actions: { 61 | onMouseMove: assign((_, { position }: { position: [number, number] }) => ({ 62 | mousePosition: position, 63 | })), 64 | onNpcMove: assign(({ context }) => ({ 65 | position: interpolate(context.position, context.mousePosition, 0.1), 66 | })), 67 | }, 68 | guards: { 69 | isNearby: ({ context }) => 70 | pointDistance(context.position, context.mousePosition) <= 200, 71 | canAttack: ({ context }) => 72 | pointDistance(context.position, context.mousePosition) <= 30, 73 | }, 74 | }).createMachine({ 75 | id: "npc-machine", 76 | context: { position: [0, 0], mousePosition: [0, 0] }, 77 | initial: "Idle", 78 | invoke: { src: "moveMouse" }, 79 | on: { 80 | "mouse-move": { 81 | actions: { type: "onMouseMove", params: ({ event }) => event }, 82 | }, 83 | }, 84 | states: { 85 | Idle: { 86 | always: { 87 | target: "Chasing", 88 | guard: "isNearby", 89 | }, 90 | }, 91 | Chasing: { 92 | invoke: { src: "moveNcp" }, 93 | always: [ 94 | { 95 | target: "Idle", 96 | guard: not("isNearby"), 97 | }, 98 | { 99 | target: "Attacking", 100 | guard: "canAttack", 101 | }, 102 | ], 103 | on: { 104 | "npc-move": { 105 | actions: { type: "onNpcMove" }, 106 | }, 107 | }, 108 | }, 109 | Attacking: { 110 | always: { 111 | target: "Chasing", 112 | guard: not("canAttack"), 113 | }, 114 | }, 115 | }, 116 | }); 117 | -------------------------------------------------------------------------------- /src/tic-tac-toe/page.tsx: -------------------------------------------------------------------------------- 1 | import { useMachine } from "@xstate/react"; 2 | import { machine } from "./machine"; 3 | 4 | export default function Page() { 5 | const [snapshot, send] = useMachine(machine); 6 | return ( 7 |
8 | {snapshot.matches("ChooseNames") ? ( 9 |
10 | 14 | send({ type: "update-name-1", name: e.target.value }) 15 | } 16 | /> 17 | 21 | send({ type: "update-name-2", name: e.target.value }) 22 | } 23 | /> 24 | 31 |
32 | ) : ( 33 |
34 | {snapshot.matches("Player1Win") || 35 | snapshot.matches("Player2Win") || 36 | snapshot.matches("NoWinner") ? ( 37 |
38 |

39 | {snapshot.matches("Player1Win") 40 | ? snapshot.context.player1Name 41 | : snapshot.matches("Player2Win") 42 | ? snapshot.context.player2Name 43 | : "No one"}{" "} 44 | won! 45 |

46 | 52 |
53 | ) : ( 54 |

55 | {snapshot.matches("Player1") 56 | ? snapshot.context.player1Name 57 | : snapshot.context.player2Name}{" "} 58 | is your turn! 59 |

60 | )} 61 | 62 |
63 |
70 | {snapshot.context.board.map((cell, i) => ( 71 | 97 | ))} 98 |
99 |
100 |
101 | )} 102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/movie-search/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, fromPromise, setup } from "xstate"; 2 | 3 | interface Movie { 4 | id: string; 5 | name: string; 6 | } 7 | 8 | const moviesDatabase = [ 9 | { id: "1", name: "Shadows of the Past" }, 10 | { id: "2", name: "The Lost City of Eldrador" }, 11 | { id: "3", name: "Realms of Wonder" }, 12 | { id: "4", name: "Echoes of Eternity" }, 13 | { id: "5", name: "Stellar Horizon" }, 14 | { id: "6", name: "The Great Prank War" }, 15 | { id: "7", name: "The Weight of Memories" }, 16 | { id: "8", name: "Love in the Time of Sunset" }, 17 | { id: "9", name: "Whispers of the Heart" }, 18 | { id: "10", name: "Quantum Rift" }, 19 | ] satisfies Movie[]; 20 | 21 | export const machine = setup({ 22 | types: { 23 | events: {} as 24 | | Readonly<{ type: "updated-search-text"; value: string }> 25 | | Readonly<{ type: "open-movie"; movieId: Movie["id"] }> 26 | | Readonly<{ type: "close-movie" }>, 27 | context: {} as Readonly<{ 28 | searchText: string; 29 | searchError: string; 30 | movies: Movie[]; 31 | openedMovie: Movie | null; 32 | }>, 33 | }, 34 | actors: { 35 | searching: fromPromise( 36 | ({ input }: { input: { searchText: string } }) => 37 | new Promise((resolve, reject) => 38 | Math.random() < 0.1 39 | ? reject(new Error("Error while searching movie")) 40 | : setTimeout(() => { 41 | resolve( 42 | moviesDatabase.filter((movie) => 43 | movie.name 44 | .toLowerCase() 45 | .includes(input.searchText.toLowerCase()) 46 | ) 47 | ); 48 | }, 1000) 49 | ) 50 | ), 51 | }, 52 | actions: { 53 | onUpdatedSearchText: assign((_, { value }: { value: string }) => ({ 54 | searchText: value, 55 | })), 56 | onSearchedMovie: assign((_, { value }: { value: Movie[] }) => ({ 57 | movies: value, 58 | })), 59 | onSearchedError: assign((_, { value }: { value: unknown }) => ({ 60 | searchError: value instanceof Error ? value.message : "Unknown error", 61 | })), 62 | onMovieOpened: assign( 63 | ({ context }, { movieId }: { movieId: Movie["id"] }) => ({ 64 | openedMovie: 65 | context.movies.find((movie) => movie.id === movieId) ?? null, 66 | }) 67 | ), 68 | onMovieClosed: assign({ openedMovie: null }), 69 | }, 70 | }).createMachine({ 71 | id: "movie-search-machine", 72 | context: { searchText: "", searchError: "", movies: [], openedMovie: null }, 73 | initial: "Idle", 74 | on: { 75 | "updated-search-text": { 76 | target: ".UpdatedSearch", 77 | actions: { type: "onUpdatedSearchText", params: ({ event }) => event }, 78 | }, 79 | "open-movie": { 80 | target: ".OpenedMovie", 81 | actions: { type: "onMovieOpened", params: ({ event }) => event }, 82 | }, 83 | }, 84 | states: { 85 | Idle: {}, 86 | UpdatedSearch: { 87 | after: { 88 | 600: { 89 | target: "Searching", 90 | }, 91 | }, 92 | }, 93 | Searching: { 94 | invoke: { 95 | src: "searching", 96 | input: ({ context }) => ({ searchText: context.searchText }), 97 | onError: { 98 | target: "SearchError", 99 | actions: { 100 | type: "onSearchedError", 101 | params: ({ event }) => ({ value: event.error }), 102 | }, 103 | }, 104 | onDone: { 105 | target: "Idle", 106 | actions: { 107 | type: "onSearchedMovie", 108 | params: ({ event }) => ({ value: event.output }), 109 | }, 110 | }, 111 | }, 112 | }, 113 | SearchError: {}, 114 | OpenedMovie: { 115 | on: { 116 | "close-movie": { 117 | target: "Idle", 118 | actions: { type: "onMovieClosed" }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }); 124 | -------------------------------------------------------------------------------- /src/tic-tac-toe/machine.ts: -------------------------------------------------------------------------------- 1 | import { assign, setup } from "xstate"; 2 | 3 | type Cell = 1 | 2 | null; 4 | 5 | type Board = [Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell]; 6 | const initBoard: Board = [ 7 | null, 8 | null, 9 | null, 10 | // row2 11 | null, 12 | null, 13 | null, 14 | // row3 15 | null, 16 | null, 17 | null, 18 | ]; 19 | 20 | const checkWinner = (board: Board) => 21 | board[0] !== null && 22 | ((board[0] === board[1] && board[0] === board[2]) || 23 | (board[0] === board[4] && board[0] === board[8]) || 24 | (board[0] === board[3] && board[0] === board[6])) 25 | ? board[0] 26 | : board[1] !== null && board[1] === board[4] && board[1] === board[7] 27 | ? board[1] 28 | : board[2] !== null && 29 | ((board[2] === board[5] && board[2] === board[8]) || 30 | (board[2] === board[4] && board[2] === board[6])) 31 | ? board[2] 32 | : null; 33 | 34 | export const machine = setup({ 35 | types: { 36 | context: {} as Readonly<{ 37 | player1Name: string; 38 | player2Name: string; 39 | board: Board; 40 | }>, 41 | events: {} as 42 | | Readonly<{ type: "update-name-1"; name: string }> 43 | | Readonly<{ type: "update-name-2"; name: string }> 44 | | Readonly<{ type: "start-game" }> 45 | | Readonly<{ type: "restart-game" }> 46 | | Readonly<{ 47 | type: "pick-cell"; 48 | player: 1 | 2; 49 | cell: number; 50 | }>, 51 | }, 52 | guards: { 53 | isValidNames: ({ context }) => 54 | context.player1Name.length > 0 && 55 | context.player2Name.length > 0 && 56 | context.player1Name !== context.player2Name, 57 | canPickCell: ({ context }, { cell }: { cell: number }) => 58 | context.board[cell] === null, 59 | isComplete: ({ context }) => context.board.every((cell) => cell !== null), 60 | hasWinner: ({ context }) => checkWinner(context.board) !== null, 61 | }, 62 | actions: { 63 | onUpdateName1: assign((_, { name }: { name: string }) => ({ 64 | player1Name: name, 65 | })), 66 | onUpdateName2: assign((_, { name }: { name: string }) => ({ 67 | player2Name: name, 68 | })), 69 | onRestartGame: assign({ board: initBoard }), 70 | onPickCell: assign( 71 | ({ context }, { cell, player }: { player: 1 | 2; cell: number }) => ({ 72 | board: context.board.map((current, i) => 73 | i !== cell ? current : player 74 | ) as Board, 75 | }) 76 | ), 77 | }, 78 | }).createMachine({ 79 | id: "tic-tac-toe-machine", 80 | context: { 81 | player1Name: "", 82 | player2Name: "", 83 | board: initBoard, 84 | }, 85 | initial: "ChooseNames", 86 | states: { 87 | ChooseNames: { 88 | on: { 89 | "update-name-1": { 90 | actions: { type: "onUpdateName1", params: ({ event }) => event }, 91 | }, 92 | "update-name-2": { 93 | actions: { type: "onUpdateName2", params: ({ event }) => event }, 94 | }, 95 | "start-game": { 96 | target: "Player1", 97 | guard: "isValidNames", 98 | }, 99 | }, 100 | }, 101 | Player1: { 102 | always: [ 103 | { 104 | guard: "hasWinner", 105 | target: "Player2Win", 106 | }, 107 | { 108 | guard: "isComplete", 109 | target: "NoWinner", 110 | }, 111 | ], 112 | on: { 113 | "pick-cell": { 114 | target: "Player2", 115 | guard: { 116 | type: "canPickCell", 117 | params: ({ event }) => ({ cell: event.cell }), 118 | }, 119 | actions: { type: "onPickCell", params: ({ event }) => event }, 120 | }, 121 | }, 122 | }, 123 | Player2: { 124 | always: [ 125 | { 126 | guard: "hasWinner", 127 | target: "Player1Win", 128 | }, 129 | { 130 | guard: "isComplete", 131 | target: "NoWinner", 132 | }, 133 | ], 134 | on: { 135 | "pick-cell": { 136 | target: "Player1", 137 | guard: { 138 | type: "canPickCell", 139 | params: ({ event }) => ({ cell: event.cell }), 140 | }, 141 | actions: { type: "onPickCell", params: ({ event }) => event }, 142 | }, 143 | }, 144 | }, 145 | NoWinner: { 146 | on: { 147 | "restart-game": { 148 | target: "ChooseNames", 149 | actions: { type: "onRestartGame" }, 150 | }, 151 | }, 152 | }, 153 | Player1Win: { 154 | on: { 155 | "restart-game": { 156 | target: "ChooseNames", 157 | actions: { type: "onRestartGame" }, 158 | }, 159 | }, 160 | }, 161 | Player2Win: { 162 | on: { 163 | "restart-game": { 164 | target: "ChooseNames", 165 | actions: { type: "onRestartGame" }, 166 | }, 167 | }, 168 | }, 169 | }, 170 | }); 171 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@xstate/react': 12 | specifier: ^4.1.1 13 | version: 4.1.1(@types/react@18.3.3)(react@18.3.1)(xstate@5.16.0) 14 | react: 15 | specifier: ^18.3.1 16 | version: 18.3.1 17 | react-dom: 18 | specifier: ^18.3.1 19 | version: 18.3.1(react@18.3.1) 20 | react-router-dom: 21 | specifier: ^6.25.1 22 | version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 23 | xstate: 24 | specifier: ^5.16.0 25 | version: 5.16.0 26 | devDependencies: 27 | '@types/react': 28 | specifier: ^18.3.3 29 | version: 18.3.3 30 | '@types/react-dom': 31 | specifier: ^18.3.0 32 | version: 18.3.0 33 | '@vitejs/plugin-react': 34 | specifier: ^4.3.1 35 | version: 4.3.1(vite@5.3.5) 36 | typescript: 37 | specifier: ^5.5.4 38 | version: 5.5.4 39 | vite: 40 | specifier: ^5.3.4 41 | version: 5.3.5 42 | 43 | packages: 44 | 45 | '@ampproject/remapping@2.3.0': 46 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 47 | engines: {node: '>=6.0.0'} 48 | 49 | '@babel/code-frame@7.24.7': 50 | resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} 51 | engines: {node: '>=6.9.0'} 52 | 53 | '@babel/compat-data@7.25.0': 54 | resolution: {integrity: sha512-P4fwKI2mjEb3ZU5cnMJzvRsRKGBUcs8jvxIoRmr6ufAY9Xk2Bz7JubRTTivkw55c7WQJfTECeqYVa+HZ0FzREg==} 55 | engines: {node: '>=6.9.0'} 56 | 57 | '@babel/core@7.24.9': 58 | resolution: {integrity: sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==} 59 | engines: {node: '>=6.9.0'} 60 | 61 | '@babel/generator@7.25.0': 62 | resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} 63 | engines: {node: '>=6.9.0'} 64 | 65 | '@babel/helper-compilation-targets@7.24.8': 66 | resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} 67 | engines: {node: '>=6.9.0'} 68 | 69 | '@babel/helper-module-imports@7.24.7': 70 | resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} 71 | engines: {node: '>=6.9.0'} 72 | 73 | '@babel/helper-module-transforms@7.25.0': 74 | resolution: {integrity: sha512-bIkOa2ZJYn7FHnepzr5iX9Kmz8FjIz4UKzJ9zhX3dnYuVW0xul9RuR3skBfoLu+FPTQw90EHW9rJsSZhyLQ3fQ==} 75 | engines: {node: '>=6.9.0'} 76 | peerDependencies: 77 | '@babel/core': ^7.0.0 78 | 79 | '@babel/helper-plugin-utils@7.24.8': 80 | resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} 81 | engines: {node: '>=6.9.0'} 82 | 83 | '@babel/helper-simple-access@7.24.7': 84 | resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} 85 | engines: {node: '>=6.9.0'} 86 | 87 | '@babel/helper-string-parser@7.24.8': 88 | resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} 89 | engines: {node: '>=6.9.0'} 90 | 91 | '@babel/helper-validator-identifier@7.24.7': 92 | resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} 93 | engines: {node: '>=6.9.0'} 94 | 95 | '@babel/helper-validator-option@7.24.8': 96 | resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} 97 | engines: {node: '>=6.9.0'} 98 | 99 | '@babel/helpers@7.25.0': 100 | resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} 101 | engines: {node: '>=6.9.0'} 102 | 103 | '@babel/highlight@7.24.7': 104 | resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} 105 | engines: {node: '>=6.9.0'} 106 | 107 | '@babel/parser@7.25.0': 108 | resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} 109 | engines: {node: '>=6.0.0'} 110 | hasBin: true 111 | 112 | '@babel/plugin-transform-react-jsx-self@7.24.7': 113 | resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} 114 | engines: {node: '>=6.9.0'} 115 | peerDependencies: 116 | '@babel/core': ^7.0.0-0 117 | 118 | '@babel/plugin-transform-react-jsx-source@7.24.7': 119 | resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} 120 | engines: {node: '>=6.9.0'} 121 | peerDependencies: 122 | '@babel/core': ^7.0.0-0 123 | 124 | '@babel/template@7.25.0': 125 | resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} 126 | engines: {node: '>=6.9.0'} 127 | 128 | '@babel/traverse@7.25.0': 129 | resolution: {integrity: sha512-ubALThHQy4GCf6mbb+5ZRNmLLCI7bJ3f8Q6LHBSRlSKSWj5a7dSUzJBLv3VuIhFrFPgjF4IzPF567YG/HSCdZA==} 130 | engines: {node: '>=6.9.0'} 131 | 132 | '@babel/types@7.25.0': 133 | resolution: {integrity: sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==} 134 | engines: {node: '>=6.9.0'} 135 | 136 | '@esbuild/aix-ppc64@0.21.5': 137 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 138 | engines: {node: '>=12'} 139 | cpu: [ppc64] 140 | os: [aix] 141 | 142 | '@esbuild/android-arm64@0.21.5': 143 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 144 | engines: {node: '>=12'} 145 | cpu: [arm64] 146 | os: [android] 147 | 148 | '@esbuild/android-arm@0.21.5': 149 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 150 | engines: {node: '>=12'} 151 | cpu: [arm] 152 | os: [android] 153 | 154 | '@esbuild/android-x64@0.21.5': 155 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 156 | engines: {node: '>=12'} 157 | cpu: [x64] 158 | os: [android] 159 | 160 | '@esbuild/darwin-arm64@0.21.5': 161 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 162 | engines: {node: '>=12'} 163 | cpu: [arm64] 164 | os: [darwin] 165 | 166 | '@esbuild/darwin-x64@0.21.5': 167 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 168 | engines: {node: '>=12'} 169 | cpu: [x64] 170 | os: [darwin] 171 | 172 | '@esbuild/freebsd-arm64@0.21.5': 173 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 174 | engines: {node: '>=12'} 175 | cpu: [arm64] 176 | os: [freebsd] 177 | 178 | '@esbuild/freebsd-x64@0.21.5': 179 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 180 | engines: {node: '>=12'} 181 | cpu: [x64] 182 | os: [freebsd] 183 | 184 | '@esbuild/linux-arm64@0.21.5': 185 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 186 | engines: {node: '>=12'} 187 | cpu: [arm64] 188 | os: [linux] 189 | 190 | '@esbuild/linux-arm@0.21.5': 191 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 192 | engines: {node: '>=12'} 193 | cpu: [arm] 194 | os: [linux] 195 | 196 | '@esbuild/linux-ia32@0.21.5': 197 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 198 | engines: {node: '>=12'} 199 | cpu: [ia32] 200 | os: [linux] 201 | 202 | '@esbuild/linux-loong64@0.21.5': 203 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 204 | engines: {node: '>=12'} 205 | cpu: [loong64] 206 | os: [linux] 207 | 208 | '@esbuild/linux-mips64el@0.21.5': 209 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 210 | engines: {node: '>=12'} 211 | cpu: [mips64el] 212 | os: [linux] 213 | 214 | '@esbuild/linux-ppc64@0.21.5': 215 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 216 | engines: {node: '>=12'} 217 | cpu: [ppc64] 218 | os: [linux] 219 | 220 | '@esbuild/linux-riscv64@0.21.5': 221 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 222 | engines: {node: '>=12'} 223 | cpu: [riscv64] 224 | os: [linux] 225 | 226 | '@esbuild/linux-s390x@0.21.5': 227 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 228 | engines: {node: '>=12'} 229 | cpu: [s390x] 230 | os: [linux] 231 | 232 | '@esbuild/linux-x64@0.21.5': 233 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 234 | engines: {node: '>=12'} 235 | cpu: [x64] 236 | os: [linux] 237 | 238 | '@esbuild/netbsd-x64@0.21.5': 239 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 240 | engines: {node: '>=12'} 241 | cpu: [x64] 242 | os: [netbsd] 243 | 244 | '@esbuild/openbsd-x64@0.21.5': 245 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 246 | engines: {node: '>=12'} 247 | cpu: [x64] 248 | os: [openbsd] 249 | 250 | '@esbuild/sunos-x64@0.21.5': 251 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 252 | engines: {node: '>=12'} 253 | cpu: [x64] 254 | os: [sunos] 255 | 256 | '@esbuild/win32-arm64@0.21.5': 257 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 258 | engines: {node: '>=12'} 259 | cpu: [arm64] 260 | os: [win32] 261 | 262 | '@esbuild/win32-ia32@0.21.5': 263 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 264 | engines: {node: '>=12'} 265 | cpu: [ia32] 266 | os: [win32] 267 | 268 | '@esbuild/win32-x64@0.21.5': 269 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 270 | engines: {node: '>=12'} 271 | cpu: [x64] 272 | os: [win32] 273 | 274 | '@jridgewell/gen-mapping@0.3.5': 275 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 276 | engines: {node: '>=6.0.0'} 277 | 278 | '@jridgewell/resolve-uri@3.1.2': 279 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 280 | engines: {node: '>=6.0.0'} 281 | 282 | '@jridgewell/set-array@1.2.1': 283 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 284 | engines: {node: '>=6.0.0'} 285 | 286 | '@jridgewell/sourcemap-codec@1.5.0': 287 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 288 | 289 | '@jridgewell/trace-mapping@0.3.25': 290 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 291 | 292 | '@remix-run/router@1.18.0': 293 | resolution: {integrity: sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==} 294 | engines: {node: '>=14.0.0'} 295 | 296 | '@rollup/rollup-android-arm-eabi@4.19.1': 297 | resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==} 298 | cpu: [arm] 299 | os: [android] 300 | 301 | '@rollup/rollup-android-arm64@4.19.1': 302 | resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==} 303 | cpu: [arm64] 304 | os: [android] 305 | 306 | '@rollup/rollup-darwin-arm64@4.19.1': 307 | resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==} 308 | cpu: [arm64] 309 | os: [darwin] 310 | 311 | '@rollup/rollup-darwin-x64@4.19.1': 312 | resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==} 313 | cpu: [x64] 314 | os: [darwin] 315 | 316 | '@rollup/rollup-linux-arm-gnueabihf@4.19.1': 317 | resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} 318 | cpu: [arm] 319 | os: [linux] 320 | 321 | '@rollup/rollup-linux-arm-musleabihf@4.19.1': 322 | resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} 323 | cpu: [arm] 324 | os: [linux] 325 | 326 | '@rollup/rollup-linux-arm64-gnu@4.19.1': 327 | resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} 328 | cpu: [arm64] 329 | os: [linux] 330 | 331 | '@rollup/rollup-linux-arm64-musl@4.19.1': 332 | resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} 333 | cpu: [arm64] 334 | os: [linux] 335 | 336 | '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': 337 | resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} 338 | cpu: [ppc64] 339 | os: [linux] 340 | 341 | '@rollup/rollup-linux-riscv64-gnu@4.19.1': 342 | resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} 343 | cpu: [riscv64] 344 | os: [linux] 345 | 346 | '@rollup/rollup-linux-s390x-gnu@4.19.1': 347 | resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} 348 | cpu: [s390x] 349 | os: [linux] 350 | 351 | '@rollup/rollup-linux-x64-gnu@4.19.1': 352 | resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} 353 | cpu: [x64] 354 | os: [linux] 355 | 356 | '@rollup/rollup-linux-x64-musl@4.19.1': 357 | resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} 358 | cpu: [x64] 359 | os: [linux] 360 | 361 | '@rollup/rollup-win32-arm64-msvc@4.19.1': 362 | resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} 363 | cpu: [arm64] 364 | os: [win32] 365 | 366 | '@rollup/rollup-win32-ia32-msvc@4.19.1': 367 | resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==} 368 | cpu: [ia32] 369 | os: [win32] 370 | 371 | '@rollup/rollup-win32-x64-msvc@4.19.1': 372 | resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==} 373 | cpu: [x64] 374 | os: [win32] 375 | 376 | '@types/babel__core@7.20.5': 377 | resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 378 | 379 | '@types/babel__generator@7.6.8': 380 | resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 381 | 382 | '@types/babel__template@7.4.4': 383 | resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 384 | 385 | '@types/babel__traverse@7.20.6': 386 | resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 387 | 388 | '@types/estree@1.0.5': 389 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 390 | 391 | '@types/prop-types@15.7.12': 392 | resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} 393 | 394 | '@types/react-dom@18.3.0': 395 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 396 | 397 | '@types/react@18.3.3': 398 | resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} 399 | 400 | '@vitejs/plugin-react@4.3.1': 401 | resolution: {integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==} 402 | engines: {node: ^14.18.0 || >=16.0.0} 403 | peerDependencies: 404 | vite: ^4.2.0 || ^5.0.0 405 | 406 | '@xstate/react@4.1.1': 407 | resolution: {integrity: sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA==} 408 | peerDependencies: 409 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 410 | xstate: ^5.11.0 411 | peerDependenciesMeta: 412 | xstate: 413 | optional: true 414 | 415 | ansi-styles@3.2.1: 416 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 417 | engines: {node: '>=4'} 418 | 419 | browserslist@4.23.2: 420 | resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} 421 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 422 | hasBin: true 423 | 424 | caniuse-lite@1.0.30001643: 425 | resolution: {integrity: sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==} 426 | 427 | chalk@2.4.2: 428 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 429 | engines: {node: '>=4'} 430 | 431 | color-convert@1.9.3: 432 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 433 | 434 | color-name@1.1.3: 435 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 436 | 437 | convert-source-map@2.0.0: 438 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 439 | 440 | csstype@3.1.3: 441 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 442 | 443 | debug@4.3.5: 444 | resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} 445 | engines: {node: '>=6.0'} 446 | peerDependencies: 447 | supports-color: '*' 448 | peerDependenciesMeta: 449 | supports-color: 450 | optional: true 451 | 452 | electron-to-chromium@1.5.2: 453 | resolution: {integrity: sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==} 454 | 455 | esbuild@0.21.5: 456 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 457 | engines: {node: '>=12'} 458 | hasBin: true 459 | 460 | escalade@3.1.2: 461 | resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} 462 | engines: {node: '>=6'} 463 | 464 | escape-string-regexp@1.0.5: 465 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 466 | engines: {node: '>=0.8.0'} 467 | 468 | fsevents@2.3.3: 469 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 470 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 471 | os: [darwin] 472 | 473 | gensync@1.0.0-beta.2: 474 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 475 | engines: {node: '>=6.9.0'} 476 | 477 | globals@11.12.0: 478 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 479 | engines: {node: '>=4'} 480 | 481 | has-flag@3.0.0: 482 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 483 | engines: {node: '>=4'} 484 | 485 | js-tokens@4.0.0: 486 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 487 | 488 | jsesc@2.5.2: 489 | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 490 | engines: {node: '>=4'} 491 | hasBin: true 492 | 493 | json5@2.2.3: 494 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 495 | engines: {node: '>=6'} 496 | hasBin: true 497 | 498 | loose-envify@1.4.0: 499 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 500 | hasBin: true 501 | 502 | lru-cache@5.1.1: 503 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 504 | 505 | ms@2.1.2: 506 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 507 | 508 | nanoid@3.3.7: 509 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 510 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 511 | hasBin: true 512 | 513 | node-releases@2.0.18: 514 | resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} 515 | 516 | picocolors@1.0.1: 517 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 518 | 519 | postcss@8.4.40: 520 | resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} 521 | engines: {node: ^10 || ^12 || >=14} 522 | 523 | react-dom@18.3.1: 524 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 525 | peerDependencies: 526 | react: ^18.3.1 527 | 528 | react-refresh@0.14.2: 529 | resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} 530 | engines: {node: '>=0.10.0'} 531 | 532 | react-router-dom@6.25.1: 533 | resolution: {integrity: sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==} 534 | engines: {node: '>=14.0.0'} 535 | peerDependencies: 536 | react: '>=16.8' 537 | react-dom: '>=16.8' 538 | 539 | react-router@6.25.1: 540 | resolution: {integrity: sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==} 541 | engines: {node: '>=14.0.0'} 542 | peerDependencies: 543 | react: '>=16.8' 544 | 545 | react@18.3.1: 546 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 547 | engines: {node: '>=0.10.0'} 548 | 549 | rollup@4.19.1: 550 | resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==} 551 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 552 | hasBin: true 553 | 554 | scheduler@0.23.2: 555 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 556 | 557 | semver@6.3.1: 558 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 559 | hasBin: true 560 | 561 | source-map-js@1.2.0: 562 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 563 | engines: {node: '>=0.10.0'} 564 | 565 | supports-color@5.5.0: 566 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 567 | engines: {node: '>=4'} 568 | 569 | to-fast-properties@2.0.0: 570 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 571 | engines: {node: '>=4'} 572 | 573 | typescript@5.5.4: 574 | resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} 575 | engines: {node: '>=14.17'} 576 | hasBin: true 577 | 578 | update-browserslist-db@1.1.0: 579 | resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} 580 | hasBin: true 581 | peerDependencies: 582 | browserslist: '>= 4.21.0' 583 | 584 | use-isomorphic-layout-effect@1.1.2: 585 | resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} 586 | peerDependencies: 587 | '@types/react': '*' 588 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 589 | peerDependenciesMeta: 590 | '@types/react': 591 | optional: true 592 | 593 | use-sync-external-store@1.2.2: 594 | resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} 595 | peerDependencies: 596 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 597 | 598 | vite@5.3.5: 599 | resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} 600 | engines: {node: ^18.0.0 || >=20.0.0} 601 | hasBin: true 602 | peerDependencies: 603 | '@types/node': ^18.0.0 || >=20.0.0 604 | less: '*' 605 | lightningcss: ^1.21.0 606 | sass: '*' 607 | stylus: '*' 608 | sugarss: '*' 609 | terser: ^5.4.0 610 | peerDependenciesMeta: 611 | '@types/node': 612 | optional: true 613 | less: 614 | optional: true 615 | lightningcss: 616 | optional: true 617 | sass: 618 | optional: true 619 | stylus: 620 | optional: true 621 | sugarss: 622 | optional: true 623 | terser: 624 | optional: true 625 | 626 | xstate@5.16.0: 627 | resolution: {integrity: sha512-qws7r2nsCsQ/Qgpn75+FYM84M88/nopGsxij/qYNSOyKJKO4n7x8B5isQKv6DMruyF4gGg7y4gsFDTzJUlKcWw==} 628 | 629 | yallist@3.1.1: 630 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 631 | 632 | snapshots: 633 | 634 | '@ampproject/remapping@2.3.0': 635 | dependencies: 636 | '@jridgewell/gen-mapping': 0.3.5 637 | '@jridgewell/trace-mapping': 0.3.25 638 | 639 | '@babel/code-frame@7.24.7': 640 | dependencies: 641 | '@babel/highlight': 7.24.7 642 | picocolors: 1.0.1 643 | 644 | '@babel/compat-data@7.25.0': {} 645 | 646 | '@babel/core@7.24.9': 647 | dependencies: 648 | '@ampproject/remapping': 2.3.0 649 | '@babel/code-frame': 7.24.7 650 | '@babel/generator': 7.25.0 651 | '@babel/helper-compilation-targets': 7.24.8 652 | '@babel/helper-module-transforms': 7.25.0(@babel/core@7.24.9) 653 | '@babel/helpers': 7.25.0 654 | '@babel/parser': 7.25.0 655 | '@babel/template': 7.25.0 656 | '@babel/traverse': 7.25.0 657 | '@babel/types': 7.25.0 658 | convert-source-map: 2.0.0 659 | debug: 4.3.5 660 | gensync: 1.0.0-beta.2 661 | json5: 2.2.3 662 | semver: 6.3.1 663 | transitivePeerDependencies: 664 | - supports-color 665 | 666 | '@babel/generator@7.25.0': 667 | dependencies: 668 | '@babel/types': 7.25.0 669 | '@jridgewell/gen-mapping': 0.3.5 670 | '@jridgewell/trace-mapping': 0.3.25 671 | jsesc: 2.5.2 672 | 673 | '@babel/helper-compilation-targets@7.24.8': 674 | dependencies: 675 | '@babel/compat-data': 7.25.0 676 | '@babel/helper-validator-option': 7.24.8 677 | browserslist: 4.23.2 678 | lru-cache: 5.1.1 679 | semver: 6.3.1 680 | 681 | '@babel/helper-module-imports@7.24.7': 682 | dependencies: 683 | '@babel/traverse': 7.25.0 684 | '@babel/types': 7.25.0 685 | transitivePeerDependencies: 686 | - supports-color 687 | 688 | '@babel/helper-module-transforms@7.25.0(@babel/core@7.24.9)': 689 | dependencies: 690 | '@babel/core': 7.24.9 691 | '@babel/helper-module-imports': 7.24.7 692 | '@babel/helper-simple-access': 7.24.7 693 | '@babel/helper-validator-identifier': 7.24.7 694 | '@babel/traverse': 7.25.0 695 | transitivePeerDependencies: 696 | - supports-color 697 | 698 | '@babel/helper-plugin-utils@7.24.8': {} 699 | 700 | '@babel/helper-simple-access@7.24.7': 701 | dependencies: 702 | '@babel/traverse': 7.25.0 703 | '@babel/types': 7.25.0 704 | transitivePeerDependencies: 705 | - supports-color 706 | 707 | '@babel/helper-string-parser@7.24.8': {} 708 | 709 | '@babel/helper-validator-identifier@7.24.7': {} 710 | 711 | '@babel/helper-validator-option@7.24.8': {} 712 | 713 | '@babel/helpers@7.25.0': 714 | dependencies: 715 | '@babel/template': 7.25.0 716 | '@babel/types': 7.25.0 717 | 718 | '@babel/highlight@7.24.7': 719 | dependencies: 720 | '@babel/helper-validator-identifier': 7.24.7 721 | chalk: 2.4.2 722 | js-tokens: 4.0.0 723 | picocolors: 1.0.1 724 | 725 | '@babel/parser@7.25.0': 726 | dependencies: 727 | '@babel/types': 7.25.0 728 | 729 | '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.24.9)': 730 | dependencies: 731 | '@babel/core': 7.24.9 732 | '@babel/helper-plugin-utils': 7.24.8 733 | 734 | '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.24.9)': 735 | dependencies: 736 | '@babel/core': 7.24.9 737 | '@babel/helper-plugin-utils': 7.24.8 738 | 739 | '@babel/template@7.25.0': 740 | dependencies: 741 | '@babel/code-frame': 7.24.7 742 | '@babel/parser': 7.25.0 743 | '@babel/types': 7.25.0 744 | 745 | '@babel/traverse@7.25.0': 746 | dependencies: 747 | '@babel/code-frame': 7.24.7 748 | '@babel/generator': 7.25.0 749 | '@babel/parser': 7.25.0 750 | '@babel/template': 7.25.0 751 | '@babel/types': 7.25.0 752 | debug: 4.3.5 753 | globals: 11.12.0 754 | transitivePeerDependencies: 755 | - supports-color 756 | 757 | '@babel/types@7.25.0': 758 | dependencies: 759 | '@babel/helper-string-parser': 7.24.8 760 | '@babel/helper-validator-identifier': 7.24.7 761 | to-fast-properties: 2.0.0 762 | 763 | '@esbuild/aix-ppc64@0.21.5': 764 | optional: true 765 | 766 | '@esbuild/android-arm64@0.21.5': 767 | optional: true 768 | 769 | '@esbuild/android-arm@0.21.5': 770 | optional: true 771 | 772 | '@esbuild/android-x64@0.21.5': 773 | optional: true 774 | 775 | '@esbuild/darwin-arm64@0.21.5': 776 | optional: true 777 | 778 | '@esbuild/darwin-x64@0.21.5': 779 | optional: true 780 | 781 | '@esbuild/freebsd-arm64@0.21.5': 782 | optional: true 783 | 784 | '@esbuild/freebsd-x64@0.21.5': 785 | optional: true 786 | 787 | '@esbuild/linux-arm64@0.21.5': 788 | optional: true 789 | 790 | '@esbuild/linux-arm@0.21.5': 791 | optional: true 792 | 793 | '@esbuild/linux-ia32@0.21.5': 794 | optional: true 795 | 796 | '@esbuild/linux-loong64@0.21.5': 797 | optional: true 798 | 799 | '@esbuild/linux-mips64el@0.21.5': 800 | optional: true 801 | 802 | '@esbuild/linux-ppc64@0.21.5': 803 | optional: true 804 | 805 | '@esbuild/linux-riscv64@0.21.5': 806 | optional: true 807 | 808 | '@esbuild/linux-s390x@0.21.5': 809 | optional: true 810 | 811 | '@esbuild/linux-x64@0.21.5': 812 | optional: true 813 | 814 | '@esbuild/netbsd-x64@0.21.5': 815 | optional: true 816 | 817 | '@esbuild/openbsd-x64@0.21.5': 818 | optional: true 819 | 820 | '@esbuild/sunos-x64@0.21.5': 821 | optional: true 822 | 823 | '@esbuild/win32-arm64@0.21.5': 824 | optional: true 825 | 826 | '@esbuild/win32-ia32@0.21.5': 827 | optional: true 828 | 829 | '@esbuild/win32-x64@0.21.5': 830 | optional: true 831 | 832 | '@jridgewell/gen-mapping@0.3.5': 833 | dependencies: 834 | '@jridgewell/set-array': 1.2.1 835 | '@jridgewell/sourcemap-codec': 1.5.0 836 | '@jridgewell/trace-mapping': 0.3.25 837 | 838 | '@jridgewell/resolve-uri@3.1.2': {} 839 | 840 | '@jridgewell/set-array@1.2.1': {} 841 | 842 | '@jridgewell/sourcemap-codec@1.5.0': {} 843 | 844 | '@jridgewell/trace-mapping@0.3.25': 845 | dependencies: 846 | '@jridgewell/resolve-uri': 3.1.2 847 | '@jridgewell/sourcemap-codec': 1.5.0 848 | 849 | '@remix-run/router@1.18.0': {} 850 | 851 | '@rollup/rollup-android-arm-eabi@4.19.1': 852 | optional: true 853 | 854 | '@rollup/rollup-android-arm64@4.19.1': 855 | optional: true 856 | 857 | '@rollup/rollup-darwin-arm64@4.19.1': 858 | optional: true 859 | 860 | '@rollup/rollup-darwin-x64@4.19.1': 861 | optional: true 862 | 863 | '@rollup/rollup-linux-arm-gnueabihf@4.19.1': 864 | optional: true 865 | 866 | '@rollup/rollup-linux-arm-musleabihf@4.19.1': 867 | optional: true 868 | 869 | '@rollup/rollup-linux-arm64-gnu@4.19.1': 870 | optional: true 871 | 872 | '@rollup/rollup-linux-arm64-musl@4.19.1': 873 | optional: true 874 | 875 | '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': 876 | optional: true 877 | 878 | '@rollup/rollup-linux-riscv64-gnu@4.19.1': 879 | optional: true 880 | 881 | '@rollup/rollup-linux-s390x-gnu@4.19.1': 882 | optional: true 883 | 884 | '@rollup/rollup-linux-x64-gnu@4.19.1': 885 | optional: true 886 | 887 | '@rollup/rollup-linux-x64-musl@4.19.1': 888 | optional: true 889 | 890 | '@rollup/rollup-win32-arm64-msvc@4.19.1': 891 | optional: true 892 | 893 | '@rollup/rollup-win32-ia32-msvc@4.19.1': 894 | optional: true 895 | 896 | '@rollup/rollup-win32-x64-msvc@4.19.1': 897 | optional: true 898 | 899 | '@types/babel__core@7.20.5': 900 | dependencies: 901 | '@babel/parser': 7.25.0 902 | '@babel/types': 7.25.0 903 | '@types/babel__generator': 7.6.8 904 | '@types/babel__template': 7.4.4 905 | '@types/babel__traverse': 7.20.6 906 | 907 | '@types/babel__generator@7.6.8': 908 | dependencies: 909 | '@babel/types': 7.25.0 910 | 911 | '@types/babel__template@7.4.4': 912 | dependencies: 913 | '@babel/parser': 7.25.0 914 | '@babel/types': 7.25.0 915 | 916 | '@types/babel__traverse@7.20.6': 917 | dependencies: 918 | '@babel/types': 7.25.0 919 | 920 | '@types/estree@1.0.5': {} 921 | 922 | '@types/prop-types@15.7.12': {} 923 | 924 | '@types/react-dom@18.3.0': 925 | dependencies: 926 | '@types/react': 18.3.3 927 | 928 | '@types/react@18.3.3': 929 | dependencies: 930 | '@types/prop-types': 15.7.12 931 | csstype: 3.1.3 932 | 933 | '@vitejs/plugin-react@4.3.1(vite@5.3.5)': 934 | dependencies: 935 | '@babel/core': 7.24.9 936 | '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.24.9) 937 | '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.9) 938 | '@types/babel__core': 7.20.5 939 | react-refresh: 0.14.2 940 | vite: 5.3.5 941 | transitivePeerDependencies: 942 | - supports-color 943 | 944 | '@xstate/react@4.1.1(@types/react@18.3.3)(react@18.3.1)(xstate@5.16.0)': 945 | dependencies: 946 | react: 18.3.1 947 | use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.3)(react@18.3.1) 948 | use-sync-external-store: 1.2.2(react@18.3.1) 949 | optionalDependencies: 950 | xstate: 5.16.0 951 | transitivePeerDependencies: 952 | - '@types/react' 953 | 954 | ansi-styles@3.2.1: 955 | dependencies: 956 | color-convert: 1.9.3 957 | 958 | browserslist@4.23.2: 959 | dependencies: 960 | caniuse-lite: 1.0.30001643 961 | electron-to-chromium: 1.5.2 962 | node-releases: 2.0.18 963 | update-browserslist-db: 1.1.0(browserslist@4.23.2) 964 | 965 | caniuse-lite@1.0.30001643: {} 966 | 967 | chalk@2.4.2: 968 | dependencies: 969 | ansi-styles: 3.2.1 970 | escape-string-regexp: 1.0.5 971 | supports-color: 5.5.0 972 | 973 | color-convert@1.9.3: 974 | dependencies: 975 | color-name: 1.1.3 976 | 977 | color-name@1.1.3: {} 978 | 979 | convert-source-map@2.0.0: {} 980 | 981 | csstype@3.1.3: {} 982 | 983 | debug@4.3.5: 984 | dependencies: 985 | ms: 2.1.2 986 | 987 | electron-to-chromium@1.5.2: {} 988 | 989 | esbuild@0.21.5: 990 | optionalDependencies: 991 | '@esbuild/aix-ppc64': 0.21.5 992 | '@esbuild/android-arm': 0.21.5 993 | '@esbuild/android-arm64': 0.21.5 994 | '@esbuild/android-x64': 0.21.5 995 | '@esbuild/darwin-arm64': 0.21.5 996 | '@esbuild/darwin-x64': 0.21.5 997 | '@esbuild/freebsd-arm64': 0.21.5 998 | '@esbuild/freebsd-x64': 0.21.5 999 | '@esbuild/linux-arm': 0.21.5 1000 | '@esbuild/linux-arm64': 0.21.5 1001 | '@esbuild/linux-ia32': 0.21.5 1002 | '@esbuild/linux-loong64': 0.21.5 1003 | '@esbuild/linux-mips64el': 0.21.5 1004 | '@esbuild/linux-ppc64': 0.21.5 1005 | '@esbuild/linux-riscv64': 0.21.5 1006 | '@esbuild/linux-s390x': 0.21.5 1007 | '@esbuild/linux-x64': 0.21.5 1008 | '@esbuild/netbsd-x64': 0.21.5 1009 | '@esbuild/openbsd-x64': 0.21.5 1010 | '@esbuild/sunos-x64': 0.21.5 1011 | '@esbuild/win32-arm64': 0.21.5 1012 | '@esbuild/win32-ia32': 0.21.5 1013 | '@esbuild/win32-x64': 0.21.5 1014 | 1015 | escalade@3.1.2: {} 1016 | 1017 | escape-string-regexp@1.0.5: {} 1018 | 1019 | fsevents@2.3.3: 1020 | optional: true 1021 | 1022 | gensync@1.0.0-beta.2: {} 1023 | 1024 | globals@11.12.0: {} 1025 | 1026 | has-flag@3.0.0: {} 1027 | 1028 | js-tokens@4.0.0: {} 1029 | 1030 | jsesc@2.5.2: {} 1031 | 1032 | json5@2.2.3: {} 1033 | 1034 | loose-envify@1.4.0: 1035 | dependencies: 1036 | js-tokens: 4.0.0 1037 | 1038 | lru-cache@5.1.1: 1039 | dependencies: 1040 | yallist: 3.1.1 1041 | 1042 | ms@2.1.2: {} 1043 | 1044 | nanoid@3.3.7: {} 1045 | 1046 | node-releases@2.0.18: {} 1047 | 1048 | picocolors@1.0.1: {} 1049 | 1050 | postcss@8.4.40: 1051 | dependencies: 1052 | nanoid: 3.3.7 1053 | picocolors: 1.0.1 1054 | source-map-js: 1.2.0 1055 | 1056 | react-dom@18.3.1(react@18.3.1): 1057 | dependencies: 1058 | loose-envify: 1.4.0 1059 | react: 18.3.1 1060 | scheduler: 0.23.2 1061 | 1062 | react-refresh@0.14.2: {} 1063 | 1064 | react-router-dom@6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 1065 | dependencies: 1066 | '@remix-run/router': 1.18.0 1067 | react: 18.3.1 1068 | react-dom: 18.3.1(react@18.3.1) 1069 | react-router: 6.25.1(react@18.3.1) 1070 | 1071 | react-router@6.25.1(react@18.3.1): 1072 | dependencies: 1073 | '@remix-run/router': 1.18.0 1074 | react: 18.3.1 1075 | 1076 | react@18.3.1: 1077 | dependencies: 1078 | loose-envify: 1.4.0 1079 | 1080 | rollup@4.19.1: 1081 | dependencies: 1082 | '@types/estree': 1.0.5 1083 | optionalDependencies: 1084 | '@rollup/rollup-android-arm-eabi': 4.19.1 1085 | '@rollup/rollup-android-arm64': 4.19.1 1086 | '@rollup/rollup-darwin-arm64': 4.19.1 1087 | '@rollup/rollup-darwin-x64': 4.19.1 1088 | '@rollup/rollup-linux-arm-gnueabihf': 4.19.1 1089 | '@rollup/rollup-linux-arm-musleabihf': 4.19.1 1090 | '@rollup/rollup-linux-arm64-gnu': 4.19.1 1091 | '@rollup/rollup-linux-arm64-musl': 4.19.1 1092 | '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1 1093 | '@rollup/rollup-linux-riscv64-gnu': 4.19.1 1094 | '@rollup/rollup-linux-s390x-gnu': 4.19.1 1095 | '@rollup/rollup-linux-x64-gnu': 4.19.1 1096 | '@rollup/rollup-linux-x64-musl': 4.19.1 1097 | '@rollup/rollup-win32-arm64-msvc': 4.19.1 1098 | '@rollup/rollup-win32-ia32-msvc': 4.19.1 1099 | '@rollup/rollup-win32-x64-msvc': 4.19.1 1100 | fsevents: 2.3.3 1101 | 1102 | scheduler@0.23.2: 1103 | dependencies: 1104 | loose-envify: 1.4.0 1105 | 1106 | semver@6.3.1: {} 1107 | 1108 | source-map-js@1.2.0: {} 1109 | 1110 | supports-color@5.5.0: 1111 | dependencies: 1112 | has-flag: 3.0.0 1113 | 1114 | to-fast-properties@2.0.0: {} 1115 | 1116 | typescript@5.5.4: {} 1117 | 1118 | update-browserslist-db@1.1.0(browserslist@4.23.2): 1119 | dependencies: 1120 | browserslist: 4.23.2 1121 | escalade: 3.1.2 1122 | picocolors: 1.0.1 1123 | 1124 | use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1): 1125 | dependencies: 1126 | react: 18.3.1 1127 | optionalDependencies: 1128 | '@types/react': 18.3.3 1129 | 1130 | use-sync-external-store@1.2.2(react@18.3.1): 1131 | dependencies: 1132 | react: 18.3.1 1133 | 1134 | vite@5.3.5: 1135 | dependencies: 1136 | esbuild: 0.21.5 1137 | postcss: 8.4.40 1138 | rollup: 4.19.1 1139 | optionalDependencies: 1140 | fsevents: 2.3.3 1141 | 1142 | xstate@5.16.0: {} 1143 | 1144 | yallist@3.1.1: {} 1145 | --------------------------------------------------------------------------------