Skip to content

Commit

Permalink
Merge pull request #62 from aod/xstate5
Browse files Browse the repository at this point in the history
Migrate to XState v5
  • Loading branch information
aod authored Jan 18, 2025
2 parents 7c78668 + 62a25f5 commit a450052
Show file tree
Hide file tree
Showing 22 changed files with 691 additions and 530 deletions.
277 changes: 229 additions & 48 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
"@xstate/immer": "^0.3.1",
"@xstate/react": "^3.0.1",
"@statelyai/inspect": "^0.4.0",
"@xstate/react": "^5.0.2",
"clsx": "^1.2.1",
"framer-motion": "^7.6.4",
"immer": "^9.0.15",
"lucide-react": "^0.95.0",
"react": "^18.0.0",
"react-dom": "^18.2.0",
"xstate": "^4.34.0"
"xstate": "^5.19.2"
},
"devDependencies": {
"@types/react": "^18.0.21",
Expand All @@ -36,7 +36,7 @@
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
"tailwindcss": "^3.2.1",
"typescript": "^4.8.4",
"typescript": "^5.7.3",
"vite": "^3.2.3",
"vitest": "^0.25.1"
}
Expand Down
23 changes: 9 additions & 14 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import { useSelector } from "@xstate/react";
import { AnimatePresence, motion } from "framer-motion";
import { useContext, useEffect, useState } from "react";
import Deck from "./Deck";
import { useEffect, useState } from "react";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

import Deck from "./Deck";
import HumanOffHand from "./HumanOffHand";
import Pile from "./Pile";
import ShownHand from "./ShownHand";
import Switcher from "./Switcher";
import {
isChoosingFaceUpCardsStor,
isGameOverStor,
isPlayingStor,
} from "../state/selectors";
import SortButton from "./SortButton";
import ResultOverlay from "./ResultOverlay";
import TitleScreenOverlay from "./TitleScreenOverlay";

