diff --git a/src/app/page.tsx b/src/app/page.tsx index 63b9a62..23be42a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import ChessGame from "@/components/ChessGame"; import StartScreen from "@/components/StartScreen"; import { Personality } from "@/lib/personalities"; +import { SavedGame, deleteSavedGame, loadSavedGames } from "@/lib/savedGames"; type ViewState = 'start' | 'game'; @@ -15,13 +16,14 @@ export default function Home() { // Game Initialization State const [gameProps, setGameProps] = useState<{ + gameId: string; initialFen?: string; initialPgn?: string; initialPersonality: Personality; initialColor: 'white' | 'black'; } | null>(null); - const [hasSavedGame, setHasSavedGame] = useState(false); + const [savedGames, setSavedGames] = useState([]); useEffect(() => { // Check for API Key @@ -31,11 +33,7 @@ export default function Home() { return; } - // Check for saved game - const savedGame = localStorage.getItem("chess_tutor_save"); - if (savedGame) { - setHasSavedGame(true); - } + setSavedGames(loadSavedGames()); setMounted(true); }, [router]); @@ -51,6 +49,7 @@ export default function Home() { : options.color; setGameProps({ + gameId: crypto.randomUUID ? crypto.randomUUID() : `game-${Date.now()}`, initialFen: options.fen, initialPgn: options.pgn, initialPersonality: options.personality, @@ -59,31 +58,25 @@ export default function Home() { setView('game'); }; - const handleResumeGame = () => { - const savedGame = localStorage.getItem("chess_tutor_save"); - if (savedGame) { - try { - const data = JSON.parse(savedGame); - if (data.fen && data.selectedPersonality) { - setGameProps({ - initialFen: data.fen, - initialPgn: data.pgn, - initialPersonality: data.selectedPersonality, - initialColor: data.playerColor || 'white' - }); - setView('game'); - } - } catch (e) { - console.error("Failed to resume game:", e); - } - } + const handleResumeGame = (game: SavedGame) => { + setGameProps({ + gameId: game.id, + initialFen: game.fen, + initialPgn: game.pgn, + initialPersonality: game.selectedPersonality, + initialColor: game.playerColor || 'white' + }); + setView('game'); }; const handleBackToMenu = () => { setView('start'); - // Re-check saved game status as it might have changed - const savedGame = localStorage.getItem("chess_tutor_save"); - setHasSavedGame(!!savedGame); + setSavedGames(loadSavedGames()); + }; + + const handleDeleteSavedGame = (id: string) => { + deleteSavedGame(id); + setSavedGames(loadSavedGames()); }; if (!mounted) return null; @@ -94,11 +87,13 @@ export default function Home() { )} {view === 'game' && gameProps && ( { await act(async () => { render( {}} @@ -94,6 +95,7 @@ describe("ChessGame Component", () => { const Tutor = require('./Tutor').Tutor; render( {}} diff --git a/src/components/ChessGame.tsx b/src/components/ChessGame.tsx index e92f255..9772a7d 100644 --- a/src/components/ChessGame.tsx +++ b/src/components/ChessGame.tsx @@ -16,8 +16,10 @@ import { GameOverModal, MoveHistoryItem } from "./GameOverModal"; import { Brain, ArrowLeft } from "lucide-react"; import { CapturedPieces } from "./CapturedPieces"; import { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection"; +import { upsertSavedGame } from "@/lib/savedGames"; interface ChessGameProps { + gameId: string; initialFen?: string; initialPgn?: string; initialPersonality: Personality; @@ -34,7 +36,7 @@ const PIECE_VALUES: Record = { 'k': 0 }; -export default function ChessGame({ initialFen, initialPgn, initialPersonality, initialColor, onBack }: ChessGameProps) { +export default function ChessGame({ gameId, initialFen, initialPgn, initialPersonality, initialColor, onBack }: ChessGameProps) { const gameRef = useRef(new Chess(initialFen || "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")); const [fen, setFen] = useState(gameRef.current.fen()); const [stockfish, setStockfish] = useState(null); @@ -156,15 +158,24 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, // Save Game State on Change useEffect(() => { const saveData = { + id: gameId, fen, language, selectedPersonality, apiKey, playerColor, // Save player color too - pgn: gameRef.current.pgn() + pgn: gameRef.current.pgn(), + updatedAt: Date.now(), + evaluation: evalP0 ? { + score: evalP0.score, + mate: evalP0.mate, + depth: evalP0.depth + } : null }; + + upsertSavedGame(saveData); localStorage.setItem("chess_tutor_save", JSON.stringify(saveData)); - }, [fen, language, selectedPersonality, apiKey, playerColor]); + }, [fen, language, selectedPersonality, apiKey, playerColor, gameId, evalP0]); // Game Over Detection useEffect(() => { diff --git a/src/components/StartScreen.tsx b/src/components/StartScreen.tsx index 89b2a54..1794786 100644 --- a/src/components/StartScreen.tsx +++ b/src/components/StartScreen.tsx @@ -1,13 +1,15 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import { Settings, ChevronDown, ChevronUp, Brain } from "lucide-react"; +import { Settings, ChevronDown, ChevronUp, Brain, Trash2 } from "lucide-react"; import { Personality, PERSONALITIES } from "@/lib/personalities"; import { useTranslation } from "@/lib/i18n/useTranslation"; import { SupportedLanguage } from "@/lib/i18n/translations"; import { detectChessFormat, ChessFormat } from "@/lib/chessFormatDetector"; import Header from "./Header"; +import { SavedGame } from "@/lib/savedGames"; +import { Chessboard } from "react-chessboard"; interface StartScreenProps { onStartGame: (options: { @@ -16,11 +18,12 @@ interface StartScreenProps { fen?: string; pgn?: string; }) => void; - onResumeGame: () => void; - hasSavedGame: boolean; + onResumeGame: (game: SavedGame) => void; + savedGames: SavedGame[]; + onDeleteSavedGame: (id: string) => void; } -export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: StartScreenProps) { +export default function StartScreen({ onStartGame, onResumeGame, savedGames, onDeleteSavedGame }: StartScreenProps) { const router = useRouter(); const [language, setLanguage] = useState('en'); const [showNewGameOptions, setShowNewGameOptions] = useState(false); @@ -29,6 +32,7 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: const [colorSelection, setColorSelection] = useState<'white' | 'black' | 'random'>('white'); const [showAdvanced, setShowAdvanced] = useState(false); const [mounted, setMounted] = useState(false); + const hasSavedGames = savedGames.length > 0; useEffect(() => { const storedLang = localStorage.getItem("chess_tutor_language"); @@ -56,6 +60,37 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: }); }; + const sortedSavedGames = useMemo( + () => [...savedGames].sort((a, b) => b.updatedAt - a.updatedAt), + [savedGames] + ); + + const formatEvaluation = (game: SavedGame) => { + if (!game.evaluation) return t.start.noEvaluation; + + if (game.evaluation.mate !== null && game.evaluation.mate !== undefined) { + const movesToMate = Math.abs(game.evaluation.mate); + const side = game.evaluation.mate > 0 ? t.game.white : t.game.black; + return `${side} #${movesToMate}`; + } + + if (typeof game.evaluation.score === 'number') { + const score = game.playerColor === 'black' + ? -(game.evaluation.score || 0) + : (game.evaluation.score || 0); + const display = (score / 100).toFixed(2); + return `${score >= 0 ? '+' : ''}${display}`; + } + + return t.start.noEvaluation; + }; + + useEffect(() => { + if (!hasSavedGames) { + setShowNewGameOptions(true); + } + }, [hasSavedGames]); + if (!mounted) return null; return ( @@ -82,32 +117,84 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: {t.start.startGame} -
- {/* Resume Option */} - {hasSavedGame && !showNewGameOptions && ( +
+ {hasSavedGames && (
- -
-
- OR -
+
+

+ {t.start.savedGamesTitle} +

+ +
+ + {sortedSavedGames.length === 0 && ( +
+ {t.start.savedGamesEmpty} +
+ )} + +
+ {sortedSavedGames.map(game => ( +
onResumeGame(game)} + className="group relative bg-gray-50 dark:bg-gray-700 p-4 rounded-xl border border-gray-200 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-300 shadow-sm hover:shadow-md transition-all cursor-pointer" + > + + +
+ +
+ +
+
+
+ {game.selectedPersonality.image} +
+
{game.selectedPersonality.name}
+
+ {t.start.opponentLabel}: {game.playerColor === 'white' ? t.game.black : t.game.white} +
+
+
+
+
+
{t.start.evaluationLabel}
+
{formatEvaluation(game)}
+
+
+
+ ))}
-
)} {/* New Game Options */} - {(!hasSavedGame || showNewGameOptions) && ( + {(!hasSavedGames || showNewGameOptions) && (
{/* Color Selection */}
@@ -223,7 +310,7 @@ export default function StartScreen({ onStartGame, onResumeGame, hasSavedGame }: )}
- {hasSavedGame && ( + {hasSavedGames && (