diff --git a/package-lock.json b/package-lock.json index 76c2576..1b0d405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4190,26 +4190,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4377,13 +4357,6 @@ "node": ">=10.13.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -19503,20 +19476,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -20016,20 +19975,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/public/index.html b/public/index.html index 24639aa..9ea38f4 100644 --- a/public/index.html +++ b/public/index.html @@ -2,8 +2,7 @@
- - + diff --git a/src/App.js b/src/App.js index 8a14fa4..f7e2931 100644 --- a/src/App.js +++ b/src/App.js @@ -1,19 +1,20 @@ -// src/App.js -import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; -import { nanoid } from 'nanoid'; -import './App.css'; -import SingleCard from './components/singlecard/SingleCard'; -import Celebration from './components/celebration/Celebration'; -import ToggleTheme from './components/toggleTheme/toggleTheme'; -import ShowConfetti from './components/confetti/Confetti'; -import GameOver from './components/gameover/GameOver'; -import CustomCursor from './components/CustomCursor/CustomCursor'; - -import { cardImages } from './data/cardImages'; -import { numbers } from './constants/numbers'; -import { secureShuffleArray, pickRandomImages } from './utils/logic'; -import useTrackViewCounter from './hooks/useTrackViewCounter'; - +import { useEffect, useState, useMemo, useCallback, useRef } from "react"; +import { nanoid } from "nanoid"; +import "./App.css"; +import SingleCard from "./components/singlecard/SingleCard"; +import Celebration from "./components/celebration/Celebration"; +import toggleTheme from "./components/toggleTheme/toggleTheme"; +import ShowConfetti from "./components/confetti/Confetti"; +import GameOver from "./components/gameover/GameOver"; +import CustomCursor from "./components/CustomCursor/CustomCursor"; + +import { cardImages } from "./data/cardImages"; +import { useHint } from "./utils/useHint"; + +const pickRandomImages = (arr, num) => { + const shuffled = [...arr].sort(() => 0.5 - Math.random()); + return shuffled.slice(0, num); +}; function App() { const [cards, setCards] = useState([]); const [turns, setTurns] = useState(0); @@ -25,17 +26,8 @@ function App() { const [celebrationStatus, setCelebrationStatus] = useState(false); const [elapsedTime, setElapsedTime] = useState(undefined); const intervalRef = useRef(null); - const hintTimeoutRef = useRef(null); - const hintIntervalRef = useRef(null); - const hintLockedRef = useRef(false); - const [hintCount, setHintCount] = useState(3); - const [hintCooldown, setHintCooldown] = useState(0); - const [hintActive, setHintActive] = useState(false); const [animateCollapse, setAnimateCollapse] = useState(false); const [gameOverMessage, setGameOverMessage] = useState(false); - const viewCounter = useTrackViewCounter(); - const REVEAL_DURATION = 2000; - const HINT_COOLDOWN = 5000; const soundEffect = useMemo(() => { const audio = new Audio(); @@ -44,7 +36,7 @@ function App() { }, []); const playSound = useCallback( - src => { + (src) => { soundEffect.src = src; soundEffect.load(); soundEffect.play().catch(() => {}); @@ -55,15 +47,15 @@ function App() { const resetTurn = useCallback(() => { setChoiceOne(null); setChoiceTwo(null); - setTurns(prev => prev + 1); + setTurns((prev) => prev + 1); setDisabled(false); }, []); - const handleTime = useCallback(start => { + const handleTime = useCallback((start) => { if (start) { if (!intervalRef.current) { intervalRef.current = setInterval(() => { - setElapsedTime(prev => (prev || 0) + 1); + setElapsedTime((prev) => (prev || 0) + 1); }, 1000); } } else if (intervalRef.current) { @@ -72,6 +64,21 @@ function App() { } }, []); + const { + hintCount, + hintCooldown, + hintActive, + hintCards, + resetHints, + } = useHint({ + cards, + handleTime, + setChoiceOne, + setChoiceTwo, + setTurns, + setDisabled, + }); + const clearTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -79,97 +86,16 @@ function App() { } }, []); - const hintCards = useCallback(() => { - if (hintActive || hintLockedRef.current) return; - - if (!cards || cards.length === 0) return; - if (hintCount === 0) return; - - setHintCount(c => c - 1); - hintLockedRef.current = true; - if (elapsedTime === undefined) handleTime(true); - const seconds = Math.floor(HINT_COOLDOWN / 1000); - setHintCooldown(seconds); - if (hintIntervalRef.current) clearInterval(hintIntervalRef.current); - hintIntervalRef.current = setInterval(() => { - setHintCooldown(s => { - if (s <= 1) { - clearInterval(hintIntervalRef.current); - hintIntervalRef.current = null; - hintLockedRef.current = false; - return 0; - } - return s - 1; - }); - }, 1000); - - const available = cards - .map((c, i) => ({ c, i })) - .filter(({ c }) => !c.matched); - if (available.length === 0) { - if (hintIntervalRef.current) { - clearInterval(hintIntervalRef.current); - hintIntervalRef.current = null; - } - setHintCooldown(0); - hintLockedRef.current = false; - return; - } - - if (available.length === 1) { - setHintActive(true); - setDisabled(true); - setChoiceOne(available[0].c); - setChoiceTwo(null); - - if (hintTimeoutRef.current) clearTimeout(hintTimeoutRef.current); - hintTimeoutRef.current = setTimeout(() => { - setChoiceOne(null); - setChoiceTwo(null); - setHintActive(false); - setDisabled(false); - }, REVEAL_DURATION); - - return; - } - - const idxA = - crypto.getRandomValues(new Uint32Array(1))[0] % available.length; - - let idxB = - crypto.getRandomValues(new Uint32Array(1))[0] % (available.length - 1); - if (idxB >= idxA) idxB += 1; - - const cardA = available[idxA].c; - const cardB = available[idxB].c; - - setHintActive(true); - setDisabled(true); - setChoiceOne(cardA); - setChoiceTwo(cardB); - setTurns(turns + 1); - - if (hintTimeoutRef.current) clearTimeout(hintTimeoutRef.current); - hintTimeoutRef.current = setTimeout(() => { - setChoiceOne(null); - setChoiceTwo(null); - setHintActive(false); - setDisabled(false); - }, REVEAL_DURATION); - }, [cards, hintCount, hintActive]); - const shuffledCards = useCallback(() => { const selected = pickRandomImages(cardImages, 6); const dup = [...selected, ...selected] .sort(() => nanoid(16).localeCompare(nanoid(16))) - .map(card => { - const crypto = globalThis.crypto || globalThis.msCrypto; + .map((card) => { const rand = new Uint32Array(1); crypto.getRandomValues(rand); return { ...card, id: rand[0], matched: false }; }); - secureShuffleArray(numbers); setChoiceOne(null); setChoiceTwo(null); setCards(dup); @@ -177,21 +103,20 @@ function App() { setMatched(0); setCelebrationStatus(false); setElapsedTime(undefined); - clearTimer(); + setAnimateCollapse(true); setTimeout(() => setAnimateCollapse(false), 1200); setGameOverMessage(false); - }, [clearTimer]); + }, []); const handleNewGame = useCallback(() => { - setHintCount(3); - setHintCooldown(0); - playSound('audio/start.mp3'); + resetHints(); + playSound("audio/start.mp3"); shuffledCards(); - }, [playSound, shuffledCards]); + }, [playSound, shuffledCards, resetHints]); const handleChoice = useCallback( - card => { + (card) => { if (disabled) return; choiceOne ? setChoiceTwo(card) : setChoiceOne(card); if (elapsedTime === undefined) handleTime(true); @@ -205,36 +130,38 @@ function App() { if (choiceOne && choiceTwo) { setDisabled(true); if (choiceOne.src === choiceTwo.src) { - playSound('/audio/match.wav'); - setCards(prev => - prev.map(c => (c.src === choiceOne.src ? { ...c, matched: true } : c)) + playSound("/audio/match.wav"); + setCards((prev) => + prev.map((c) => + c.src === choiceOne.src ? { ...c, matched: true } : c + ) ); - setMatched(prev => prev + 2); + setMatched((prev) => prev + 2); resetTurn(); } else { - playSound('/audio/fail.wav'); + playSound("/audio/fail.wav"); const t = setTimeout(() => resetTurn(), 1000); return () => clearTimeout(t); } } else if (choiceOne) { - playSound('/audio/swap.wav'); + playSound("/audio/swap.wav"); } - }, [choiceOne, choiceTwo, resetTurn, playSound]); + }, [choiceOne, choiceTwo, resetTurn, playSound, hintActive]); useEffect(() => { if (matched === cards.length && turns) { handleTime(false); - const storedHigh = globalThis.localStorage.getItem('highscore'); - const storedRun = globalThis.localStorage.getItem('runtime'); + const storedHigh = globalThis.localStorage.getItem("highscore"); + const storedRun = globalThis.localStorage.getItem("runtime"); const better = storedHigh === null || turns < Number(storedHigh) || (turns === Number(storedHigh) && elapsedTime < Number(storedRun)); if (better) { - globalThis.localStorage.setItem('highscore', turns); - globalThis.localStorage.setItem('runtime', elapsedTime); - playSound('audio/celebration.mp3'); + globalThis.localStorage.setItem("highscore", turns); + globalThis.localStorage.setItem("runtime", elapsedTime); + playSound("audio/celebration.mp3"); setCelebrationStatus(true); setHighScore(turns); setGameOverMessage(false); @@ -246,25 +173,16 @@ function App() { useEffect(() => { shuffledCards(); - const hs = Number(globalThis.localStorage.getItem('highscore') || 0); + const hs = Number(globalThis.localStorage.getItem("highscore") || 0); setHighScore(hs); return () => { clearTimer(); - if (hintTimeoutRef.current) { - clearTimeout(hintTimeoutRef.current); - hintTimeoutRef.current = null; - } - if (hintIntervalRef.current) { - clearInterval(hintIntervalRef.current); - hintIntervalRef.current = null; - } - hintLockedRef.current = false; - setHintCooldown(0); + resetHints(); }; - }, [shuffledCards, clearTimer]); + }, [shuffledCards, clearTimer, resetHints]); return ( -
+
- {hintCount === 1 ? 'Hint Remaining: ' : 'Hints Remaining: '} +
+ {hintCount === 1 ? "Hint Remaining: " : "Hints Remaining: "} {hintCount}
Turns: {turns}
-HighScore: {highScore}
-Runtime: {globalThis.localStorage.getItem('runtime') || 0}
+Runtime: {globalThis.localStorage.getItem("runtime") || 0}
Time Elapsed: {elapsedTime || 'Not started'}
- {viewCounter !== null &&This memory got {viewCounter} views
} +Time Elapsed: {elapsedTime || "Not started"}
{gameOverMessage && (