From 528d5a6ae53c1f9b155754623a4c7f7c25069e70 Mon Sep 17 00:00:00 2001 From: TonNom Date: Sat, 17 May 2025 13:35:11 +0200 Subject: [PATCH 01/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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 }, ]; From 56776eba0857af5ed53e3ff7940032ee0594e442 Mon Sep 17 00:00:00 2001 From: TonNom Date: Sat, 31 May 2025 15:24:27 +0200 Subject: [PATCH 27/35] Ui enhancement --- src/components/VariationHeader.tsx | 2 +- src/pages/opening-trainer.tsx | 152 +++++++++++++++++++---------- 2 files changed, 104 insertions(+), 50 deletions(-) diff --git a/src/components/VariationHeader.tsx b/src/components/VariationHeader.tsx index d8216844..aead9764 100644 --- a/src/components/VariationHeader.tsx +++ b/src/components/VariationHeader.tsx @@ -36,7 +36,7 @@ const VariationHeader: React.FC = ({ ) : trainingMode ? ( Play the correct move to continue. ) : ( - Play the move indicated by the arrow to continue. + Play the move indicated by the arrow. )} ); diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 9ca87625..47096ca8 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -334,26 +334,33 @@ export default function OpeningPage() { }); return ( - + boxSizing: "border-box", + overflowX: "hidden", + }} + > {/* Left area: evaluation bar + board */} - + {selectedVariation && !allDone && game && ( + }} + > {/* Evaluation bar on the left, vertically centered */} - + {/* Chessboard */} - + )} + {/* Right area: progress panel, buttons, text */} - + {/* Centered container for title and buttons */} - + = (selectedVariation?.moves.length || 0)} /> + {/* Progress bar at the bottom right, always visible */} - + Date: Sat, 31 May 2025 15:26:18 +0200 Subject: [PATCH 28/35] fr to en --- src/pages/opening-trainer.tsx | 62 +++++++++++++++++------------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 47096ca8..28739d84 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -446,57 +446,57 @@ export default function OpeningPage() { px: { xs: 2, sm: 3, md: 4 }, pt: { xs: 2, md: 4 }, mr: { xs: 1, sm: 2, md: 6, lg: 10 }, - backgroundColor: "#424242", // Fond gris clair cohérent - border: "2px solid", // Bord bleu + backgroundColor: "#424242", // Consistent light gray background + border: "2px solid", // Blue border borderColor: "primary.main", - borderRadius: 2, // Coins arrondis - boxShadow: 3, // Ombre cohérente + borderRadius: 2, // Rounded corners + boxShadow: 3, // Consistent shadow }} > {/* Centered container for title and buttons */} = (selectedVariation?.moves.length || 0)} + variationName={selectedVariation?.name} + trainingMode={trainingMode} + onSetTrainingMode={setTrainingMode} + variationComplete={moveIdx >= (selectedVariation?.moves.length || 0)} /> {/* Progress bar at the bottom right, always visible */} From ab5b81416cc3e03818f26c9c011fbbbabfd9edec Mon Sep 17 00:00:00 2001 From: TonNom Date: Sun, 1 Jun 2025 11:17:29 +0200 Subject: [PATCH 29/35] UI edit --- src/pages/_app.tsx | 1 - src/pages/global-fix.css | 8 -------- src/pages/opening-trainer.tsx | 21 +++++++++++---------- 3 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 src/pages/global-fix.css diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 04c7e02b..f863fb7d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,7 +5,6 @@ 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 deleted file mode 100644 index 082d75b4..00000000 --- a/src/pages/global-fix.css +++ /dev/null @@ -1,8 +0,0 @@ -html, body { - width: 100vw; - min-height: 100vh; - margin: 0; - padding: 0; - overflow-x: hidden; - box-sizing: border-box; -} diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 28739d84..8a15a5b1 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -336,17 +336,17 @@ export default function OpeningPage() { return ( {/* Left area: evaluation bar + board */} @@ -377,7 +377,7 @@ export default function OpeningPage() { flexDirection: "row", alignItems: "center", justifyContent: "center", - mr: `${evalBarGap * 2}px`, // Right margin for visual balance + mr: 0, // Supprime la margin-right supplémentaire pour éviter le débordement }} > {/* Evaluation bar on the left, vertically centered */} @@ -437,15 +437,16 @@ export default function OpeningPage() { Date: Sun, 1 Jun 2025 11:21:12 +0200 Subject: [PATCH 30/35] light mode fix --- src/pages/opening-trainer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 8a15a5b1..878bf452 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -378,6 +378,7 @@ export default function OpeningPage() { alignItems: "center", justifyContent: "center", mr: 0, // Supprime la margin-right supplémentaire pour éviter le débordement + boxShadow: 4, // Ajout : ombre légère sur le board }} > {/* Evaluation bar on the left, vertically centered */} @@ -447,7 +448,7 @@ export default function OpeningPage() { px: { xs: 1, sm: 2, md: 3 }, // Réduit le padding horizontal pt: { xs: 2, md: 4 }, mr: { xs: 0, sm: 1, md: 2, lg: 0 }, // Réduit la marge droite - backgroundColor: "#424242", // Consistent light gray background + backgroundColor: (theme) => theme.palette.mode === "dark" ? "#424242" : "background.paper", border: "2px solid", // Blue border borderColor: "primary.main", borderRadius: 2, // Rounded corners From d069d814e820fe9f44e47a59f460944eb0989e5f Mon Sep 17 00:00:00 2001 From: TonNom Date: Fri, 4 Jul 2025 11:44:24 +0200 Subject: [PATCH 31/35] correction du chemin d'importation pour italian.ts --- src/data/{openings to learn => openings_to_learn}/italian.ts | 0 src/pages/opening-trainer.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/data/{openings to learn => openings_to_learn}/italian.ts (100%) diff --git a/src/data/openings to learn/italian.ts b/src/data/openings_to_learn/italian.ts similarity index 100% rename from src/data/openings to learn/italian.ts rename to src/data/openings_to_learn/italian.ts diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 878bf452..51507156 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -1,4 +1,4 @@ -import { italianGameVariations } from "../data/openings to learn/italian"; +import { italianGameVariations } from "../data/openings_to_learn/italian"; import { Box } from "@mui/material"; import { useState, useMemo, useEffect, useCallback } from "react"; import Board from "../components/board"; From 175db356a02e703b743ba3de3b5af1f0ae5b8827 Mon Sep 17 00:00:00 2001 From: Speedauge <192351844+Speedauge@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:09:56 +0200 Subject: [PATCH 32/35] Improve opening trainer logic and format components --- src/components/LinearProgressBar.tsx | 2 +- src/components/OpeningControls.tsx | 7 +- src/components/OpeningProgress.tsx | 6 +- src/components/VariationHeader.tsx | 42 +- src/components/board/index.tsx | 28 +- src/data/openings_to_learn/italian.ts | 618 +++++++++++++++++++++++++- src/hooks/useMistakeHandler.ts | 20 +- src/pages/choose-opening.tsx | 23 +- src/pages/opening-trainer.tsx | 240 +++++----- 9 files changed, 809 insertions(+), 177 deletions(-) diff --git a/src/components/LinearProgressBar.tsx b/src/components/LinearProgressBar.tsx index 84049f36..2f599c53 100644 --- a/src/components/LinearProgressBar.tsx +++ b/src/components/LinearProgressBar.tsx @@ -60,6 +60,6 @@ function LinearProgressBar({ value, label, ...rest }: LinearProgressBarProps) { ); -}; +} export default LinearProgressBar; diff --git a/src/components/OpeningControls.tsx b/src/components/OpeningControls.tsx index 9e2f9c34..bc465a98 100644 --- a/src/components/OpeningControls.tsx +++ b/src/components/OpeningControls.tsx @@ -1,4 +1,3 @@ - import { Button, Stack } from "@mui/material"; import { memo } from "react"; @@ -23,12 +22,14 @@ function OpeningControls({ disabled = false, }: OpeningControlsProps) { return ( - + - {variationComplete ? ( - Variation complete! Next variation loading… + + Variation complete! Next variation loading… + ) : trainingMode ? ( - Play the correct move to continue. + + Play the correct move to continue. + ) : ( - Play the move indicated by the arrow. + + Play the move indicated by the arrow. + )} ); diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index 1ae28752..48ea7f8f 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -24,8 +24,8 @@ import tinycolor from "tinycolor2"; export interface TrainingFeedback { square: string; // ex: 'e4' - icon: string; // chemin de l'icône - alt: string; // texte alternatif + icon: string; // chemin de l'icône + alt: string; // texte alternatif } export interface Props { @@ -222,13 +222,15 @@ export default function Board({ const customArrows: Arrow[] = useMemo(() => { if (bestMoveUci && showBestMoveArrow) { // Priorité à la flèche d'ouverture - return [[ - bestMoveUci.slice(0, 2), - bestMoveUci.slice(2, 4), - tinycolor(CLASSIFICATION_COLORS[MoveClassification.Best]) - .spin(-boardHue) - .toHexString(), - ] as Arrow]; + return [ + [ + bestMoveUci.slice(0, 2), + bestMoveUci.slice(2, 4), + tinycolor(CLASSIFICATION_COLORS[MoveClassification.Best]) + .spin(-boardHue) + .toHexString(), + ] as Arrow, + ]; } // Fallback moteur const bestMove = position?.lastEval?.bestMove; @@ -333,7 +335,9 @@ export default function Board({ )} @@ -371,7 +375,9 @@ export default function Board({ )} diff --git a/src/data/openings_to_learn/italian.ts b/src/data/openings_to_learn/italian.ts index 0dbc24e9..78baa97c 100644 --- a/src/data/openings_to_learn/italian.ts +++ b/src/data/openings_to_learn/italian.ts @@ -10,51 +10,409 @@ export interface Variation { export const italianGameVariations: Variation[] = [ { 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"], + 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: "Italian Game Line 2", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nf6", "d4", "Nxe4", "dxe5", "Bc5", "Qd5", "Bxf2+", "Kf1", "O-O", "Qxe4"], + moves: [ + "e4", + "e5", + "Nf3", + "Nc6", + "Bc4", + "Nf6", + "d4", + "Nxe4", + "dxe5", + "Bc5", + "Qd5", + "Bxf2+", + "Kf1", + "O-O", + "Qxe4", + ], }, { 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"], + moves: [ + "e4", + "e5", + "Nf3", + "Nc6", + "Bc4", + "Bc5", + "c3", + "Nf6", + "d4", + "exd4", + "cxd4", + "Bb6", + "e5", + "Ng4", + "h3", + "Nh6", + "d5", + "Na5", + "Bg5", + "f6", + "exf6", + ], }, { 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"], + 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: "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"], + 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: "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"], + 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: "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"], + 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: "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"], + 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: "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"], + 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: "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"], + 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: "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#"], + 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: "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"], + 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: "Italian Game Line 13", @@ -62,38 +420,254 @@ export const italianGameVariations: Variation[] = [ }, { name: "Italian Game Line 14", - moves: ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "c3", "h6", "d4", "exd4", "cxd4", "Bb4+", "Nc3", "d6", "O-O"], + moves: [ + "e4", + "e5", + "Nf3", + "Nc6", + "Bc4", + "Bc5", + "c3", + "h6", + "d4", + "exd4", + "cxd4", + "Bb4+", + "Nc3", + "d6", + "O-O", + ], }, { name: "Italian Game Line 15", - moves: ["e4", "e5", "Nf3", "d6", "d4", "exd4", "Nxd4", "Nf6", "Nc3", "Be7", "Bf4", "O-O", "Qd2", "a6", "O-O-O"], + moves: [ + "e4", + "e5", + "Nf3", + "d6", + "d4", + "exd4", + "Nxd4", + "Nf6", + "Nc3", + "Be7", + "Bf4", + "O-O", + "Qd2", + "a6", + "O-O-O", + ], }, { name: "Italian Game Line 16", - moves: ["e4", "e5", "Nf3", "d6", "d4", "exd4", "Nxd4", "Be7", "Nc3", "Nf6", "Bf4", "O-O", "Qd2", "a6", "O-O-O"], + moves: [ + "e4", + "e5", + "Nf3", + "d6", + "d4", + "exd4", + "Nxd4", + "Be7", + "Nc3", + "Nf6", + "Bf4", + "O-O", + "Qd2", + "a6", + "O-O-O", + ], }, { 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"], + moves: [ + "e4", + "e5", + "Nf3", + "d6", + "d4", + "Nc6", + "Bb5", + "exd4", + "Qxd4", + "Bd7", + "Bxc6", + "Bxc6", + "Nc3", + "Nf6", + "Bg5", + "Be7", + "O-O-O", + ], }, { 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"], + 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"], + 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"], + 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"], + 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#"], + 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 +]; diff --git a/src/hooks/useMistakeHandler.ts b/src/hooks/useMistakeHandler.ts index 1e63e6df..578c8c54 100644 --- a/src/hooks/useMistakeHandler.ts +++ b/src/hooks/useMistakeHandler.ts @@ -49,9 +49,14 @@ export function useMistakeHandler({ } const expected = expectedMove.move(selectedVariation.moves[moveIdx]); if (!expected || last.from !== expected.from || last.to !== expected.to) { - const mistakeType = (last.captured || last.san.includes("#")) ? "Blunder" : "Mistake"; + const mistakeType = + last.captured || last.san.includes("#") ? "Blunder" : "Mistake"; setTimeout(() => { - setLastMistakeVisible({ from: last.from, to: last.to, type: mistakeType }); + setLastMistakeVisible({ + from: last.from, + to: last.to, + type: mistakeType, + }); setTimeout(() => { undoMove(); setLastMistakeVisible(null); @@ -60,6 +65,13 @@ export function useMistakeHandler({ } else { setMoveIdx(moveIdx + 1); } - }, [selectedVariation, game, moveIdx, isUserTurn, setMoveIdx, setLastMistakeVisible, undoMove]); + }, [ + selectedVariation, + game, + moveIdx, + isUserTurn, + setMoveIdx, + setLastMistakeVisible, + undoMove, + ]); } - diff --git a/src/pages/choose-opening.tsx b/src/pages/choose-opening.tsx index 7b2d2289..e5564330 100644 --- a/src/pages/choose-opening.tsx +++ b/src/pages/choose-opening.tsx @@ -15,7 +15,8 @@ const openings: Opening[] = [ { key: "italian", name: "Italian Game", - description: "A classic and popular opening for beginners and intermediate players.", + description: + "A classic and popular opening for beginners and intermediate players.", available: true, }, { @@ -27,7 +28,8 @@ const openings: Opening[] = [ { key: "england-gambit", name: "England Gambit", - description: "Coming soon ! An aggressive gambit to surprise your opponent.", + description: + "Coming soon ! An aggressive gambit to surprise your opponent.", available: false, }, ]; @@ -82,24 +84,23 @@ export default function ChooseOpeningPage() { border: "1px solid", borderColor: "divider", background: (theme) => - theme.palette.mode === "dark" - ? "#232323" - : "#fafbfc", - transition: "box-shadow 0.2s, border-color 0.2s, background 0.2s", + 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 + "&:hover": opening.available ? { boxShadow: 6, borderColor: "primary.main", background: (theme) => - theme.palette.mode === "dark" - ? "#232323" - : "#f0f7fa", + theme.palette.mode === "dark" ? "#232323" : "#f0f7fa", } : {}, }} - onClick={opening.available ? () => handleChoose(opening.key) : undefined} + onClick={ + opening.available ? () => handleChoose(opening.key) : undefined + } > {opening.name} diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 51507156..d51a0ff5 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -1,18 +1,16 @@ import { italianGameVariations } from "../data/openings_to_learn/italian"; -import { Box } from "@mui/material"; +import { Box, Grid2 as Grid } 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"; import { useChessActions } from "../hooks/useChessActions"; -import { Color } from "../types/enums"; +import { Color, EngineName } from "../types/enums"; import { CurrentPosition } from "../types/eval"; import OpeningProgress from "../components/OpeningProgress"; -import { Grid2 as Grid } from "@mui/material"; 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"; @@ -33,7 +31,9 @@ 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 [lastMistakeVisible, setLastMistakeVisible] = useState( + null + ); // Atom Jotai for game state const [gameAtomInstance] = useState(() => atom(new Chess())); const [game, setGame] = useAtom(gameAtomInstance); @@ -59,30 +59,32 @@ export default function OpeningPage() { // 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 && - game && - moveIdx < selectedVariation.moves.length - ) { + if (selectedVariation && game && moveIdx < selectedVariation.moves.length) { 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 ( + moveObj.from + + moveObj.to + + (moveObj.promotion ? moveObj.promotion : "") + ); } } return undefined; - }, [selectedVariation, game, moveIdx, isUserTurn]); + }, [selectedVariation, game, moveIdx]); // Writable atom for currentPosition (read/write) // Instead of a local atom, use the engine evaluation mechanism already present in the project // (see useEngine, useGameData, or similar hooks if available) // Here, we use a simple effect to update the evaluation after each move using the engine - const [currentPositionAtom, setCurrentPositionAtom] = useState(() => atom({ - lastEval: { lines: [] }, - eval: { moveClassification: undefined, lines: [] }, - })); + const [currentPositionAtom, setCurrentPositionAtom] = useState(() => + atom({ + lastEval: { lines: [] }, + eval: { moveClassification: undefined, lines: [] }, + }) + ); // Engine integration for real-time evaluation const engine = useEngine(EngineName.Stockfish17Lite); @@ -100,10 +102,12 @@ export default function OpeningPage() { multiPv: 2, setPartialEval: (evalResult) => { if (!cancelled) { - setCurrentPositionAtom(atom({ - lastEval: evalResult, - eval: evalResult, - })); + setCurrentPositionAtom( + atom({ + lastEval: evalResult, + eval: evalResult, + }) + ); } }, }); @@ -126,51 +130,26 @@ export default function OpeningPage() { if (!result) break; // Stop if invalid move } setGame(chess); - } catch (e) { + } catch { // Error handling: avoid crash setGame(new Chess()); } }, [selectedVariation, moveIdx, setGame]); - // Validate user move: if wrong move, undo and annotate + // Validate user move using the custom mistake handler + const checkMistake = useMistakeHandler({ + selectedVariation, + game, + moveIdx, + isUserTurn, + setMoveIdx, + setLastMistakeVisible, + undoMove, + }); + useEffect(() => { - if (!selectedVariation || !game) return; - if (moveIdx >= selectedVariation.moves.length) return; - if (!isUserTurn) return; // Only validate user moves - 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; - 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) { - // Wrong move: wait 200ms before showing error icon, then undo after 1.5s - let mistakeType = "Mistake"; - if (last.captured || last.san.includes("#")) mistakeType = "Blunder"; - // Do NOT set lastMistakeVisible immediately - mistakeTimeout = setTimeout(() => { - setLastMistakeVisible({ from: last.from, to: last.to, type: mistakeType }); - }, 200); - undoTimeout = setTimeout(() => { - setLastMistakeVisible(null); - undoMove(); - }, 1500); - } else { - setLastMistakeVisible(null); - setMoveIdx((idx) => idx + 1); - } - } catch (e) { - 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]); + checkMistake(); + }, [checkMistake]); // Automatically advance opponent moves after a correct user move useEffect(() => { @@ -181,7 +160,10 @@ export default function OpeningPage() { // 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) { + while ( + nextIdx < selectedVariation.moves.length && + colorToPlay !== learningColor + ) { nextIdx++; colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black; } @@ -213,15 +195,17 @@ export default function OpeningPage() { 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 []; + const [completedVariations, setCompletedVariations] = useState( + () => { + if (typeof window === "undefined") return []; + try { + const raw = localStorage.getItem(progressStorageKey); + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } } - }); + ); // Mark a variation as completed useEffect(() => { @@ -233,28 +217,41 @@ export default function OpeningPage() { localStorage.setItem(progressStorageKey, JSON.stringify(updated)); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [moveIdx, selectedVariation, currentVariantIdx, progressMode]); + }, [ + moveIdx, + selectedVariation, + currentVariantIdx, + progressStorageKey, + completedVariations, + ]); // When loading, if there is a completed variation, jump to the first incomplete one useEffect(() => { - if (completedVariations.length > 0 && completedVariations.length < variations.length) { + if ( + completedVariations.length > 0 && + completedVariations.length < variations.length + ) { // Find the first incomplete variation - const firstIncomplete = variations.findIndex((_, idx) => !completedVariations.includes(idx)); + const firstIncomplete = variations.findIndex( + (_, idx) => !completedVariations.includes(idx) + ); if (firstIncomplete !== -1 && currentVariantIdx !== firstIncomplete) { setCurrentVariantIdx(firstIncomplete); setMoveIdx(0); setLastMistakeVisible(null); setGame(new Chess()); } - } else if (completedVariations.length === variations.length && currentVariantIdx !== variations.length) { + } else if ( + completedVariations.length === variations.length && + currentVariantIdx !== variations.length + ) { // All done, move to the end setCurrentVariantIdx(variations.length - 1); // Correction: do not exceed max index setMoveIdx(0); setLastMistakeVisible(null); setGame(new Chess()); } - }, [completedVariations, variations.length, currentVariantIdx, setGame]); + }, [completedVariations, variations, currentVariantIdx, setGame]); // Reset progress const handleResetProgress = useCallback(() => { @@ -264,7 +261,14 @@ export default function OpeningPage() { setMoveIdx(0); setLastMistakeVisible(null); setGame(new Chess()); - }, [setCompletedVariations, setCurrentVariantIdx, setMoveIdx, setLastMistakeVisible, setGame]); + }, [ + setCompletedVariations, + setCurrentVariantIdx, + setMoveIdx, + setLastMistakeVisible, + setGame, + progressStorageKey, + ]); // Determine the target square of the last move played (for overlay) const lastMoveSquare = useMemo(() => { @@ -280,7 +284,11 @@ export default function OpeningPage() { if (!lastMoveSquare) return undefined; // 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: "Incorrect move" }; + return { + square: lastMoveSquare, + icon: "/icons/mistake.png", + alt: "Incorrect move", + }; } // Show nothing if the move is correct return undefined; @@ -311,7 +319,7 @@ export default function OpeningPage() { localStorage.setItem(progressStorageKey, JSON.stringify(newCompleted)); } if (currentVariantIdx < variations.length - 1) { - setCurrentVariantIdx(idx => idx + 1); + setCurrentVariantIdx((idx) => idx + 1); setMoveIdx(0); setLastMistakeVisible(null); setGame(new Chess()); @@ -320,18 +328,17 @@ export default function OpeningPage() { setLastMistakeVisible(null); setGame(new Chess()); } - }, [completedVariations, currentVariantIdx, setCompletedVariations, setCurrentVariantIdx, setMoveIdx, setLastMistakeVisible, setGame, variations.length]); - - // Use mistake handler hook - useMistakeHandler({ - selectedVariation, - game, - moveIdx, - isUserTurn, + }, [ + completedVariations, + currentVariantIdx, + setCompletedVariations, + setCurrentVariantIdx, setMoveIdx, setLastMistakeVisible, - undoMove, - }); + setGame, + variations.length, + progressStorageKey, + ]); return ( theme.palette.mode === "dark" ? "#424242" : "background.paper", + backgroundColor: (theme) => + theme.palette.mode === "dark" ? "#424242" : "background.paper", border: "2px solid", // Blue border borderColor: "primary.main", borderRadius: 2, // Rounded corners @@ -458,47 +466,49 @@ export default function OpeningPage() { {/* Centered container for title and buttons */} = (selectedVariation?.moves.length || 0)} + variationName={selectedVariation?.name} + trainingMode={trainingMode} + onSetTrainingMode={setTrainingMode} + variationComplete={ + moveIdx >= (selectedVariation?.moves.length || 0) + } /> {/* Progress bar at the bottom right, always visible */} From 7f7ff56f16d81ab4a978db407ebccf6af1beea67 Mon Sep 17 00:00:00 2001 From: Speedauge <192351844+Speedauge@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:24:54 +0200 Subject: [PATCH 33/35] fix : vertical scrolling --- src/pages/opening-trainer.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index d51a0ff5..896f7328 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -343,18 +343,10 @@ export default function OpeningPage() { return ( {/* Left area: evaluation bar + board */} Date: Fri, 4 Jul 2025 18:46:02 +0200 Subject: [PATCH 34/35] =?UTF-8?q?Am=C3=A9lioration=20de=20la=20mise=20en?= =?UTF-8?q?=20page=20du=20panneau=20de=20progression=20avec=20des=20ajuste?= =?UTF-8?q?ments=20de=20style=20et=20de=20marges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/opening-trainer.tsx | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 896f7328..425f2d28 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -435,25 +435,21 @@ export default function OpeningPage() { {/* Right area: progress panel, buttons, text */} - theme.palette.mode === "dark" ? "#424242" : "background.paper", - border: "2px solid", // Blue border + backgroundColor: "secondary.main", borderColor: "primary.main", - borderRadius: 2, // Rounded corners - boxShadow: 3, // Consistent shadow + borderWidth: 2, + boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)", }} + padding={3} + rowGap={3} + style={{ maxWidth: "420px" }} > {/* Centered container for title and buttons */} Date: Fri, 4 Jul 2025 18:53:50 +0200 Subject: [PATCH 35/35] fix : UI improvement --- src/pages/opening-trainer.tsx | 96 +++++++---------------------------- 1 file changed, 19 insertions(+), 77 deletions(-) diff --git a/src/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx index 425f2d28..4a2a6ba9 100644 --- a/src/pages/opening-trainer.tsx +++ b/src/pages/opening-trainer.tsx @@ -9,7 +9,6 @@ import { Color, EngineName } from "../types/enums"; import { CurrentPosition } from "../types/eval"; import OpeningProgress from "../components/OpeningProgress"; import { useScreenSize } from "../hooks/useScreenSize"; -import EvaluationBar from "../components/board/evaluationBar"; import { useEngine } from "../hooks/useEngine"; import OpeningControls from "../components/OpeningControls"; import VariationHeader from "../components/VariationHeader"; @@ -295,19 +294,14 @@ export default function OpeningPage() { }, [lastMistakeVisible, lastMoveSquare]); const screenSize = useScreenSize(); - // Responsive constants - const evalBarWidth = 32; // px - const evalBarGap = 8; // px // Dynamic board size calculation const boardSize = useMemo(() => { const { width, height } = screenSize; - let maxBoardWidth = width - 300 - evalBarWidth; if (typeof window !== "undefined" && window.innerWidth < 900) { - maxBoardWidth = width - evalBarWidth - 24; - return Math.max(180, Math.min(maxBoardWidth, height - 150)); + return Math.max(180, Math.min(width, height - 150)); } - return Math.max(240, Math.min(maxBoardWidth, height * 0.83)); + return Math.max(240, Math.min(width - 300, height * 0.83)); }, [screenSize]); // Handler for skip variation @@ -361,75 +355,21 @@ export default function OpeningPage() { }} > {selectedVariation && !allDone && game && ( - - {/* Evaluation bar on the left, vertically centered */} - - - - {/* Chessboard */} - - - - + )} @@ -450,6 +390,8 @@ export default function OpeningPage() { padding={3} rowGap={3} style={{ maxWidth: "420px" }} + size={{ xs: 12, md: "grow" }} + > {/* Centered container for title and buttons */}