diff --git a/src/components/LinearProgressBar.tsx b/src/components/LinearProgressBar.tsx
index 211ffa82..2f599c53 100644
--- a/src/components/LinearProgressBar.tsx
+++ b/src/components/LinearProgressBar.tsx
@@ -7,11 +7,17 @@ import {
linearProgressClasses,
} from "@mui/material";
-const LinearProgressBar = (
- props: LinearProgressProps & { value: number; label: string }
-) => {
- if (props.value === 0) return null;
+/**
+ * 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) {
+ // Allow rendering if label === "" (OpeningProgress case), otherwise hide if value === 0
+ if (value === 0 && label !== "") return null;
return (
-
- {props.label}
+
+ {label}
({
borderRadius: "5px",
height: "5px",
@@ -45,11 +55,11 @@ const LinearProgressBar = (
{`${Math.round(
- props.value
+ value
)}%`}
);
-};
+}
export default LinearProgressBar;
diff --git a/src/components/OpeningControls.tsx b/src/components/OpeningControls.tsx
new file mode 100644
index 00000000..bc465a98
--- /dev/null
+++ b/src/components/OpeningControls.tsx
@@ -0,0 +1,50 @@
+import { Button, Stack } from "@mui/material";
+import { memo } from "react";
+
+/**
+ * Control buttons for skipping or resetting the opening variation.
+ */
+export interface OpeningControlsProps {
+ moveIdx: number;
+ selectedVariationMovesLength: number;
+ allDone: boolean;
+ onSkip: () => void;
+ onReset: () => void;
+ disabled?: boolean;
+}
+
+function OpeningControls({
+ moveIdx,
+ selectedVariationMovesLength,
+ allDone,
+ onSkip,
+ onReset,
+ disabled = false,
+}: OpeningControlsProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default memo(OpeningControls);
diff --git a/src/components/OpeningProgress.tsx b/src/components/OpeningProgress.tsx
new file mode 100644
index 00000000..b732e672
--- /dev/null
+++ b/src/components/OpeningProgress.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useState, memo } from "react";
+import LinearProgressBar from "./LinearProgressBar";
+import { Box } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+
+// Props:
+// - total: total number of variations
+// - currentVariationIndex: index of the current variation (optional, for display)
+
+/**
+ * Progress bar for opening training, showing completed variations out of total.
+ */
+export interface OpeningProgressProps {
+ total: number;
+ // List of completed variation indexes
+ completed: number[];
+}
+
+function OpeningProgress({ total, completed }: OpeningProgressProps) {
+ const [progress, setProgress] = useState(completed);
+ const theme = useTheme();
+
+ useEffect(() => {
+ setProgress(completed);
+ }, [completed]);
+
+ // Calculate percentage
+ const percent = total > 0 ? (progress.length / total) * 100 : 0;
+ const label = `${progress.length} / ${total}`;
+
+ return (
+
+
+
+ {label}
+
+
+
+
+
+
+ );
+}
+
+export default memo(OpeningProgress);
diff --git a/src/components/VariationHeader.tsx b/src/components/VariationHeader.tsx
new file mode 100644
index 00000000..73446d11
--- /dev/null
+++ b/src/components/VariationHeader.tsx
@@ -0,0 +1,70 @@
+import { Typography, Stack, Button } from "@mui/material";
+import { memo } from "react";
+
+/**
+ * Header for the opening variation panel.
+ */
+export 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.
+
+ )}
+ >
+);
+
+export default memo(VariationHeader);
diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx
index d02404b9..48ea7f8f 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,9 @@ export interface Props {
showBestMoveArrow?: boolean;
showPlayerMoveIconAtom?: PrimitiveAtom;
showEvaluationBar?: boolean;
+ trainingFeedback?: TrainingFeedback;
+ bestMoveUci?: string;
+ hidePlayerHeaders?: boolean;
}
export default function Board({
@@ -48,6 +57,9 @@ export default function Board({
showBestMoveArrow = false,
showPlayerMoveIconAtom,
showEvaluationBar = false,
+ trainingFeedback,
+ bestMoveUci,
+ hidePlayerHeaders = false,
}: Props) {
const boardRef = useRef(null);
const game = useAtomValue(gameAtom);
@@ -208,9 +220,21 @@ 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,
+ ];
+ }
+ // Fallback moteur
const bestMove = position?.lastEval?.bestMove;
const moveClassification = position?.eval?.moveClassification;
-
if (
bestMove &&
showBestMoveArrow &&
@@ -226,12 +250,10 @@ export default function Board({
.spin(-boardHue)
.toHexString(),
] as Arrow;
-
return [bestMoveArrow];
}
-
return [];
- }, [position, showBestMoveArrow, boardHue]);
+ }, [bestMoveUci, position, showBestMoveArrow, boardHue]);
const SquareRenderer: CustomSquareRenderer = useMemo(() => {
return getSquareRenderer({
@@ -239,12 +261,14 @@ export default function Board({
clickedSquaresAtom,
playableSquaresAtom,
showPlayerMoveIconAtom,
+ trainingFeedback, // nouvelle prop transmise
});
}, [
currentPositionAtom,
clickedSquaresAtom,
playableSquaresAtom,
showPlayerMoveIconAtom,
+ trainingFeedback,
]);
const customPieces = useMemo(
@@ -306,11 +330,16 @@ export default function Board({
paddingLeft={showEvaluationBar ? 2 : 0}
size="grow"
>
-
+ {/* Enlève l'affichage des PlayerHeader si hidePlayerHeaders est true */}
+ {!hidePlayerHeaders && (
+
+ )}
-
+ {!hidePlayerHeaders && (
+
+ )}
);
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 && (
+
+ )}
+ {/* Aucun affichage de message texte d'erreur ici, seulement l'icône */}
{moveClassification && showPlayerMoveIcon && square === toSquare && (
void;
+ setLastMistakeVisible: (mistake: Mistake | null) => void;
+ 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,
+ 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) {
+ const mistakeType =
+ last.captured || last.san.includes("#") ? "Blunder" : "Mistake";
+ 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/choose-opening.tsx b/src/pages/choose-opening.tsx
new file mode 100644
index 00000000..e5564330
--- /dev/null
+++ b/src/pages/choose-opening.tsx
@@ -0,0 +1,119 @@
+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/pages/opening-trainer.tsx b/src/pages/opening-trainer.tsx
new file mode 100644
index 00000000..4a2a6ba9
--- /dev/null
+++ b/src/pages/opening-trainer.tsx
@@ -0,0 +1,447 @@
+import { italianGameVariations } from "../data/openings_to_learn/italian";
+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, EngineName } from "../types/enums";
+import { CurrentPosition } from "../types/eval";
+import OpeningProgress from "../components/OpeningProgress";
+import { useScreenSize } from "../hooks/useScreenSize";
+import { useEngine } from "../hooks/useEngine";
+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 {
+ // Always returns white for now
+ 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
+ );
+ // Atom Jotai for game state
+ const [gameAtomInstance] = useState(() => atom(new Chess()));
+ const [game, setGame] = useAtom(gameAtomInstance);
+ const { undoMove } = useChessActions(gameAtomInstance);
+
+ // List of variations to learn (all)
+ const variations = italianGameVariations;
+ const selectedVariation = variations[currentVariantIdx] || null;
+
+ // Learning color (fixed for the variation)
+ const learningColor = useMemo(() => {
+ if (!selectedVariation) return Color.White;
+ return getLearningColor(); // No argument needed
+ }, [selectedVariation]);
+
+ // Indicates if it's the user's turn to play
+ const isUserTurn = useMemo(() => {
+ if (!selectedVariation) return false;
+ // 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]);
+
+ // 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) {
+ 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]);
+
+ // 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: [] },
+ })
+ );
+
+ // Engine integration for real-time evaluation
+ const engine = useEngine(EngineName.Stockfish17Lite);
+
+ useEffect(() => {
+ if (!game || !engine || !engine.getIsReady()) return;
+ let cancelled = false;
+ const fen = game.fen();
+ // Delay the analysis to prioritize move animation
+ const timeout = setTimeout(() => {
+ if (cancelled) return;
+ engine.evaluatePositionWithUpdate({
+ fen,
+ depth: 14,
+ multiPv: 2,
+ setPartialEval: (evalResult) => {
+ if (!cancelled) {
+ setCurrentPositionAtom(
+ atom({
+ lastEval: evalResult,
+ eval: evalResult,
+ })
+ );
+ }
+ },
+ });
+ }, 200); // 200ms allows time for move animation
+ return () => {
+ cancelled = true;
+ clearTimeout(timeout);
+ engine.stopAllCurrentJobs();
+ };
+ }, [game, moveIdx, engine]);
+
+ // Reset on each variation or 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 if invalid move
+ }
+ setGame(chess);
+ } catch {
+ // Error handling: avoid crash
+ setGame(new Chess());
+ }
+ }, [selectedVariation, moveIdx, setGame]);
+
+ // Validate user move using the custom mistake handler
+ const checkMistake = useMistakeHandler({
+ selectedVariation,
+ game,
+ moveIdx,
+ isUserTurn,
+ setMoveIdx,
+ setLastMistakeVisible,
+ undoMove,
+ });
+
+ useEffect(() => {
+ checkMistake();
+ }, [checkMistake]);
+
+ // Automatically advance opponent moves after a correct user move
+ useEffect(() => {
+ if (!selectedVariation) return;
+ if (moveIdx >= selectedVariation.moves.length) return;
+ // If it's not the user's turn, automatically advance opponent moves
+ if (!isUserTurn) {
+ // 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
+ ) {
+ nextIdx++;
+ colorToPlay = nextIdx % 2 === 0 ? Color.White : Color.Black;
+ }
+ if (nextIdx !== moveIdx) {
+ // Delay increased to 500ms to allow time for user move animation
+ setTimeout(() => setMoveIdx(nextIdx), 500);
+ }
+ }
+ }, [moveIdx, isUserTurn, selectedVariation, learningColor]);
+
+ // Automatically chain variations
+ useEffect(() => {
+ if (!selectedVariation) return;
+ if (moveIdx >= selectedVariation.moves.length) {
+ // Success: move to the next variation after a short delay
+ if (currentVariantIdx < variations.length - 1) {
+ setTimeout(() => {
+ setCurrentVariantIdx((idx) => idx + 1);
+ setMoveIdx(0);
+ }, 800);
+ }
+ }
+ }, [moveIdx, selectedVariation, currentVariantIdx, variations.length]);
+
+ // If all variations are completed
+ const allDone = currentVariantIdx >= variations.length;
+
+ // Progress management (persisted by 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 [];
+ }
+ }
+ );
+
+ // Mark a variation as completed
+ 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));
+ }
+ }
+ }, [
+ 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
+ ) {
+ // Find the first incomplete variation
+ 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
+ ) {
+ // 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, currentVariantIdx, setGame]);
+
+ // Reset progress
+ const handleResetProgress = useCallback(() => {
+ localStorage.removeItem(progressStorageKey);
+ setCompletedVariations([]);
+ setCurrentVariantIdx(0);
+ setMoveIdx(0);
+ setLastMistakeVisible(null);
+ setGame(new Chess());
+ }, [
+ setCompletedVariations,
+ setCurrentVariantIdx,
+ setMoveIdx,
+ setLastMistakeVisible,
+ setGame,
+ progressStorageKey,
+ ]);
+
+ // Determine the target square of the last move played (for 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]);
+
+ // Determine the type of icon to display (success/error)
+ const trainingFeedback = useMemo(() => {
+ 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",
+ };
+ }
+ // Show nothing if the move is correct
+ return undefined;
+ }, [lastMistakeVisible, lastMoveSquare]);
+
+ const screenSize = useScreenSize();
+
+ // Dynamic board size calculation
+ const boardSize = useMemo(() => {
+ const { width, height } = screenSize;
+ if (typeof window !== "undefined" && window.innerWidth < 900) {
+ return Math.max(180, Math.min(width, height - 150));
+ }
+ return Math.max(240, Math.min(width - 300, 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,
+ progressStorageKey,
+ ]);
+
+ return (
+
+ {/* Left area: evaluation bar + board */}
+
+ {selectedVariation && !allDone && game && (
+
+ )}
+
+
+ {/* Right area: progress panel, buttons, text */}
+
+ {/* Centered container for title and buttons */}
+
+ = (selectedVariation?.moves.length || 0)
+ }
+ />
+
+
+ {/* Progress bar at the bottom right, always visible */}
+
+
+
+
+
+
+ );
+}
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..8d08eca4 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 Trainer",
+ icon: "mdi:book-open-variant",
+ href: "/choose-opening", // Redirige désormais vers la page de choix d'ouverture
+ },
];
interface Props {
diff --git a/src/sections/play/board.tsx b/src/sections/play/board.tsx
index 209a5438..7c98db92 100644
--- a/src/sections/play/board.tsx
+++ b/src/sections/play/board.tsx
@@ -79,6 +79,7 @@ export default function BoardContainer() {
blackPlayer={black}
boardOrientation={playerColor}
currentPositionAtom={gameDataAtom}
+ hidePlayerHeaders={true}
/>
);
}