Skip to content

Commit

Permalink
refactor game to use context/provider
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgrunwald committed Nov 6, 2024
1 parent 0282873 commit e00ef7f
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 194 deletions.
57 changes: 21 additions & 36 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import './App.css';
import { PicketSign } from './components/PicketSign';
import { ResultsDialog } from './components/ResultsDialog';
import { Game, NewGame } from './game';
import { GameContext } from './contexts/gameContext';

function App() {
const [game, setGame] = useState<Game>(NewGame());
const [duration, setDuration] = useState('0m 0s');
const {
score,
attempts,
counts,
cards,
hasMatchAllCards,
reset,
handleClick,
duration
} = useContext(GameContext);
const [showDialog, setShowDialog] = useState(false);

useEffect(() => {
const interval = setInterval(() => {
setDuration(game.getDuration());
}, 1000);

return () => clearInterval(interval);
}, [duration, setDuration, game]);

useEffect(() => {
let timeout: number | undefined;

if (game.hasFlippedTwoCardsWithoutMatch()) {
timeout = setTimeout(() => {
setGame(game.resetUnmatchedCards());
}, 1000);
}

return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [game, setGame]);

useEffect(() => {
let timeout: number | undefined;
if (game.hasMatchAllCards()) {
if (hasMatchAllCards) {
timeout = setTimeout(() => setShowDialog(true), 1200);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [game]);
}, [hasMatchAllCards]);

return (
<>
<div id="game-container">
<div id="game">
{game.getCards().map((card) => (
{cards.map((card) => (
<div
className="card"
key={card.id}
onClick={() => setGame(game.handleClick(card))}
onClick={() => handleClick(card)}
>
<PicketSign card={card} />
</div>
Expand All @@ -67,21 +51,22 @@ function App() {
</p>
<div className="row">
<p className="attempts">
<strong>Picket signs flipped:</strong> {game.getAttempts()}
<strong>Picket signs flipped:</strong> {attempts}
</p>
<p className="score">
<strong>Matches:</strong> {game.getScore()}
<strong>Matches:</strong> {score}
</p>
</div>
</div>
</div>
{showDialog && (
<ResultsDialog
game={game}
counts={counts}
attempts={attempts}
onClose={() => setShowDialog(false)}
onReset={() => {
setShowDialog(false);
setGame(game.reset());
reset();
}}
duration={duration}
/>
Expand Down
13 changes: 7 additions & 6 deletions src/components/ResultsDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { useCallback, useMemo } from 'react';
import { Game } from '../../game';
import './ResultsDialog.css';

export const ResultsDialog = ({
onClose,
onReset,
duration,
game
counts,
attempts
}: {
onClose: () => void;
onReset: () => void;
duration: string;
game: Game;
counts: number[];
attempts: number;
}) => {
const squares = useMemo(
() => game.getCounts().map((c) => (c === 1 ? '🟩' : c === 2 ? '🟨' : '🟥')),
[game]
() => counts.map((c) => (c === 1 ? '🟩' : c === 2 ? '🟨' : '🟥')),
[counts]
);

const resultString = useMemo(() => {
Expand All @@ -42,7 +43,7 @@ export const ResultsDialog = ({
<strong>Time spent:</strong> {duration}
</p>
<p className="attempts">
<strong>Picket signs flipped:</strong> {game.getAttempts()}
<strong>Picket signs flipped:</strong> {attempts}
</p>

<div className="board">
Expand Down
147 changes: 147 additions & 0 deletions src/contexts/game.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState
} from 'react';
import { Card, getInitialCards } from '../card';
import { GameContext } from './gameContext';

const hasTwoMatchingCards = (cards: Card[]) => {
const [one, two] = cards;
return one?.value === two?.value;
};

const getFaceUp = (cards: Card[]) =>
cards.filter((c) => c.isFaceUp && !c.isMatched);

export const GameProvider = ({ children }: PropsWithChildren) => {
const [start, setStart] = useState<Date | null>(null);
const [end, setEnd] = useState<Date | null>(null);
const [duration, setDuration] = useState('0m 0s');
const [attempts, setAttempts] = useState(0);
const [cards, setCards] = useState(getInitialCards());

// computed values
const faceUpCards = useMemo(() => getFaceUp(cards), [cards]);
const hasFlippedTwoCards = useMemo(
() => faceUpCards.length >= 2,
[faceUpCards]
);
const hasMatchAllCards = useMemo(
() => cards.find((c) => !c.isMatched) === undefined,
[cards]
);
const counts = useMemo(() => cards.map((c) => c.count), [cards]);
const score = useMemo(
() => cards.filter((c) => c.isMatched).length / 2,
[cards]
);
const hasFlippedTwoCardsWithoutMatch = useMemo(() => {
return !hasFlippedTwoCards || hasTwoMatchingCards(cards) ? false : true;
}, [cards, hasFlippedTwoCards]);

const resetUnmatchedCards = useCallback(() => {
setCards(cards.map((c) => (c.isMatched ? c : { ...c, isFaceUp: false })));
}, [cards, setCards]);

// functions
const getDuration = useCallback(() => {
if (start === null) {
return '0m 0s';
}

const newEnd = end === null ? new Date() : end;

const diff = newEnd.getTime() - start.getTime();
const totalSeconds = Math.floor(diff / 1000);

// Calculate minutes and remaining seconds
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;

return `${minutes}m ${seconds}s`;
}, [start, end]);

const handleClick = useCallback(
(card: Card) => {
if (start === null) {
setStart(new Date());
}
if (card.isFaceUp || card.isMatched || hasFlippedTwoCards) {
return;
}
setAttempts((prev) => prev + 1);

setCards((oldCards) => {
let updatedCards = oldCards.map((c) =>
c.id === card.id ? { ...c, isFaceUp: true, count: c.count + 1 } : c
);

if (hasTwoMatchingCards(getFaceUp(updatedCards))) {
updatedCards = updatedCards.map((c) =>
c.isFaceUp ? { ...c, isMatched: true } : c
);
}
return updatedCards;
});
},
[hasFlippedTwoCards, start]
);

const reset = useCallback(() => {
setStart(null);
setEnd(null);
setAttempts(0);
setCards(getInitialCards());
}, [setStart, setEnd, setAttempts, setCards]);

// side effects
useEffect(() => {
if (end === null && hasMatchAllCards) {
setEnd(new Date());
}
}, [end, hasMatchAllCards]);

useEffect(() => {
const interval = setInterval(() => {
setDuration(getDuration());
}, 1000);

return () => clearInterval(interval);
}, [duration, getDuration, setDuration]);

useEffect(() => {
let timeout: number | undefined;

if (hasFlippedTwoCardsWithoutMatch) {
timeout = setTimeout(() => {
resetUnmatchedCards();
}, 1000);
}

return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [hasFlippedTwoCardsWithoutMatch, resetUnmatchedCards]);

return (
<GameContext.Provider
value={{
score,
attempts,
counts,
hasMatchAllCards,
reset,
cards,
handleClick,
duration
}}
>
{children}
</GameContext.Provider>
);
};
14 changes: 14 additions & 0 deletions src/contexts/gameContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext } from 'react';
import { Card } from '../card';

export const GameContext = createContext({
score: 0,
attempts: 0,
cards: [] as Card[],
counts: [] as number[],
duration: '',
hasMatchAllCards: false,
reset: () => {},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleClick: (_card: Card) => {}
});
Loading

0 comments on commit e00ef7f

Please sign in to comment.