export default function App() {
const { zhitheadService } = useContext(GlobalStateContext);
const isPlaying = useSelector(zhitheadService, isPlayingStor);
const isChoosingFaceUpCards = useSelector(
zhitheadService,
isChoosingFaceUpCardsStor
const isPlaying = GlobalStateContext.useSelector(selectors.isPlaying);
const isChoosingFaceUpCards = GlobalStateContext.useSelector(
selectors.isChoosingFaceUpCards
);
const isGameOver = useSelector(zhitheadService, isGameOverStor);
const isGameOver = GlobalStateContext.useSelector(selectors.isGameOver);

const [windowHeight, setWindowHeight] = useState(window.innerHeight);
useEffect(() => {
Expand Down
13 changes: 5 additions & 8 deletions src/components/Deck.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { useSelector } from "@xstate/react";
import clsx from "clsx";
import { useContext } from "react";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

import Card from "./ui/Card";
import CardHolder from "./ui/CardHolder";
import Count from "./ui/Count";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

export default function Deck() {
const globalServices = useContext(GlobalStateContext);
const deck = useSelector(
globalServices.zhitheadService,
(state) => state.context.deck
);
const deck = GlobalStateContext.useSelector(selectors.getDeck);
const hasDeck = Boolean(deck.length);

return (
Expand Down
18 changes: 8 additions & 10 deletions src/components/HumanOffHand.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { useSelector } from "@xstate/react";
import { createElement, useContext } from "react";
import { isChoosingFaceUpCardsStor } from "../state/selectors";
import { createElement } from "react";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

import OffHand from "./ui/OffHand";

export default function HumanOffHand() {
const { zhitheadService } = useContext(GlobalStateContext);
const offHand = useSelector(
zhitheadService,
(state) => state.context.human.offHand
const offHand = GlobalStateContext.useSelector(
selectors.getPlayerOffHand("human")
);
const isChoosingFaceUpCards = useSelector(
zhitheadService,
isChoosingFaceUpCardsStor
const isChoosingFaceUpCards = GlobalStateContext.useSelector(
selectors.isChoosingFaceUpCards
);

return createElement(OffHand, {
Expand Down
13 changes: 7 additions & 6 deletions src/components/Pile.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { useSelector } from "@xstate/react";
import { AnimatePresence, motion, TargetAndTransition } from "framer-motion";
import { useContext } from "react";

import { getRank, Rank, Card as TCard } from "../lib";
import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";
import { BreakpointsContext } from "./providers/BreakpointsProvider";

import Card from "./ui/Card";
import CardHolder from "./ui/CardHolder";
import Count from "./ui/Count";
import { GlobalStateContext } from "./providers/GlobalStateProvider";
import Fire from "./ui/Fire";
import { BreakpointsContext } from "./providers/BreakpointsProvider";

export default function Pile() {
const { zhitheadService } = useContext(GlobalStateContext);
const pile = useSelector(zhitheadService, (state) => state.context.pile);
const { send } = zhitheadService;
const { send } = GlobalStateContext.useActorRef();

const pile = GlobalStateContext.useSelector(selectors.getPile);
const topCard = pile.at(-1);
const shouldBurnPile =
topCard !== undefined && getRank(topCard) === Rank.Num10;
Expand Down
13 changes: 4 additions & 9 deletions src/components/ResultOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { useSelector } from "@xstate/react";
import { motion, Variants } from "framer-motion";
import { useContext, useState } from "react";
import { hasWonStor } from "../state/selectors";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

export default function ResultOverlay() {
const { zhitheadService: zh } = useContext(GlobalStateContext);
const { send } = zh;

const _hasWon = useSelector(zh, hasWonStor);
const [hasWon, setHasWon] = useState<boolean | null>(null);
if (hasWon === null) setHasWon(_hasWon);
const { send } = GlobalStateContext.useActorRef();
const hasWon = GlobalStateContext.useSelector(selectors.hasWon);

const msgs = {
won: {
Expand Down
44 changes: 17 additions & 27 deletions src/components/ShownHand.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useSelector } from "@xstate/react";
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import { useContext } from "react";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";
import { canPlay } from "../lib";
import { Player } from "../state/machines/zhithead.machine";
import { isChoosingFaceUpCardsStor, isPlayingStor } from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

import Hand from "./ui/Hand";
import OffHand from "./ui/OffHand";

Expand All @@ -14,31 +14,21 @@ interface ShownHandProps {
}

export default function ShownHand(props: ShownHandProps) {
const { zhitheadService } = useContext(GlobalStateContext);
const isPlaying = useSelector(zhitheadService, isPlayingStor);
const isChoosingFaceUpCards = useSelector(
zhitheadService,
isChoosingFaceUpCardsStor
const isPlaying = GlobalStateContext.useSelector(selectors.isPlaying);
const isChoosingFaceUpCards = GlobalStateContext.useSelector(
selectors.isChoosingFaceUpCards
);

const shownHand = useSelector(
zhitheadService,
(state) => state.context.shownHand[props.player]
const shownHand = GlobalStateContext.useSelector(
selectors.getPlayerShownHand(props.player)
);
const hand = useSelector(
zhitheadService,
(state) => state.context[props.player].hand
const hand = GlobalStateContext.useSelector(
selectors.getPlayerHand(props.player)
);
const offHand = useSelector(
zhitheadService,
(state) => state.context[props.player].offHand
const offHand = GlobalStateContext.useSelector(
selectors.getPlayerOffHand(props.player)
);
const pile = useSelector(zhitheadService, (state) => state.context.pile);

// TODO: Is this the correct way to access services?
const human = useSelector(zhitheadService, (state) => state.children.human);
const { send } = human;

const pile = GlobalStateContext.useSelector(selectors.getPile);
const human = GlobalStateContext.useSelector(selectors.getHumanActor);
const flipped = props.player === "bot";

return (
Expand All @@ -57,7 +47,7 @@ export default function ShownHand(props: ShownHandProps) {
hand={hand}
onCardClick={(card, _, n) => {
if (props.player === "human") {
send({ type: "CHOOSE_CARD", card, n });
human?.send({ type: "CHOOSE_CARD", card, n });
}
}}
grayOut={
Expand All @@ -76,7 +66,7 @@ export default function ShownHand(props: ShownHandProps) {
offHand={offHand}
onCardPositionedClick={(card, _, n) => {
if (props.player === "human") {
send({ type: "CHOOSE_CARD", card, n });
human?.send({ type: "CHOOSE_CARD", card, n });
}
}}
grayOutFaceUpCard={(card) => !!hand.length || !canPlay(card, pile)}
Expand Down
16 changes: 6 additions & 10 deletions src/components/SortButton.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { useSelector } from "@xstate/react";
import { motion } from "framer-motion";
import { SortAsc } from "lucide-react";
import { useContext } from "react";

import * as selectors from "../state/selectors";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

export default function SortButton() {
const { zhitheadService } = useContext(GlobalStateContext);
const { send } = zhitheadService;
const handLength = useSelector(
zhitheadService,
(state) => state.context.human.hand.length
);
const { send } = GlobalStateContext.useActorRef();
const hand = GlobalStateContext.useSelector(selectors.getPlayerHand("human"));

return (
<motion.button
onClick={() => send("SORT_HAND")}
onClick={() => send({ type: "SORT_HAND" })}
className="flex items-center justify-center rounded-full bg-black p-1.5 sm:p-2"
title="Sort your hand"
initial={{ y: 100 }}
animate={{ y: !handLength ? 100 : 0 }}
animate={{ y: !hand.length ? 100 : 0 }}
exit={{ y: 100 }}
whileHover={{ scale: 1.15 }}
whileTap={{ scale: 0.9 }}
Expand Down
29 changes: 13 additions & 16 deletions src/components/Switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import { createElement, useContext } from "react";
import { useSelector } from "@xstate/react";
import { GlobalStateContext } from "./providers/GlobalStateProvider";
import UISwitcher from "./ui/Switcher";
import { createElement } from "react";

import * as selectors from "../state/selectors";
import { offHandLen } from "../lib";
import { Player } from "../state/machines/zhithead.machine";
import { GlobalStateContext } from "./providers/GlobalStateProvider";

import UISwitcher from "./ui/Switcher";

interface SwitcherProps {
player: Player;
}

export default function Switcher(props: SwitcherProps) {
const { zhitheadService } = useContext(GlobalStateContext);

const hand = useSelector(
zhitheadService,
(state) => state.context[props.player].hand
const hand = GlobalStateContext.useSelector(
selectors.getPlayerHand(props.player)
);
const offHand = useSelector(
zhitheadService,
(state) => state.context[props.player].offHand
const offHand = GlobalStateContext.useSelector(
selectors.getPlayerOffHand(props.player)
);
const shownHand = useSelector(
zhitheadService,
(state) => state.context.shownHand[props.player]
const shownHand = GlobalStateContext.useSelector(
selectors.getPlayerShownHand(props.player)
);

const { send } = zhitheadService;
const { send } = GlobalStateContext.useActorRef();

return createElement(UISwitcher, {
left: ["Hand", hand.length],
Expand Down
29 changes: 15 additions & 14 deletions src/components/providers/GlobalStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { createContext, PropsWithChildren } from "react";
import { useInterpret } from "@xstate/react";
import { createActorContext } from "@xstate/react";
import { createBrowserInspector } from "@statelyai/inspect";

import { zhitheadMachine } from "../../state/machines/zhithead.machine";
import { InterpreterFrom } from "xstate";

export const GlobalStateContext = createContext({
zhitheadService: {} as InterpreterFrom<typeof zhitheadMachine>,
});
declare global {
interface Window {
inspector: ReturnType<typeof createBrowserInspector> | undefined;
}
}

export default function GlobalStateProvider(props: PropsWithChildren) {
const zhitheadService = useInterpret(zhitheadMachine);
const inspector = createBrowserInspector({
autoStart: false,
});
window.inspector = inspector;

return (
<GlobalStateContext.Provider value={{ zhitheadService }}>
{props.children}
</GlobalStateContext.Provider>
);
}
export const GlobalStateContext = createActorContext(zhitheadMachine, {
inspect: inspector.inspect,
});
1 change: 1 addition & 0 deletions src/components/ui/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";

import { Card as TCard, getRank, getSuite, Rank, Suite } from "../../lib";

export interface CardProps {
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/Hand.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { motion, Variants } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import Card from "./Card";

import { Cards, Card as TCard, getRank } from "../../lib";

import Card from "./Card";

export interface HandProps {
hand: Cards;
onCardClick?: (card: TCard, index: number, n?: number) => void;
Expand Down
2 changes: 2 additions & 0 deletions src/components/ui/OffHand.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { motion } from "framer-motion";
import { useState } from "react";

import { Card as TCard, getRank, Player } from "../../lib";

import Card from "./Card";
import CardHolder from "./CardHolder";

Expand Down
1 change: 1 addition & 0 deletions src/components/ui/Switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LayoutGroup, motion } from "framer-motion";
import { PropsWithChildren, useId } from "react";

import Count from "./Count";

interface SwitcherProps {
Expand Down
Loading

0 comments on commit a450052

Please sign in to comment.