Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e86ffd74 | ||
|
|
f3f3925652 | ||
|
|
588eab0caf | ||
|
|
1cf7035449 | ||
|
|
91a111a49b | ||
|
|
0b0fb1d502 | ||
|
|
9d349916aa | ||
|
|
c3eb4b4817 | ||
|
|
84489a3b6e | ||
|
|
3a0b5e1f7f | ||
|
|
25a264bf08 | ||
|
|
21ad0f4fa4 | ||
|
|
c74c44c521 | ||
|
|
2fea366313 | ||
|
|
1ebb863b81 | ||
|
|
ffa6468b70 | ||
|
|
9426b8499c |
+20
-10
@@ -1,16 +1,26 @@
|
|||||||
# Developement
|
|
||||||
.git
|
|
||||||
.prettierrc
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
# Build
|
|
||||||
build
|
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Builds
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Dev configs
|
||||||
|
.prettierrc
|
||||||
|
.prettierignore
|
||||||
|
jsconfig.json
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Release scripts
|
||||||
|
release
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: '🐛 Bug Report'
|
||||||
|
about: If something isn't working as expected 🤔.
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Bug'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Report
|
||||||
|
|
||||||
|
**Current Behavior**
|
||||||
|
A clear and concise description of the behavior.
|
||||||
|
|
||||||
|
**Input Code**
|
||||||
|
|
||||||
|
- REPL or Repo link if applicable:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var your => (code) => here;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected behavior/code**
|
||||||
|
A clear and concise description of what you expected to happen (or code).
|
||||||
|
|
||||||
|
**Babel Configuration (.babelrc, package.json, cli command)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"your": { "config": "here" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
- Babel version(s): [e.g. v6.0.0, v7.0.0-beta.34]
|
||||||
|
- Node/npm version: [e.g. Node 8/npm 5]
|
||||||
|
- OS: [e.g. OSX 10.13.4, Windows 10]
|
||||||
|
- Monorepo: [e.g. yes/no/Lerna]
|
||||||
|
- How you are using Babel: [e.g. `cli`, `register`, `loader`]
|
||||||
|
|
||||||
|
**Possible Solution**
|
||||||
|
|
||||||
|
<!--- Only if you have suggestions on a fix for the bug -->
|
||||||
|
|
||||||
|
**Additional context/Screenshots**
|
||||||
|
Add any other context about the problem here. If applicable, add screenshots to help explain.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: '✨ Feature Request'
|
||||||
|
about: I have a suggestion (and may want to implement it 🙂)!
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Suggestion'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Request
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I have an issue when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen. Add any considered drawbacks.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Teachability, Documentation, Adoption, Migration Strategy**
|
||||||
|
If you can, explain how users will be able to use this and possibly write out a version the docs.
|
||||||
|
Maybe a screenshot or design?
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
name: '🤗 Support Question'
|
||||||
|
about: If you have a question 💬
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Question'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
+15
-4
@@ -1,12 +1,23 @@
|
|||||||
{
|
{
|
||||||
"useTabs": true,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
"endOfLine": "lf",
|
"endOfLine": "lf",
|
||||||
"jsxSingleQuote": true,
|
"jsxSingleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.scss",
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"printWidth": 500,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"useTabs": true,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"trailingComma": "all",
|
|
||||||
"singleAttributePerLine": true,
|
"singleAttributePerLine": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 500
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
+18
-11
@@ -1,23 +1,30 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM node:lts-alpine as build
|
FROM node:lts-alpine AS base
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
|
||||||
|
# Production dependencies
|
||||||
|
FROM base AS prod-deps
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
# Build
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Final stage
|
# 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
|
EXPOSE 3000
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
COPY --from=build /usr/src/app/build .
|
|
||||||
|
|
||||||
RUN npm i -g serve
|
|
||||||
|
|
||||||
CMD serve
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Roxanne Roulland
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,6 +1,71 @@
|
|||||||
# Minesweeper
|
<div style="display: flex; align-items: center;gap: 20px;">
|
||||||
|
<img src="./assets/logo_circle.png" alt="logo" width="100">
|
||||||
|
<h1>Minesweeper</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Docker
|
<p style="margin-top: 1em;">
|
||||||
|
Live Demo: <a href="https://minesweeper.alexisvasseur.fr">https://minesweeper.alexisvasseur.fr</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📌 Table of content
|
||||||
|
|
||||||
|
- [Description](#description)
|
||||||
|
- [Techno used](#techno-used)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Local](#local)
|
||||||
|
- [Docker](#docker)
|
||||||
|
- [Credits](#credits)
|
||||||
|
|
||||||
|
## 📖 Description
|
||||||
|
|
||||||
|
Minesweeper is a game where the player has to clear a minefield without detonating any mine. The game is won when all the cells without a mine are revealed. The game is lost when a mine is revealed.
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 10px">
|
||||||
|
<img src="./assets/game.png" alt="game" width="45%" height="auto" />
|
||||||
|
<img src="./assets/leaderboard.png" alt="game" width="45%" height="auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ⚙️ Techno used
|
||||||
|
|
||||||
|
<ul style="columns: 2">
|
||||||
|
<li>React</li>
|
||||||
|
<li>Recoil</li>
|
||||||
|
<li>TypeScript</li>
|
||||||
|
<li>SCSS</li>
|
||||||
|
<li>Docker</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
## ⬇️ Installation
|
||||||
|
|
||||||
|
### 💻 Local
|
||||||
|
|
||||||
|
- Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://www.github.com/KeunotorCagoule/minesweeper.git
|
||||||
|
```
|
||||||
|
|
||||||
|
- Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the server as _development_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the server as _production_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm i -g serve
|
||||||
|
serve build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🐳 Docker
|
||||||
|
|
||||||
- Build the image
|
- Build the image
|
||||||
|
|
||||||
@@ -13,3 +78,8 @@
|
|||||||
```bash
|
```bash
|
||||||
docker run --name minesweeper -dp 3000:3000 minesweeper
|
docker run --name minesweeper -dp 3000:3000 minesweeper
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# 👷♂️ Credits
|
||||||
|
|
||||||
|
- [UnEpicier](https://www.github.com/UnEpicier)
|
||||||
|
- [KeunotorCagoule](https://www.github.com/KeunotorCagoule)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
@@ -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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
href="%PUBLIC_URL%/favicon.ico"
|
|
||||||
/>
|
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1"
|
content="width=device-width, initial-scale=1"
|
||||||
/>
|
/>
|
||||||
|
<meta
|
||||||
|
name="mobile-web-app-capable"
|
||||||
|
content="yes"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
content="#000000"
|
content="#000000"
|
||||||
@@ -18,18 +18,27 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Démineur en React & Typescript"
|
content="Démineur en React & Typescript"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="favicon.ico"
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
href="%PUBLIC_URL%/logo192.png"
|
href="logo192.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="manifest"
|
rel="manifest"
|
||||||
href="%PUBLIC_URL%/manifest.json"
|
href="manifest.json"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Démineur</title>
|
<title>Démineur</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="/src/index.tsx"
|
||||||
|
></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Generated
+5265
-13955
File diff suppressed because it is too large
Load Diff
+27
-33
@@ -2,44 +2,38 @@
|
|||||||
"name": "demineur",
|
"name": "demineur",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Démineur en ReactJS & TypeScript",
|
"description": "Démineur en ReactJS & TypeScript",
|
||||||
"author": "KeunotorCagoulé",
|
"author": "UnEpicier",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"start": "serve -s -l 8000 dist",
|
||||||
"docker:build": "docker rmi minesweeper && docker build -t minesweeper ."
|
"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": {
|
"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": {
|
"dependencies": {
|
||||||
"extends": [
|
"serve": "^14.2.4"
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .helpButton {
|
& > .help-button {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20px 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@@ -3,38 +3,72 @@
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
// ------------------------------------------------------ React --------------------------------------------------------
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { gameStateAtom } from './contexts/gameState';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// --------------------------------------------------- Components ------------------------------------------------------
|
// --------------------------------------------------- Components ------------------------------------------------------
|
||||||
import Home from './components/Home/Home';
|
import Home from '../components/Home/Home';
|
||||||
import Timer from './components/Stats/Timer/Timer';
|
import Timer from '../components/Stats/Timer/Timer';
|
||||||
import MinesCounter from './components/Stats/MinesCounter/MinesCounter';
|
import MinesCounter from '../components/Stats/MinesCounter/MinesCounter';
|
||||||
import Grid from './components/Grid/Grid';
|
import Grid from '../components/Grid/Grid';
|
||||||
import Help from './components/Help/Help';
|
import Help from '../components/Help/Help';
|
||||||
import LeaderBoard from './components/Leaderboard/Leaderboard';
|
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 --------------------------------------------------------
|
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||||
import Logo from './assets/logo';
|
import Logo from '../assets/logo';
|
||||||
import Question from './assets/question';
|
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [helpDisplayed, displayHelp] = useState(false);
|
const [helpDisplayed, displayHelp] = useState(false);
|
||||||
|
|
||||||
const [gameState] = useRecoilState(gameStateAtom);
|
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 (
|
return (
|
||||||
<div className='App'>
|
<div className='App'>
|
||||||
{gameState.status === 'settings' && <Home />}
|
<LangToggler />
|
||||||
{(gameState.status === 'idle' || gameState.status === 'playing') && (
|
{gameStatus === 'settings' && <Home />}
|
||||||
|
{(gameStatus === 'idle' || gameStatus === 'playing') && (
|
||||||
<>
|
<>
|
||||||
<div className='stats'>
|
<div className='stats'>
|
||||||
<Timer />
|
<Timer />
|
||||||
@@ -43,11 +77,10 @@ const App = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Grid />
|
<Grid />
|
||||||
<button
|
<button
|
||||||
className='helpButton'
|
className='help-button'
|
||||||
onClick={() => displayHelp(true)}
|
onClick={() => displayHelp(true)}
|
||||||
>
|
>
|
||||||
<Question />
|
<span>{config.how}</span>
|
||||||
<span>Comment jouer</span>
|
|
||||||
</button>
|
</button>
|
||||||
{helpDisplayed && (
|
{helpDisplayed && (
|
||||||
<Help
|
<Help
|
||||||
@@ -58,7 +91,7 @@ const App = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{gameState.status === 'board' && <LeaderBoard />}
|
{gameStatus === 'board' && <LeaderBoard />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const Difficulty = (props: any) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
d='M48 256h416'
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x='384'
|
||||||
|
y='128'
|
||||||
|
width='32'
|
||||||
|
height='256'
|
||||||
|
rx='16'
|
||||||
|
ry='16'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x='96'
|
||||||
|
y='128'
|
||||||
|
width='32'
|
||||||
|
height='256'
|
||||||
|
rx='16'
|
||||||
|
ry='16'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x='32'
|
||||||
|
y='192'
|
||||||
|
width='16'
|
||||||
|
height='128'
|
||||||
|
rx='8'
|
||||||
|
ry='8'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x='464'
|
||||||
|
y='192'
|
||||||
|
width='16'
|
||||||
|
height='128'
|
||||||
|
rx='8'
|
||||||
|
ry='8'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Difficulty;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,26 +0,0 @@
|
|||||||
const Question = (props: any) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='0 0 512 512'
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d='M160 164s1.44-33 33.54-59.46C212.6 88.83 235.49 84.28 256 84c18.73-.23 35.47 2.94 45.48 7.82C318.59 100.2 352 120.6 352 164c0 45.67-29.18 66.37-62.35 89.18S248 298.36 248 324'
|
|
||||||
fill='none'
|
|
||||||
stroke='currentColor'
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeMiterlimit='10'
|
|
||||||
strokeWidth='40'
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx='248'
|
|
||||||
cy='399.99'
|
|
||||||
r='32'
|
|
||||||
fill='currentColor'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Question;
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const Size = (props: any) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth='32'
|
||||||
|
d='M304 96h112v112M405.77 106.2L111.98 400.02M208 416H96V304'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Size;
|
||||||
@@ -2,40 +2,26 @@
|
|||||||
//! Imports
|
//! Imports
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||||
import { useState } from 'react';
|
import { useLangStore } from '@/store/langState';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { gameStateAtom } from '../../contexts/gameState';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ Hooks --------------------------------------------------------
|
|
||||||
import { useActions } from './hooks/useActions';
|
import { useActions } from './hooks/useActions';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
|
||||||
import { genGrid } from '../../utils/generation';
|
|
||||||
import { getCellDisplayContent } from '../../utils/gameInteractions';
|
import { getCellDisplayContent } from '../../utils/gameInteractions';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||||
import Podium from '../../assets/podium';
|
import Podium from '../../assets/podium';
|
||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const Grid = () => {
|
const Grid = () => {
|
||||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
const config = useLangStore((state) => state.config);
|
||||||
const [grid, setGrid] = useState(genGrid(gameState.gridSize));
|
const { grid, gridSize, endType, handleLeftClick, handleRightClick, openLeaderboard } = useActions();
|
||||||
|
|
||||||
const { handleLeftClick, handleRightClick } = useActions(grid, setGrid, gameState, setGameState);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='grid'
|
className='grid'
|
||||||
style={{
|
style={{
|
||||||
gridTemplateRows: `repeat(${gameState.gridSize}, var(--size))`,
|
gridTemplateRows: `repeat(${gridSize}, var(--size))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{grid.map((row, rowIndex) => (
|
{grid.map((row, rowIndex) => (
|
||||||
@@ -43,7 +29,7 @@ const Grid = () => {
|
|||||||
key={`row${rowIndex}`}
|
key={`row${rowIndex}`}
|
||||||
className='row'
|
className='row'
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${gameState.gridSize}, var(--size))`,
|
gridTemplateColumns: `repeat(${gridSize}, var(--size))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.map((cell, cellIndex) => {
|
{row.map((cell, cellIndex) => {
|
||||||
@@ -60,19 +46,19 @@ const Grid = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{gameState.endType !== '' && (
|
{endType !== '' && (
|
||||||
<div
|
<div
|
||||||
className='overlay'
|
className='overlay'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--overlayColor': gameState.endType === 'win' ? '#5f8e59' : '#ca5940',
|
'--overlayColor': endType === 'win' ? '#5f8e59' : '#ca5940',
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className='overlayTitle'>{gameState.endType === 'win' ? 'Gagné !' : 'Perdu !'}</p>
|
<p className='overlayTitle'>{endType === 'win' ? config.win : config.loose}</p>
|
||||||
<button
|
<button
|
||||||
className='overlayButton'
|
className='overlayButton'
|
||||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'board' }))}
|
onClick={openLeaderboard}
|
||||||
>
|
>
|
||||||
<Podium />
|
<Podium />
|
||||||
Leaderboard
|
Leaderboard
|
||||||
|
|||||||
@@ -2,22 +2,33 @@
|
|||||||
//! Imports
|
//! Imports
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||||
import { MouseEvent, useCallback, useState } from 'react';
|
import { MouseEvent, useCallback, useState } from 'react';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
import { GridType } from '../../../types/game';
|
||||||
|
|
||||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
|
||||||
import { SetterOrUpdater } from 'recoil';
|
|
||||||
import { Difficulty, GridType } from '../../../types/game';
|
|
||||||
import { GameState } from '../../../types/gameState';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
|
||||||
import { discoverAroundCell, revealAllGrid } from '../../../utils/gameInteractions';
|
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 [, updateGrid] = useState({});
|
||||||
const forceUpdate = useCallback(() => updateGrid({}), []);
|
const forceUpdate = useCallback(() => updateGrid({}), []);
|
||||||
|
|
||||||
@@ -30,25 +41,19 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
|||||||
let bombsCount: number | null = null;
|
let bombsCount: number | null = null;
|
||||||
|
|
||||||
// Start the game with making sure the clicked cell will be empty after generation
|
// Start the game with making sure the clicked cell will be empty after generation
|
||||||
if (gameState.status === 'idle') {
|
if (status === 'idle') {
|
||||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gameState.gridSize, gameState.difficulty as Difficulty, true, rowIndex, colIndex);
|
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gridSize, difficulty, true, rowIndex, colIndex);
|
||||||
prev = newGrid;
|
prev = newGrid;
|
||||||
|
|
||||||
bombsCount = _bombsCount;
|
bombsCount = _bombsCount;
|
||||||
|
|
||||||
setGameState((prevGameState) => ({
|
setStatus('playing');
|
||||||
...prevGameState,
|
setBombs(_bombsCount);
|
||||||
status: 'playing',
|
|
||||||
bombs: _bombsCount,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the clicked cell hide a bomb, loose immediatly
|
// If the clicked cell hide a bomb, loose immediatly
|
||||||
if (prev[rowIndex][colIndex].value === 'bomb') {
|
if (prev[rowIndex][colIndex].value === 'bomb') {
|
||||||
setGameState((prevGameState) => ({
|
setEndType('loose');
|
||||||
...prevGameState,
|
|
||||||
endType: 'loose',
|
|
||||||
}));
|
|
||||||
return revealAllGrid(grid, true);
|
return revealAllGrid(grid, true);
|
||||||
}
|
}
|
||||||
// Discover surronding cells if the clicked is empty
|
// 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;
|
prev[rowIndex][colIndex].hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasWin(prev, bombsCount || gameState.bombs, gameState.placedFlags)) {
|
if (hasWin(prev, bombsCount || bombs, placedFlags)) {
|
||||||
setGameState((prevGameState) => ({
|
setEndType('win');
|
||||||
...prevGameState,
|
|
||||||
endType: 'win',
|
|
||||||
}));
|
|
||||||
return revealAllGrid(prev, true);
|
return revealAllGrid(prev, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,27 +85,21 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
|||||||
let bombsCount: number | null = null;
|
let bombsCount: number | null = null;
|
||||||
|
|
||||||
// Start the game without making sure the clicked cell will be empty after generation
|
// Start the game without making sure the clicked cell will be empty after generation
|
||||||
if (gameState.status === 'idle') {
|
if (status === 'idle') {
|
||||||
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gameState.gridSize, gameState.difficulty as Difficulty, false);
|
const { grid: newGrid, bombsCount: _bombsCount } = startGame(gridSize, difficulty, false);
|
||||||
prev = newGrid;
|
prev = newGrid;
|
||||||
|
|
||||||
bombsCount = _bombsCount;
|
bombsCount = _bombsCount;
|
||||||
|
|
||||||
setGameState((prevGameState) => ({
|
setStatus('playing');
|
||||||
...prevGameState,
|
setBombs(_bombsCount);
|
||||||
status: 'playing',
|
|
||||||
bombs: _bombsCount,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prev = prev.map((row, idx) => {
|
prev = prev.map((row, idx) => {
|
||||||
if (idx === rowIndex) {
|
if (idx === rowIndex) {
|
||||||
return row.map((col, idy) => {
|
return row.map((col, idy) => {
|
||||||
if (idy === colIndex) {
|
if (idy === colIndex) {
|
||||||
setGameState((prevGameState) => ({
|
setPlacedFlags(col.flag ? placedFlags - 1 : placedFlags + 1);
|
||||||
...prevGameState,
|
|
||||||
placedFlags: col.flag ? prevGameState.placedFlags - 1 : prevGameState.placedFlags + 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
@@ -116,11 +112,8 @@ export const useActions = (grid: GridType, setGrid: React.Dispatch<React.SetStat
|
|||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasWin(prev, bombsCount || gameState.bombs, gameState.placedFlags)) {
|
if (hasWin(prev, bombsCount || bombs, placedFlags)) {
|
||||||
setGameState((prev) => ({
|
setEndType('win');
|
||||||
...prev,
|
|
||||||
endType: 'win',
|
|
||||||
}));
|
|
||||||
return revealAllGrid(grid, true);
|
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 {
|
return {
|
||||||
|
grid,
|
||||||
|
gridSize,
|
||||||
|
endType,
|
||||||
handleLeftClick,
|
handleLeftClick,
|
||||||
handleRightClick,
|
handleRightClick,
|
||||||
|
openLeaderboard,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
//! Imports
|
//! Imports
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------- Context & Stockage --------------------------------------------------
|
|
||||||
import localforage from 'localforage';
|
import localforage from 'localforage';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { gameStateAtom } from '../../contexts/gameState';
|
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';
|
import './styles.scss';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -24,65 +24,73 @@ const store = localforage.createInstance({
|
|||||||
description: 'Store 10 best game time',
|
description: 'Store 10 best game time',
|
||||||
});
|
});
|
||||||
|
|
||||||
const Home = () => {
|
export default function Home() {
|
||||||
const difficultyRef = useRef<HTMLSelectElement>(null);
|
const difficultyRef = useRef<HTMLSelectElement>(null);
|
||||||
const sizeRef = useRef<HTMLSelectElement>(null);
|
const sizeRef = useRef<HTMLSelectElement>(null);
|
||||||
|
|
||||||
const [, setGameState] = useRecoilState(gameStateAtom);
|
const { setDifficulty, setGridSize, setStatus } = useGameStateStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setDifficulty: state.setDifficulty,
|
||||||
|
setGridSize: state.setGridSize,
|
||||||
|
setStatus: state.setStatus,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const config = useLangStore((state) => state.config);
|
||||||
const initIDB = async () => {
|
|
||||||
if ((await store.getItem('leaderboard')) === null) {
|
|
||||||
await store.setItem('leaderboard', []);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
initIDB();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = useCallback(() => {
|
||||||
if (difficultyRef.current !== null && sizeRef.current !== null) {
|
if (difficultyRef.current !== null && sizeRef.current !== null) {
|
||||||
const difficulty = difficultyRef.current.value;
|
const difficulty = difficultyRef.current.value;
|
||||||
const size = sizeRef.current.value;
|
const size = sizeRef.current.value;
|
||||||
|
|
||||||
setGameState((prev) => ({
|
(async () => {
|
||||||
...prev,
|
if ((await store.getItem(`${difficulty}:${size}`)) === null) {
|
||||||
difficulty: difficulty,
|
await store.setItem(`${difficulty}:${size}`, []);
|
||||||
gridSize: parseInt(size),
|
}
|
||||||
status: 'idle',
|
})();
|
||||||
}));
|
|
||||||
|
setGridSize(parseInt(size) as 12 | 16 | 20);
|
||||||
|
setDifficulty(difficulty as 'beginner' | 'intermediate' | 'expert');
|
||||||
|
setStatus('idle');
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='gameContainer'>
|
<div className='gameContainer'>
|
||||||
<h1 className='title'>Démineur</h1>
|
<h1 className='title'>{config.appTitle}</h1>
|
||||||
|
|
||||||
<div className='settingsContainer'>
|
<div className='settingsContainer'>
|
||||||
<div className='selectorBox'>
|
<div className='selectorBox'>
|
||||||
<p className='settingLabel'>Taille de la grille</p>
|
<p className='settingLabel'>
|
||||||
|
<Size />
|
||||||
|
{config.settings.size.label}
|
||||||
|
</p>
|
||||||
<select
|
<select
|
||||||
ref={sizeRef}
|
ref={sizeRef}
|
||||||
name='gridSize'
|
name='gridSize'
|
||||||
className='select'
|
className='select'
|
||||||
defaultValue={'12'}
|
defaultValue={'12'}
|
||||||
>
|
>
|
||||||
<option value='12'>Petit</option>
|
<option value='12'>{config.settings.size.values.small}</option>
|
||||||
<option value='16'>Moyen</option>
|
<option value='16'>{config.settings.size.values.medium}</option>
|
||||||
<option value='20'>Grand</option>
|
<option value='20'>{config.settings.size.values.large}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='selectorBox'>
|
<div className='selectorBox'>
|
||||||
<p className='settingLabel'>Difficulté</p>
|
<p className='settingLabel'>
|
||||||
|
<Difficulty />
|
||||||
|
{config.settings.difficulty.label}
|
||||||
|
</p>
|
||||||
<select
|
<select
|
||||||
ref={difficultyRef}
|
ref={difficultyRef}
|
||||||
name='difficulty'
|
name='difficulty'
|
||||||
className='select'
|
className='select'
|
||||||
defaultValue={'beginner'}
|
defaultValue={'beginner'}
|
||||||
>
|
>
|
||||||
<option value='beginner'>Facile</option>
|
<option value='beginner'>{config.settings.difficulty.values.beginner}</option>
|
||||||
<option value='intermediate'>Moyen</option>
|
<option value='intermediate'>{config.settings.difficulty.values.intermediate}</option>
|
||||||
<option value='expert'>Difficile</option>
|
<option value='expert'>{config.settings.difficulty.values.expert}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,11 +98,9 @@ const Home = () => {
|
|||||||
className='startButton'
|
className='startButton'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
Commencer
|
{config.start}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
|
||||||
|
|||||||
@@ -30,8 +30,16 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
& > .settingLabel {
|
& > .settingLabel {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #f39440;
|
color: #f39440;
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .select {
|
& > .select {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
//! 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 { key, setKey } = useLangStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className='lang-goggler'
|
||||||
|
onClick={() => {
|
||||||
|
if (key === 'fr') {
|
||||||
|
localStorage.setItem('lang', 'en');
|
||||||
|
setKey('eb');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('lang', 'fr');
|
||||||
|
setKey('fr');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key == 'fr' ? (
|
||||||
|
<img
|
||||||
|
src={fr}
|
||||||
|
alt='fr flag'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={en}
|
||||||
|
alt='en flag'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LangToggler;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.lang-goggler {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-self: center;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: scale 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
width: 45px;
|
||||||
|
height: 25px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
//! Imports
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||||
|
import useLeaderboard from './hooks/useLeaderboard';
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||||
|
import CalendarIcon from '../../assets/calendar';
|
||||||
|
import Replay from '../../assets/replay';
|
||||||
|
import Settings from '../../assets/settings';
|
||||||
|
import TimerIcon from '../../assets/timer';
|
||||||
|
import Trash from '../../assets/trash';
|
||||||
|
import DifficultyIcon from '../../assets/difficulty';
|
||||||
|
import Size from '../../assets/size';
|
||||||
|
import './styles.scss';
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const LeaderBoard = () => {
|
||||||
|
const { config, clearLeaderboard, clearAllLeaderboards, data, difficulty, gridSize, openSettings, replay } = useLeaderboard();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className='title'>{config.leaderboardTitle}</h1>
|
||||||
|
|
||||||
|
<div className='board'>
|
||||||
|
<div className='header'>
|
||||||
|
<p>#</p>
|
||||||
|
<p>
|
||||||
|
<CalendarIcon />
|
||||||
|
{config.date}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<DifficultyIcon style={{ rotate: '-45deg' }} />
|
||||||
|
{config.settings.difficulty.label}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Size />
|
||||||
|
{config.settings.size.label}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<TimerIcon />
|
||||||
|
{config.time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='scoresContainer'>
|
||||||
|
{data.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='score'
|
||||||
|
key={`score#${index}`}
|
||||||
|
>
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
<span>{item.date}</span>
|
||||||
|
<span>{config.settings.difficulty.values[difficulty]}</span>
|
||||||
|
<span>{gridSize}</span>
|
||||||
|
<span>{item.time}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='controls'>
|
||||||
|
<button
|
||||||
|
className='controlButton home'
|
||||||
|
onClick={openSettings}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
|
||||||
|
{config.settingsBtn}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className='controlButton replay'
|
||||||
|
onClick={replay}
|
||||||
|
>
|
||||||
|
<Replay />
|
||||||
|
|
||||||
|
{config.replay}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className='controlButton clear'
|
||||||
|
onClick={clearLeaderboard}
|
||||||
|
>
|
||||||
|
<Trash />
|
||||||
|
{config.delete}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className='controlButton clearAll'
|
||||||
|
onClick={clearAllLeaderboards}
|
||||||
|
>
|
||||||
|
<Replay />
|
||||||
|
{config.deleteAll}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeaderBoard;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
//! Imports
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------- Context & Stockage --------------------------------------------------
|
|
||||||
import localforage from 'localforage';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { gameStateAtom } from '../../contexts/gameState';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
|
||||||
import { LeaderBoardItem } from '../../types/game';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
|
||||||
import CalendarIcon from '../../assets/calendar';
|
|
||||||
import Replay from '../../assets/replay';
|
|
||||||
import Settings from '../../assets/settings';
|
|
||||||
import TimerIcon from '../../assets/timer';
|
|
||||||
import Trash from '../../assets/trash';
|
|
||||||
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 [, setGameState] = useRecoilState(gameStateAtom);
|
|
||||||
const [data, setData] = useState<LeaderBoardItem[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getData = async () => {
|
|
||||||
const storeData = await store.getItem<LeaderBoardItem[]>('leaderboard');
|
|
||||||
|
|
||||||
setData(storeData || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
}, [data.length]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className='title'>Time Leaderboard</h1>
|
|
||||||
|
|
||||||
<div className='board'>
|
|
||||||
<div className='header'>
|
|
||||||
<p>#</p>
|
|
||||||
<p>
|
|
||||||
<CalendarIcon />
|
|
||||||
Date
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<TimerIcon />
|
|
||||||
Time
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='scoresContainer'>
|
|
||||||
{data.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='score'
|
|
||||||
key={`score#${index}`}
|
|
||||||
>
|
|
||||||
<span>{index + 1}</span>
|
|
||||||
<span>{item.date}</span>
|
|
||||||
<span>{item.time}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='controls'>
|
|
||||||
<button
|
|
||||||
className='controlButton home'
|
|
||||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
|
||||||
>
|
|
||||||
<Settings />
|
|
||||||
Paramètres
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className='controlButton clear'
|
|
||||||
onClick={() => {
|
|
||||||
setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }));
|
|
||||||
store.setItem('leaderboard', []);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className='controlButton replay'
|
|
||||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
|
||||||
>
|
|
||||||
<Replay />
|
|
||||||
Rejouer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LeaderBoard;
|
|
||||||
@@ -9,15 +9,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 95svw;
|
width: 95svw;
|
||||||
max-width: 500px;
|
max-width: 600px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > .header {
|
& > .header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1em repeat(2, 1fr);
|
grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
|
||||||
gap: 30px;
|
gap: 20px;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
|
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 1.3em;
|
font-size: 1.1em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
height: 1.3em;
|
height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -47,8 +47,10 @@
|
|||||||
|
|
||||||
& > .score {
|
& > .score {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1em repeat(2, 1fr);
|
grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
|
||||||
gap: 30px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #e1e1e1;
|
background-color: #e1e1e1;
|
||||||
color: #333;
|
color: #333;
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
justify-self: end;
|
text-align: right;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +75,8 @@
|
|||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -100,12 +103,16 @@
|
|||||||
background-color: #f39440;
|
background-color: #f39440;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.replay {
|
||||||
|
background-color: #f39440;
|
||||||
|
}
|
||||||
|
|
||||||
&.clear {
|
&.clear {
|
||||||
background-color: #ca5940;
|
background-color: #ca5940;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.replay {
|
&.clearAll {
|
||||||
background-color: #f39440;
|
background-color: hsl(0, 57%, 52%);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
|
|||||||
@@ -2,21 +2,26 @@
|
|||||||
//! Imports
|
//! Imports
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ----------------------------------------------------- Recoil --------------------------------------------------------
|
// -------------------------------------------------- Hooks & Utls -----------------------------------------------------
|
||||||
import { useRecoilState } from 'recoil';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { gameStateAtom } from '../../../contexts/gameState';
|
import { useGameStateStore } from '../../../store/gameState';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||||
import Flag from '../../../assets/flag';
|
import Flag from '../../../assets/flag';
|
||||||
import '../styles.scss';
|
import '../styles.scss';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const MinesCounter = () => {
|
const MinesCounter = () => {
|
||||||
const [{ bombs, placedFlags }] = useRecoilState(gameStateAtom);
|
const { bombs, placedFlags } = useGameStateStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
bombs: state.bombs,
|
||||||
|
placedFlags: state.placedFlags,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='statContainer'>
|
<div className='stat-container'>
|
||||||
<Flag />
|
<Flag />
|
||||||
<p>
|
<p>
|
||||||
{placedFlags} / {bombs}
|
{placedFlags} / {bombs}
|
||||||
|
|||||||
@@ -2,39 +2,42 @@
|
|||||||
//! Imports
|
//! Imports
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ React --------------------------------------------------------
|
// -------------------------------------------------- Hooks & Utils ----------------------------------------------------
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { useGameStateStore } from '@/store/gameState';
|
||||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { gameStateAtom } from '../../../contexts/gameState';
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
|
||||||
import { saveToLocalStorage } from '../../../utils/generics';
|
import { saveToLocalStorage } from '../../../utils/generics';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
// ------------------------------------------------- Assets & Styles ---------------------------------------------------
|
||||||
import Clock from '../../../assets/clock';
|
import Clock from '../../../assets/clock';
|
||||||
import '../styles.scss';
|
import '../styles.scss';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const Timer = () => {
|
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 [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [startedTime, setStartedTime] = useState<number | null>(null);
|
const [startedTime, setStartedTime] = useState<number | null>(null);
|
||||||
const [displayedTime, setDisplayedTime] = useState<string>('00:00');
|
const [displayedTime, setDisplayedTime] = useState<string>('00:00');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameState.status === 'win' || gameState.status === 'loose') {
|
if (endType === 'win' || endType === 'loose') {
|
||||||
setStartedTime(null);
|
setStartedTime(null);
|
||||||
}
|
}
|
||||||
}, [gameState.status]);
|
}, [status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!startedTime && gameState.endType === '' && gameState.status === 'playing') {
|
if (!startedTime && endType === '' && status === 'playing') {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
setStartedTime(now);
|
setStartedTime(now);
|
||||||
|
|
||||||
@@ -52,25 +55,22 @@ const Timer = () => {
|
|||||||
|
|
||||||
setIntervalId(interval);
|
setIntervalId(interval);
|
||||||
}
|
}
|
||||||
}, [startedTime, gameState.endType, gameState.status]);
|
}, [startedTime, endType, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (intervalId && gameState.endType !== '') {
|
if (intervalId && endType !== '') {
|
||||||
setGameState((prev) => ({
|
setGameTime(displayedTime);
|
||||||
...prev,
|
|
||||||
gameTime: displayedTime,
|
|
||||||
}));
|
|
||||||
|
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
|
||||||
if (gameState.endType === 'win') {
|
if (endType === 'win') {
|
||||||
saveToLocalStorage(displayedTime);
|
saveToLocalStorage(displayedTime, difficulty, gridSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [intervalId, gameState.endType, setGameState, displayedTime]);
|
}, [intervalId, endType, difficulty, gridSize, displayedTime, setGameTime]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='statContainer'>
|
<div className='stat-container'>
|
||||||
<Clock />
|
<Clock />
|
||||||
<p>{displayedTime}</p>
|
<p>{displayedTime}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.statContainer {
|
.stat-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 25px 40px;
|
grid-template-columns: 25px 40px;
|
||||||
align-items: center;
|
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: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+15
-8
@@ -1,15 +1,22 @@
|
|||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
//! Imports
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// --------------------------------------------------- Components ------------------------------------------------------
|
||||||
import React from 'react';
|
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 './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>
|
<React.StrictMode>
|
||||||
<RecoilRoot>
|
<App />
|
||||||
<App />
|
|
||||||
</RecoilRoot>
|
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"appTitle": "Minesweeper",
|
||||||
|
"settings": {
|
||||||
|
"size": {
|
||||||
|
"label": "Grid size",
|
||||||
|
"values": {
|
||||||
|
"small": "Small",
|
||||||
|
"medium": "Medium",
|
||||||
|
"large": "Large"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficulty": {
|
||||||
|
"label": "Difficulty",
|
||||||
|
"values": {
|
||||||
|
"beginner": "Beginner",
|
||||||
|
"intermediate": "Intermediate",
|
||||||
|
"expert": "Expert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start": "Start",
|
||||||
|
"how": "How to play?",
|
||||||
|
"win": "Win!",
|
||||||
|
"loose": "Loose!",
|
||||||
|
"leaderboardTitle": "Leaderboard",
|
||||||
|
"date": "Date",
|
||||||
|
"time": "Time",
|
||||||
|
"settingsBtn": "Settings",
|
||||||
|
"replay": "Replay",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteAll": "Delete all"
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"appTitle": "Démineur",
|
||||||
|
"settings": {
|
||||||
|
"size": {
|
||||||
|
"label": "Taille de grille",
|
||||||
|
"values": {
|
||||||
|
"small": "Petit",
|
||||||
|
"medium": "Moyen",
|
||||||
|
"large": "Grand"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficulty": {
|
||||||
|
"label": "Difficulté",
|
||||||
|
"values": {
|
||||||
|
"beginner": "Facile",
|
||||||
|
"intermediate": "Intermédiaire",
|
||||||
|
"expert": "Difficile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start": "Commencer",
|
||||||
|
"how": "Comment jouer ?",
|
||||||
|
"win": "Gagné !",
|
||||||
|
"loose": "Perdu !",
|
||||||
|
"leaderboardTitle": "Tableau des scores",
|
||||||
|
"date": "Date",
|
||||||
|
"time": "Temps",
|
||||||
|
"settingsBtn": "Paramètres",
|
||||||
|
"replay": "Rejouer",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"deleteAll": "Tout supprimer"
|
||||||
|
}
|
||||||
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 }),
|
||||||
|
}));
|
||||||
+11
-4
@@ -7,7 +7,7 @@ import localforage from 'localforage';
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
// ------------------------------------------------------ Types --------------------------------------------------------
|
||||||
import { LeaderBoardItem } from '../types/game';
|
import { Difficulty, LeaderBoardItem } from '../types/game';
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
export const getRandomArbitrary = (min: number, max: number): number => {
|
export const getRandomArbitrary = (min: number, max: number): number => {
|
||||||
@@ -22,8 +22,8 @@ const store = localforage.createInstance({
|
|||||||
description: 'Store 10 best game time',
|
description: 'Store 10 best game time',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const saveToLocalStorage = async (time: string) => {
|
export const saveToLocalStorage = async (time: string, difficulty: Difficulty, gridSize: number) => {
|
||||||
let leaderboard = await store.getItem<LeaderBoardItem[]>('leaderboard');
|
let leaderboard = await store.getItem<LeaderBoardItem[]>(`${difficulty}:${gridSize}`);
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleString().split(' ')[0];
|
const currentDate = new Date().toLocaleString().split(' ')[0];
|
||||||
|
|
||||||
@@ -42,5 +42,12 @@ export const saveToLocalStorage = async (time: string) => {
|
|||||||
leaderboard = leaderboard.slice(0, 20);
|
leaderboard = leaderboard.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
await store.setItem('leaderboard', leaderboard);
|
await store.setItem(`${difficulty}:${gridSize}`, leaderboard);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDifficultyLabel = (difficulty: Difficulty): string => {
|
||||||
|
if (difficulty === 'beginner') return 'Débutant';
|
||||||
|
if (difficulty === 'intermediate') return 'Intermédiaire';
|
||||||
|
if (difficulty === 'expert') return 'Expert';
|
||||||
|
return difficulty;
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -14,8 +14,11 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules"]
|
"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