From 528d5a6ae53c1f9b155754623a4c7f7c25069e70 Mon Sep 17 00:00:00 2001 From: TonNom Date: Sat, 17 May 2025 13:35:11 +0200 Subject: [PATCH 01/26] Sauvegarde avant rebase --- src/components/LinearProgressBar.tsx | 2 - src/components/OpeningProgress.tsx | 93 ++++++++ src/components/board/index.tsx | 10 + src/components/board/squareRenderer.tsx | 25 ++ src/data/openings to learn/italian.ts | 87 +++++++ src/pages/opening.tsx | 294 ++++++++++++++++++++++++ src/public/openings/italian-game.png | 1 + src/sections/layout/NavMenu.tsx | 7 +- 8 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 src/components/OpeningProgress.tsx create mode 100644 src/data/openings to learn/italian.ts create mode 100644 src/pages/opening.tsx create mode 100644 src/public/openings/italian-game.png diff --git a/src/components/LinearProgressBar.tsx b/src/components/LinearProgressBar.tsx index 26c35c78..b5be7b5c 100644 --- a/src/components/LinearProgressBar.tsx +++ b/src/components/LinearProgressBar.tsx @@ -9,8 +9,6 @@ import { const LinearProgressBar = ( props: LinearProgressProps & { value: number; label: string } ) => { - if (props.value === 0) return null; - return ( void; +} + +const getStorageKey = (openingKey: string, mode: string) => `${openingKey}-progress-${mode}`; + +const OpeningProgress: React.FC = ({ + total, + openingKey, + mode, + completed, + onReset, +}) => { + const [progress, setProgress] = useState(completed); + const theme = useTheme(); + + useEffect(() => { + setProgress(completed); + }, [completed]); + + // Calcul du pourcentage + const percent = total > 0 ? (progress.length / total) * 100 : 0; + const label = `${progress.length} / ${total}`; + + // Réinitialisation + const handleReset = () => { + localStorage.removeItem(getStorageKey(openingKey, mode)); + setProgress([]); + onReset && onReset(); + }; + + return ( + + + {label} + + + + + + + ); +}; + +export default OpeningProgress; diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index 0b8174b3..c7e065ba 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -22,6 +22,12 @@ import PlayerHeader from "./playerHeader"; import { boardHueAtom, pieceSetAtom } from "./states"; import tinycolor from "tinycolor2"; +export interface TrainingFeedback { + square: string; // ex: 'e4' + icon: string; // chemin de l'icône + alt: string; // texte alternatif +} + export interface Props { id: string; canPlay?: Color | boolean; @@ -34,6 +40,7 @@ export interface Props { showBestMoveArrow?: boolean; showPlayerMoveIconAtom?: PrimitiveAtom; showEvaluationBar?: boolean; + trainingFeedback?: TrainingFeedback; } export default function Board({ @@ -48,6 +55,7 @@ export default function Board({ showBestMoveArrow = false, showPlayerMoveIconAtom, showEvaluationBar = false, + trainingFeedback, }: Props) { const boardRef = useRef(null); const game = useAtomValue(gameAtom); @@ -239,12 +247,14 @@ export default function Board({ clickedSquaresAtom, playableSquaresAtom, showPlayerMoveIconAtom, + trainingFeedback, // nouvelle prop transmise }); }, [ currentPositionAtom, clickedSquaresAtom, playableSquaresAtom, showPlayerMoveIconAtom, + trainingFeedback, ]); const customPieces = useMemo( diff --git a/src/components/board/squareRenderer.tsx b/src/components/board/squareRenderer.tsx index 67cb7673..02cae926 100644 --- a/src/components/board/squareRenderer.tsx +++ b/src/components/board/squareRenderer.tsx @@ -15,6 +15,11 @@ export interface Props { clickedSquaresAtom: PrimitiveAtom; playableSquaresAtom: PrimitiveAtom; showPlayerMoveIconAtom?: PrimitiveAtom; + trainingFeedback?: { + square: string; + icon: string; + alt: string; + }; } export function getSquareRenderer({ @@ -22,6 +27,7 @@ export function getSquareRenderer({ clickedSquaresAtom, playableSquaresAtom, showPlayerMoveIconAtom = atom(false), + trainingFeedback, }: Props) { const squareRenderer = forwardRef( (props, ref) => { @@ -64,6 +70,25 @@ export function getSquareRenderer({ {children} {highlightSquareStyle &&
} {playableSquareStyle &&
} + {/* Affichage de l’icône de feedback training si demandé et sur la bonne case */} + {trainingFeedback && trainingFeedback.square === square && ( + {trainingFeedback.alt} + )} + {/* Aucun affichage de message texte d'erreur ici, seulement l'icône */} {moveClassification && showPlayerMoveIcon && square === toSquare && ( (null); + // Atom Jotai pour l'état du jeu + const [gameAtomInstance] = useState(() => atom(new Chess())); + const [game, setGame] = useAtom(gameAtomInstance); + const { undoMove } = useChessActions(gameAtomInstance); + + // Liste des variantes à apprendre (toutes) + const variations = italianGameVariations; + const selectedVariation = variations[currentVariantIdx] || null; + + // Couleur d'apprentissage (fixe pour la variante) + const learningColor = useMemo(() => { + if (!selectedVariation) return Color.White; + return getLearningColor(selectedVariation); + }, [selectedVariation]); + + // Indique si c'est à l'utilisateur de jouer + const isUserTurn = useMemo(() => { + if (!selectedVariation) return false; + // moveIdx % 2 === 0 => blanc, 1 => noir (si la séquence commence par blanc) + const colorToPlay = moveIdx % 2 === 0 ? Color.White : Color.Black; + return colorToPlay === learningColor; + }, [moveIdx, learningColor, selectedVariation]); + + // Génération du coup attendu au format UCI pour la flèche (uniquement si c'est à l'utilisateur de jouer) + const bestMoveUci = useMemo(() => { + if ( + selectedVariation && + game && + moveIdx < selectedVariation.moves.length && + isUserTurn + ) { + const chess = new Chess(game.fen()); + const san = selectedVariation.moves[moveIdx]; + const moves = chess.moves({ verbose: true }); + const moveObj = moves.find((m) => m.san === san); + if (moveObj) { + return moveObj.from + moveObj.to + (moveObj.promotion ? moveObj.promotion : ""); + } + } + return undefined; + }, [selectedVariation, game, moveIdx, isUserTurn]); + + // Atom writable pour currentPosition (lecture/écriture) + const currentPositionAtom = useMemo( + () => + atom({ + lastEval: bestMoveUci + ? { + bestMove: bestMoveUci, + lines: [ + { + pv: [bestMoveUci], + depth: 10, + multiPv: 1, + }, + ], + } + : { lines: [] }, + eval: { + moveClassification: undefined, + lines: [], + }, + }), + [bestMoveUci] + ); + + // Réinitialisation à chaque variante ou progression + useEffect(() => { + if (!selectedVariation) return; + try { + const chess = new Chess(); + for (let i = 0; i < moveIdx; i++) { + const move = selectedVariation.moves[i]; + const result = chess.move(move); + if (!result) break; // Stop si coup invalide + } + setGame(chess); + } catch (e) { + // Gestion d'erreur : on évite le crash + setGame(new Chess()); + } + }, [selectedVariation, moveIdx, setGame]); + + // Validation du coup utilisateur : si mauvais coup, undo et annotation + useEffect(() => { + if (!selectedVariation || !game) return; + if (moveIdx >= selectedVariation.moves.length) return; + if (!isUserTurn) return; // On ne valide que les coups utilisateur + try { + const history = game.history({ verbose: true }); + if (history.length !== moveIdx + 1) return; + const last = history[history.length - 1]; + const expectedMove = new Chess(); + for (let i = 0; i < moveIdx; i++) expectedMove.move(selectedVariation.moves[i]); + const expected = expectedMove.move(selectedVariation.moves[moveIdx]); + if (!expected || last.from !== expected.from || last.to !== expected.to) { + // Mauvais coup : annuler et annoter + let mistakeType = "Mistake"; + if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; + setLastMistake({ from: last.from, to: last.to, type: mistakeType }); + setTimeout(() => undoMove(), 350); + } else { + setLastMistake(null); + setMoveIdx((idx) => idx + 1); + } + } catch (e) { + // Gestion d'erreur : on évite le crash + setLastMistake(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [game.history().length, trainingMode, selectedVariation, isUserTurn]); + + // Avance automatique des coups adverses après un coup utilisateur correct + useEffect(() => { + if (!selectedVariation) return; + if (moveIdx >= selectedVariation.moves.length) return; + // Si ce n'est pas à l'utilisateur de jouer, on avance automatiquement les coups adverses + if (!isUserTurn) { + // On joue tous les coups adverses jusqu'au prochain coup utilisateur ou fin de séquence + let nextIdx = moveIdx; + let colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black; + while (nextIdx < selectedVariation.moves.length && colorToPlay !== learningColor) { + nextIdx++; + colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black; + } + if (nextIdx !== moveIdx) { + // Délai augmenté à 500ms pour laisser le temps à l’animation du coup utilisateur + setTimeout(() => setMoveIdx(nextIdx), 500); + } + } + }, [moveIdx, isUserTurn, selectedVariation, learningColor]); + + // Enchaînement automatique des variantes + useEffect(() => { + if (!selectedVariation) return; + if (moveIdx >= selectedVariation.moves.length) { + // Succès : passer à la variante suivante après un court délai + if (currentVariantIdx < variations.length - 1) { + setTimeout(() => { + setCurrentVariantIdx((idx) => idx + 1); + setMoveIdx(0); + setLastMistake(null); + }, 800); + } + } + }, [moveIdx, selectedVariation, currentVariantIdx, variations.length]); + + // Si toutes les variantes sont terminées + const allDone = currentVariantIdx >= variations.length; + + // Gestion de la progression (persistée par mode) + const openingKey = "italian"; + const progressMode = trainingMode ? "training" : "learning"; + const progressStorageKey = `${openingKey}-progress-${progressMode}`; + const [completedVariations, setCompletedVariations] = useState(() => { + if (typeof window === "undefined") return []; + try { + const raw = localStorage.getItem(progressStorageKey); + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } + }); + + // Marquer une variation comme terminée + useEffect(() => { + if (!selectedVariation) return; + if (moveIdx >= selectedVariation.moves.length) { + if (!completedVariations.includes(currentVariantIdx)) { + const updated = [...completedVariations, currentVariantIdx]; + setCompletedVariations(updated); + localStorage.setItem(progressStorageKey, JSON.stringify(updated)); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [moveIdx, selectedVariation, currentVariantIdx, progressMode]); + + // Réinitialisation de la progression + const handleResetProgress = () => { + localStorage.removeItem(progressStorageKey); + setCompletedVariations([]); + }; + + // Détermination de la case cible du dernier coup joué (pour overlay) + const lastMoveSquare = useMemo(() => { + if (!game) return null; + const history = game.history({ verbose: true }); + if (history.length === 0) return null; + const last = history[history.length - 1]; + return last.to; + }, [game]); + + // Détermination du type d’icône à afficher (succès/erreur) + const trainingFeedback = useMemo(() => { + if (!trainingMode || !lastMoveSquare) return null; + // Afficher l'icône uniquement si le dernier coup a été joué par l'humain + // (c'est-à-dire si ce n'est plus à l'humain de jouer) + if (isUserTurn) return null; + if (lastMistake && lastMistake.to === lastMoveSquare) { + return { icon: "/icons/mistake.png", alt: "Coup incorrect" }; + } + if (lastMistake === null && game.history().length > 0) { + // Remplacer l'icône de validation par book.png + return { icon: "/icons/book.png", alt: "Coup correct" }; + } + return null; + }, [trainingMode, lastMistake, lastMoveSquare, game, isUserTurn]); + + // Affichage principal + return ( + + + {/* Zone de gauche : contrôles et explications */} + + {/* Titre de la variante */} + + {selectedVariation?.name} + + {/* Espace aéré avant les boutons */} + + + + + + {moveIdx >= selectedVariation.moves.length ? ( + Variation complete! Next variation loading… + ) : trainingMode ? ( + Play the correct move to continue. Mistakes will be marked. + ) : ( + Play the move indicated by the arrow to continue. + )} + + {/* Barre de progression en bas à gauche, toujours visible */} + + + + + {/* Zone de droite : échiquier responsive */} + + {selectedVariation && !allDone && game && ( + + + + )} + + + + ); +} diff --git a/src/public/openings/italian-game.png b/src/public/openings/italian-game.png new file mode 100644 index 00000000..c3313f2e --- /dev/null +++ b/src/public/openings/italian-game.png @@ -0,0 +1 @@ +// Illustration d'ouverture Italienne pour la page Opening diff --git a/src/sections/layout/NavMenu.tsx b/src/sections/layout/NavMenu.tsx index 337b2299..c669c0fc 100644 --- a/src/sections/layout/NavMenu.tsx +++ b/src/sections/layout/NavMenu.tsx @@ -1,4 +1,4 @@ -import NavLink from "@/components/NavLink"; +import NavLink from "../../components/NavLink"; import { Icon } from "@iconify/react"; import { Box, @@ -19,6 +19,11 @@ const MenuOptions = [ icon: "streamline:database", href: "/database", }, + { + text: "Opening", + icon: "mdi:book-open-variant", + href: "/opening", + }, ]; interface Props { From 7e55a898d720ee993c314205910cb24cf686efe4 Mon Sep 17 00:00:00 2001 From: TonNom Date: Sun, 18 May 2025 11:28:28 +0200 Subject: [PATCH 02/26] Italian variations enhanced --- src/data/openings to learn/italian.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/data/openings to learn/italian.ts b/src/data/openings to learn/italian.ts index 14d838ff..e89bbfe6 100644 --- a/src/data/openings to learn/italian.ts +++ b/src/data/openings to learn/italian.ts @@ -13,12 +13,12 @@ export const italianGameVariations: Variation[] = [ moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d3", "d6", "O-O", "O-O"], }, { - name: "Evans Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "b4", "Bxb4", "c3", "Ba5", "d4", "exd4", "O-O"], + name: "Evans Gambit Accepted", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "b4", "Bxb4", "c3", "Ba5", "d4", "d6", "Qb3"], }, { name: "Two Knights Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6"], + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d5", "exd5", "e5", "d5", "Bb5", "Nxe4", "d4"], }, { name: "Fried Liver Attack", @@ -26,23 +26,19 @@ export const italianGameVariations: Variation[] = [ }, { name: "Traxler Counterattack", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "Bc5"], + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "Bc5", "Nxf7", "Bxf2+",], }, { - name: "Lolli Attack", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "d5", "exd5", "Nxd5", "d4"], + name: "Scotch Gambit Accepted", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qa5", "Nxe4", "Be7", "Bg5", "O-O", "Bxe7", "Nxe7", "Nxd4", "Qb6"], }, { - name: "Scotch Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4"], - }, - { - name: "Hungarian Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Be7"], + name: "Benima Defense", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Be7", "d4", "exd4", "Nxd4", "Nf6", "Nxc6", "bxc6", "e5", "Nd5", "Qg4", "O-O", "Bh6", "Bf6"], }, { name: "Paris Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "d6"], + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "d6", "d4", "exd4", "Nxd4", "Nf6", "O-O", "Nxd4", "Qxd4", "Be7", "Nc3", "O-O", "Bf4"], }, { name: "Rousseau Gambit", From dff2d0da04f4ef85068802e6275a162caeaac59e Mon Sep 17 00:00:00 2001 From: TonNom Date: Sun, 18 May 2025 22:19:34 +0200 Subject: [PATCH 03/26] Italian variations --- src/data/openings to learn/italian.ts | 90 ++++++++++++++++----------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/src/data/openings to learn/italian.ts b/src/data/openings to learn/italian.ts index e89bbfe6..0dbc24e9 100644 --- a/src/data/openings to learn/italian.ts +++ b/src/data/openings to learn/italian.ts @@ -9,75 +9,91 @@ export interface Variation { export const italianGameVariations: Variation[] = [ { - name: "Giuoco Piano", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d3", "d6", "O-O", "O-O"], + name: "Italian Game Line 1", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "h6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nf6", "e5", "Ne4", "O-O", "Nxc3", "bxc3", "Bxc3", "Qb3", "Bxa1", "Bxf7+", "Kf8", "Ba3+", "d6", "exd6", "cxd6", "Bg6", "Qf6", "Bxd6+", "Ne7", "Re1"], }, { - name: "Evans Gambit Accepted", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "b4", "Bxb4", "c3", "Ba5", "d4", "d6", "Qb3"], + name: "Italian Game Line 2", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "Nxe4", "dxe5", "Bc5", "Qd5", "Bxf2+", "Kf1", "O-O", "Qxe4"], }, { - name: "Two Knights Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d5", "exd5", "e5", "d5", "Bb5", "Nxe4", "d4"], + name: "Italian Game Line 3", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb6", "e5", "Ng4", "h3", "Nh6", "d5", "Na5", "Bg5", "f6", "exf6"], }, { - name: "Fried Liver Attack", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "d5", "exd5", "Nxd5", "Nxf7", "Kxf7", "Qf3+", "Ke6", "Nc3"], + name: "Italian Game Line 4", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qa5", "Nxe4", "Be6", "Neg5", "O-O-O", "Nxe6", "fxe6", "Rxe6", "Bd6", "Bg5", "Rde8", "Qe2"], }, { - name: "Traxler Counterattack", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "Bc5", "Nxf7", "Bxf2+",], + name: "Italian Game Line 5", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qh5", "Nxe4", "Be6", "Bg5", "Bd6", "Nxd6+", "cxd6", "Bf4", "Qd5", "c3"], }, { - name: "Scotch Gambit Accepted", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qa5", "Nxe4", "Be7", "Bg5", "O-O", "Bxe7", "Nxe7", "Nxd4", "Qb6"], + name: "Italian Game Line 6", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Ne5", "bxc3", "Nxc4", "Qd4", "O-O", "Qxc4", "Nd6", "Qb3"], }, { - name: "Benima Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Be7", "d4", "exd4", "Nxd4", "Nf6", "Nxc6", "bxc6", "e5", "Nd5", "Qg4", "O-O", "Bh6", "Bf6"], + name: "Italian Game Line 7", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Nxc3", "bxc3", "Bxc3", "Ba3", "d6", "Rc1", "Ba5", "Qa4", "O-O", "d5", "Ne5", "Nxe5", "dxe5", "Qxa5"], }, { - name: "Paris Defense", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "d6", "d4", "exd4", "Nxd4", "Nf6", "O-O", "Nxd4", "Qxd4", "Be7", "Nc3", "O-O", "Bf4"], + name: "Italian Game Line 8", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Nxc3", "bxc3", "Bxc3", "Ba3", "d6", "Rc1", "Bb4", "Bxb4", "Nxb4", "Qe1+", "Qe7", "Qxb4"], }, { - name: "Rousseau Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "f5"], + name: "Italian Game Line 9", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Bf6", "Re1", "Ne7", "Rxe4", "d6", "Bg5", "Bxg5", "Nxg5", "O-O", "Nxh7", "Kxh7", "Qh5+", "Kg8", "Rh4"], }, { - name: "Blackburne Shilling Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nd4"], + name: "Italian Game Line 10", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Bf6", "Re1", "Ne7", "Rxe4", "d6", "Bg5", "Bxg5", "Nxg5", "h6", "Bb5+", "c6", "Nxf7", "Kxf7", "Qf3+", "Kg8", "Rae1", "cxb5", "Rxe7"], }, { - name: "Jerome Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "Bxf7+", "Kxf7", "Nxe5+", "Nxe5", "Qh5+"], + name: "Italian Game Line 11", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Ne5", "bxc3", "Nxc4", "Qd4", "Ncd6", "Qxg7", "Qf6", "Qxf6", "Nxf6", "Re1+", "Kf8", "Bh6+", "Kg8", "Re5", "Nde4", "Nd2", "d6", "Nxe4", "Nxe4", "Re8#"], }, { - name: "Rosentreter Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "d4", "exd4", "c3"], + name: "Italian Game Line 12", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Ne5", "bxc3", "Nxc4", "Qd4", "Ncd6", "Qxg7", "Qf6", "Qxf6", "Nxf6", "Re1+", "Kf8", "Bh6+", "Kg8", "Re5", "Nfe4", "Re1", "f6", "Re7", "Nf5", "Re8+", "Kf7", "Rxh8", "Nxh6", "Rxe4"], }, { - name: "Neumann Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "c3", "Nf6", "Bc4"], + name: "Italian Game Line 13", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "d6", "d5"], }, { - name: "Alexandre Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "f5"], + name: "Italian Game Line 14", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "h6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "d6", "O-O"], }, { - name: "Lucchini Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "d3", "f5"], + name: "Italian Game Line 15", + moves: ["e4", "e5", "Nf3", "d6", "d4", "exd4", "Nxd4", "Nf6", "Nc3", "Be7", "Bf4", "O-O", "Qd2", "a6", "O-O-O"], }, { - name: "Ponziani-Steinitz Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "Nxe4"], + name: "Italian Game Line 16", + moves: ["e4", "e5", "Nf3", "d6", "d4", "exd4", "Nxd4", "Be7", "Nc3", "Nf6", "Bf4", "O-O", "Qd2", "a6", "O-O-O"], }, { - name: "Kloss Gambit", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "d5", "exd5", "Nb4"], + name: "Italian Game Line 17", + moves: ["e4", "e5", "Nf3", "d6", "d4", "Nc6", "Bb5", "exd4", "Qxd4", "Bd7", "Bxc6", "Bxc6", "Nc3", "Nf6", "Bg5", "Be7", "O-O-O"], }, { - name: "Fegatello Attack", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "Ng5", "d5", "exd5", "Nxd5", "Nxf7", "Kxf7", "Qf3+", "Ke6", "Nc3"], + name: "Italian Game Line 18", + moves: ["e4", "e5", "Nf3", "d6", "d4", "Nc6", "Bb5", "Bd7", "Nc3", "exd4", "Nxd4", "Nxd4", "Bxd7+", "Qxd7", "Qxd4", "Nf6", "Bg5", "Be7", "O-O-O"], }, -]; + { + name: "Italian Game Line 19", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qd8", "Rxe4+", "Be7", "Nxd4", "O-O", "Nxc6", "bxc6", "Qxd8", "Bxd8", "Rc4"], + }, + { + name: "Italian Game Line 20", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "exd4", "O-O", "Nxe4", "Re1", "d5", "Bxd5", "Qxd5", "Nc3", "Qd8", "Rxe4+", "Be7", "Nxd4", "O-O", "Nxc6", "Qxd1+", "Nxd1", "bxc6", "Rxe7"], + }, + { + name: "Italian Game Line 21", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Bf6", "Re1", "Ne7", "Rxe4", "d6", "Bg5", "Bxg5", "Nxg5", "h6", "Bb5+", "Bd7", "Qe2", "hxg5", "Re1", "O-O", "Rxe7", "Bxb5", "Qxb5"], + }, + { + name: "Italian Game Line 22", + moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "Nf6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "Nxe4", "O-O", "Bxc3", "d5", "Bf6", "Re1", "Ne7", "Rxe4", "d6", "Bg5", "Bxg5", "Nxg5", "h6", "Bb5+", "Kf8", "Qh5", "g6", "Qf3", "hxg5", "Qf6", "Rh4", "Rxh4", "gxh4", "Re1", "Bd7", "Rxe7", "Qxe7", "Qh8#"], + }, +]; \ No newline at end of file From a36035c3227dcb6aeb17b41dcefbd62a4b97453e Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:04:59 +0200 Subject: [PATCH 04/26] small icon to indicate incorrect moves --- src/pages/opening.tsx | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index 919800ff..cff88afa 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -21,6 +21,7 @@ export default function OpeningPage() { const [moveIdx, setMoveIdx] = useState(0); const [trainingMode, setTrainingMode] = useState(false); const [lastMistake, setLastMistake] = useState(null); + const [lastMistakeVisible, setLastMistakeVisible] = useState(null); // Atom Jotai pour l'état du jeu const [gameAtomInstance] = useState(() => atom(new Chess())); const [game, setGame] = useAtom(gameAtomInstance); @@ -109,6 +110,8 @@ export default function OpeningPage() { if (!selectedVariation || !game) return; if (moveIdx >= selectedVariation.moves.length) return; if (!isUserTurn) return; // On ne valide que les coups utilisateur + let mistakeTimeout: NodeJS.Timeout | null = null; + let undoTimeout: NodeJS.Timeout | null = null; try { const history = game.history({ verbose: true }); if (history.length !== moveIdx + 1) return; @@ -117,19 +120,31 @@ export default function OpeningPage() { for (let i = 0; i < moveIdx; i++) expectedMove.move(selectedVariation.moves[i]); const expected = expectedMove.move(selectedVariation.moves[moveIdx]); if (!expected || last.from !== expected.from || last.to !== expected.to) { - // Mauvais coup : annuler et annoter + // Mauvais coup : attendre 200ms avant d'afficher l'icône d'erreur, puis undo après 1,5s let mistakeType = "Mistake"; if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; setLastMistake({ from: last.from, to: last.to, type: mistakeType }); - setTimeout(() => undoMove(), 350); + mistakeTimeout = setTimeout(() => { + setLastMistakeVisible({ from: last.from, to: last.to, type: mistakeType }); + }, 200); + undoTimeout = setTimeout(() => { + setLastMistake(null); + setLastMistakeVisible(null); + undoMove(); + }, 1500); } else { setLastMistake(null); + setLastMistakeVisible(null); setMoveIdx((idx) => idx + 1); } } catch (e) { - // Gestion d'erreur : on évite le crash setLastMistake(null); + setLastMistakeVisible(null); } + return () => { + if (mistakeTimeout) clearTimeout(mistakeTimeout); + if (undoTimeout) clearTimeout(undoTimeout); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.history().length, trainingMode, selectedVariation, isUserTurn]); @@ -215,19 +230,14 @@ export default function OpeningPage() { // Détermination du type d’icône à afficher (succès/erreur) const trainingFeedback = useMemo(() => { - if (!trainingMode || !lastMoveSquare) return null; - // Afficher l'icône uniquement si le dernier coup a été joué par l'humain - // (c'est-à-dire si ce n'est plus à l'humain de jouer) - if (isUserTurn) return null; - if (lastMistake && lastMistake.to === lastMoveSquare) { - return { icon: "/icons/mistake.png", alt: "Coup incorrect" }; + if (!trainingMode || !lastMoveSquare) return undefined; + // Afficher l'icône de croix rouge uniquement si le dernier coup a été mal joué par l'humain + if (lastMistakeVisible && lastMistakeVisible.to === lastMoveSquare) { + return { square: lastMoveSquare, icon: "/icons/mistake.png", alt: "Coup incorrect" }; } - if (lastMistake === null && game.history().length > 0) { - // Remplacer l'icône de validation par book.png - return { icon: "/icons/book.png", alt: "Coup correct" }; - } - return null; - }, [trainingMode, lastMistake, lastMoveSquare, game, isUserTurn]); + // Ne rien afficher si le coup est correct + return undefined; + }, [trainingMode, lastMistakeVisible, lastMoveSquare]); // Affichage principal return ( @@ -283,7 +293,7 @@ export default function OpeningPage() { currentPositionAtom={currentPositionAtom} boardOrientation={learningColor} // Nouvelle prop pour feedback visuel sur la case - trainingFeedback={trainingFeedback && lastMoveSquare ? { square: lastMoveSquare, ...trainingFeedback } : undefined} + trainingFeedback={trainingFeedback} /> )} From 89ea0f17417cd472a4a4559ad04657bc098d8929 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:08:14 +0200 Subject: [PATCH 05/26] the reset button is now reseting the board --- src/pages/opening.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index cff88afa..ff3b6328 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -217,6 +217,11 @@ export default function OpeningPage() { const handleResetProgress = () => { localStorage.removeItem(progressStorageKey); setCompletedVariations([]); + setCurrentVariantIdx(0); + setMoveIdx(0); + setLastMistake(null); + setLastMistakeVisible(null); + setGame(new Chess()); }; // Détermination de la case cible du dernier coup joué (pour overlay) From d31dbe793f53876d6be1888dbeebf55897295599 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:12:34 +0200 Subject: [PATCH 06/26] Quick fix / enhancement --- src/components/OpeningProgress.tsx | 2 +- src/pages/opening.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/OpeningProgress.tsx b/src/components/OpeningProgress.tsx index 311f42d9..6e69f0de 100644 --- a/src/components/OpeningProgress.tsx +++ b/src/components/OpeningProgress.tsx @@ -84,7 +84,7 @@ const OpeningProgress: React.FC = ({ }} onClick={handleReset} > - Réinitialiser + Reset Progress ); diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index ff3b6328..3ebdb3c7 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -235,14 +235,14 @@ export default function OpeningPage() { // Détermination du type d’icône à afficher (succès/erreur) const trainingFeedback = useMemo(() => { - if (!trainingMode || !lastMoveSquare) return undefined; - // Afficher l'icône de croix rouge uniquement si le dernier coup a été mal joué par l'humain + if (!lastMoveSquare) return undefined; + // Afficher l'icône de croix rouge si le dernier coup a été mal joué par l'humain if (lastMistakeVisible && lastMistakeVisible.to === lastMoveSquare) { return { square: lastMoveSquare, icon: "/icons/mistake.png", alt: "Coup incorrect" }; } // Ne rien afficher si le coup est correct return undefined; - }, [trainingMode, lastMistakeVisible, lastMoveSquare]); + }, [lastMistakeVisible, lastMoveSquare]); // Affichage principal return ( From aad8c43502e6e5be70d533ea2214b8f7674cc5b0 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:20:19 +0200 Subject: [PATCH 07/26] Quick fix / resizing the board --- src/pages/opening.tsx | 118 ++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index 3ebdb3c7..0f38a630 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -9,6 +9,8 @@ import { Color } from "../types/enums"; import { CurrentPosition } from "../types/eval"; import type { Variation } from "../data/openings to learn/italian"; import OpeningProgress from "../components/OpeningProgress"; +import { Grid2 as Grid } from "@mui/material"; +import { useScreenSize } from "../hooks/useScreenSize"; // Détermine la couleur d'apprentissage pour la variante (par défaut blanc, mais extensible) function getLearningColor(variation: Variation): Color { @@ -244,66 +246,72 @@ export default function OpeningPage() { return undefined; }, [lastMistakeVisible, lastMoveSquare]); + const screenSize = useScreenSize(); + const boardSize = useMemo(() => { + const width = screenSize.width; + const height = screenSize.height; + if (typeof window !== "undefined" && window.innerWidth < 900) { + return Math.min(width, height - 150); + } + return Math.min(width - 300, height * 0.83); + }, [screenSize]); + // Affichage principal return ( - - - {/* Zone de gauche : contrôles et explications */} - - {/* Titre de la variante */} - + + + {/* Conteneur centré pour le titre et les boutons */} + + {selectedVariation?.name} - {/* Espace aéré avant les boutons */} - - - - - - {moveIdx >= selectedVariation.moves.length ? ( - Variation complete! Next variation loading… - ) : trainingMode ? ( - Play the correct move to continue. Mistakes will be marked. - ) : ( - Play the move indicated by the arrow to continue. - )} - - {/* Barre de progression en bas à gauche, toujours visible */} - - - - - {/* Zone de droite : échiquier responsive */} - - {selectedVariation && !allDone && game && ( - - - + + + + + {moveIdx >= selectedVariation.moves.length ? ( + Variation complete! Next variation loading… + ) : trainingMode ? ( + Play the correct move to continue. Mistakes will be marked. + ) : ( + Play the move indicated by the arrow to continue. )} - - + {/* Barre de progression en bas à gauche, toujours visible */} + + + + + {/* Zone de droite : échiquier responsive */} + + {selectedVariation && !allDone && game && ( + + + + )} + + ); } From 7785bcdafcdbac09c21c298d5c5ee5c901819232 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:27:19 +0200 Subject: [PATCH 08/26] Quick fix / vertical scroll fixed --- src/pages/_app.tsx | 1 + src/pages/global-fix.css | 8 ++++++++ src/pages/opening.tsx | 23 +++++++++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/pages/global-fix.css diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f863fb7d..04c7e02b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,6 +5,7 @@ import "@fontsource/roboto/700.css"; import { AppProps } from "next/app"; import Layout from "@/sections/layout"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import "./global-fix.css"; const queryClient = new QueryClient(); diff --git a/src/pages/global-fix.css b/src/pages/global-fix.css new file mode 100644 index 00000000..082d75b4 --- /dev/null +++ b/src/pages/global-fix.css @@ -0,0 +1,8 @@ +html, body { + width: 100vw; + min-height: 100vh; + margin: 0; + padding: 0; + overflow-x: hidden; + box-sizing: border-box; +} diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index 0f38a630..7e1d8131 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -258,7 +258,16 @@ export default function OpeningPage() { // Affichage principal return ( - + {/* Conteneur centré pour le titre et les boutons */} @@ -295,7 +304,17 @@ export default function OpeningPage() { {/* Zone de droite : échiquier responsive */} {selectedVariation && !allDone && game && ( - + Date: Mon, 19 May 2025 19:33:29 +0200 Subject: [PATCH 09/26] Quick fix : board position --- src/pages/opening.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index 7e1d8131..cc421a60 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -302,7 +302,7 @@ export default function OpeningPage() { {/* Zone de droite : échiquier responsive */} - + {selectedVariation && !allDone && game && ( Date: Mon, 19 May 2025 19:39:07 +0200 Subject: [PATCH 10/26] comments translated in English --- src/pages/opening.tsx | 107 +++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index cc421a60..af3a0882 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -12,9 +12,9 @@ import OpeningProgress from "../components/OpeningProgress"; import { Grid2 as Grid } from "@mui/material"; import { useScreenSize } from "../hooks/useScreenSize"; -// Détermine la couleur d'apprentissage pour la variante (par défaut blanc, mais extensible) +// Determine the learning color for the variation (default white, but extensible) function getLearningColor(variation: Variation): Color { - // TODO: utiliser variation.color si défini, sinon blanc + // TODO: use variation.color if defined, otherwise white return Color.White; } @@ -24,30 +24,30 @@ export default function OpeningPage() { const [trainingMode, setTrainingMode] = useState(false); const [lastMistake, setLastMistake] = useState(null); const [lastMistakeVisible, setLastMistakeVisible] = useState(null); - // Atom Jotai pour l'état du jeu + // Atom Jotai for game state const [gameAtomInstance] = useState(() => atom(new Chess())); const [game, setGame] = useAtom(gameAtomInstance); const { undoMove } = useChessActions(gameAtomInstance); - // Liste des variantes à apprendre (toutes) + // List of variations to learn (all) const variations = italianGameVariations; const selectedVariation = variations[currentVariantIdx] || null; - // Couleur d'apprentissage (fixe pour la variante) + // Learning color (fixed for the variation) const learningColor = useMemo(() => { if (!selectedVariation) return Color.White; return getLearningColor(selectedVariation); }, [selectedVariation]); - // Indique si c'est à l'utilisateur de jouer + // Indicates if it's the user's turn to play const isUserTurn = useMemo(() => { if (!selectedVariation) return false; - // moveIdx % 2 === 0 => blanc, 1 => noir (si la séquence commence par blanc) + // moveIdx % 2 === 0 => white, 1 => black (if the sequence starts with white) const colorToPlay = moveIdx % 2 === 0 ? Color.White : Color.Black; return colorToPlay === learningColor; }, [moveIdx, learningColor, selectedVariation]); - // Génération du coup attendu au format UCI pour la flèche (uniquement si c'est à l'utilisateur de jouer) + // Generate the expected move in UCI format for the arrow (only if it's the user's turn to play) const bestMoveUci = useMemo(() => { if ( selectedVariation && @@ -66,7 +66,7 @@ export default function OpeningPage() { return undefined; }, [selectedVariation, game, moveIdx, isUserTurn]); - // Atom writable pour currentPosition (lecture/écriture) + // Writable atom for currentPosition (read/write) const currentPositionAtom = useMemo( () => atom({ @@ -90,7 +90,7 @@ export default function OpeningPage() { [bestMoveUci] ); - // Réinitialisation à chaque variante ou progression + // Reset on each variation or progression useEffect(() => { if (!selectedVariation) return; try { @@ -98,20 +98,20 @@ export default function OpeningPage() { for (let i = 0; i < moveIdx; i++) { const move = selectedVariation.moves[i]; const result = chess.move(move); - if (!result) break; // Stop si coup invalide + if (!result) break; // Stop if invalid move } setGame(chess); } catch (e) { - // Gestion d'erreur : on évite le crash + // Error handling: avoid crash setGame(new Chess()); } }, [selectedVariation, moveIdx, setGame]); - // Validation du coup utilisateur : si mauvais coup, undo et annotation + // Validate user move: if wrong move, undo and annotate useEffect(() => { if (!selectedVariation || !game) return; if (moveIdx >= selectedVariation.moves.length) return; - if (!isUserTurn) return; // On ne valide que les coups utilisateur + if (!isUserTurn) return; // Only validate user moves let mistakeTimeout: NodeJS.Timeout | null = null; let undoTimeout: NodeJS.Timeout | null = null; try { @@ -122,7 +122,7 @@ export default function OpeningPage() { for (let i = 0; i < moveIdx; i++) expectedMove.move(selectedVariation.moves[i]); const expected = expectedMove.move(selectedVariation.moves[moveIdx]); if (!expected || last.from !== expected.from || last.to !== expected.to) { - // Mauvais coup : attendre 200ms avant d'afficher l'icône d'erreur, puis undo après 1,5s + // Wrong move: wait 200ms before showing error icon, then undo after 1.5s let mistakeType = "Mistake"; if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; setLastMistake({ from: last.from, to: last.to, type: mistakeType }); @@ -150,13 +150,13 @@ export default function OpeningPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.history().length, trainingMode, selectedVariation, isUserTurn]); - // Avance automatique des coups adverses après un coup utilisateur correct + // Automatically advance opponent moves after a correct user move useEffect(() => { if (!selectedVariation) return; if (moveIdx >= selectedVariation.moves.length) return; - // Si ce n'est pas à l'utilisateur de jouer, on avance automatiquement les coups adverses + // If it's not the user's turn, automatically advance opponent moves if (!isUserTurn) { - // On joue tous les coups adverses jusqu'au prochain coup utilisateur ou fin de séquence + // Play all opponent moves until the next user move or end of sequence let nextIdx = moveIdx; let colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black; while (nextIdx < selectedVariation.moves.length && colorToPlay !== learningColor) { @@ -164,17 +164,17 @@ export default function OpeningPage() { colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black; } if (nextIdx !== moveIdx) { - // Délai augmenté à 500ms pour laisser le temps à l’animation du coup utilisateur + // Delay increased to 500ms to allow time for user move animation setTimeout(() => setMoveIdx(nextIdx), 500); } } }, [moveIdx, isUserTurn, selectedVariation, learningColor]); - // Enchaînement automatique des variantes + // Automatically chain variations useEffect(() => { if (!selectedVariation) return; if (moveIdx >= selectedVariation.moves.length) { - // Succès : passer à la variante suivante après un court délai + // Success: move to the next variation after a short delay if (currentVariantIdx < variations.length - 1) { setTimeout(() => { setCurrentVariantIdx((idx) => idx + 1); @@ -185,10 +185,10 @@ export default function OpeningPage() { } }, [moveIdx, selectedVariation, currentVariantIdx, variations.length]); - // Si toutes les variantes sont terminées + // If all variations are completed const allDone = currentVariantIdx >= variations.length; - // Gestion de la progression (persistée par mode) + // Progress management (persisted by mode) const openingKey = "italian"; const progressMode = trainingMode ? "training" : "learning"; const progressStorageKey = `${openingKey}-progress-${progressMode}`; @@ -202,7 +202,7 @@ export default function OpeningPage() { } }); - // Marquer une variation comme terminée + // Mark a variation as completed useEffect(() => { if (!selectedVariation) return; if (moveIdx >= selectedVariation.moves.length) { @@ -215,7 +215,7 @@ export default function OpeningPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [moveIdx, selectedVariation, currentVariantIdx, progressMode]); - // Réinitialisation de la progression + // Reset progress const handleResetProgress = () => { localStorage.removeItem(progressStorageKey); setCompletedVariations([]); @@ -226,7 +226,7 @@ export default function OpeningPage() { setGame(new Chess()); }; - // Détermination de la case cible du dernier coup joué (pour overlay) + // Determine the target square of the last move played (for overlay) const lastMoveSquare = useMemo(() => { if (!game) return null; const history = game.history({ verbose: true }); @@ -235,14 +235,14 @@ export default function OpeningPage() { return last.to; }, [game]); - // Détermination du type d’icône à afficher (succès/erreur) + // Determine the type of icon to display (success/error) const trainingFeedback = useMemo(() => { if (!lastMoveSquare) return undefined; - // Afficher l'icône de croix rouge si le dernier coup a été mal joué par l'humain + // Show red cross icon if the last move was incorrectly played by the human if (lastMistakeVisible && lastMistakeVisible.to === lastMoveSquare) { - return { square: lastMoveSquare, icon: "/icons/mistake.png", alt: "Coup incorrect" }; + return { square: lastMoveSquare, icon: "/icons/mistake.png", alt: "Incorrect move" }; } - // Ne rien afficher si le coup est correct + // Show nothing if the move is correct return undefined; }, [lastMistakeVisible, lastMoveSquare]); @@ -256,7 +256,7 @@ export default function OpeningPage() { return Math.min(width - 300, height * 0.83); }, [screenSize]); - // Affichage principal + // Main display return ( - {/* Conteneur centré pour le titre et les boutons */} + {/* Centered container for title and buttons */} {selectedVariation?.name} @@ -290,7 +290,7 @@ export default function OpeningPage() { Play the move indicated by the arrow to continue. )} - {/* Barre de progression en bas à gauche, toujours visible */} + {/* Progress bar at the bottom left, always visible */} - {/* Zone de droite : échiquier responsive */} - + {/* Right area: responsive chessboard, always with right margin */} + {selectedVariation && !allDone && game && ( - + // Responsive square chessboard box + From d0002d2d94de6bbacffc12558f5c2beed01be330 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:46:01 +0200 Subject: [PATCH 11/26] Skip variations button added --- src/components/OpeningProgress.tsx | 18 +-------------- src/pages/opening.tsx | 37 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/components/OpeningProgress.tsx b/src/components/OpeningProgress.tsx index 6e69f0de..075e9d54 100644 --- a/src/components/OpeningProgress.tsx +++ b/src/components/OpeningProgress.tsx @@ -69,23 +69,7 @@ const OpeningProgress: React.FC = ({ - + {/* The reset button has been removed. Reset is now handled in the parent page. */} ); }; diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index af3a0882..77547fb1 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -291,7 +291,7 @@ export default function OpeningPage() { )} {/* Progress bar at the bottom left, always visible */} - + + {/* Action buttons: Skip and Reset, side by side, same style */} + + + + {/* Right area: responsive chessboard, always with right margin */} From 6df2f6f51732bda237df876786cec42d4705ddd1 Mon Sep 17 00:00:00 2001 From: TonNom Date: Mon, 19 May 2025 19:47:57 +0200 Subject: [PATCH 12/26] repositioning of the buttons --- src/pages/opening.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index 77547fb1..14530f86 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -268,17 +268,17 @@ export default function OpeningPage() { boxSizing: 'border-box', overflowX: 'hidden', // avoid horizontal scroll }}> - + {/* Centered container for title and buttons */} - + {selectedVariation?.name} - - @@ -291,16 +291,15 @@ export default function OpeningPage() { )} {/* Progress bar at the bottom left, always visible */} - + {/* Action buttons: Skip and Reset, side by side, same style */} - + + + +); + +export default OpeningControls; diff --git a/src/components/VariationHeader.tsx b/src/components/VariationHeader.tsx new file mode 100644 index 00000000..61ef310e --- /dev/null +++ b/src/components/VariationHeader.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Typography, Stack, Button } from "@mui/material"; + +interface VariationHeaderProps { + variationName?: string; + trainingMode: boolean; + onSetTrainingMode: (training: boolean) => void; + variationComplete: boolean; +} + +const VariationHeader: React.FC = ({ + variationName, + trainingMode, + onSetTrainingMode, + variationComplete, +}) => ( + <> + + {variationName} + + + + + + + {variationComplete ? ( + Variation complete! Next variation loading… + ) : trainingMode ? ( + Play the correct move to continue. + ) : ( + Play the move indicated by the arrow to continue. + )} + +); + +export default VariationHeader; diff --git a/src/hooks/useMistakeHandler.ts b/src/hooks/useMistakeHandler.ts new file mode 100644 index 00000000..3992ba7e --- /dev/null +++ b/src/hooks/useMistakeHandler.ts @@ -0,0 +1,53 @@ +import { useCallback } from "react"; +import { Chess } from "chess.js"; + +interface Mistake { + from: string; + to: string; + type: string; +} + +interface UseMistakeHandlerParams { + selectedVariation: { moves: string[] } | null; + game: Chess; + moveIdx: number; + isUserTurn: boolean; + setMoveIdx: (idx: number) => void; + setLastMistakeVisible: (mistake: Mistake | null) => void; + undoMove: () => void; +} + +export function useMistakeHandler({ + selectedVariation, + game, + moveIdx, + isUserTurn, + setMoveIdx, + setLastMistakeVisible, + undoMove, +}: UseMistakeHandlerParams) { + return useCallback(() => { + if (!selectedVariation || !game) return; + if (moveIdx >= selectedVariation.moves.length) return; + if (!isUserTurn) return; + const history = game.history({ verbose: true }); + if (history.length !== moveIdx + 1) return; + const last = history[history.length - 1]; + const expectedMove = new Chess(); + for (let i = 0; i < moveIdx; i++) expectedMove.move(selectedVariation.moves[i]); + const expected = expectedMove.move(selectedVariation.moves[moveIdx]); + if (!expected || last.from !== expected.from || last.to !== expected.to) { + let mistakeType = "Mistake"; + if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; + setTimeout(() => { + setLastMistakeVisible({ from: last.from, to: last.to, type: mistakeType }); + setTimeout(() => { + undoMove(); + setLastMistakeVisible(null); + }, 1300); + }, 200); + } else { + setMoveIdx(moveIdx + 1); + } + }, [selectedVariation, game, moveIdx, isUserTurn, setMoveIdx, setLastMistakeVisible, undoMove]); +} diff --git a/src/pages/opening.tsx b/src/pages/opening.tsx index b173ed34..9ca87625 100644 --- a/src/pages/opening.tsx +++ b/src/pages/opening.tsx @@ -1,6 +1,6 @@ import { italianGameVariations } from "../data/openings to learn/italian"; -import { Box, Typography, Button, Stack } from "@mui/material"; -import { useState, useMemo, useEffect } from "react"; +import { Box } from "@mui/material"; +import { useState, useMemo, useEffect, useCallback } from "react"; import Board from "../components/board"; import { Chess } from "chess.js"; import { atom, useAtom } from "jotai"; @@ -13,6 +13,9 @@ import { useScreenSize } from "../hooks/useScreenSize"; import EvaluationBar from "../components/board/evaluationBar"; import { useEngine } from "../hooks/useEngine"; import { EngineName } from "../types/enums"; +import OpeningControls from "../components/OpeningControls"; +import VariationHeader from "../components/VariationHeader"; +import { useMistakeHandler } from "../hooks/useMistakeHandler"; // Returns the learning color for the variation (default is white, but can be extended) function getLearningColor(): Color { @@ -20,11 +23,17 @@ function getLearningColor(): Color { return Color.White; } +interface Mistake { + from: string; + to: string; + type: string; +} + export default function OpeningPage() { - const [currentVariantIdx, setCurrentVariantIdx] = useState(0); - const [moveIdx, setMoveIdx] = useState(0); - const [trainingMode, setTrainingMode] = useState(false); - const [lastMistakeVisible, setLastMistakeVisible] = useState(null); + const [currentVariantIdx, setCurrentVariantIdx] = useState(0); + const [moveIdx, setMoveIdx] = useState(0); + const [trainingMode, setTrainingMode] = useState(false); + const [lastMistakeVisible, setLastMistakeVisible] = useState(null); // Atom Jotai for game state const [gameAtomInstance] = useState(() => atom(new Chess())); const [game, setGame] = useAtom(gameAtomInstance); @@ -248,14 +257,14 @@ export default function OpeningPage() { }, [completedVariations, variations.length, currentVariantIdx, setGame]); // Reset progress - const handleResetProgress = () => { + const handleResetProgress = useCallback(() => { localStorage.removeItem(progressStorageKey); setCompletedVariations([]); setCurrentVariantIdx(0); setMoveIdx(0); setLastMistakeVisible(null); setGame(new Chess()); - }; + }, [setCompletedVariations, setCurrentVariantIdx, setMoveIdx, setLastMistakeVisible, setGame]); // Determine the target square of the last move played (for overlay) const lastMoveSquare = useMemo(() => { @@ -293,6 +302,37 @@ export default function OpeningPage() { return Math.max(240, Math.min(maxBoardWidth, height * 0.83)); }, [screenSize]); + // Handler for skip variation + const handleSkipVariation = useCallback(() => { + let newCompleted = completedVariations; + if (!completedVariations.includes(currentVariantIdx)) { + newCompleted = [...completedVariations, currentVariantIdx]; + setCompletedVariations(newCompleted); + localStorage.setItem(progressStorageKey, JSON.stringify(newCompleted)); + } + if (currentVariantIdx < variations.length - 1) { + setCurrentVariantIdx(idx => idx + 1); + setMoveIdx(0); + setLastMistakeVisible(null); + setGame(new Chess()); + } else { + setMoveIdx(0); + setLastMistakeVisible(null); + setGame(new Chess()); + } + }, [completedVariations, currentVariantIdx, setCompletedVariations, setCurrentVariantIdx, setMoveIdx, setLastMistakeVisible, setGame, variations.length]); + + // Use mistake handler hook + useMistakeHandler({ + selectedVariation, + game, + moveIdx, + isUserTurn, + setMoveIdx, + setLastMistakeVisible, + undoMove, + }); + return ( {/* Left area: evaluation bar + board */} {selectedVariation && !allDone && game && ( {/* Centered container for title and buttons */} - - - {selectedVariation?.name} - - - - - - {moveIdx >= selectedVariation.moves.length ? ( - Variation complete! Next variation loading… - ) : trainingMode ? ( - Play the correct move to continue. - ) : ( - Play the move indicated by the arrow to continue. - )} + + = (selectedVariation?.moves.length || 0)} + /> {/* Progress bar at the bottom right, always visible */} @@ -409,46 +437,13 @@ export default function OpeningPage() { total={variations.length} completed={completedVariations} /> - {/* Action buttons: Skip and Reset, side by side, same style */} - - - - + From 198734473f530ba70f44285797465b62b08cc4f1 Mon Sep 17 00:00:00 2001 From: TonNom Date: Sun, 25 May 2025 20:06:18 +0200 Subject: [PATCH 24/26] code structure enhanced --- src/components/LinearProgressBar.tsx | 24 ++++++++---- src/components/OpeningControls.tsx | 58 +++++++++++++++------------- src/components/OpeningProgress.tsx | 14 +++---- src/components/VariationHeader.tsx | 10 +++-- src/hooks/useMistakeHandler.ts | 22 ++++++++--- 5 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/components/LinearProgressBar.tsx b/src/components/LinearProgressBar.tsx index b5be7b5c..a852de67 100644 --- a/src/components/LinearProgressBar.tsx +++ b/src/components/LinearProgressBar.tsx @@ -6,9 +6,15 @@ import { linearProgressClasses, } from "@mui/material"; -const LinearProgressBar = ( - props: LinearProgressProps & { value: number; label: string } -) => { +/** + * A styled linear progress bar with optional label and percentage display. + */ +interface LinearProgressBarProps extends LinearProgressProps { + value: number; + label: string; +} + +function LinearProgressBar({ value, label, ...rest }: LinearProgressBarProps) { return ( - - {props.label} + + {label} ({ borderRadius: "5px", height: "5px", @@ -42,7 +52,7 @@ const LinearProgressBar = ( {`${Math.round( - props.value + value )}%`} diff --git a/src/components/OpeningControls.tsx b/src/components/OpeningControls.tsx index 293be674..9e2f9c34 100644 --- a/src/components/OpeningControls.tsx +++ b/src/components/OpeningControls.tsx @@ -1,7 +1,11 @@ -import React from "react"; + import { Button, Stack } from "@mui/material"; +import { memo } from "react"; -interface OpeningControlsProps { +/** + * Control buttons for skipping or resetting the opening variation. + */ +export interface OpeningControlsProps { moveIdx: number; selectedVariationMovesLength: number; allDone: boolean; @@ -10,34 +14,36 @@ interface OpeningControlsProps { disabled?: boolean; } -const OpeningControls: React.FC = ({ +function OpeningControls({ moveIdx, selectedVariationMovesLength, allDone, onSkip, onReset, disabled = false, -}) => ( - - - - -); +}: OpeningControlsProps) { + return ( + + + + + ); +} -export default OpeningControls; +export default memo(OpeningControls); diff --git a/src/components/OpeningProgress.tsx b/src/components/OpeningProgress.tsx index 120307fc..c09eb578 100644 --- a/src/components/OpeningProgress.tsx +++ b/src/components/OpeningProgress.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState, memo } from "react"; import LinearProgressBar from "./LinearProgressBar"; import { Box } from "@mui/material"; import { useTheme } from "@mui/material/styles"; @@ -7,16 +7,16 @@ import { useTheme } from "@mui/material/styles"; // - total: total number of variations // - currentVariationIndex: index of the current variation (optional, for display) -interface OpeningProgressProps { +/** + * Progress bar for opening training, showing completed variations out of total. + */ +export interface OpeningProgressProps { total: number; // List of completed variation indexes completed: number[]; } -const OpeningProgress: React.FC = ({ - total, - completed, -}) => { +function OpeningProgress({ total, completed }: OpeningProgressProps) { const [progress, setProgress] = useState(completed); const theme = useTheme(); @@ -55,4 +55,4 @@ const OpeningProgress: React.FC = ({ ); }; -export default OpeningProgress; +export default memo(OpeningProgress); diff --git a/src/components/VariationHeader.tsx b/src/components/VariationHeader.tsx index 61ef310e..d8216844 100644 --- a/src/components/VariationHeader.tsx +++ b/src/components/VariationHeader.tsx @@ -1,7 +1,11 @@ -import React from "react"; + import { Typography, Stack, Button } from "@mui/material"; +import { memo } from "react"; -interface VariationHeaderProps { +/** + * Header for the opening variation panel. + */ +export interface VariationHeaderProps { variationName?: string; trainingMode: boolean; onSetTrainingMode: (training: boolean) => void; @@ -37,4 +41,4 @@ const VariationHeader: React.FC = ({ ); -export default VariationHeader; +export default memo(VariationHeader); diff --git a/src/hooks/useMistakeHandler.ts b/src/hooks/useMistakeHandler.ts index 3992ba7e..1e63e6df 100644 --- a/src/hooks/useMistakeHandler.ts +++ b/src/hooks/useMistakeHandler.ts @@ -1,13 +1,19 @@ import { useCallback } from "react"; import { Chess } from "chess.js"; -interface Mistake { +/** + * A mistake made by the user during opening training. + */ +export interface Mistake { from: string; to: string; type: string; } -interface UseMistakeHandlerParams { +/** + * Params for the useMistakeHandler hook. + */ +export interface UseMistakeHandlerParams { selectedVariation: { moves: string[] } | null; game: Chess; moveIdx: number; @@ -17,6 +23,10 @@ interface UseMistakeHandlerParams { undoMove: () => void; } +/** + * Custom hook that checks if the user's move matches the expected move in the opening sequence, + * and handles mistakes (visual feedback + undo). + */ export function useMistakeHandler({ selectedVariation, game, @@ -34,11 +44,12 @@ export function useMistakeHandler({ if (history.length !== moveIdx + 1) return; const last = history[history.length - 1]; const expectedMove = new Chess(); - for (let i = 0; i < moveIdx; i++) expectedMove.move(selectedVariation.moves[i]); + for (let i = 0; i < moveIdx; i++) { + expectedMove.move(selectedVariation.moves[i]); + } const expected = expectedMove.move(selectedVariation.moves[moveIdx]); if (!expected || last.from !== expected.from || last.to !== expected.to) { - let mistakeType = "Mistake"; - if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; + const mistakeType = (last.captured || last.san.includes("#")) ? "Blunder" : "Mistake"; setTimeout(() => { setLastMistakeVisible({ from: last.from, to: last.to, type: mistakeType }); setTimeout(() => { @@ -51,3 +62,4 @@ export function useMistakeHandler({ } }, [selectedVariation, game, moveIdx, isUserTurn, setMoveIdx, setLastMistakeVisible, undoMove]); } + From 95ad425b58193ebf0575de941fef16bf7852bdf2 Mon Sep 17 00:00:00 2001 From: TonNom Date: Fri, 30 May 2025 09:43:56 +0200 Subject: [PATCH 25/26] enhancement --- src/pages/{opening.tsx => opening-trainer.tsx} | 0 src/sections/layout/NavMenu.tsx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/pages/{opening.tsx => opening-trainer.tsx} (100%) diff --git a/src/pages/opening.tsx b/src/pages/opening-trainer.tsx similarity index 100% rename from src/pages/opening.tsx rename to src/pages/opening-trainer.tsx diff --git a/src/sections/layout/NavMenu.tsx b/src/sections/layout/NavMenu.tsx index c669c0fc..e1d8ff3b 100644 --- a/src/sections/layout/NavMenu.tsx +++ b/src/sections/layout/NavMenu.tsx @@ -20,9 +20,9 @@ const MenuOptions = [ href: "/database", }, { - text: "Opening", + text: "Opening Trainer", icon: "mdi:book-open-variant", - href: "/opening", + href: "/opening-trainer", }, ]; From a2c6f8412f5d75280c3c5a5157cb14f9107941c9 Mon Sep 17 00:00:00 2001 From: TonNom Date: Fri, 30 May 2025 10:03:36 +0200 Subject: [PATCH 26/26] Added a page to choose the opening --- src/pages/choose-opening.tsx | 118 ++++++++++++++++++++++++++++++++ src/sections/layout/NavMenu.tsx | 2 +- 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/pages/choose-opening.tsx diff --git a/src/pages/choose-opening.tsx b/src/pages/choose-opening.tsx new file mode 100644 index 00000000..7b2d2289 --- /dev/null +++ b/src/pages/choose-opening.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { useRouter } from "next/router"; +import { PageTitle } from "@/components/pageTitle"; +import { Box, Typography, Paper, Stack } from "@mui/material"; + +// List of available openings (evolutive) +interface Opening { + key: string; + name: string; + description?: string; + available: boolean; +} + +const openings: Opening[] = [ + { + key: "italian", + name: "Italian Game", + description: "A classic and popular opening for beginners and intermediate players.", + available: true, + }, + { + key: "caro-kann", + name: "Caro-Kann", + description: "Coming soon ! A solid defense for strategic players.", + available: false, + }, + { + key: "england-gambit", + name: "England Gambit", + description: "Coming soon ! An aggressive gambit to surprise your opponent.", + available: false, + }, +]; + +export default function ChooseOpeningPage() { + const router = useRouter(); + + const handleChoose = (openingKey: string) => { + router.push(`/opening-trainer?opening=${openingKey}`); + }; + + return ( + <> + + + + Choose an opening + + + {openings.map((opening) => ( + + theme.palette.mode === "dark" + ? "#232323" + : "#fafbfc", + transition: "box-shadow 0.2s, border-color 0.2s, background 0.2s", + cursor: opening.available ? "pointer" : "not-allowed", + opacity: opening.available ? 1 : 0.6, + '&:hover': opening.available + ? { + boxShadow: 6, + borderColor: "primary.main", + background: (theme) => + theme.palette.mode === "dark" + ? "#232323" + : "#f0f7fa", + } + : {}, + }} + onClick={opening.available ? () => handleChoose(opening.key) : undefined} + > + + {opening.name} + + {opening.description && ( + + {opening.description} + + )} + + ))} + + + + ); +} diff --git a/src/sections/layout/NavMenu.tsx b/src/sections/layout/NavMenu.tsx index e1d8ff3b..8d08eca4 100644 --- a/src/sections/layout/NavMenu.tsx +++ b/src/sections/layout/NavMenu.tsx @@ -22,7 +22,7 @@ const MenuOptions = [ { text: "Opening Trainer", icon: "mdi:book-open-variant", - href: "/opening-trainer", + href: "/choose-opening", // Redirige désormais vers la page de choix d'ouverture }, ];