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 |9 | {snapshot.context.mousePosition[0]},{snapshot.context.mousePosition[1]} 10 |
11 |{snapshot.value}
12 | 23 |{notification.message}
21 | ))} 22 | > 23 | )} 24 |{post.title}
32 |{post.body}
33 |{snapshot.context.openedMovie?.name}
26 | 29 |Done
; 29 | } 30 | 31 | return ( 32 | 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: ActorRefFromReceived message: {snapshot.context.receivedMessage}
34 | )} 35 | 36 | 41 |{post.title}
53 |{post.body}
54 |Time left: {time} seconds
49 | {isCompleted ? ( 50 | <>> 51 | ) : isPaused ? ( 52 | 53 | ) : ( 54 | 55 | )} 56 | {isPaused && !isCompleted &&Paused
} 57 | {isCompleted &&Done!
} 58 |{post.title}
76 |{post.body}
77 |Done
; 50 | } 51 | 52 | return ( 53 | 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{notification.message}
71 | ))} 72 | > 73 | )} 74 |Done
; 70 | } 71 | 72 | return ( 73 | 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 |39 | {snapshot.matches("Player1Win") 40 | ? snapshot.context.player1Name 41 | : snapshot.matches("Player2Win") 42 | ? snapshot.context.player2Name 43 | : "No one"}{" "} 44 | won! 45 |
46 | 52 |55 | {snapshot.matches("Player1") 56 | ? snapshot.context.player1Name 57 | : snapshot.context.player2Name}{" "} 58 | is your turn! 59 |
60 | )} 61 | 62 |