style : analysis card UI rework adjustments

This commit is contained in:
GuillaumeSD
2025-06-03 02:27:52 +02:00
parent d04c4d99df
commit 6310f7a375
13 changed files with 171 additions and 141 deletions

View File

@@ -54,7 +54,7 @@ export default function PrettyMoveSan({
</Typography> </Typography>
)} )}
<Typography component="span" {...typographyProps}> <Typography component="span" noWrap {...typographyProps}>
{text} {text}
{additionalText} {additionalText}
</Typography> </Typography>

View File

@@ -36,6 +36,7 @@ export default function GameAnalysis() {
const { reset: resetGame } = useChessActions(gameAtom); const { reset: resetGame } = useChessActions(gameAtom);
const [gameEval, setGameEval] = useAtom(gameEvalAtom); const [gameEval, setGameEval] = useAtom(gameEvalAtom);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const setBoardOrientation = useSetAtom(boardOrientationAtom); const setBoardOrientation = useSetAtom(boardOrientationAtom);
const router = useRouter(); const router = useRouter();
@@ -50,12 +51,12 @@ export default function GameAnalysis() {
} }
}, [gameId, setGameEval, setBoardOrientation, resetBoard, resetGame]); }, [gameId, setGameEval, setBoardOrientation, resetBoard, resetGame]);
const isGameLoaded = game.history().length > 0; const showMovesTab = game.history().length > 0 || board.history().length > 0;
useEffect(() => { useEffect(() => {
if (tab === 1 && !isGameLoaded) setTab(0); if (tab === 1 && !showMovesTab) setTab(0);
if (tab === 2 && !gameEval) setTab(0); if (tab === 2 && !gameEval) setTab(0);
}, [isGameLoaded, gameEval, tab]); }, [showMovesTab, gameEval, tab]);
return ( return (
<Grid container gap={4} justifyContent="space-evenly" alignItems="start"> <Grid container gap={4} justifyContent="space-evenly" alignItems="start">
@@ -65,7 +66,7 @@ export default function GameAnalysis() {
<Grid <Grid
container container
justifyContent="center" justifyContent="start"
alignItems="center" alignItems="center"
borderRadius={2} borderRadius={2}
border={1} border={1}
@@ -81,26 +82,24 @@ export default function GameAnalysis() {
maxWidth: "1200px", maxWidth: "1200px",
}} }}
rowGap={2} rowGap={2}
maxHeight={{ lg: "calc(95vh - 60px)", xs: "900px" }} height={{ xs: tab === 1 ? "40rem" : "auto", lg: "calc(95vh - 60px)" }}
display="grid" display="flex"
gridTemplateRows={ flexDirection="column"
gameEval flexWrap="nowrap"
? "repeat(2, auto) max-content fit-content(100%) fit-content(100%) auto"
: "repeat(2, auto) max-content fit-content(100%)"
}
size={{ size={{
xs: 12, xs: 12,
lg: "grow", lg: "grow",
}} }}
> >
{isLgOrGreater ? ( {isLgOrGreater ? (
<Box width="100%">
<PanelHeader key="analysis-panel-header" /> <PanelHeader key="analysis-panel-header" />
<Divider sx={{ marginX: "5%", marginTop: 2.5 }} />
</Box>
) : ( ) : (
<PanelToolBar key="review-panel-toolbar" /> <PanelToolBar key="review-panel-toolbar" />
)} )}
{isLgOrGreater && <Divider sx={{ marginX: "5%" }} />}
{!isLgOrGreater && !gameEval && <Divider sx={{ marginX: "5%" }} />} {!isLgOrGreater && !gameEval && <Divider sx={{ marginX: "5%" }} />}
{!isLgOrGreater && !gameEval && ( {!isLgOrGreater && !gameEval && (
<PanelHeader key="analysis-panel-header" /> <PanelHeader key="analysis-panel-header" />
@@ -108,6 +107,7 @@ export default function GameAnalysis() {
{!isLgOrGreater && ( {!isLgOrGreater && (
<Box <Box
width="95%"
sx={{ sx={{
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: "divider",
@@ -142,7 +142,7 @@ export default function GameAnalysis() {
sx={{ sx={{
textTransform: "none", textTransform: "none",
minHeight: 15, minHeight: 15,
display: isGameLoaded ? undefined : "none", display: showMovesTab ? undefined : "none",
padding: "5px 0em 12px", padding: "5px 0em 12px",
}} }}
disableFocusRipple disableFocusRipple
@@ -177,24 +177,24 @@ export default function GameAnalysis() {
id="tabContent0" id="tabContent0"
/> />
{isGameLoaded && (
<ClassificationTab <ClassificationTab
role="tabpanel" role="tabpanel"
hidden={tab !== 1 && !isLgOrGreater} hidden={tab !== 1 && !isLgOrGreater}
id="tabContent1" id="tabContent1"
/> />
)}
{isLgOrGreater && ( {isLgOrGreater && (
<Box> <Box width="100%">
<Divider sx={{ marginX: "5%", marginBottom: 1.5 }} /> <Divider sx={{ marginX: "5%", marginBottom: 1.5 }} />
<PanelToolBar key="review-panel-toolbar" /> <PanelToolBar key="review-panel-toolbar" />
</Box> </Box>
)} )}
{!isLgOrGreater && gameEval && <Divider sx={{ marginX: "5%" }} />}
{!isLgOrGreater && gameEval && ( {!isLgOrGreater && gameEval && (
<Box width="100%">
<Divider sx={{ marginX: "5%", marginBottom: 2.5 }} />
<PanelHeader key="analysis-panel-header" /> <PanelHeader key="analysis-panel-header" />
</Box>
)} )}
</Grid> </Grid>

View File

@@ -25,7 +25,7 @@ export default function BoardContainer() {
// 1200 is the lg layout breakpoint // 1200 is the lg layout breakpoint
if (window?.innerWidth < 1200) { if (window?.innerWidth < 1200) {
return Math.min(width, height - 150); return Math.min(width - 15, height - 150);
} }
return Math.min(width - 700, height * 0.92); return Math.min(width - 700, height * 0.92);

View File

@@ -0,0 +1,35 @@
import { Grid2 as Grid, Grid2Props as GridProps, List } from "@mui/material";
import LineEvaluation from "./lineEvaluation";
import {
boardAtom,
currentPositionAtom,
engineMultiPvAtom,
} from "../../../states";
import { useAtomValue } from "jotai";
import { LineEval } from "@/types/eval";
export default function EngineLines(props: GridProps) {
const board = useAtomValue(boardAtom);
const linesNumber = useAtomValue(engineMultiPvAtom);
const position = useAtomValue(currentPositionAtom);
const linesSkeleton: LineEval[] = Array.from({ length: linesNumber }).map(
(_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 })
);
const engineLines = position?.eval?.lines?.length
? position.eval.lines
: linesSkeleton;
if (board.isCheckmate()) return null;
return (
<Grid container justifyContent="center" alignItems="center" {...props}>
<List sx={{ width: "95%", padding: 0 }}>
{engineLines.map((line) => (
<LineEvaluation key={line.multiPv} line={line} />
))}
</List>
</Grid>
);
}

View File

@@ -1,7 +1,7 @@
import { LineEval } from "@/types/eval"; import { LineEval } from "@/types/eval";
import { ListItem, Skeleton, Typography } from "@mui/material"; import { ListItem, Skeleton, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom } from "../../states"; import { boardAtom } from "../../../states";
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess"; import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import PrettyMoveSan from "@/components/prettyMoveSan"; import PrettyMoveSan from "@/components/prettyMoveSan";

View File

@@ -1,45 +1,47 @@
import { Grid2 as Grid, Grid2Props as GridProps, List } from "@mui/material";
import { useAtomValue } from "jotai";
import { import {
boardAtom, Grid2 as Grid,
currentPositionAtom, Grid2Props as GridProps,
engineMultiPvAtom, Stack,
gameEvalAtom, Typography,
} from "../../states"; } from "@mui/material";
import LineEvaluation from "./lineEvaluation"; import { useAtomValue } from "jotai";
import { LineEval } from "@/types/eval"; import { boardAtom, gameAtom, gameEvalAtom } from "../../states";
import PlayersMetric from "./playersMetric"; import PlayersMetric from "./playersMetric";
import MoveInfo from "./moveInfo"; import MoveInfo from "./moveInfo";
import Opening from "./opening"; import Opening from "./opening";
import EngineLines from "./engineLines";
export default function AnalysisTab(props: GridProps) { export default function AnalysisTab(props: GridProps) {
const linesNumber = useAtomValue(engineMultiPvAtom);
const position = useAtomValue(currentPositionAtom);
const board = useAtomValue(boardAtom);
const gameEval = useAtomValue(gameEvalAtom); const gameEval = useAtomValue(gameEvalAtom);
const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const linesSkeleton: LineEval[] = Array.from({ length: linesNumber }).map( const boardHistory = board.history();
(_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 }) const gameHistory = game.history();
);
const engineLines = position?.eval?.lines?.length const isGameOver =
? position.eval.lines boardHistory.length > 0 &&
: linesSkeleton; (board.isCheckmate() ||
board.isDraw() ||
boardHistory.join() === gameHistory.join());
return ( return (
<Grid <Grid
container container
size={12} size={{ xs: 12, lg: gameEval ? 11 : 12 }}
justifyContent="center" justifyContent={{ xs: "center", lg: gameEval ? "start" : "center" }}
alignItems="start" alignItems="center"
height="100%" flexWrap={{ lg: gameEval ? "nowrap" : undefined }}
rowGap={0.8} gap={2}
marginY={{ lg: gameEval ? 1 : undefined }}
{...props} {...props}
sx={ sx={props.hidden ? { display: "none" } : props.sx}
props.hidden >
? { display: "none" } <Stack
: { overflow: "hidden", overflowY: "auto", ...props.sx } justifyContent="center"
} alignItems="center"
rowGap={1}
minWidth={gameEval ? "min(25rem, 95vw)" : undefined}
> >
{gameEval && ( {gameEval && (
<PlayersMetric <PlayersMetric
@@ -61,14 +63,14 @@ export default function AnalysisTab(props: GridProps) {
<Opening /> <Opening />
<Grid container justifyContent="center" alignItems="center" size={12}> {isGameOver && (
<List sx={{ width: { xs: "95%", lg: "90%" }, padding: 0 }}> <Typography align="center" fontSize="0.9rem" noWrap>
{!board.isCheckmate() && Game is over
engineLines.map((line) => ( </Typography>
<LineEvaluation key={line.multiPv} line={line} /> )}
))} </Stack>
</List>
</Grid> <EngineLines size={{ lg: gameEval ? undefined : 12 }} />
</Grid> </Grid>
); );
} }

View File

@@ -1,6 +1,6 @@
import { Grid2 as Grid, Skeleton, Stack, Typography } from "@mui/material"; import { Skeleton, Stack, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, currentPositionAtom, gameAtom } from "../../states"; import { boardAtom, currentPositionAtom } from "../../states";
import { useMemo } from "react"; import { useMemo } from "react";
import { moveLineUciToSan } from "@/lib/chess"; import { moveLineUciToSan } from "@/lib/chess";
import { MoveClassification } from "@/types/enums"; import { MoveClassification } from "@/types/enums";
@@ -10,7 +10,6 @@ import PrettyMoveSan from "@/components/prettyMoveSan";
export default function MoveInfo() { export default function MoveInfo() {
const position = useAtomValue(currentPositionAtom); const position = useAtomValue(currentPositionAtom);
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);
const game = useAtomValue(gameAtom);
const bestMove = position?.lastEval?.bestMove; const bestMove = position?.lastEval?.bestMove;
@@ -23,36 +22,22 @@ export default function MoveInfo() {
return moveLineUciToSan(lastPosition)(bestMove); return moveLineUciToSan(lastPosition)(bestMove);
}, [bestMove, board]); }, [bestMove, board]);
const boardHistory = board.history();
const gameHistory = game.history();
if (board.history().length === 0) return null; if (board.history().length === 0) return null;
const isGameOver =
boardHistory.length > 0 &&
(board.isCheckmate() ||
board.isDraw() ||
boardHistory.join() === gameHistory.join());
if (!bestMoveSan) { if (!bestMoveSan) {
return ( return (
<Grid <Stack direction="row" alignItems="center" columnGap={5} marginTop={0.8}>
size={12}
justifyItems="center"
alignContent="center"
marginTop={0.5}
>
<Skeleton <Skeleton
variant="rounded" variant="rounded"
animation="wave" animation="wave"
width={"12em"} width={"12em"}
sx={{ color: "transparent", maxWidth: "7vw", maxHeight: "3.5vw" }} sx={{ color: "transparent", maxWidth: "7vw" }}
> >
<Typography align="center" fontSize="0.9rem"> <Typography align="center" fontSize="0.9rem">
placeholder placeholder
</Typography> </Typography>
</Skeleton> </Skeleton>
</Grid> </Stack>
); );
} }
@@ -66,12 +51,13 @@ export default function MoveInfo() {
moveClassification !== MoveClassification.Perfect; moveClassification !== MoveClassification.Perfect;
return ( return (
<Grid <Stack
container direction="row"
columnGap={5} alignItems="center"
justifyContent="center" justifyContent="center"
size={12} columnGap={4}
marginTop={0.5} marginTop={0.5}
flexWrap="wrap"
> >
{moveClassification && ( {moveClassification && (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
@@ -121,13 +107,7 @@ export default function MoveInfo() {
/> />
</Stack> </Stack>
)} )}
</Stack>
{isGameOver && (
<Typography align="center" fontSize="0.9rem">
Game is over
</Typography>
)}
</Grid>
); );
} }

View File

@@ -12,7 +12,7 @@ export default function Opening() {
if (!opening) { if (!opening) {
return ( return (
<Grid size={12} justifyItems="center" alignContent="center"> <Grid justifyItems="center" alignContent="center">
<Skeleton <Skeleton
variant="rounded" variant="rounded"
animation="wave" animation="wave"
@@ -28,7 +28,7 @@ export default function Opening() {
} }
return ( return (
<Grid size={12}> <Grid>
<Typography align="center" fontSize="0.9rem"> <Typography align="center" fontSize="0.9rem">
{opening} {opening}
</Typography> </Typography>

View File

@@ -1,4 +1,4 @@
import { Grid2 as Grid, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
interface Props { interface Props {
title: string; title: string;
@@ -12,21 +12,20 @@ export default function PlayersMetric({
blackValue, blackValue,
}: Props) { }: Props) {
return ( return (
<Grid <Stack
container
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
flexDirection="row"
columnGap={{ xs: "8vw", md: 10 }} columnGap={{ xs: "8vw", md: 10 }}
size={12}
> >
<ValueBlock value={whiteValue} color="white" /> <ValueBlock value={whiteValue} color="white" />
<Typography align="center" fontSize="0.8em"> <Typography align="center" fontSize="0.8em" noWrap>
{title} {title}
</Typography> </Typography>
<ValueBlock value={blackValue} color="black" /> <ValueBlock value={blackValue} color="black" />
</Grid> </Stack>
); );
} }
@@ -50,6 +49,7 @@ const ValueBlock = ({
padding={0.8} padding={0.8}
fontWeight="500" fontWeight="500"
border="1px solid #424242" border="1px solid #424242"
noWrap
> >
{value} {value}
</Typography> </Typography>

View File

@@ -8,8 +8,8 @@ export default function ClassificationTab(props: GridProps) {
container container
justifyContent="center" justifyContent="center"
alignItems="start" alignItems="start"
height="100%" size={12}
maxHeight={{ xs: "18rem", lg: "none" }} flexGrow={1}
{...props} {...props}
sx={ sx={
props.hidden ? { display: "none" } : { overflow: "hidden", ...props.sx } props.hidden ? { display: "none" } : { overflow: "hidden", ...props.sx }

View File

@@ -2,15 +2,19 @@ import { Grid2 as Grid } from "@mui/material";
import MovesLine from "./movesLine"; import MovesLine from "./movesLine";
import { useMemo } from "react"; import { useMemo } from "react";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { gameAtom, gameEvalAtom } from "../../../states"; import { boardAtom, gameAtom, gameEvalAtom } from "../../../states";
import { MoveClassification } from "@/types/enums"; import { MoveClassification } from "@/types/enums";
export default function MovesPanel() { export default function MovesPanel() {
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const gameEval = useAtomValue(gameEvalAtom); const gameEval = useAtomValue(gameEvalAtom);
const gameMoves = useMemo(() => { const gameMoves = useMemo(() => {
const history = game.history(); const gameHistory = game.history();
const boardHistory = board.history();
const history = gameHistory.length ? gameHistory : boardHistory;
if (!history.length) return undefined; if (!history.length) return undefined;
const moves: { san: string; moveClassification?: MoveClassification }[][] = const moves: { san: string; moveClassification?: MoveClassification }[][] =
@@ -20,14 +24,18 @@ export default function MovesPanel() {
const items = [ const items = [
{ {
san: history[i], san: history[i],
moveClassification: gameEval?.positions[i + 1]?.moveClassification, moveClassification: gameHistory.length
? gameEval?.positions[i + 1]?.moveClassification
: undefined,
}, },
]; ];
if (history[i + 1]) { if (history[i + 1]) {
items.push({ items.push({
san: history[i + 1], san: history[i + 1],
moveClassification: gameEval?.positions[i + 2]?.moveClassification, moveClassification: gameHistory.length
? gameEval?.positions[i + 2]?.moveClassification
: undefined,
}); });
} }
@@ -35,17 +43,19 @@ export default function MovesPanel() {
} }
return moves; return moves;
}, [game, gameEval]); }, [game, board, gameEval]);
if (!gameMoves?.length) return null;
return ( return (
<Grid <Grid
container container
justifyContent="center" justifyContent="center"
alignItems="start" alignItems="start"
gap={0.7} gap={0.5}
paddingY={1} paddingY={1}
sx={{ scrollbarWidth: "thin", overflowY: "auto" }} sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
height="100%" maxHeight="100%"
size={6} size={6}
id="moves-panel" id="moves-panel"
> >

View File

@@ -23,6 +23,7 @@ export default function MoveItem({
moveColor, moveColor,
}: Props) { }: Props) {
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const { goToMove } = useChessActions(boardAtom); const { goToMove } = useChessActions(boardAtom);
const position = useAtomValue(currentPositionAtom); const position = useAtomValue(currentPositionAtom);
const color = getMoveColor(moveClassification); const color = getMoveColor(moveClassification);
@@ -42,7 +43,8 @@ export default function MoveItem({
const handleClick = () => { const handleClick = () => {
if (isCurrentMove) return; if (isCurrentMove) return;
goToMove(moveIdx, game); const gameToUse = game.moveNumber() > 1 ? game : board;
goToMove(moveIdx, gameToUse);
}; };
return ( return (
@@ -82,7 +84,11 @@ export default function MoveItem({
/> />
)} )}
<PrettyMoveSan san={san} color={moveColor} /> <PrettyMoveSan
san={san}
color={moveColor}
typographyProps={{ fontSize: "0.9rem" }}
/>
</Grid> </Grid>
); );
} }

View File

@@ -87,20 +87,17 @@ export default function GraphTab(props: GridProps) {
<Grid <Grid
container container
justifyContent="center" justifyContent="center"
alignItems="start" alignItems="center"
height="100%" minHeight="min(10rem, 8vh)"
height={{ xs: "8rem", lg: "none" }}
maxHeight="10rem"
{...props} {...props}
sx={ sx={props.hidden ? { display: "none" } : props.sx}
props.hidden size={12}
? { display: "none" }
: { overflow: "hidden", overflowY: "auto", ...props.sx }
}
> >
<Box <Box
width="max(35rem, 90%)" height="100%"
maxWidth="100%" width={{ xs: "100%", lg: "90%" }}
height="min(8rem, 8vh)"
maxHeight="6rem"
sx={{ sx={{
backgroundColor: "#2e2e2e", backgroundColor: "#2e2e2e",
borderRadius: "15px", borderRadius: "15px",