diff --git a/TODO.MD b/TODO.MD index dccadb2..cf7f6d6 100644 --- a/TODO.MD +++ b/TODO.MD @@ -21,7 +21,7 @@ Liste d'améliorations inspirées de Chess.com pour rendre l'application plus co ## Expérience utilisateur - **Notifications** : alertes pour les défis reçus, les débuts de tournois ou l'arrivée de nouveaux puzzles. - **Personnalisation** : thèmes d'échiquier supplémentaires et options pour les pièces, inspirés de ceux de Chess.com. -- **Progression et succès** : badges et objectifs à débloquer en fonction des activités (puzzles résolus, parties jouées, etc.). +- **Progression et succès** : badges et objectifs à débloquer en fonction des activités (puzzles résolus, parties jouées, etc.). *(en cours : suivi local des puzzles résolus et parties jouées)* ## Autres idées - **Support mobile amélioré** : application ou PWA optimisée pour smartphone avec notifications push. diff --git a/src/components/AchievementsModal.tsx b/src/components/AchievementsModal.tsx new file mode 100644 index 0000000..7221f03 --- /dev/null +++ b/src/components/AchievementsModal.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import AccessibleModal from './AccessibleModal'; +import { useAchievementsStore } from '../store/useAchievementsStore'; + +interface AchievementsModalProps { + isOpen: boolean; + onClose: () => void; +} + +const AchievementsModal: React.FC = ({ isOpen, onClose }) => { + const { achievements, puzzlesSolved, gamesPlayed } = useAchievementsStore(); + + return ( + +
+

Puzzles résolus : {puzzlesSolved}

+

Parties jouées : {gamesPlayed}

+
    + {achievements.length === 0 ? ( +
  • Aucun succès débloqué pour le moment.
  • + ) : ( + achievements.map((a, i) => ( +
  • {a}
  • + )) + )} +
