diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 4d965dc..706f0d8 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -9,7 +9,7 @@ on: jobs: prettier-format: runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/README.md b/README.md index 886e780..55fb154 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,18 @@ ## Open Tasks: - [x] Fix Distorted images [@D3nn7](https://github.com/D3nn7) -- [x] Mobile Viewport fix +- [x] Mobile Viewport fix - [x] Display always new Pictures, when restarting the game [@KingPaulus](https://github.com/KingPaulus) - [x] Saving the Highscore in Local Storage [@bijanRegmi](https://github.com/BijanRegmi) - [x] Add winning Alert Message with a celebrating sound [@needl3](https://github.com/needl3) - [x] Add a sound by finding a match [@needl3](https://github.com/needl3) -- [x] Confetti Effect on Celebration 🎉 #45 [@SandeepKrSuman](https://github.com/SandeepKrSuman) +- [x] Confetti Effect on Celebration 🎉 #45 [@SandeepKrSuman](https://github.com/SandeepKrSuman) - [x] Count the time and show it next to the highscore, and in the Higscore Alert from [@needl3](https://github.com/needl3) -- [X] Build a Begin animation or sound #44 [@abhisheksharm-3](https://github.com/abhisheksharm-3) +- [x] Build a Begin animation or sound #44 [@abhisheksharm-3](https://github.com/abhisheksharm-3) - [x] Button, that makes a Screenshot of the Highscore to share it with friends [@insane-22](https://github.com/insane-22) - - [ ] Functionality Buttons on default Overlay - - [ ] Add API for Safari Share window - - [ ] Fix listener preloading, at the moment it needs two clicks for opening + - [ ] Functionality Buttons on default Overlay + - [ ] Add API for Safari Share window + - [ ] Fix listener preloading, at the moment it needs two clicks for opening - [x] Add light and dark Mode theme [@Surajit0573](https://github.com/Surajit0573) - [x] Animation on Pictures if they match #22 [@mohiwalla](https://github.com/mohiwalla) - [x] Add runtime in Local Storage and show with High score [@barkha-gupta](https://github.com/barkha-gupta) @@ -43,27 +43,27 @@ - -## How to contribute +## How to contribute 1. **Fork the repo** (optional) 2. **Clone the repository** to your local machine 3. **Create a new branch** for your changes 4. **Make your changes**: - - Add your name to the credits - - [Run the app](#run-the-app) - - [Test your changes](#test) + - Add your name to the credits + - [Run the app](#run-the-app) + - [Test your changes](#test) 5. **Push your changes** and create a Pull Request (PR) 6. **Wait for a review** and merge the PR 7. **Celebrate your first PR! 🎉** 8. **Repeat and keep contributing** ;) ### How to run the App + 1. **`npm install`** - - Installs all dependencies required for the project. + - Installs all dependencies required for the project. 2. **`npm start`** - - Runs the app in development mode. + - Runs the app in development mode. 3. Open [http://localhost:3000](http://localhost:3000) in your browser to view the app. @@ -71,14 +71,15 @@ The page will reload if you make edits.\ You will also see any lint errors in the console. ### How to test your Change + - First on localhost while developing - Then on the Vercel Link of your PR - - Vercel will deploy your changes automatically to a new Link - - You can find the Link in the PR + - Vercel will deploy your changes automatically to a new Link + - You can find the Link in the PR - Test mobile and Desktop Viewport Have fun while celebrating Hacktober - + Lets Hack! Feel free to make PR and complete the readme with your changes @@ -106,6 +107,7 @@ npm run format - 💡 The action will provide instructions on how to fix formatting issues ## Credits + Sound Effect from Pixabay Mixkit diff --git a/src/App.js b/src/App.js index 8a14fa4..45363c5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,5 @@ // src/App.js -import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; -import { nanoid } from 'nanoid'; +import { useEffect, useCallback } from 'react'; import './App.css'; import SingleCard from './components/singlecard/SingleCard'; import Celebration from './components/celebration/Celebration'; @@ -9,194 +8,99 @@ 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 controllers +import { + useGameController, + useTimerController, + useAudioController, + useScoreController, + useHintController, +} from './controllers'; import useTrackViewCounter from './hooks/useTrackViewCounter'; function App() { - const [cards, setCards] = useState([]); - const [turns, setTurns] = useState(0); - const [choiceOne, setChoiceOne] = useState(null); - const [choiceTwo, setChoiceTwo] = useState(null); - const [disabled, setDisabled] = useState(false); - const [highScore, setHighScore] = useState(0); - const [matched, setMatched] = useState(0); - 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); + // Initialize controllers + const gameController = useGameController(); + const timerController = useTimerController(); + const audioController = useAudioController(); + const scoreController = useScoreController(); + const hintController = useHintController(); const viewCounter = useTrackViewCounter(); - const REVEAL_DURATION = 2000; - const HINT_COOLDOWN = 5000; - const soundEffect = useMemo(() => { - const audio = new Audio(); - audio.autoplay = true; - return audio; - }, []); - - const playSound = useCallback( - src => { - soundEffect.src = src; - soundEffect.load(); - soundEffect.play().catch(() => {}); - }, - [soundEffect] - ); - - const resetTurn = useCallback(() => { - setChoiceOne(null); - setChoiceTwo(null); - setTurns(prev => prev + 1); - setDisabled(false); - }, []); - - const handleTime = useCallback(start => { - if (start) { - if (!intervalRef.current) { - intervalRef.current = setInterval(() => { - setElapsedTime(prev => (prev || 0) + 1); - }, 1000); - } - } else if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }, []); - - const clearTimer = useCallback(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }, []); - - 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; - const rand = new Uint32Array(1); - crypto.getRandomValues(rand); - return { ...card, id: rand[0], matched: false }; - }); - - secureShuffleArray(numbers); - setChoiceOne(null); - setChoiceTwo(null); - setCards(dup); - setTurns(0); - setMatched(0); - setCelebrationStatus(false); - setElapsedTime(undefined); - clearTimer(); - setAnimateCollapse(true); - setTimeout(() => setAnimateCollapse(false), 1200); - setGameOverMessage(false); - }, [clearTimer]); + // Destructure controller methods and state + const { + cards, + turns, + choiceOne, + choiceTwo, + disabled, + matched, + animateCollapse, + gameStarted, + setDisabled, + resetTurn, + initializeGame, + resetGameState: resetGame, + handleChoice: gameHandleChoice, + updateMatchedCards, + } = gameController; + + const { elapsedTime, handleTime, clearTimer, resetTimer } = timerController; + const { playSounds } = audioController; + const { + highScore, + celebrationStatus, + gameOverMessage, + checkGameCompletion, + resetGameState, + } = scoreController; + const { + hintCount, + hintCooldown, + hintActive, + hintCards, + resetHints, + cleanupHints, + } = hintController; + + // Enhanced hint handler that uses the hint controller + const handleHintClick = useCallback(() => { + hintCards( + cards, + turns, + elapsedTime, + gameController.setChoiceOne, + gameController.setChoiceTwo, + setDisabled, + gameController.setTurns, + handleTime + ); + }, [ + cards, + turns, + elapsedTime, + hintCards, + setDisabled, + handleTime, + gameController, + ]); const handleNewGame = useCallback(() => { - setHintCount(3); - setHintCooldown(0); - playSound('audio/start.mp3'); - shuffledCards(); - }, [playSound, shuffledCards]); + resetHints(); + resetGameState(); + resetTimer(); + playSounds.start(); + initializeGame(); + }, [resetHints, resetGameState, resetTimer, playSounds, initializeGame]); const handleChoice = useCallback( card => { - if (disabled) return; - choiceOne ? setChoiceTwo(card) : setChoiceOne(card); - if (elapsedTime === undefined) handleTime(true); + if (elapsedTime === undefined && !gameStarted) { + handleTime(true); + } + gameHandleChoice(card); }, - [choiceOne, disabled, elapsedTime, handleTime] + [elapsedTime, gameStarted, handleTime, gameHandleChoice] ); useEffect(() => { @@ -205,63 +109,55 @@ 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)) - ); - setMatched(prev => prev + 2); + playSounds.match(); + updateMatchedCards(choiceOne.src); resetTurn(); } else { - playSound('/audio/fail.wav'); + playSounds.fail(); const t = setTimeout(() => resetTurn(), 1000); return () => clearTimeout(t); } } else if (choiceOne) { - playSound('/audio/swap.wav'); + playSounds.swap(); } - }, [choiceOne, choiceTwo, resetTurn, playSound]); + }, [ + choiceOne, + choiceTwo, + hintActive, + setDisabled, + playSounds, + updateMatchedCards, + resetTurn, + ]); useEffect(() => { if (matched === cards.length && turns) { handleTime(false); - 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'); - setCelebrationStatus(true); - setHighScore(turns); - setGameOverMessage(false); - } else { - setGameOverMessage(true); - } + checkGameCompletion( + matched, + cards.length, + turns, + elapsedTime, + playSounds.celebration + ); } - }, [matched, cards.length, turns, elapsedTime, handleTime, playSound]); + }, [ + matched, + cards.length, + turns, + elapsedTime, + handleTime, + checkGameCompletion, + playSounds.celebration, + ]); useEffect(() => { - shuffledCards(); - const hs = Number(globalThis.localStorage.getItem('highscore') || 0); - setHighScore(hs); + initializeGame(); 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); + cleanupHints(); }; - }, [shuffledCards, clearTimer]); + }, [initializeGame, clearTimer, cleanupHints]); return (
@@ -281,7 +177,7 @@ function App() {