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..d3902cf 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,21 @@ -// 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 { useHint } from "./utils/useHint"; + +const crypto = globalThis.crypto || globalThis.msCrypto; +import useTrackViewCounter from "./hooks/useTrackViewCounter"; + import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { nanoid } from 'nanoid'; import './App.css'; @@ -25,12 +42,6 @@ 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(); @@ -72,6 +83,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); @@ -162,8 +188,12 @@ function App() { 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 crypto = globalThis.crypto || globalThis.msCrypto; + const rand = new Uint32Array(1); crypto.getRandomValues(rand); return { ...card, id: rand[0], matched: false }; @@ -177,18 +207,20 @@ function App() { setMatched(0); setCelebrationStatus(false); setElapsedTime(undefined); - clearTimer(); + setAnimateCollapse(true); setTimeout(() => setAnimateCollapse(false), 1200); setGameOverMessage(false); - }, [clearTimer]); + }, []); const handleNewGame = useCallback(() => { + resetHints(); + playSound("audio/start.mp3"); setHintCount(3); setHintCooldown(0); playSound('audio/start.mp3'); shuffledCards(); - }, [playSound, shuffledCards]); + }, [playSound, shuffledCards, resetHints]); const handleChoice = useCallback( card => { @@ -219,7 +251,7 @@ function App() { } else if (choiceOne) { playSound('/audio/swap.wav'); } - }, [choiceOne, choiceTwo, resetTurn, playSound]); + }, [choiceOne, choiceTwo, resetTurn, playSound, hintActive]); useEffect(() => { if (matched === cards.length && turns) { @@ -250,18 +282,9 @@ function App() { 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 (
@@ -276,6 +299,18 @@ function App() { {celebrationStatus && } A&A Match
+
+ +
+ +

+ {hintCount === 1 ? "Hint Remaining: " : "Hints Remaining: "}

@@ -293,6 +328,8 @@ function App() {
+
+ {cards.map((card) => (
diff --git a/src/components/singlecard/SingleCard.js b/src/components/singlecard/SingleCard.js index ff349ff..4109236 100644 --- a/src/components/singlecard/SingleCard.js +++ b/src/components/singlecard/SingleCard.js @@ -8,6 +8,51 @@ export default function SingleCard({ card, handleChoice, flipped, disabled }) { } }; + const handleClick = () => { + if (!disabled) { + handleChoice(card); + } + }; + + return ( +
+
+
+ card front blur background +card front + +
+ +
return (
diff --git a/src/index.js b/src/index.js index 6832e78..1b3a5f8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,15 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; // <-- make sure this is 'react-dom/client' import './index.css'; import App from './App'; -ReactDOM.render( +const container = document.getElementById('root'); +const root = createRoot(container); // <-- create root +root.render( + +); , document.getElementById('root') ); diff --git a/src/utils/logic.js b/src/utils/logic.js index 40ce1b8..ab61fd0 100644 --- a/src/utils/logic.js +++ b/src/utils/logic.js @@ -1,4 +1,3 @@ -// src/utils/logic.js export function secureShuffleArray(array) { const cryptoObj = globalThis.crypto || globalThis.msCrypto; for (let i = array.length - 1; i > 0; i--) { diff --git a/src/utils/useHint.js b/src/utils/useHint.js new file mode 100644 index 0000000..9c4cf4e --- /dev/null +++ b/src/utils/useHint.js @@ -0,0 +1,96 @@ +// src/utils/useHint.js +import { useState, useRef, useCallback } from "react"; + +export function useHint({ + cards, + handleTime, + setChoiceOne, + setChoiceTwo, + setTurns, + setDisabled, +}) { + const [hintCount, setHintCount] = useState(3); + const [hintCooldown, setHintCooldown] = useState(0); + const [hintActive, setHintActive] = useState(false); + + const hintTimeoutRef = useRef(null); + const hintIntervalRef = useRef(null); + const hintLockedRef = useRef(false); + + const REVEAL_DURATION = 2000; + const HINT_COOLDOWN = 5000; + + 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; + + 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.filter(c => !c.matched); + if (available.length === 0) { + clearInterval(hintIntervalRef.current); + hintLockedRef.current = false; + setHintCooldown(0); + return; + } + + const cryptoObj = globalThis.crypto || globalThis.msCrypto; + + const idxA = cryptoObj.getRandomValues(new Uint32Array(1))[0] % available.length; + let idxB = cryptoObj.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((t) => t + 1); + + if (hintTimeoutRef.current) clearTimeout(hintTimeoutRef.current); + hintTimeoutRef.current = setTimeout(() => { + setChoiceOne(null); + setChoiceTwo(null); + setHintActive(false); + setDisabled(false); + }, REVEAL_DURATION); + }, [cards, hintCount, hintActive, handleTime, setChoiceOne, setChoiceTwo, setTurns, setDisabled]); + + const resetHints = useCallback(() => { + setHintCount(3); + setHintCooldown(0); + hintLockedRef.current = false; + if (hintTimeoutRef.current) clearTimeout(hintTimeoutRef.current); + if (hintIntervalRef.current) clearInterval(hintIntervalRef.current); + }, []); + + return { + hintCount, + hintCooldown, + hintActive, + hintCards, + resetHints, + }; +}