├── src ├── vite-env.d.ts ├── assets │ ├── MemorySong.mp3 │ ├── images │ │ ├── app-bg.jpg │ │ ├── clock.png │ │ ├── score.png │ │ ├── sound.png │ │ ├── fruit-bg.png │ │ ├── gameboard.png │ │ ├── restart-btn.png │ │ ├── sidebar-bg.png │ │ ├── sound-mute.png │ │ └── fruits │ │ │ ├── apple.png │ │ │ ├── durian.png │ │ │ ├── pineapple.png │ │ │ ├── starfruit.png │ │ │ ├── dragonfruit.png │ │ │ ├── mangosteen.png │ │ │ ├── watermelon.png │ │ │ └── passionfruit.png │ └── react.svg ├── main.tsx ├── index.css ├── components │ ├── GameBoard.tsx │ ├── StartModal.tsx │ ├── SideBar.tsx │ ├── FruitCard.tsx │ └── Overlay.tsx ├── App.tsx └── context │ └── MemoryContext.tsx ├── postcss.config.js ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── index.html ├── tailwind.config.js ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── package.json ├── public └── vite.svg └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/MemorySong.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/MemorySong.mp3 -------------------------------------------------------------------------------- /src/assets/images/app-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/app-bg.jpg -------------------------------------------------------------------------------- /src/assets/images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/clock.png -------------------------------------------------------------------------------- /src/assets/images/score.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/score.png -------------------------------------------------------------------------------- /src/assets/images/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/sound.png -------------------------------------------------------------------------------- /src/assets/images/fruit-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruit-bg.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/images/gameboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/gameboard.png -------------------------------------------------------------------------------- /src/assets/images/restart-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/restart-btn.png -------------------------------------------------------------------------------- /src/assets/images/sidebar-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/sidebar-bg.png -------------------------------------------------------------------------------- /src/assets/images/sound-mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/sound-mute.png -------------------------------------------------------------------------------- /src/assets/images/fruits/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/apple.png -------------------------------------------------------------------------------- /src/assets/images/fruits/durian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/durian.png -------------------------------------------------------------------------------- /src/assets/images/fruits/pineapple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/pineapple.png -------------------------------------------------------------------------------- /src/assets/images/fruits/starfruit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/starfruit.png -------------------------------------------------------------------------------- /src/assets/images/fruits/dragonfruit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/dragonfruit.png -------------------------------------------------------------------------------- /src/assets/images/fruits/mangosteen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/mangosteen.png -------------------------------------------------------------------------------- /src/assets/images/fruits/watermelon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/watermelon.png -------------------------------------------------------------------------------- /src/assets/images/fruits/passionfruit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendy278/Memory-Games/HEAD/src/assets/images/fruits/passionfruit.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Memory Game 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | backgroundImage: { 7 | "app-banner": "url(assets/images/app-bg.jpg)", 8 | "gameboard-background": "url(assets/images/gameboard.png)", 9 | "fruit-background": "url(assets/images/fruit-bg.png)", 10 | "sidebar-background": "url(assets/images/sidebar-bg.png)", 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* FruitCard.css */ 6 | 7 | .perspective-1000 { 8 | perspective: 500px; 9 | } 10 | 11 | .flip-card-inner { 12 | width: 100%; 13 | height: 100%; 14 | transition: transform 0.6s; 15 | transform-style: preserve-3d; 16 | position: relative; 17 | } 18 | 19 | .flip-card-front, 20 | .flip-card-back { 21 | width: 100%; 22 | height: 100%; 23 | position: absolute; 24 | backface-visibility: hidden; 25 | } 26 | 27 | .flip-card-back { 28 | transform: rotateY(180deg); 29 | } 30 | 31 | .flip-card-flipped { 32 | transform: rotateY(180deg); 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/GameBoard.tsx: -------------------------------------------------------------------------------- 1 | import FruitCard from "./FruitCard"; 2 | import { useContext } from "react"; 3 | import { GameContext } from "../context/MemoryContext"; 4 | 5 | const GameBoard = () => { 6 | const gameContext = useContext(GameContext); 7 | 8 | if (!gameContext) { 9 | return null; 10 | } 11 | 12 | const { cards } = gameContext; 13 | 14 | return ( 15 |
16 | {cards.map((_, index) => ( 17 | 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | export default GameBoard; 24 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memorygame", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-confetti": "^6.1.0", 15 | "react-dom": "^18.3.1" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.9.0", 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.3.0", 21 | "@vitejs/plugin-react": "^4.3.1", 22 | "autoprefixer": "^10.4.20", 23 | "eslint": "^9.9.0", 24 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 25 | "eslint-plugin-react-refresh": "^0.4.9", 26 | "globals": "^15.9.0", 27 | "postcss": "^8.4.47", 28 | "tailwindcss": "^3.4.13", 29 | "typescript": "^5.5.3", 30 | "typescript-eslint": "^8.0.1", 31 | "vite": "^5.4.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/StartModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface StartModalProps { 4 | isModalOpen: boolean; 5 | onStart: () => void; 6 | closeModal: () => void; 7 | } 8 | 9 | const StartModal: React.FC = ({ 10 | isModalOpen, 11 | onStart, 12 | closeModal, 13 | }) => { 14 | return ( 15 |
20 |
21 |

22 | Start the Game 23 |

24 | 33 |

34 | Game Build By : Rendev 35 |

36 |
37 |
38 | ); 39 | }; 40 | 41 | export default StartModal; 42 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import ClockBtn from "../assets/images/clock.png"; 3 | import RestartBtn from "../assets/images/restart-btn.png"; 4 | import { GameContext } from "../context/MemoryContext"; 5 | 6 | const SideBar: React.FC = () => { 7 | const gameContext = useContext(GameContext); 8 | 9 | if (!gameContext) { 10 | return null; 11 | } 12 | 13 | const { timeLeft, restartGame, gameOver } = gameContext; 14 | 15 | return ( 16 |
20 |
21 | Clock Icon 22 |

23 | Time Left:{" "} 24 | 25 | {gameOver ? "Game Over" : `${timeLeft} secs`} 26 | 27 |

28 |
29 | 30 |
31 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default SideBar; 45 | -------------------------------------------------------------------------------- /src/components/FruitCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { GameContext } from "../context/MemoryContext"; 3 | 4 | interface FruitCardProps { 5 | fruitId: number; 6 | } 7 | 8 | const FruitCard: React.FC = ({ fruitId }) => { 9 | const gameContext = useContext(GameContext); 10 | 11 | if (!gameContext) { 12 | return null; 13 | } 14 | 15 | const { cards, flipCard, showIcons } = gameContext; 16 | 17 | // Ensure cards and fruitId are valid 18 | if (!cards || cards.length === 0 || fruitId < 0 || fruitId >= cards.length) { 19 | return null; 20 | } 21 | 22 | const card = cards[fruitId]; 23 | 24 | const handleClick = () => { 25 | if (!card.flipped && !showIcons) { 26 | flipCard(fruitId); 27 | } 28 | }; 29 | 30 | return ( 31 |
35 |
40 |
41 |

?

42 |
43 |
44 | {card.fruit} 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default FruitCard; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import GameBoard from "./components/GameBoard"; 3 | import SideBar from "./components/SideBar"; 4 | import GameContextProvider, { GameContext } from "./context/MemoryContext"; 5 | import Overlay from "./components/Overlay"; 6 | import StartModal from "./components/StartModal"; 7 | 8 | const App: React.FC = () => { 9 | const [isModalOpen, setIsModalOpen] = useState(true); // Track modal state 10 | 11 | return ( 12 | 13 | 14 | {}} 16 | isModalOpen={isModalOpen} 17 | closeModal={() => setIsModalOpen(false)} 18 | /> 19 | 20 | ); 21 | }; 22 | 23 | const GameContent: React.FC<{ 24 | setIsModalOpen: React.Dispatch>; 25 | }> = ({ setIsModalOpen }) => { 26 | const { startGame, isGameStarted } = useContext(GameContext)!; 27 | 28 | const handleStartGame = () => { 29 | startGame(); // Start the game logic 30 | setIsModalOpen(false); // Close modal after starting the game 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 | {isGameStarted && } 38 | {isGameStarted && } 39 |
40 | 41 | setIsModalOpen(false)} // Close modal function 45 | /> 46 |
47 | ); 48 | }; 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/components/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { GameContext } from "../context/MemoryContext"; // Pastikan Anda mengimpor tipe GameContextType 3 | import ScoreIcon from "../assets/images/score.png"; 4 | import RestartIcon from "../assets/images/restart-btn.png"; 5 | import Confetti from "react-confetti"; 6 | 7 | const Overlay: React.FC = () => { 8 | const context = useContext(GameContext); 9 | 10 | // Periksa apakah context terdefinisi 11 | if (!context) { 12 | throw new Error("Overlay must be used within a GameContextProvider"); // Tambahkan error handling 13 | } 14 | 15 | const { gameOver, gameWon, restartGame, score } = context; // Ambil nilai dari context 16 | 17 | if (!gameOver && !gameWon) return null; // Return null if neither game over nor won 18 | 19 | return ( 20 |
21 |

22 | {gameOver ? "Game Over 😥" : "You Win 🎉"} 23 |

24 | 25 |
26 | Score icon 27 |

28 | Your Score: {score} 29 |

30 |
31 | 32 |
33 | 41 |
42 | 43 | {gameWon && ( 44 | 51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Overlay; 57 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/context/MemoryContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useEffect, 4 | useRef, 5 | useState, 6 | ReactNode, 7 | } from "react"; 8 | 9 | // Import fruit images 10 | import apple from "../assets/images/fruits/apple.png"; 11 | import dragonfruit from "../assets/images/fruits/dragonfruit.png"; 12 | import durian from "../assets/images/fruits/durian.png"; 13 | import mangosteen from "../assets/images/fruits/mangosteen.png"; 14 | import passionfruit from "../assets/images/fruits/passionfruit.png"; 15 | import pineapple from "../assets/images/fruits/pineapple.png"; 16 | import starfruit from "../assets/images/fruits/starfruit.png"; 17 | import watermelon from "../assets/images/fruits/watermelon.png"; 18 | 19 | // Import memory song 20 | import memorySong from "../assets/MemorySong.mp3"; 21 | 22 | interface Card { 23 | id: number; 24 | fruit: string; 25 | flipped: boolean; 26 | } 27 | 28 | interface GameContextType { 29 | cards: Card[]; 30 | flipCard: (index: number) => void; 31 | matchedPairs: string[]; 32 | showIcons: boolean; 33 | timeLeft: number; 34 | gameOver: boolean; 35 | gameWon: boolean; 36 | restartGame: () => void; 37 | score: number; 38 | startGame: () => void; 39 | isGameStarted: boolean; // State to track if the game has started 40 | } 41 | 42 | interface GameContextProviderProps { 43 | children: ReactNode; 44 | } 45 | 46 | export const GameContext = createContext( 47 | undefined 48 | ); 49 | 50 | const GameContextProvider: React.FC = ({ 51 | children, 52 | }) => { 53 | const [cards, setCards] = useState([]); 54 | const [flippedCards, setFlippedCards] = useState([]); 55 | const [matchedPairs, setMatchedPairs] = useState([]); 56 | const [showIcons, setShowIcons] = useState(true); 57 | const [timeLeft, setTimeLeft] = useState(45); 58 | const [gameOver, setGameOver] = useState(false); 59 | const [gameWon, setGameWon] = useState(false); 60 | const [isGameStarted, setIsGameStarted] = useState(false); // Initialize game state 61 | const timerRef = useRef | null>(null); // Fix for NodeJS issue 62 | const [score, setScore] = useState(0); 63 | const audioRef = useRef(null); 64 | 65 | const initializeGame = () => { 66 | const fruitIcons: string[] = [ 67 | apple, 68 | dragonfruit, 69 | durian, 70 | mangosteen, 71 | passionfruit, 72 | pineapple, 73 | starfruit, 74 | watermelon, 75 | ]; 76 | 77 | const pairIcons = [...fruitIcons, ...fruitIcons]; 78 | const shuffledIcons = pairIcons.sort(() => Math.random() - 0.5); 79 | 80 | const cardItems: Card[] = shuffledIcons.map((fruit, index) => ({ 81 | id: index, 82 | fruit, 83 | flipped: false, 84 | })); 85 | 86 | setCards(cardItems); 87 | setShowIcons(true); 88 | setIsGameStarted(true); // Mark game as started 89 | 90 | setTimeout(() => { 91 | setShowIcons(false); 92 | }, 3000); 93 | 94 | if (timerRef.current) clearInterval(timerRef.current); 95 | setTimeLeft(45); 96 | timerRef.current = setInterval(() => { 97 | setTimeLeft((prev) => { 98 | if (prev <= 1) { 99 | if (timerRef.current) clearInterval(timerRef.current); 100 | if (!gameWon) setGameOver(true); 101 | return 0; 102 | } 103 | return prev - 1; 104 | }); 105 | }, 1000); 106 | }; 107 | 108 | const startGame = () => { 109 | if (!audioRef.current) { 110 | audioRef.current = new Audio(memorySong); 111 | } 112 | audioRef.current.currentTime = 0; // Reset audio to start 113 | audioRef.current.play(); 114 | initializeGame(); 115 | }; 116 | 117 | useEffect(() => { 118 | return () => { 119 | if (timerRef.current) clearInterval(timerRef.current); 120 | if (audioRef.current) audioRef.current.pause(); 121 | }; 122 | }, []); 123 | 124 | const flipCard = (index: number) => { 125 | if (flippedCards.length === 2 || cards[index].flipped) return; 126 | 127 | const updatedCards = [...cards]; 128 | updatedCards[index].flipped = true; 129 | setCards(updatedCards); 130 | 131 | setFlippedCards((prevFlippedCards) => { 132 | const newFlippedCards = [...prevFlippedCards, index]; 133 | 134 | if (newFlippedCards.length === 2) { 135 | const [firstIndex, secondIndex] = newFlippedCards; 136 | 137 | if ( 138 | updatedCards[firstIndex].fruit === updatedCards[secondIndex].fruit 139 | ) { 140 | const newMatchedPairs = [ 141 | ...matchedPairs, 142 | updatedCards[firstIndex].fruit, 143 | ]; 144 | setMatchedPairs(newMatchedPairs); 145 | 146 | if (newMatchedPairs.length === cards.length / 2) { 147 | if (timerRef.current) clearInterval(timerRef.current); 148 | setTimeout(() => { 149 | setGameWon(true); 150 | setScore(timeLeft * 10); 151 | }, 1000); 152 | } 153 | } else { 154 | setTimeout(() => { 155 | const resetCards = [...updatedCards]; 156 | resetCards[firstIndex].flipped = false; 157 | resetCards[secondIndex].flipped = false; 158 | setCards(resetCards); 159 | }, 1000); 160 | } 161 | 162 | return []; 163 | } 164 | 165 | return newFlippedCards; 166 | }); 167 | }; 168 | 169 | const restartGame = () => { 170 | setFlippedCards([]); 171 | setMatchedPairs([]); 172 | setGameOver(false); 173 | setGameWon(false); 174 | setScore(0); 175 | setIsGameStarted(false); // Reset game state 176 | initializeGame(); 177 | }; 178 | 179 | return ( 180 | 195 | {children} 196 | 197 | ); 198 | }; 199 | 200 | export default GameContextProvider; 201 | --------------------------------------------------------------------------------