✨ Add lang toggler
This commit is contained in:
+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;
|
||||
|
||||
+69
-32
@@ -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,47 +21,83 @@ 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);
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
{gameState.status === 'settings' && <Home />}
|
||||
{(gameState.status === 'idle' || gameState.status === 'playing') && (
|
||||
<>
|
||||
<div className='stats'>
|
||||
<Timer />
|
||||
<Logo className={'logo'} />
|
||||
<MinesCounter />
|
||||
</div>
|
||||
<Grid />
|
||||
<button
|
||||
className='helpButton'
|
||||
onClick={() => displayHelp(true)}
|
||||
>
|
||||
<Question />
|
||||
<span>Comment jouer</span>
|
||||
</button>
|
||||
{helpDisplayed && (
|
||||
<Help
|
||||
onClose={() => {
|
||||
displayHelp(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{gameState.status === 'board' && <LeaderBoard />}
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
const storedLang = localStorage.getItem('lang');
|
||||
|
||||
if (storedLang === null) {
|
||||
localStorage.setItem('lang', 'fr');
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedLang === 'fr') {
|
||||
setLang({
|
||||
key: 'fr',
|
||||
config: frConf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLang({
|
||||
key: 'en',
|
||||
config: enConf,
|
||||
});
|
||||
}, [lang.key]);
|
||||
|
||||
if (lang.config) {
|
||||
return (
|
||||
<div className='App'>
|
||||
<LangToggler />
|
||||
{gameState.status === 'settings' && <Home />}
|
||||
{(gameState.status === 'idle' || gameState.status === 'playing') && (
|
||||
<>
|
||||
<div className='stats'>
|
||||
<Timer />
|
||||
<Logo className={'logo'} />
|
||||
<MinesCounter />
|
||||
</div>
|
||||
<Grid />
|
||||
<button
|
||||
className='helpButton'
|
||||
onClick={() => displayHelp(true)}
|
||||
>
|
||||
<span>{lang.config.how}</span>
|
||||
</button>
|
||||
{helpDisplayed && (
|
||||
<Help
|
||||
onClose={() => {
|
||||
displayHelp(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{gameState.status === 'board' && <LeaderBoard />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
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;
|
||||
@@ -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' }))}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------ React --------------------------------------------------------
|
||||
import { useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------- Context & Stockage --------------------------------------------------
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------- Styles --------------------------------------------------------
|
||||
@@ -31,6 +32,7 @@ const Home = () => {
|
||||
const sizeRef = useRef<HTMLSelectElement>(null);
|
||||
|
||||
const [, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const onClick = () => {
|
||||
if (difficultyRef.current !== null && sizeRef.current !== null) {
|
||||
@@ -55,13 +57,13 @@ 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'>
|
||||
<Size />
|
||||
Taille de la grille
|
||||
{lang.config.settings.size.label}
|
||||
</p>
|
||||
<select
|
||||
ref={sizeRef}
|
||||
@@ -69,16 +71,16 @@ const Home = () => {
|
||||
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'>
|
||||
<Difficulty />
|
||||
Difficulté
|
||||
{lang.config.settings.difficulty.label}
|
||||
</p>
|
||||
<select
|
||||
ref={difficultyRef}
|
||||
@@ -86,9 +88,9 @@ const Home = () => {
|
||||
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>
|
||||
|
||||
@@ -96,7 +98,7 @@ const Home = () => {
|
||||
className='startButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
Commencer
|
||||
{lang.config.start}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
|
||||
import localforage from 'localforage';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { gameStateAtom } from '../../contexts/gameState';
|
||||
import { langAtom } from '../../contexts/langState';
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------- Utils & Types ----------------------------------------------------
|
||||
@@ -40,6 +41,8 @@ const LeaderBoard = () => {
|
||||
const [gameState, setGameState] = useRecoilState(gameStateAtom);
|
||||
const [data, setData] = useState<LeaderBoardItem[]>([]);
|
||||
|
||||
const [lang] = useRecoilState(langAtom);
|
||||
|
||||
const getData = async () => {
|
||||
const storeData = await store.getItem<LeaderBoardItem[]>(`${gameState.difficulty}:${gameState.gridSize}`);
|
||||
|
||||
@@ -63,26 +66,26 @@ const LeaderBoard = () => {
|
||||
|
||||
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' }} />
|
||||
Difficulté
|
||||
{lang.config.settings.difficulty.label}
|
||||
</p>
|
||||
<p>
|
||||
<Size />
|
||||
Taille
|
||||
{lang.config.settings.size.label}
|
||||
</p>
|
||||
<p>
|
||||
<TimerIcon />
|
||||
Time
|
||||
{lang.config.time}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +98,7 @@ const LeaderBoard = () => {
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
<span>{item.date}</span>
|
||||
<span>{getDifficultyLabel(gameState.difficulty as Difficulty)}</span>
|
||||
<span>{lang.config.settings.difficulty.values[gameState.difficulty]}</span>
|
||||
<span>{gameState.gridSize}</span>
|
||||
<span>{item.time}</span>
|
||||
</div>
|
||||
@@ -110,7 +113,8 @@ const LeaderBoard = () => {
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
||||
>
|
||||
<Settings />
|
||||
Paramètres
|
||||
|
||||
{lang.config.settingsBtn}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -118,7 +122,8 @@ const LeaderBoard = () => {
|
||||
onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
|
||||
>
|
||||
<Replay />
|
||||
Rejouer
|
||||
|
||||
{lang.config.replay}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -129,7 +134,7 @@ const LeaderBoard = () => {
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
Supprimer
|
||||
{lang.config.delete}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -137,7 +142,7 @@ const LeaderBoard = () => {
|
||||
onClick={clearAllLeaderboards}
|
||||
>
|
||||
<Replay />
|
||||
Tout supprimer
|
||||
{lang.config.deleteAll}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
& > .header {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 0.7fr 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,10 +47,10 @@
|
||||
|
||||
& > .score {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 0.7fr 1fr;
|
||||
grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
gap: 20px;
|
||||
padding: 10px 20px;
|
||||
background-color: #e1e1e1;
|
||||
color: #333;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user