Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
588eab0caf | ||
|
|
1cf7035449 | ||
|
|
91a111a49b | ||
|
|
0b0fb1d502 | ||
|
|
9d349916aa | ||
|
|
c3eb4b4817 | ||
|
|
84489a3b6e | ||
|
|
3a0b5e1f7f | ||
|
|
25a264bf08 | ||
|
|
21ad0f4fa4 | ||
|
|
c74c44c521 | ||
|
|
2fea366313 | ||
|
|
1ebb863b81 | ||
|
|
ffa6468b70 | ||
|
|
9426b8499c |
@@ -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: ''
|
||||
---
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -13,3 +78,8 @@
|
||||
```bash
|
||||
docker run --name minesweeper -dp 3000:3000 minesweeper
|
||||
```
|
||||
|
||||
# 👩❤️👨 Credits
|
||||
|
||||
- [KeunotorCagoule](https://www.github.com/KeunotorCagoule)
|
||||
- [UnEpicier](https://www.github.com/UnEpicier)
|
||||
|
||||
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 |
+1
-1
@@ -20,7 +20,7 @@
|
||||
|
||||
& > .helpButton {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 10px 15px;
|
||||
|
||||
+41
-4
@@ -3,12 +3,13 @@
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from './contexts/gameState';
|
||||
import { langAtom } from './contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// --------------------------------------------------- Components ------------------------------------------------------
|
||||
@@ -20,19 +21,53 @@ 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 Question from './assets/question';
|
||||
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') && (
|
||||
<>
|
||||
@@ -46,8 +81,7 @@ const App = () => {
|
||||
className='helpButton'
|
||||
onClick={() => displayHelp(true)}
|
||||
>
|
||||
<Question />
|
||||
<span>Comment jouer</span>
|
||||
<span>{lang.config.how}</span>
|
||||
</button>
|
||||
{helpDisplayed && (
|
||||
<Help
|
||||
@@ -61,6 +95,9 @@ const App = () => {
|
||||
{gameState.status === 'board' && <LeaderBoard />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -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;
|
||||
@@ -9,6 +9,7 @@ import { useState } from 'react';
|
||||
// ----------------------------------------------------- Context -------------------------------------------------------
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Hooks --------------------------------------------------------
|
||||
@@ -29,6 +30,8 @@ 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);
|
||||
|
||||
return (
|
||||
@@ -69,7 +72,7 @@ const Grid = () => {
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<p className='overlayTitle'>{gameState.endType === 'win' ? 'Gagné !' : 'Perdu !'}</p>
|
||||
<p className='overlayTitle'>{gameState.endType === 'win' ? lang.config.win : lang.config.loose}</p>
|
||||
<button
|
||||
className='overlayButton'
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'board' }))}
|
||||
|
||||
@@ -10,9 +10,12 @@ import { useEffect, useRef } from 'react';
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||
import Size from '../../assets/size';
|
||||
import Difficulty from '../../assets/difficulty';
|
||||
import './styles.scss';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -29,21 +32,20 @@ const Home = () => {
|
||||
const sizeRef = useRef<HTMLSelectElement>(null);
|
||||
|
||||
const [, setGameState] = useRecoilState(gameStateAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const initIDB = async () => {
|
||||
if ((await store.getItem('leaderboard')) === null) {
|
||||
await store.setItem('leaderboard', []);
|
||||
}
|
||||
};
|
||||
initIDB();
|
||||
}, []);
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const onClick = () => {
|
||||
if (difficultyRef.current !== null && sizeRef.current !== null) {
|
||||
const difficulty = difficultyRef.current.value;
|
||||
const size = sizeRef.current.value;
|
||||
|
||||
const initIDB = async () => {
|
||||
if ((await store.getItem(`${difficulty}:${size}`)) === null) {
|
||||
await store.setItem(`${difficulty}:${size}`, []);
|
||||
}
|
||||
};
|
||||
initIDB();
|
||||
|
||||
setGameState((prev) => ({
|
||||
...prev,
|
||||
difficulty: difficulty,
|
||||
@@ -55,34 +57,40 @@ const Home = () => {
|
||||
|
||||
return (
|
||||
<div className='gameContainer'>
|
||||
<h1 className='title'>Démineur</h1>
|
||||
<h1 className='title'>{lang.config.appTitle}</h1>
|
||||
|
||||
<div className='settingsContainer'>
|
||||
<div className='selectorBox'>
|
||||
<p className='settingLabel'>Taille de la grille</p>
|
||||
<p className='settingLabel'>
|
||||
<Size />
|
||||
{lang.config.settings.size.label}
|
||||
</p>
|
||||
<select
|
||||
ref={sizeRef}
|
||||
name='gridSize'
|
||||
className='select'
|
||||
defaultValue={'12'}
|
||||
>
|
||||
<option value='12'>Petit</option>
|
||||
<option value='16'>Moyen</option>
|
||||
<option value='20'>Grand</option>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className='selectorBox'>
|
||||
<p className='settingLabel'>Difficulté</p>
|
||||
<p className='settingLabel'>
|
||||
<Difficulty />
|
||||
{lang.config.settings.difficulty.label}
|
||||
</p>
|
||||
<select
|
||||
ref={difficultyRef}
|
||||
name='difficulty'
|
||||
className='select'
|
||||
defaultValue={'beginner'}
|
||||
>
|
||||
<option value='beginner'>Facile</option>
|
||||
<option value='intermediate'>Moyen</option>
|
||||
<option value='expert'>Difficile</option>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +98,7 @@ const Home = () => {
|
||||
className='startButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
Commencer
|
||||
{lang.config.start}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,16 @@
|
||||
gap: 20px;
|
||||
|
||||
& > .settingLabel {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 1.2rem;
|
||||
color: #f39440;
|
||||
|
||||
& > svg {
|
||||
width: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
& > .select {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
|
||||
import fr from '../../assets/fr.png';
|
||||
import en from '../../assets/en.png';
|
||||
import './styles.scss';
|
||||
|
||||
const LangToggler = () => {
|
||||
const [lang, setLang] = useRecoilState(langAtom);
|
||||
|
||||
return (
|
||||
<button
|
||||
className='langToggler'
|
||||
onClick={() => {
|
||||
if (lang.key === 'fr') {
|
||||
localStorage.setItem('lang', 'en');
|
||||
setLang((prev) => ({
|
||||
...prev,
|
||||
key: 'en',
|
||||
}));
|
||||
} else {
|
||||
localStorage.setItem('lang', 'fr');
|
||||
setLang((prev) => ({
|
||||
...prev,
|
||||
key: 'fr',
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{lang.key == 'fr' ? (
|
||||
<img
|
||||
src={fr}
|
||||
alt='fr flag'
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={en}
|
||||
alt='en flag'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LangToggler;
|
||||
@@ -0,0 +1,23 @@
|
||||
.langToggler {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+56
-21
@@ -10,10 +10,12 @@ import { useEffect, useState } from 'react';
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
||||
import { LeaderBoardItem } from '../../types/game';
|
||||
// -------------------------------------------------- Utils & Types ----------------------------------------------------
|
||||
import { Difficulty, LeaderBoardItem } from '../../types/game';
|
||||
import { getDifficultyLabel } from '../../utils/generics';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Assets --------------------------------------------------------
|
||||
@@ -22,6 +24,8 @@ 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';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,35 +38,54 @@ const store = localforage.createInstance({
|
||||
});
|
||||
|
||||
const LeaderBoard = () => {
|
||||
const [, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [data, setData] = useState<LeaderBoardItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const getData = async () => {
|
||||
const storeData = await store.getItem<LeaderBoardItem[]>('leaderboard');
|
||||
const storeData = await store.getItem<LeaderBoardItem[]>(`${gameState.difficulty}:${gameState.gridSize}`);
|
||||
|
||||
setData(storeData || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length === 0) {
|
||||
getData();
|
||||
}
|
||||
}, [data.length]);
|
||||
}, [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();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className='title'>Time Leaderboard</h1>
|
||||
<h1 className='title'>{lang.config.leaderboardTitle}</h1>
|
||||
|
||||
<div className='board'>
|
||||
<div className='header'>
|
||||
<p>#</p>
|
||||
<p>
|
||||
<CalendarIcon />
|
||||
Date
|
||||
{lang.config.date}
|
||||
</p>
|
||||
<p>
|
||||
<DifficultyIcon style={{ rotate: '-45deg' }} />
|
||||
{lang.config.settings.difficulty.label}
|
||||
</p>
|
||||
<p>
|
||||
<Size />
|
||||
{lang.config.settings.size.label}
|
||||
</p>
|
||||
<p>
|
||||
<TimerIcon />
|
||||
Time
|
||||
{lang.config.time}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +98,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>{item.time}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -88,18 +113,8 @@ const LeaderBoard = () => {
|
||||
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
|
||||
{lang.config.settingsBtn}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -107,7 +122,27 @@ const LeaderBoard = () => {
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
||||
>
|
||||
<Replay />
|
||||
Rejouer
|
||||
|
||||
{lang.config.replay}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='controlButton clear'
|
||||
onClick={async () => {
|
||||
await store.setItem(`${gameState.difficulty}:${gameState.gridSize}`, []);
|
||||
await getData();
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
{lang.config.delete}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='controlButton clearAll'
|
||||
onClick={clearAllLeaderboards}
|
||||
>
|
||||
<Replay />
|
||||
{lang.config.deleteAll}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -9,15 +9,15 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 95svw;
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
& > .header {
|
||||
display: grid;
|
||||
grid-template-columns: 1em repeat(2, 1fr);
|
||||
gap: 30px;
|
||||
grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 15px 20px;
|
||||
background-color: #333;
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 1.3em;
|
||||
font-size: 1.1em;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
|
||||
& > svg {
|
||||
height: 1.3em;
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@@ -47,8 +47,10 @@
|
||||
|
||||
& > .score {
|
||||
display: grid;
|
||||
grid-template-columns: 1em repeat(2, 1fr);
|
||||
gap: 30px;
|
||||
grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 10px 20px;
|
||||
background-color: #e1e1e1;
|
||||
color: #333;
|
||||
@@ -59,7 +61,7 @@
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +75,8 @@
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
@@ -100,12 +103,16 @@
|
||||
background-color: #f39440;
|
||||
}
|
||||
|
||||
&.replay {
|
||||
background-color: #f39440;
|
||||
}
|
||||
|
||||
&.clear {
|
||||
background-color: #ca5940;
|
||||
}
|
||||
|
||||
&.replay {
|
||||
background-color: #f39440;
|
||||
&.clearAll {
|
||||
background-color: hsl(0, 57%, 52%);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
|
||||
@@ -11,7 +11,8 @@ import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../../contexts/gameState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Utils --------------------------------------------------------
|
||||
// -------------------------------------------------- Utils & Types ----------------------------------------------------
|
||||
import { Difficulty } from '../../../types/game';
|
||||
import { saveToLocalStorage } from '../../../utils/generics';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -64,7 +65,7 @@ const Timer = () => {
|
||||
clearInterval(intervalId);
|
||||
|
||||
if (gameState.endType === 'win') {
|
||||
saveToLocalStorage(displayedTime);
|
||||
saveToLocalStorage(displayedTime, gameState.difficulty as Difficulty, gameState.gridSize);
|
||||
}
|
||||
}
|
||||
}, [intervalId, gameState.endType, setGameState, displayedTime]);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const langAtom = atom({
|
||||
key: 'lang',
|
||||
default: {
|
||||
key: null as any,
|
||||
config: null as any,
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
+11
-4
@@ -7,7 +7,7 @@ import localforage from 'localforage';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ Types --------------------------------------------------------
|
||||
import { LeaderBoardItem } from '../types/game';
|
||||
import { Difficulty, LeaderBoardItem } from '../types/game';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
export const getRandomArbitrary = (min: number, max: number): number => {
|
||||
@@ -22,8 +22,8 @@ const store = localforage.createInstance({
|
||||
description: 'Store 10 best game time',
|
||||
});
|
||||
|
||||
export const saveToLocalStorage = async (time: string) => {
|
||||
let leaderboard = await store.getItem<LeaderBoardItem[]>('leaderboard');
|
||||
export const saveToLocalStorage = async (time: string, difficulty: Difficulty, gridSize: number) => {
|
||||
let leaderboard = await store.getItem<LeaderBoardItem[]>(`${difficulty}:${gridSize}`);
|
||||
|
||||
const currentDate = new Date().toLocaleString().split(' ')[0];
|
||||
|
||||
@@ -42,5 +42,12 @@ export const saveToLocalStorage = async (time: string) => {
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user