Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions src/components/LinearProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Grid
container
Expand All @@ -22,13 +28,17 @@ const LinearProgressBar = (
columnGap={2}
size={12}
>
<Typography variant="caption" align="center">
{props.label}
<Typography variant="caption" align="center" aria-label="progress-label">
{label}
</Typography>
<Grid sx={{ width: "100%" }}>
<LinearProgress
variant="determinate"
{...props}
value={value}
aria-valuenow={value}
aria-valuemax={100}
aria-label={label}
{...rest}
sx={(theme) => ({
borderRadius: "5px",
height: "5px",
Expand All @@ -45,7 +55,7 @@ const LinearProgressBar = (
</Grid>
<Grid>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value
value
)}%`}</Typography>
</Grid>
</Grid>
Expand Down
49 changes: 49 additions & 0 deletions src/components/OpeningControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

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 (
<Stack direction="row" spacing={2} sx={{ width: '100%' }}>
<Button
variant="outlined"
color="primary"
fullWidth
disabled={moveIdx >= selectedVariationMovesLength || allDone || disabled}
onClick={onSkip}
>
Skip variation
</Button>
<Button
variant="outlined"
color="primary"
fullWidth
onClick={onReset}
disabled={disabled}
>
Reset progress
</Button>
</Stack>
);
}

export default memo(OpeningControls);
58 changes: 58 additions & 0 deletions src/components/OpeningProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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<number[]>(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 (
<Box
width={{ xs: "100%", sm: 320, md: 340 }}
sx={{
mt: { xs: 2, md: 3 },
mb: { xs: 0, md: 1 },
px: { xs: 0, sm: 1 },
alignSelf: "flex-end",
position: "relative",
left: 0,
bottom: 0,
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 1,
}}
>
<Box minWidth={48}>
<span style={{ fontSize: 14, color: theme.palette.text.secondary }}>{label}</span>
</Box>
<Box flex={1} minWidth={0}>
<LinearProgressBar value={percent} label={""} />
</Box>
</Box>
);
};

export default memo(OpeningProgress);
44 changes: 44 additions & 0 deletions src/components/VariationHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

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<VariationHeaderProps> = ({
variationName,
trainingMode,
onSetTrainingMode,
variationComplete,
}) => (
<>
<Typography variant="h4" gutterBottom sx={{ mb: 2, wordBreak: 'break-word', textAlign: 'center', width: '100%' }}>
{variationName}
</Typography>

<Stack direction="row" spacing={2} sx={{ mb: 3, justifyContent: 'center', width: '100%' }}>
<Button variant={trainingMode ? "contained" : "outlined"} onClick={() => onSetTrainingMode(true)} fullWidth>
Training Mode
</Button>
<Button variant={!trainingMode ? "contained" : "outlined"} onClick={() => onSetTrainingMode(false)} fullWidth>
Learning Mode
</Button>
</Stack>
{variationComplete ? (
<Typography color="success.main" sx={{ mb: 2, textAlign: 'center' }}>Variation complete! Next variation loading…</Typography>
) : trainingMode ? (
<Typography color="text.secondary" sx={{ mb: 2, textAlign: 'center' }}>Play the correct move to continue.</Typography>
) : (
<Typography color="text.secondary" sx={{ mb: 2, textAlign: 'center' }}>Play the move indicated by the arrow to continue.</Typography>
)}
</>
);

export default memo(VariationHeader);
55 changes: 41 additions & 14 deletions src/components/board/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +40,9 @@ export interface Props {
showBestMoveArrow?: boolean;
showPlayerMoveIconAtom?: PrimitiveAtom<boolean>;
showEvaluationBar?: boolean;
trainingFeedback?: TrainingFeedback;
bestMoveUci?: string;
hidePlayerHeaders?: boolean;
}

export default function Board({
Expand All @@ -48,6 +57,9 @@ export default function Board({
showBestMoveArrow = false,
showPlayerMoveIconAtom,
showEvaluationBar = false,
trainingFeedback,
bestMoveUci,
hidePlayerHeaders = false,
}: Props) {
const boardRef = useRef<HTMLDivElement>(null);
const game = useAtomValue(gameAtom);
Expand Down Expand Up @@ -208,9 +220,19 @@ 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 &&
Expand All @@ -226,25 +248,25 @@ 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({
currentPositionAtom: currentPositionAtom,
clickedSquaresAtom,
playableSquaresAtom,
showPlayerMoveIconAtom,
trainingFeedback, // nouvelle prop transmise
});
}, [
currentPositionAtom,
clickedSquaresAtom,
playableSquaresAtom,
showPlayerMoveIconAtom,
trainingFeedback,
]);

const customPieces = useMemo(
Expand Down Expand Up @@ -306,11 +328,14 @@ export default function Board({
paddingLeft={showEvaluationBar ? 2 : 0}
size="grow"
>
<PlayerHeader
color={boardOrientation === Color.White ? Color.Black : Color.White}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? blackPlayer : whitePlayer}
/>
{/* Enlève l'affichage des PlayerHeader si hidePlayerHeaders est true */}
{!hidePlayerHeaders && (
<PlayerHeader
color={boardOrientation === Color.White ? Color.Black : Color.White}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? blackPlayer : whitePlayer}
/>
)}

<Grid
container
Expand Down Expand Up @@ -342,11 +367,13 @@ export default function Board({
/>
</Grid>

<PlayerHeader
color={boardOrientation}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? whitePlayer : blackPlayer}
/>
{!hidePlayerHeaders && (
<PlayerHeader
color={boardOrientation}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? whitePlayer : blackPlayer}
/>
)}
</Grid>
</Grid>
);
Expand Down
25 changes: 25 additions & 0 deletions src/components/board/squareRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ export interface Props {
clickedSquaresAtom: PrimitiveAtom<Square[]>;
playableSquaresAtom: PrimitiveAtom<Square[]>;
showPlayerMoveIconAtom?: PrimitiveAtom<boolean>;
trainingFeedback?: {
square: string;
icon: string;
alt: string;
};
}

export function getSquareRenderer({
currentPositionAtom,
clickedSquaresAtom,
playableSquaresAtom,
showPlayerMoveIconAtom = atom(false),
trainingFeedback,
}: Props) {
const squareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
(props, ref) => {
Expand Down Expand Up @@ -64,6 +70,25 @@ export function getSquareRenderer({
{children}
{highlightSquareStyle && <div style={highlightSquareStyle} />}
{playableSquareStyle && <div style={playableSquareStyle} />}
{/* Affichage de l’icône de feedback training si demandé et sur la bonne case */}
{trainingFeedback && trainingFeedback.square === square && (
<img
src={trainingFeedback.icon}
alt={trainingFeedback.alt}
style={{
position: "absolute",
top: 0,
right: 0,
transform: "translate(35%,-35%)",
width: 28,
height: 28,
zIndex: 120,
pointerEvents: "none",
opacity: 0.95,
}}
/>
)}
{/* Aucun affichage de message texte d'erreur ici, seulement l'icône */}
{moveClassification && showPlayerMoveIcon && square === toSquare && (
<Image
src={`/icons/${moveClassification}.png`}
Expand Down
Loading