+
+
+ ); +}; + +export default AchievementsModal; diff --git a/src/components/ChessMasterDatabase.tsx b/src/components/ChessMasterDatabase.tsx index dad5f38..b74872d 100644 --- a/src/components/ChessMasterDatabase.tsx +++ b/src/components/ChessMasterDatabase.tsx @@ -9,7 +9,7 @@ import ThemeSelector from './ThemeSelector'; import BoardThemeSelector from './BoardThemeSelector'; import AccessibleModal from './AccessibleModal'; import { toast, ToastContainer } from 'react-toastify'; -import { ClipboardCheck as ChessBoard, Search, BookOpen, Clock, Download, Upload, BookOpen as OpeningIcon, HelpCircle, BarChart3, GitCompare, Users as PlayersIcon, Zap, Puzzle } from 'lucide-react'; +import { ClipboardCheck as ChessBoard, Search, BookOpen, Clock, Download, Upload, BookOpen as OpeningIcon, HelpCircle, BarChart3, GitCompare, Users as PlayersIcon, Zap, Puzzle, Award } from 'lucide-react'; import type { ChessGame } from '../data/masterGames'; import { parsePGN, convertPGNToGame } from '../data/importGames'; import { exportToPGN, downloadPGN } from '../data/exportGames'; @@ -29,6 +29,7 @@ import PlayersDatabase from './PlayersDatabase'; import PlayerImportButton from './PlayerImportButton'; import PlayComputer from '../screens/PlayComputer'; import PuzzleTrainer from '../screens/PuzzleTrainer'; +import AchievementsModal from './AchievementsModal'; const ChessMasterDatabase = () => { const { @@ -62,6 +63,7 @@ const ChessMasterDatabase = () => { const { importGamesMutation } = useChessQuery(); const [showStatistics, setShowStatistics] = useState(false); const [showComparison, setShowComparison] = useState(false); + const [showAchievements, setShowAchievements] = useState(false); const [filteredGamesList, setFilteredGamesList] = useState(filteredGames); // Update filtered games when store changes @@ -192,6 +194,13 @@ const ChessMasterDatabase = () => { Puzzles + @@ -457,6 +466,13 @@ const ChessMasterDatabase = () => { onClose={() => setShowComparison(false)} /> )} + + {showAchievements && ( + setShowAchievements(false)} + /> + )} diff --git a/src/screens/PlayComputer.tsx b/src/screens/PlayComputer.tsx index 9f46675..8a1d407 100644 --- a/src/screens/PlayComputer.tsx +++ b/src/screens/PlayComputer.tsx @@ -8,11 +8,13 @@ import { openings } from '../data/openings'; import CapturedPieces from '../components/CapturedPieces'; import { Save, Download } from 'lucide-react'; import { getCapturedPieces } from '../utils/capturedPieces'; +import { useAchievementsStore } from '../store/useAchievementsStore'; const PlayComputer: React.FC = () => { const games = useChessStore((s) => s.games); const boardOrientation = useChessStore((s) => s.boardOrientation); const flipBoard = useChessStore((s) => s.flipBoard); + const incrementGamesPlayed = useAchievementsStore((s) => s.incrementGamesPlayed); const [chess] = useState(() => new Chess()); const [history, setHistory] = useState([chess.fen()]); @@ -242,6 +244,7 @@ const PlayComputer: React.FC = () => { setOpeningId(''); loadFen('startpos'); setCaptured({ white: [], black: [] }); + incrementGamesPlayed(); }} className="px-2 py-1 border rounded bg-blue-500 text-white" > diff --git a/src/screens/PuzzleTrainer.tsx b/src/screens/PuzzleTrainer.tsx index 3b78100..3c0d37f 100644 --- a/src/screens/PuzzleTrainer.tsx +++ b/src/screens/PuzzleTrainer.tsx @@ -3,6 +3,7 @@ import { Chess } from 'chess.js'; import { ChessboardDisplay } from '../components/ChessboardDisplay'; import { MoveHistory } from '../components/MoveHistory'; import { puzzles, ChessPuzzle } from '../data/puzzles'; +import { useAchievementsStore } from '../store/useAchievementsStore'; const PuzzleTrainer: React.FC = () => { const [index, setIndex] = useState(0); @@ -10,6 +11,7 @@ const PuzzleTrainer: React.FC = () => { const [chess] = useState(() => new Chess(puzzles[0].fen)); const [history, setHistory] = useState([puzzles[0].fen]); const [status, setStatus] = useState(''); + const incrementPuzzlesSolved = useAchievementsStore((s) => s.incrementPuzzlesSolved); const currentPuzzle: ChessPuzzle = puzzles[index]; @@ -37,6 +39,7 @@ const PuzzleTrainer: React.FC = () => { setHistory((h) => [...h, chess.fen()]); if (step + 1 === currentPuzzle.moves.length) { setStatus('Bravo!'); + incrementPuzzlesSolved(); } setStep((s) => s + 1); return true; @@ -46,7 +49,7 @@ const PuzzleTrainer: React.FC = () => { setStatus('Incorrect, réessayez'); return false; }, - [chess, currentPuzzle.moves, step] + [chess, currentPuzzle.moves, step, incrementPuzzlesSolved] ); const nextPuzzle = useCallback(() => { diff --git a/src/store/useAchievementsStore.ts b/src/store/useAchievementsStore.ts new file mode 100644 index 0000000..d027c3f --- /dev/null +++ b/src/store/useAchievementsStore.ts @@ -0,0 +1,66 @@ +import { create } from 'zustand'; +import { toast } from 'react-toastify'; + +interface AchievementsState { + puzzlesSolved: number; + gamesPlayed: number; + achievements: string[]; + incrementPuzzlesSolved: () => void; + incrementGamesPlayed: () => void; +} + +const PUZZLE_THRESHOLDS = [1, 5, 10]; +const GAME_THRESHOLDS = [1, 10]; + +const persist = (state: AchievementsState) => { + try { + localStorage.setItem('achievements-state', JSON.stringify(state)); + } catch { + // ignore + } +}; + +export const useAchievementsStore = create((set, get) => ({ + puzzlesSolved: 0, + gamesPlayed: 0, + achievements: [], + incrementPuzzlesSolved: () => { + const count = get().puzzlesSolved + 1; + const achievements = [...get().achievements]; + if (PUZZLE_THRESHOLDS.includes(count)) { + const label = `Puzzles résolus : ${count}`; + achievements.push(label); + toast.success(`Succès débloqué \u2013 ${label}`); + } + const newState = { ...get(), puzzlesSolved: count, achievements }; + set(newState); + persist(newState); + }, + incrementGamesPlayed: () => { + const count = get().gamesPlayed + 1; + const achievements = [...get().achievements]; + if (GAME_THRESHOLDS.includes(count)) { + const label = `Parties jouées : ${count}`; + achievements.push(label); + toast.success(`Succès débloqué \u2013 ${label}`); + } + const newState = { ...get(), gamesPlayed: count, achievements }; + set(newState); + persist(newState); + } +})); + +// Load initial state from localStorage +try { + const saved = localStorage.getItem('achievements-state'); + if (saved) { + const data = JSON.parse(saved); + useAchievementsStore.setState({ + puzzlesSolved: data.puzzlesSolved || 0, + gamesPlayed: data.gamesPlayed || 0, + achievements: Array.isArray(data.achievements) ? data.achievements : [] + }); + } +} catch { + // ignore +}