Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions src/components/AchievementsModal.tsx
Original file line number Diff line number Diff line change
@@ -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<AchievementsModalProps> = ({ isOpen, onClose }) => {
const { achievements, puzzlesSolved, gamesPlayed } = useAchievementsStore();

return (
<AccessibleModal isOpen={isOpen} onClose={onClose} title="Succès" maxWidth="600px">
<div className="space-y-4">
<p className="text-gray-700 dark:text-gray-300">Puzzles résolus : {puzzlesSolved}</p>
<p className="text-gray-700 dark:text-gray-300">Parties jouées : {gamesPlayed}</p>
<ul className="list-disc pl-5 space-y-1">
{achievements.length === 0 ? (
<li className="text-gray-500">Aucun succès débloqué pour le moment.</li>
) : (
achievements.map((a, i) => (
<li key={i} className="text-gray-700 dark:text-gray-300">{a}</li>
))
)}
</ul>
</div>
</AccessibleModal>
);
};

export default AchievementsModal;
18 changes: 17 additions & 1 deletion src/components/ChessMasterDatabase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<ChessGame[]>(filteredGames);

// Update filtered games when store changes
Expand Down Expand Up @@ -192,6 +194,13 @@ const ChessMasterDatabase = () => {
<Puzzle className="w-5 h-5" />
Puzzles
</button>
<button
onClick={() => setShowAchievements(true)}
className="px-4 py-2 rounded-md flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<Award className="w-5 h-5" />
Succès
</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -457,6 +466,13 @@ const ChessMasterDatabase = () => {
onClose={() => setShowComparison(false)}
/>
)}

{showAchievements && (
<AchievementsModal
isOpen={showAchievements}
onClose={() => setShowAchievements(false)}
/>
)}
</main>
</div>
</DndProvider>
Expand Down
3 changes: 3 additions & 0 deletions src/screens/PlayComputer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([chess.fen()]);
Expand Down Expand Up @@ -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"
>
Expand Down
5 changes: 4 additions & 1 deletion src/screens/PuzzleTrainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ 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);
const [step, setStep] = useState(0);
const [chess] = useState(() => new Chess(puzzles[0].fen));
const [history, setHistory] = useState<string[]>([puzzles[0].fen]);
const [status, setStatus] = useState<string>('');
const incrementPuzzlesSolved = useAchievementsStore((s) => s.incrementPuzzlesSolved);

const currentPuzzle: ChessPuzzle = puzzles[index];

Expand Down Expand Up @@ -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;
Expand All @@ -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(() => {
Expand Down
66 changes: 66 additions & 0 deletions src/store/useAchievementsStore.ts
Original file line number Diff line number Diff line change
@@ -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<AchievementsState>((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
}