Add lang toggler

This commit is contained in:
2024-02-19 14:04:56 +01:00
parent 1cf7035449
commit 588eab0caf
14 changed files with 249 additions and 87 deletions
+1 -1
View File
@@ -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
View File
@@ -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;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

-26
View File
@@ -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;
+4 -1
View File
@@ -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' }))}
+13 -11
View File
@@ -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;
+23
View File
@@ -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;
}
}
+15 -10
View File
@@ -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>
</>
+6 -6
View File
@@ -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;
+9
View File
@@ -0,0 +1,9 @@
import { atom } from 'recoil';
export const langAtom = atom({
key: 'lang',
default: {
key: null as any,
config: null as any,
},
});
+32
View File
@@ -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"
}
+32
View File
@@ -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"
}