Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e86ffd74 |
+19
-9
@@ -1,16 +1,26 @@
|
||||
# Developement
|
||||
.git
|
||||
.prettierrc
|
||||
.gitignore
|
||||
README.md
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
# Build
|
||||
build
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Git
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
|
||||
# Builds
|
||||
dist
|
||||
|
||||
# Dev configs
|
||||
.prettierrc
|
||||
.prettierignore
|
||||
jsconfig.json
|
||||
|
||||
# Markdown files
|
||||
*.md
|
||||
|
||||
# Release scripts
|
||||
release
|
||||
+15
-4
@@ -1,12 +1,23 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"endOfLine": "lf",
|
||||
"jsxSingleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.scss",
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"printWidth": 500,
|
||||
"proseWrap": "always",
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleAttributePerLine": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 500
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
+18
-11
@@ -1,23 +1,30 @@
|
||||
# Build stage
|
||||
FROM node:lts-alpine as build
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Production dependencies
|
||||
FROM base AS prod-deps
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Build
|
||||
FROM base AS build
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
# Final stage
|
||||
FROM node:lts-alpine as final
|
||||
FROM node:lts-alpine AS final
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/dist /app/dist
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --from=build /usr/src/app/build .
|
||||
|
||||
RUN npm i -g serve
|
||||
|
||||
CMD serve
|
||||
CMD ["npm", "start"]
|
||||
@@ -79,7 +79,7 @@ Minesweeper is a game where the player has to clear a minefield without detonati
|
||||
docker run --name minesweeper -dp 3000:3000 minesweeper
|
||||
```
|
||||
|
||||
# 👩❤️👨 Credits
|
||||
# 👷♂️ Credits
|
||||
|
||||
- [KeunotorCagoule](https://www.github.com/KeunotorCagoule)
|
||||
- [UnEpicier](https://www.github.com/UnEpicier)
|
||||
- [KeunotorCagoule](https://www.github.com/KeunotorCagoule)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js';
|
||||
import { Linter } from 'eslint';
|
||||
import react from 'eslint-plugin-react';
|
||||
import globals from 'globals';
|
||||
|
||||
const config: Linter.Config[] = [
|
||||
js.configs.recommended,
|
||||
react.configs.flat.recommended,
|
||||
{
|
||||
plugins: {
|
||||
react: react,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default config;
|
||||
@@ -1,15 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="%PUBLIC_URL%/favicon.ico"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<meta
|
||||
name="mobile-web-app-capable"
|
||||
content="yes"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#000000"
|
||||
@@ -18,18 +18,27 @@
|
||||
name="description"
|
||||
content="Démineur en React & Typescript"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="favicon.ico"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="%PUBLIC_URL%/logo192.png"
|
||||
href="logo192.png"
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href="%PUBLIC_URL%/manifest.json"
|
||||
href="manifest.json"
|
||||
/>
|
||||
|
||||
<title>Démineur</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/index.tsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+5265
-13955
File diff suppressed because it is too large
Load Diff
+27
-33
@@ -2,44 +2,38 @@
|
||||
"name": "demineur",
|
||||
"version": "1.0.0",
|
||||
"description": "Démineur en ReactJS & TypeScript",
|
||||
"author": "KeunotorCagoulé",
|
||||
"author": "UnEpicier",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "serve -s -l 8000 dist",
|
||||
"docker:build": "docker rmi minesweeper && docker build -t minesweeper ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.53",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"localforage": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"recoil": "^0.7.7",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"dotenv": "^17.0.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"globals": "^16.3.0",
|
||||
"jiti": "^2.4.2",
|
||||
"localforage": "^1.10.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sass": "^1.89.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-pwa": "^1.0.1",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dependencies": {
|
||||
"serve": "^14.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useEffect, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from './contexts/gameState';
|
||||
import { langAtom } from './contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// --------------------------------------------------- Components ------------------------------------------------------
|
||||
import Home from './components/Home/Home';
|
||||
import Timer from './components/Stats/Timer/Timer';
|
||||
import MinesCounter from './components/Stats/MinesCounter/MinesCounter';
|
||||
import Grid from './components/Grid/Grid';
|
||||
import Help from './components/Help/Help';
|
||||
import LeaderBoard from './components/Leaderboard/Leaderboard';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Langs --------------------------------------------------------
|
||||
import frConf from './langs/fr.json';
|
||||
import enConf from './langs/en.json';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||
import Logo from './assets/logo';
|
||||
import './App.scss';
|
||||
import LangToggler from './components/LangToggler/LangToggler';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const App = () => {
|
||||
const [helpDisplayed, displayHelp] = useState(false);
|
||||
|
||||
const [gameState] = useRecoilState(gameStateAtom);
|
||||
const [lang, setLang] = useRecoilState(langAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const storedLang = localStorage.getItem('lang');
|
||||
|
||||
if (storedLang === null) {
|
||||
localStorage.setItem('lang', 'fr');
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedLang === 'fr') {
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLang({
|
||||
key: 'en',
|
||||
config: enConf,
|
||||
});
|
||||
}, [lang.key]);
|
||||
|
||||
if (lang.config) {
|
||||
return (
|
||||
<div className='App'>
|
||||
<LangToggler />
|
||||
{gameState.status === 'settings' && <Home />}
|
||||
{(gameState.status === 'idle' || gameState.status === 'playing') && (
|
||||
<>
|
||||
<div className='stats'>
|
||||
<Timer />
|
||||
<Logo className={'logo'} />
|
||||
<MinesCounter />
|
||||
</div>
|
||||
<Grid />
|
||||
<button
|
||||
className='helpButton'
|
||||
onClick={() => displayHelp(true)}
|
||||
>
|
||||
<span>{lang.config.how}</span>
|
||||
</button>
|
||||
{helpDisplayed && (
|
||||
<Help
|
||||
onClose={() => {
|
||||
displayHelp(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{gameState.status === 'board' && <LeaderBoard />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
& > .helpButton {
|
||||
& > .help-button {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
@@ -0,0 +1,99 @@
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useEffect, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// --------------------------------------------------- Components ------------------------------------------------------
|
||||
import Home from '../components/Home/Home';
|
||||
import Timer from '../components/Stats/Timer/Timer';
|
||||
import MinesCounter from '../components/Stats/MinesCounter/MinesCounter';
|
||||
import Grid from '../components/Grid/Grid';
|
||||
import Help from '../components/Help/Help';
|
||||
import LeaderBoard from '../components/Leaderboard/Leaderboard';
|
||||
import LangToggler from '../components/LangToggler/LangToggler';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { useLangStore } from '../store/langState';
|
||||
import { useGameStateStore } from '@/store/gameState';
|
||||
import frConf from '../langs/fr.json';
|
||||
import enConf from '../langs/en.json';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||
import Logo from '../assets/logo';
|
||||
import './App.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const App = () => {
|
||||
const [helpDisplayed, displayHelp] = useState(false);
|
||||
|
||||
const gameStatus = useGameStateStore((state) => state.status);
|
||||
const { key, config, setLang } = useLangStore();
|
||||
|
||||
useEffect(() => {
|
||||
const storedLang = localStorage.getItem('lang');
|
||||
|
||||
if (storedLang === null) {
|
||||
localStorage.setItem('lang', 'fr');
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedLang === 'fr') {
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLang({
|
||||
key: 'en',
|
||||
config: enConf,
|
||||
});
|
||||
}, [key]);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
<LangToggler />
|
||||
{gameStatus === 'settings' && <Home />}
|
||||
{(gameStatus === 'idle' || gameStatus === 'playing') && (
|
||||
<>
|
||||
<div className='stats'>
|
||||
<Timer />
|
||||
<Logo className={'logo'} />
|
||||
<MinesCounter />
|
||||
</div>
|
||||
<Grid />
|
||||
<button
|
||||
className='help-button'
|
||||
onClick={() => displayHelp(true)}
|
||||
>
|
||||
<span>{config.how}</span>
|
||||
</button>
|
||||
{helpDisplayed && (
|
||||
<Help
|
||||
onClose={() => {
|
||||
displayHelp(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{gameStatus === 'board' && <LeaderBoard />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -2,43 +2,26 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Hooks --------------------------------------------------------
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { useLangStore } from '@/store/langState';
|
||||
import { useActions } from './hooks/useActions';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
||||
import { genGrid } from '../../utils/generation';
|
||||
import { getCellDisplayContent } from '../../utils/gameInteractions';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||
import Podium from '../../assets/podium';
|
||||
import './styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const Grid = () => {
|
||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [grid, setGrid] = useState(genGrid(gameState.gridSize));
|
||||
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const { handleLeftClick, handleRightClick } = useActions(grid, setGrid, gameState, setGameState);
|
||||
const config = useLangStore((state) => state.config);
|
||||
const { grid, gridSize, endType, handleLeftClick, handleRightClick, openLeaderboard } = useActions();
|
||||
|
||||
return (
|
||||
<div
|
||||
className='grid'
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${gameState.gridSize}, var(--size))`,
|
||||
gridTemplateRows: `repeat(${gridSize}, var(--size))`,
|
||||
}}
|
||||
>
|
||||
{grid.map((row, rowIndex) => (
|
||||
@@ -46,7 +29,7 @@ const Grid = () => {
|
||||
key={`row${rowIndex}`}
|
||||
className='row'
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${gameState.gridSize}, var(--size))`,
|
||||
gridTemplateColumns: `repeat(${gridSize}, var(--size))`,
|
||||
}}
|
||||
>
|
||||
{row.map((cell, cellIndex) => {
|
||||
@@ -63,19 +46,19 @@ const Grid = () => {
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
{gameState.endType !== '' && (
|
||||
{endType !== '' && (
|
||||
<div
|
||||
className='overlay'
|
||||
style={
|
||||
{
|
||||
'--overlayColor': gameState.endType === 'win' ? '#5f8e59' : '#ca5940',
|
||||
'--overlayColor': endType === 'win' ? '#5f8e59' : '#ca5940',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<p className='overlayTitle'>{gameState.endType === 'win' ? lang.config.win : lang.config.loose}</p>
|
||||
<p className='overlayTitle'>{endType === 'win' ? config.win : config.loose}</p>
|
||||
<button
|
||||
className='overlayButton'
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'board' }))}
|
||||
onClick={openLeaderboard}
|
||||
>
|
||||
<Podium />
|
||||
Leaderboard
|
||||
|
||||
@@ -2,22 +2,33 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { MouseEvent, useCallback, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
||||
import { SetterOrUpdater } from 'recoil';
|
||||
import { Difficulty, GridType } from '../../../types/game';
|
||||
import { GameState } from '../../../types/gameState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
||||
import { GridType } from '../../../types/game';
|
||||
import { discoverAroundCell, revealAllGrid } from '../../../utils/gameInteractions';
|
||||
import { startGame } from '../../../utils/generation';
|
||||
import { genGrid, startGame } from '../../../utils/generation';
|
||||
import { useGameStateStore } from '@/store/gameState';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStateAction<GridType>>, gameState: GameState, setGameState: SetterOrUpdater<GameState>) => {
|
||||
export const useActions = () => {
|
||||
const { gridSize, endType, status, difficulty, bombs, placedFlags, setStatus, setBombs, setEndType, setPlacedFlags } = useGameStateStore(
|
||||
useShallow((state) => ({
|
||||
gridSize: state.gridSize,
|
||||
endType: state.endType,
|
||||
status: state.status,
|
||||
difficulty: state.difficulty,
|
||||
bombs: state.bombs,
|
||||
placedFlags: state.placedFlags,
|
||||
|
||||
setStatus: state.setStatus,
|
||||
setBombs: state.setBombs,
|
||||
setEndType: state.setEndType,
|
||||
setPlacedFlags: state.setPlacedFlags,
|
||||
})),
|
||||
);
|
||||
const [grid, setGrid] = useState(genGrid(gridSize));
|
||||
|
||||
const [, updateGrid] = useState({});
|
||||
const forceUpdate = useCallback(() => updateGrid({}), []);
|
||||
|
||||
@@ -30,25 +41,19 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
||||
let bombsCount: number | null = null;
|
||||
|
||||
// Start the game with making sure the clicked cell will be empty after generation
|
||||
if (gameState.status === 'idle') {
|
||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gameState.gridSize, gameState.difficulty as Difficulty, true, rowIndex, colIndex);
|
||||
if (status === 'idle') {
|
||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gridSize, difficulty, true, rowIndex, colIndex);
|
||||
prev = newGrid;
|
||||
|
||||
bombsCount = _bombsCount;
|
||||
|
||||
setGameState((prevGameState) => ({
|
||||
...prevGameState,
|
||||
status: 'playing',
|
||||
bombs: _bombsCount,
|
||||
}));
|
||||
setStatus('playing');
|
||||
setBombs(_bombsCount);
|
||||
}
|
||||
|
||||
// If the clicked cell hide a bomb, loose immediatly
|
||||
if (prev[rowIndex][colIndex].value === 'bomb') {
|
||||
setGameState((prevGameState) => ({
|
||||
...prevGameState,
|
||||
endType: 'loose',
|
||||
}));
|
||||
setEndType('loose');
|
||||
return revealAllGrid(grid, true);
|
||||
}
|
||||
// Discover surronding cells if the clicked is empty
|
||||
@@ -58,11 +63,8 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
||||
prev[rowIndex][colIndex].hidden = false;
|
||||
}
|
||||
|
||||
if (hasWin(prev, bombsCount || gameState.bombs, gameState.placedFlags)) {
|
||||
setGameState((prevGameState) => ({
|
||||
...prevGameState,
|
||||
endType: 'win',
|
||||
}));
|
||||
if (hasWin(prev, bombsCount || bombs, placedFlags)) {
|
||||
setEndType('win');
|
||||
return revealAllGrid(prev, true);
|
||||
}
|
||||
|
||||
@@ -83,27 +85,21 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
||||
let bombsCount: number | null = null;
|
||||
|
||||
// Start the game without making sure the clicked cell will be empty after generation
|
||||
if (gameState.status === 'idle') {
|
||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gameState.gridSize, gameState.difficulty as Difficulty, false);
|
||||
if (status === 'idle') {
|
||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gridSize, difficulty, false);
|
||||
prev = newGrid;
|
||||
|
||||
bombsCount = _bombsCount;
|
||||
|
||||
setGameState((prevGameState) => ({
|
||||
...prevGameState,
|
||||
status: 'playing',
|
||||
bombs: _bombsCount,
|
||||
}));
|
||||
setStatus('playing');
|
||||
setBombs(_bombsCount);
|
||||
}
|
||||
|
||||
prev = prev.map((row, idx) => {
|
||||
if (idx === rowIndex) {
|
||||
return row.map((col, idy) => {
|
||||
if (idy === colIndex) {
|
||||
setGameState((prevGameState) => ({
|
||||
...prevGameState,
|
||||
placedFlags: col.flag ? prevGameState.placedFlags - 1 : prevGameState.placedFlags + 1,
|
||||
}));
|
||||
setPlacedFlags(col.flag ? placedFlags - 1 : placedFlags + 1);
|
||||
|
||||
return {
|
||||
...col,
|
||||
@@ -116,11 +112,8 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
||||
return row;
|
||||
});
|
||||
|
||||
if (hasWin(prev, bombsCount || gameState.bombs, gameState.placedFlags)) {
|
||||
setGameState((prev) => ({
|
||||
...prev,
|
||||
endType: 'win',
|
||||
}));
|
||||
if (hasWin(prev, bombsCount || bombs, placedFlags)) {
|
||||
setEndType('win');
|
||||
return revealAllGrid(grid, true);
|
||||
}
|
||||
|
||||
@@ -129,9 +122,17 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
||||
}
|
||||
};
|
||||
|
||||
const openLeaderboard = useCallback(() => {
|
||||
setStatus('board');
|
||||
}, [setStatus]);
|
||||
|
||||
return {
|
||||
grid,
|
||||
gridSize,
|
||||
endType,
|
||||
handleLeftClick,
|
||||
handleRightClick,
|
||||
openLeaderboard,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useEffect, useRef } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------- Context & Stockage --------------------------------------------------
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { useCallback, useRef } from 'react';
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useGameStateStore } from '../../store/gameState';
|
||||
import { useLangStore } from '../../store/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||
import Size from '../../assets/size';
|
||||
import Difficulty from '../../assets/difficulty';
|
||||
import './styles.scss';
|
||||
@@ -27,43 +24,46 @@ const store = localforage.createInstance({
|
||||
description: 'Store 10 best game time',
|
||||
});
|
||||
|
||||
const Home = () => {
|
||||
export default function Home() {
|
||||
const difficultyRef = useRef<HTMLSelectElement>(null);
|
||||
const sizeRef = useRef<HTMLSelectElement>(null);
|
||||
|
||||
const [, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
const { setDifficulty, setGridSize, setStatus } = useGameStateStore(
|
||||
useShallow((state) => ({
|
||||
setDifficulty: state.setDifficulty,
|
||||
setGridSize: state.setGridSize,
|
||||
setStatus: state.setStatus,
|
||||
})),
|
||||
);
|
||||
|
||||
const onClick = () => {
|
||||
const config = useLangStore((state) => state.config);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (difficultyRef.current !== null && sizeRef.current !== null) {
|
||||
const difficulty = difficultyRef.current.value;
|
||||
const size = sizeRef.current.value;
|
||||
|
||||
const initIDB = async () => {
|
||||
(async () => {
|
||||
if ((await store.getItem(`${difficulty}:${size}`)) === null) {
|
||||
await store.setItem(`${difficulty}:${size}`, []);
|
||||
}
|
||||
};
|
||||
initIDB();
|
||||
})();
|
||||
|
||||
setGameState((prev) => ({
|
||||
...prev,
|
||||
difficulty: difficulty,
|
||||
gridSize: parseInt(size),
|
||||
status: 'idle',
|
||||
}));
|
||||
setGridSize(parseInt(size) as 12 | 16 | 20);
|
||||
setDifficulty(difficulty as 'beginner' | 'intermediate' | 'expert');
|
||||
setStatus('idle');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='gameContainer'>
|
||||
<h1 className='title'>{lang.config.appTitle}</h1>
|
||||
<h1 className='title'>{config.appTitle}</h1>
|
||||
|
||||
<div className='settingsContainer'>
|
||||
<div className='selectorBox'>
|
||||
<p className='settingLabel'>
|
||||
<Size />
|
||||
{lang.config.settings.size.label}
|
||||
{config.settings.size.label}
|
||||
</p>
|
||||
<select
|
||||
ref={sizeRef}
|
||||
@@ -71,16 +71,16 @@ const Home = () => {
|
||||
className='select'
|
||||
defaultValue={'12'}
|
||||
>
|
||||
<option value='12'>{lang.config.settings.size.values.small}</option>
|
||||
<option value='16'>{lang.config.settings.size.values.medium}</option>
|
||||
<option value='20'>{lang.config.settings.size.values.large}</option>
|
||||
<option value='12'>{config.settings.size.values.small}</option>
|
||||
<option value='16'>{config.settings.size.values.medium}</option>
|
||||
<option value='20'>{config.settings.size.values.large}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className='selectorBox'>
|
||||
<p className='settingLabel'>
|
||||
<Difficulty />
|
||||
{lang.config.settings.difficulty.label}
|
||||
{config.settings.difficulty.label}
|
||||
</p>
|
||||
<select
|
||||
ref={difficultyRef}
|
||||
@@ -88,9 +88,9 @@ const Home = () => {
|
||||
className='select'
|
||||
defaultValue={'beginner'}
|
||||
>
|
||||
<option value='beginner'>{lang.config.settings.difficulty.values.beginner}</option>
|
||||
<option value='intermediate'>{lang.config.settings.difficulty.values.intermediate}</option>
|
||||
<option value='expert'>{lang.config.settings.difficulty.values.expert}</option>
|
||||
<option value='beginner'>{config.settings.difficulty.values.beginner}</option>
|
||||
<option value='intermediate'>{config.settings.difficulty.values.intermediate}</option>
|
||||
<option value='expert'>{config.settings.difficulty.values.expert}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -98,11 +98,9 @@ const Home = () => {
|
||||
className='startButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
{lang.config.start}
|
||||
{config.start}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { useLangStore } from '../../store/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||
import fr from '../../assets/fr.png';
|
||||
import en from '../../assets/en.png';
|
||||
import './styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const LangToggler = () => {
|
||||
const [lang, setLang] = useRecoilState(langAtom);
|
||||
const { key, setKey } = useLangStore();
|
||||
|
||||
return (
|
||||
<button
|
||||
className='langToggler'
|
||||
className='lang-goggler'
|
||||
onClick={() => {
|
||||
if (lang.key === 'fr') {
|
||||
if (key === 'fr') {
|
||||
localStorage.setItem('lang', 'en');
|
||||
setLang((prev) => ({
|
||||
...prev,
|
||||
key: 'en',
|
||||
}));
|
||||
setKey('eb');
|
||||
} else {
|
||||
localStorage.setItem('lang', 'fr');
|
||||
setLang((prev) => ({
|
||||
...prev,
|
||||
key: 'fr',
|
||||
}));
|
||||
setKey('fr');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{lang.key == 'fr' ? (
|
||||
{key == 'fr' ? (
|
||||
<img
|
||||
src={fr}
|
||||
alt='fr flag'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.langToggler {
|
||||
.lang-goggler {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
|
||||
@@ -2,20 +2,8 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useEffect, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------- Context & Stockage --------------------------------------------------
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------- Utils & Types ----------------------------------------------------
|
||||
import { Difficulty, LeaderBoardItem } from '../../types/game';
|
||||
import { getDifficultyLabel } from '../../utils/generics';
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import useLeaderboard from './hooks/useLeaderboard';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||
@@ -29,63 +17,31 @@ import Size from '../../assets/size';
|
||||
import './styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const store = localforage.createInstance({
|
||||
name: 'leaderboard',
|
||||
driver: localforage.INDEXEDDB,
|
||||
version: 1.0,
|
||||
storeName: 'leaderboard',
|
||||
description: 'Store 10 best game time',
|
||||
});
|
||||
|
||||
const LeaderBoard = () => {
|
||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [data, setData] = useState<LeaderBoardItem[]>([]);
|
||||
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const getData = async () => {
|
||||
const storeData = await store.getItem<LeaderBoardItem[]>(`${gameState.difficulty}:${gameState.gridSize}`);
|
||||
|
||||
setData(storeData || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length === 0) {
|
||||
getData();
|
||||
}
|
||||
}, [data.length, getData]);
|
||||
|
||||
const clearAllLeaderboards = async () => {
|
||||
const keys = await store.keys();
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
await store.removeItem(keys[i]);
|
||||
}
|
||||
getData();
|
||||
};
|
||||
const { config, clearLeaderboard, clearAllLeaderboards, data, difficulty, gridSize, openSettings, replay } = useLeaderboard();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className='title'>{lang.config.leaderboardTitle}</h1>
|
||||
<h1 className='title'>{config.leaderboardTitle}</h1>
|
||||
|
||||
<div className='board'>
|
||||
<div className='header'>
|
||||
<p>#</p>
|
||||
<p>
|
||||
<CalendarIcon />
|
||||
{lang.config.date}
|
||||
{config.date}
|
||||
</p>
|
||||
<p>
|
||||
<DifficultyIcon style={{ rotate: '-45deg' }} />
|
||||
{lang.config.settings.difficulty.label}
|
||||
{config.settings.difficulty.label}
|
||||
</p>
|
||||
<p>
|
||||
<Size />
|
||||
{lang.config.settings.size.label}
|
||||
{config.settings.size.label}
|
||||
</p>
|
||||
<p>
|
||||
<TimerIcon />
|
||||
{lang.config.time}
|
||||
{config.time}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -98,8 +54,8 @@ const LeaderBoard = () => {
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
<span>{item.date}</span>
|
||||
<span>{lang.config.settings.difficulty.values[gameState.difficulty]}</span>
|
||||
<span>{gameState.gridSize}</span>
|
||||
<span>{config.settings.difficulty.values[difficulty]}</span>
|
||||
<span>{gridSize}</span>
|
||||
<span>{item.time}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -110,31 +66,28 @@ const LeaderBoard = () => {
|
||||
<div className='controls'>
|
||||
<button
|
||||
className='controlButton home'
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
||||
onClick={openSettings}
|
||||
>
|
||||
<Settings />
|
||||
|
||||
{lang.config.settingsBtn}
|
||||
{config.settingsBtn}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='controlButton replay'
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
||||
onClick={replay}
|
||||
>
|
||||
<Replay />
|
||||
|
||||
{lang.config.replay}
|
||||
{config.replay}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='controlButton clear'
|
||||
onClick={async () => {
|
||||
await store.setItem(`${gameState.difficulty}:${gameState.gridSize}`, []);
|
||||
await getData();
|
||||
}}
|
||||
onClick={clearLeaderboard}
|
||||
>
|
||||
<Trash />
|
||||
{lang.config.delete}
|
||||
{config.delete}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -142,7 +95,7 @@ const LeaderBoard = () => {
|
||||
onClick={clearAllLeaderboards}
|
||||
>
|
||||
<Replay />
|
||||
{lang.config.deleteAll}
|
||||
{config.deleteAll}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useGameStateStore } from '@/store/gameState';
|
||||
import { useLangStore } from '@/store/langState';
|
||||
import { LeaderBoardItem } from '@/types/game';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import localforage from 'localforage';
|
||||
|
||||
const store = localforage.createInstance({
|
||||
name: 'leaderboard',
|
||||
driver: localforage.INDEXEDDB,
|
||||
version: 1.0,
|
||||
storeName: 'leaderboard',
|
||||
description: 'Store 10 best game time',
|
||||
});
|
||||
|
||||
export default function useLeaderboard() {
|
||||
const { gridSize, difficulty, setBombs, setEndType, setGameTime, setPlacedFlags, setStatus } = useGameStateStore(
|
||||
useShallow((state) => ({
|
||||
gridSize: state.gridSize,
|
||||
difficulty: state.difficulty,
|
||||
|
||||
setStatus: state.setStatus,
|
||||
setEndType: state.setEndType,
|
||||
setPlacedFlags: state.setPlacedFlags,
|
||||
setBombs: state.setBombs,
|
||||
setGameTime: state.setGameTime,
|
||||
})),
|
||||
);
|
||||
const config = useLangStore((state) => state.config);
|
||||
|
||||
const [data, setData] = useState<LeaderBoardItem[]>([]);
|
||||
|
||||
const getData = useCallback(async () => {
|
||||
const storeData = await store.getItem<LeaderBoardItem[]>(`${difficulty}:${gridSize}`);
|
||||
|
||||
setData(storeData || []);
|
||||
}, [difficulty, gridSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length === 0) {
|
||||
getData();
|
||||
}
|
||||
}, [data, getData]);
|
||||
|
||||
const clearAllLeaderboards = useCallback(async () => {
|
||||
const keys = await store.keys();
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
await store.removeItem(keys[i]);
|
||||
}
|
||||
getData();
|
||||
}, [getData]);
|
||||
|
||||
const clearLeaderboard = useCallback(async () => {
|
||||
await store.setItem(`${difficulty}:${gridSize}`, []);
|
||||
await getData();
|
||||
}, []);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
setStatus('settings');
|
||||
setEndType('');
|
||||
setPlacedFlags(0);
|
||||
setBombs(0);
|
||||
setGameTime('');
|
||||
}, [setStatus, setEndType, setPlacedFlags, setBombs, setGameTime]);
|
||||
|
||||
const replay = useCallback(() => {
|
||||
setStatus('idle');
|
||||
setEndType('');
|
||||
setPlacedFlags(0);
|
||||
setBombs(0);
|
||||
setGameTime('');
|
||||
}, [setStatus, setEndType, setPlacedFlags, setBombs, setGameTime]);
|
||||
|
||||
return {
|
||||
config,
|
||||
clearLeaderboard,
|
||||
clearAllLeaderboards,
|
||||
data,
|
||||
difficulty,
|
||||
gridSize,
|
||||
openSettings,
|
||||
replay,
|
||||
};
|
||||
}
|
||||
@@ -2,21 +2,26 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Recoil --------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../../contexts/gameState';
|
||||
// -------------------------------------------------- Hooks & Utls -----------------------------------------------------
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useGameStateStore } from '../../../store/gameState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||
import Flag from '../../../assets/flag';
|
||||
import '../styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const MinesCounter = () => {
|
||||
const [{ bombs, placedFlags }] = useRecoilState(gameStateAtom);
|
||||
const { bombs, placedFlags } = useGameStateStore(
|
||||
useShallow((state) => ({
|
||||
bombs: state.bombs,
|
||||
placedFlags: state.placedFlags,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='statContainer'>
|
||||
<div className='stat-container'>
|
||||
<Flag />
|
||||
<p>
|
||||
{placedFlags} / {bombs}
|
||||
|
||||
@@ -2,40 +2,42 @@
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||
import { useEffect, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../../contexts/gameState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------- Utils & Types ----------------------------------------------------
|
||||
import { Difficulty } from '../../../types/game';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useGameStateStore } from '@/store/gameState';
|
||||
import { saveToLocalStorage } from '../../../utils/generics';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||
import Clock from '../../../assets/clock';
|
||||
import '../styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const Timer = () => {
|
||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
||||
const { status, endType, difficulty, gridSize, setGameTime } = useGameStateStore(
|
||||
useShallow((state) => ({
|
||||
status: state.status,
|
||||
endType: state.endType,
|
||||
difficulty: state.difficulty,
|
||||
gridSize: state.gridSize,
|
||||
|
||||
setGameTime: state.setGameTime,
|
||||
})),
|
||||
);
|
||||
|
||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||
const [startedTime, setStartedTime] = useState<number | null>(null);
|
||||
const [displayedTime, setDisplayedTime] = useState<string>('00:00');
|
||||
|
||||
useEffect(() => {
|
||||
if (gameState.status === 'win' || gameState.status === 'loose') {
|
||||
if (endType === 'win' || endType === 'loose') {
|
||||
setStartedTime(null);
|
||||
}
|
||||
}, [gameState.status]);
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!startedTime && gameState.endType === '' && gameState.status === 'playing') {
|
||||
if (!startedTime && endType === '' && status === 'playing') {
|
||||
const now = Date.now();
|
||||
setStartedTime(now);
|
||||
|
||||
@@ -53,25 +55,22 @@ const Timer = () => {
|
||||
|
||||
setIntervalId(interval);
|
||||
}
|
||||
}, [startedTime, gameState.endType, gameState.status]);
|
||||
}, [startedTime, endType, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (intervalId && gameState.endType !== '') {
|
||||
setGameState((prev) => ({
|
||||
...prev,
|
||||
gameTime: displayedTime,
|
||||
}));
|
||||
if (intervalId && endType !== '') {
|
||||
setGameTime(displayedTime);
|
||||
|
||||
clearInterval(intervalId);
|
||||
|
||||
if (gameState.endType === 'win') {
|
||||
saveToLocalStorage(displayedTime, gameState.difficulty as Difficulty, gameState.gridSize);
|
||||
if (endType === 'win') {
|
||||
saveToLocalStorage(displayedTime, difficulty, gridSize);
|
||||
}
|
||||
}
|
||||
}, [intervalId, gameState.endType, gameState.difficulty, gameState.gridSize, setGameState, displayedTime]);
|
||||
}, [intervalId, endType, difficulty, gridSize, displayedTime, setGameTime]);
|
||||
|
||||
return (
|
||||
<div className='statContainer'>
|
||||
<div className='stat-container'>
|
||||
<Clock />
|
||||
<p>{displayedTime}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.statContainer {
|
||||
.stat-container {
|
||||
display: grid;
|
||||
grid-template-columns: 25px 40px;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
/**
|
||||
* Values:
|
||||
* - Status: 'settings' | 'idle' | 'playing' | 'board'
|
||||
* - Difficulty: 'beginner' | 'intermediate' | 'expert'
|
||||
* - Grid Size: 12, 16, 20
|
||||
* - Bombs: ]0;{GridSize}]
|
||||
* - Placed Flags: [0;{GridSize}]
|
||||
* - gameTime: Timestamp
|
||||
* - End Type: 'win' | 'loose'
|
||||
*/
|
||||
|
||||
export const gameStateAtom = atom({
|
||||
key: 'gameState',
|
||||
default: {
|
||||
status: 'settings',
|
||||
difficulty: 'beginner',
|
||||
gridSize: 12,
|
||||
bombs: 0,
|
||||
placedFlags: 0,
|
||||
gameTime: '',
|
||||
endType: '',
|
||||
},
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const langAtom = atom({
|
||||
key: 'lang',
|
||||
default: {
|
||||
key: null as any,
|
||||
config: null as any,
|
||||
},
|
||||
});
|
||||
+15
-8
@@ -1,15 +1,22 @@
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
//! Imports
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// --------------------------------------------------- Components ------------------------------------------------------
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './app/App';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
import './index.scss';
|
||||
import App from './App';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
const appElement = document.getElementById('root');
|
||||
if (!appElement) throw Error('No app root element found');
|
||||
|
||||
root.render(
|
||||
const reactRoot = createRoot(appElement);
|
||||
|
||||
reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<App />
|
||||
</RecoilRoot>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,48 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type GameStateStore = {
|
||||
status: 'settings' | 'idle' | 'playing' | 'board';
|
||||
difficulty: 'beginner' | 'intermediate' | 'expert';
|
||||
gridSize: 12 | 16 | 20;
|
||||
bombs: number;
|
||||
placedFlags: number;
|
||||
gameTime: string;
|
||||
endType: '' | 'win' | 'loose';
|
||||
|
||||
setStatus: (status: 'settings' | 'idle' | 'playing' | 'board') => void;
|
||||
setDifficulty: (difficulty: 'beginner' | 'intermediate' | 'expert') => void;
|
||||
setGridSize: (gridSize: 12 | 16 | 20) => void;
|
||||
setBombs: (amount: number) => void;
|
||||
setPlacedFlags: (amount: number) => void;
|
||||
setGameTime: (time: string) => void;
|
||||
setEndType: (endType: '' | 'win' | 'loose') => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Values:
|
||||
* - Status: 'settings' | 'idle' | 'playing' | 'board'
|
||||
* - Difficulty: 'beginner' | 'intermediate' | 'expert'
|
||||
* - Grid Size: 12, 16, 20
|
||||
* - Bombs: ]0;{GridSize}]
|
||||
* - Placed Flags: [0;{GridSize}]
|
||||
* - gameTime: Timestamp
|
||||
* - End Type: 'win' | 'loose'
|
||||
*/
|
||||
|
||||
export const useGameStateStore = create<GameStateStore>((set) => ({
|
||||
status: 'settings',
|
||||
difficulty: 'beginner',
|
||||
gridSize: 12,
|
||||
bombs: 0,
|
||||
placedFlags: 0,
|
||||
gameTime: '',
|
||||
endType: '',
|
||||
|
||||
setStatus: (newStatus) => set({ status: newStatus }),
|
||||
setDifficulty: (newDifficulty) => set({ difficulty: newDifficulty }),
|
||||
setGridSize: (size) => set({ gridSize: size }),
|
||||
setBombs: (amount) => set({ bombs: amount }),
|
||||
setPlacedFlags: (amount) => set({ placedFlags: amount }),
|
||||
setGameTime: (time) => set({ gameTime: time }),
|
||||
setEndType: (type) => set({ endType: type }),
|
||||
}));
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type LangStore = {
|
||||
key: any;
|
||||
config: any;
|
||||
|
||||
setKey: (key: any) => void;
|
||||
setLang: ({ key, config }: { key: any; config: any }) => void;
|
||||
};
|
||||
|
||||
export const useLangStore = create<LangStore>((set) => ({
|
||||
key: null,
|
||||
config: null,
|
||||
setKey: (key) => set({ key: key }),
|
||||
setLang: ({ key, config }) => set({ key: key, config: config }),
|
||||
}));
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -14,8 +14,11 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/node_modules/*", "dist"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import eslint from 'vite-plugin-eslint';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const alias = {
|
||||
'@app': path.resolve(__dirname, 'src/app'),
|
||||
'@assets': path.resolve(__dirname, 'src/assets'),
|
||||
'@config': path.resolve(__dirname, 'src/config'),
|
||||
'@components': path.resolve(__dirname, 'src/components'),
|
||||
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@pages': path.resolve(__dirname, 'src/pages'),
|
||||
'@providers': path.resolve(__dirname, 'src/providers'),
|
||||
'@selectors': path.resolve(__dirname, 'src/store/selectors'),
|
||||
'@reducers': path.resolve(__dirname, 'src/store/reducers'),
|
||||
'@services': path.resolve(__dirname, 'src/store/services'),
|
||||
'@store': path.resolve(__dirname, 'src/store'),
|
||||
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||
};
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
eslint(),
|
||||
VitePWA({
|
||||
srcDir: 'src',
|
||||
filename: 'sw.js',
|
||||
strategies: 'injectManifest',
|
||||
manifest: false,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src/'),
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port: 8000,
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user