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 { & > .helpButton {
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;
+41 -4
View File
@@ -3,12 +3,13 @@
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ React -------------------------------------------------------- // ------------------------------------------------------ React --------------------------------------------------------
import { useState } from 'react'; import { useEffect, useState } from 'react';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------- Context ------------------------------------------------------- // ----------------------------------------------------- Context -------------------------------------------------------
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { gameStateAtom } from './contexts/gameState'; import { gameStateAtom } from './contexts/gameState';
import { langAtom } from './contexts/langState';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------- Components ------------------------------------------------------ // --------------------------------------------------- Components ------------------------------------------------------
@@ -20,19 +21,53 @@ import Help from './components/Help/Help';
import LeaderBoard from './components/Leaderboard/Leaderboard'; import LeaderBoard from './components/Leaderboard/Leaderboard';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ Langs --------------------------------------------------------
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';
import LangToggler from './components/LangToggler/LangToggler';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
const App = () => { const App = () => {
const [helpDisplayed, displayHelp] = useState(false); const [helpDisplayed, displayHelp] = useState(false);
const [gameState] = useRecoilState(gameStateAtom); 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 ( return (
<div className='App'> <div className='App'>
<LangToggler />
{gameState.status === 'settings' && <Home />} {gameState.status === 'settings' && <Home />}
{(gameState.status === 'idle' || gameState.status === 'playing') && ( {(gameState.status === 'idle' || gameState.status === 'playing') && (
<> <>
@@ -46,8 +81,7 @@ const App = () => {
className='helpButton' className='helpButton'
onClick={() => displayHelp(true)} onClick={() => displayHelp(true)}
> >
<Question /> <span>{lang.config.how}</span>
<span>Comment jouer</span>
</button> </button>
{helpDisplayed && ( {helpDisplayed && (
<Help <Help
@@ -61,6 +95,9 @@ const App = () => {
{gameState.status === 'board' && <LeaderBoard />} {gameState.status === 'board' && <LeaderBoard />}
</div> </div>
); );
}
return <></>;
}; };
export default App; 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 ------------------------------------------------------- // ----------------------------------------------------- Context -------------------------------------------------------
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { gameStateAtom } from '../../contexts/gameState'; import { gameStateAtom } from '../../contexts/gameState';
import { langAtom } from '../../contexts/langState';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ Hooks -------------------------------------------------------- // ------------------------------------------------------ Hooks --------------------------------------------------------
@@ -29,6 +30,8 @@ const Grid = () => {
const [gameState, setGameState] = useRecoilState(gameStateAtom); const [gameState, setGameState] = useRecoilState(gameStateAtom);
const [grid, setGrid] = useState(genGrid(gameState.gridSize)); const [grid, setGrid] = useState(genGrid(gameState.gridSize));
const [lang] = useRecoilState(langAtom);
const { handleLeftClick, handleRightClick } = useActions(grid, setGrid, gameState, setGameState); const { handleLeftClick, handleRightClick } = useActions(grid, setGrid, gameState, setGameState);
return ( return (
@@ -69,7 +72,7 @@ const Grid = () => {
} as React.CSSProperties } 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 <button
className='overlayButton' className='overlayButton'
onClick={() => setGameState((prev) => ({ ...prev, status: 'board' }))} onClick={() => setGameState((prev) => ({ ...prev, status: 'board' }))}
+13 -11
View File
@@ -3,13 +3,14 @@
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------ React -------------------------------------------------------- // ------------------------------------------------------ React --------------------------------------------------------
import { useRef } from 'react'; import { useEffect, useRef } from 'react';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------- Context & Stockage -------------------------------------------------- // ----------------------------------------------- Context & Stockage --------------------------------------------------
import localforage from 'localforage'; import localforage from 'localforage';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { gameStateAtom } from '../../contexts/gameState'; import { gameStateAtom } from '../../contexts/gameState';
import { langAtom } from '../../contexts/langState';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------- Styles -------------------------------------------------------- // ----------------------------------------------------- Styles --------------------------------------------------------
@@ -31,6 +32,7 @@ const Home = () => {
const sizeRef = useRef<HTMLSelectElement>(null); const sizeRef = useRef<HTMLSelectElement>(null);
const [, setGameState] = useRecoilState(gameStateAtom); const [, setGameState] = useRecoilState(gameStateAtom);
const [lang] = useRecoilState(langAtom);
const onClick = () => { const onClick = () => {
if (difficultyRef.current !== null && sizeRef.current !== null) { if (difficultyRef.current !== null && sizeRef.current !== null) {
@@ -55,13 +57,13 @@ const Home = () => {
return ( return (
<div className='gameContainer'> <div className='gameContainer'>
<h1 className='title'>Démineur</h1> <h1 className='title'>{lang.config.appTitle}</h1>
<div className='settingsContainer'> <div className='settingsContainer'>
<div className='selectorBox'> <div className='selectorBox'>
<p className='settingLabel'> <p className='settingLabel'>
<Size /> <Size />
Taille de la grille {lang.config.settings.size.label}
</p> </p>
<select <select
ref={sizeRef} ref={sizeRef}
@@ -69,16 +71,16 @@ const Home = () => {
className='select' className='select'
defaultValue={'12'} defaultValue={'12'}
> >
<option value='12'>Petit</option> <option value='12'>{lang.config.settings.size.values.small}</option>
<option value='16'>Moyen</option> <option value='16'>{lang.config.settings.size.values.medium}</option>
<option value='20'>Grand</option> <option value='20'>{lang.config.settings.size.values.large}</option>
</select> </select>
</div> </div>
<div className='selectorBox'> <div className='selectorBox'>
<p className='settingLabel'> <p className='settingLabel'>
<Difficulty /> <Difficulty />
Difficulté {lang.config.settings.difficulty.label}
</p> </p>
<select <select
ref={difficultyRef} ref={difficultyRef}
@@ -86,9 +88,9 @@ const Home = () => {
className='select' className='select'
defaultValue={'beginner'} defaultValue={'beginner'}
> >
<option value='beginner'>Facile</option> <option value='beginner'>{lang.config.settings.difficulty.values.beginner}</option>
<option value='intermediate'>Moyen</option> <option value='intermediate'>{lang.config.settings.difficulty.values.intermediate}</option>
<option value='expert'>Difficile</option> <option value='expert'>{lang.config.settings.difficulty.values.expert}</option>
</select> </select>
</div> </div>
@@ -96,7 +98,7 @@ const Home = () => {
className='startButton' className='startButton'
onClick={onClick} onClick={onClick}
> >
Commencer {lang.config.start}
</button> </button>
</div> </div>
</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 localforage from 'localforage';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { gameStateAtom } from '../../contexts/gameState'; import { gameStateAtom } from '../../contexts/gameState';
import { langAtom } from '../../contexts/langState';
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------- Utils & Types ---------------------------------------------------- // -------------------------------------------------- Utils & Types ----------------------------------------------------
@@ -40,6 +41,8 @@ const LeaderBoard = () => {
const [gameState, setGameState] = useRecoilState(gameStateAtom); const [gameState, setGameState] = useRecoilState(gameStateAtom);
const [data, setData] = useState<LeaderBoardItem[]>([]); const [data, setData] = useState<LeaderBoardItem[]>([]);
const [lang] = useRecoilState(langAtom);
const getData = async () => { const getData = async () => {
const storeData = await store.getItem<LeaderBoardItem[]>(`${gameState.difficulty}:${gameState.gridSize}`); const storeData = await store.getItem<LeaderBoardItem[]>(`${gameState.difficulty}:${gameState.gridSize}`);
@@ -63,26 +66,26 @@ const LeaderBoard = () => {
return ( return (
<> <>
<h1 className='title'>Time Leaderboard</h1> <h1 className='title'>{lang.config.leaderboardTitle}</h1>
<div className='board'> <div className='board'>
<div className='header'> <div className='header'>
<p>#</p> <p>#</p>
<p> <p>
<CalendarIcon /> <CalendarIcon />
Date {lang.config.date}
</p> </p>
<p> <p>
<DifficultyIcon style={{ rotate: '-45deg' }} /> <DifficultyIcon style={{ rotate: '-45deg' }} />
Difficulté {lang.config.settings.difficulty.label}
</p> </p>
<p> <p>
<Size /> <Size />
Taille {lang.config.settings.size.label}
</p> </p>
<p> <p>
<TimerIcon /> <TimerIcon />
Time {lang.config.time}
</p> </p>
</div> </div>
@@ -95,7 +98,7 @@ const LeaderBoard = () => {
> >
<span>{index + 1}</span> <span>{index + 1}</span>
<span>{item.date}</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>{gameState.gridSize}</span>
<span>{item.time}</span> <span>{item.time}</span>
</div> </div>
@@ -110,7 +113,8 @@ const LeaderBoard = () => {
onClick={() => setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))} onClick={() => setGameState((prev) => ({ ...prev, status: 'settings', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
> >
<Settings /> <Settings />
Paramètres
{lang.config.settingsBtn}
</button> </button>
<button <button
@@ -118,7 +122,8 @@ const LeaderBoard = () => {
onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))} onClick={() => setGameState((prev) => ({ ...prev, status: 'idle', endType: '', placedFlags: 0, bombs: 0, gameTime: '' }))}
> >
<Replay /> <Replay />
Rejouer
{lang.config.replay}
</button> </button>
<button <button
@@ -129,7 +134,7 @@ const LeaderBoard = () => {
}} }}
> >
<Trash /> <Trash />
Supprimer {lang.config.delete}
</button> </button>
<button <button
@@ -137,7 +142,7 @@ const LeaderBoard = () => {
onClick={clearAllLeaderboards} onClick={clearAllLeaderboards}
> >
<Replay /> <Replay />
Tout supprimer {lang.config.deleteAll}
</button> </button>
</div> </div>
</> </>
+6 -6
View File
@@ -16,8 +16,8 @@
& > .header { & > .header {
display: grid; display: grid;
grid-template-columns: 1em 1fr 1fr 0.7fr 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,10 +47,10 @@
& > .score { & > .score {
display: grid; display: grid;
grid-template-columns: 1em 1fr 1fr 0.7fr 1fr; grid-template-columns: 1em 1fr 1fr 0.8fr 1fr;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 30px; gap: 20px;
padding: 10px 20px; padding: 10px 20px;
background-color: #e1e1e1; background-color: #e1e1e1;
color: #333; 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"
}