Squashed commit of the following:
commit dfc79cf287823383a25a650d5788ee5250b1c316
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sun May 11 01:32:35 2025 +0200
fix : style
commit bccfa5a3358302c2f037cc2dcfbd0a1df5e2974e
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sun May 11 01:01:12 2025 +0200
feat : players clocks v1
commit 5f65009f200686433904710d5f9ceb1ba166fa9d
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sat May 10 21:58:02 2025 +0200
fix : merge issues
commit f93dc6104e2d3fbb60088f578c2d1f13bf6519e9
Merge: a9f3728 fea1f3f
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sat May 10 21:53:11 2025 +0200
Merge branch 'main' into feat/add-players-clocks
commit a9f372808ef403dfb823c4cf93c837412cc55c53
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date: Mon Jan 6 23:10:28 2025 +0100
fix : rename
commit aedf9c252023bebe4da4327b7526371fa75b7b3e
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date: Sun Jan 5 17:30:27 2025 +0100
feat : add players clocks
This commit is contained in:
@@ -8,7 +8,7 @@ export interface Props {
|
||||
color: Color;
|
||||
}
|
||||
|
||||
const PIECE_SCALE = 0.6;
|
||||
const PIECE_SCALE = 0.55;
|
||||
|
||||
export default function CapturedPieces({ fen, color }: Props) {
|
||||
const cssProps = useMemo(() => {
|
||||
@@ -22,7 +22,7 @@ export default function CapturedPieces({ fen, color }: Props) {
|
||||
}, [fen, color]);
|
||||
|
||||
return (
|
||||
<Grid container alignItems="end" columnGap={0.6} size="auto">
|
||||
<Grid container alignItems="end" columnGap={0.5} size="auto">
|
||||
{cssProps.map((cssProp, i) => (
|
||||
<span
|
||||
key={i}
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function Board({
|
||||
|
||||
<Grid
|
||||
container
|
||||
rowGap={1}
|
||||
rowGap={1.5}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
paddingLeft={showEvaluationBar ? 2 : 0}
|
||||
@@ -278,7 +278,7 @@ export default function Board({
|
||||
>
|
||||
<PlayerHeader
|
||||
color={boardOrientation === Color.White ? Color.Black : Color.White}
|
||||
fen={gameFen}
|
||||
gameAtom={gameAtom}
|
||||
player={boardOrientation === Color.White ? blackPlayer : whitePlayer}
|
||||
/>
|
||||
|
||||
@@ -318,7 +318,7 @@ export default function Board({
|
||||
|
||||
<PlayerHeader
|
||||
color={boardOrientation}
|
||||
fen={gameFen}
|
||||
gameAtom={gameAtom}
|
||||
player={boardOrientation === Color.White ? whitePlayer : blackPlayer}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -1,35 +1,113 @@
|
||||
import { Color } from "@/types/enums";
|
||||
import { Player } from "@/types/game";
|
||||
import { Avatar, Grid2 as Grid, Typography } from "@mui/material";
|
||||
import { Avatar, Grid2 as Grid, Stack, Typography } from "@mui/material";
|
||||
import CapturedPieces from "./capturedPieces";
|
||||
import { PrimitiveAtom, useAtomValue } from "jotai";
|
||||
import { Chess } from "chess.js";
|
||||
import { useMemo } from "react";
|
||||
import { getPaddedNumber } from "@/lib/helpers";
|
||||
|
||||
export interface Props {
|
||||
player: Player;
|
||||
color: Color;
|
||||
fen: string;
|
||||
gameAtom: PrimitiveAtom<Chess>;
|
||||
}
|
||||
|
||||
export default function PlayerHeader({ color, player, fen }: Props) {
|
||||
export default function PlayerHeader({ color, player, gameAtom }: Props) {
|
||||
const game = useAtomValue(gameAtom);
|
||||
|
||||
const gameFen = game.fen();
|
||||
|
||||
const clock = useMemo(() => {
|
||||
const turn = game.turn();
|
||||
|
||||
if (turn === color) {
|
||||
const history = game.history({ verbose: true });
|
||||
const previousFen = history.at(-1)?.before;
|
||||
|
||||
const comment = game
|
||||
.getComments()
|
||||
.find(({ fen }) => fen === previousFen)?.comment;
|
||||
|
||||
return getClock(comment);
|
||||
}
|
||||
|
||||
const comment = game.getComment();
|
||||
return getClock(comment);
|
||||
}, [game, color]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
columnGap={2}
|
||||
size={12}
|
||||
>
|
||||
{player.avatarUrl && (
|
||||
<Stack direction="row">
|
||||
<Avatar
|
||||
src={player.avatarUrl}
|
||||
alt={player.name}
|
||||
variant="circular"
|
||||
sx={{ width: 24, height: 24 }}
|
||||
/>
|
||||
)}
|
||||
<Typography>
|
||||
{player.rating ? `${player.name} (${player.rating})` : player.name}
|
||||
</Typography>
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: color === Color.White ? "white" : "black",
|
||||
color: color === Color.White ? "black" : "white",
|
||||
border: "1px solid black",
|
||||
}}
|
||||
>
|
||||
{player.name[0].toUpperCase()}
|
||||
</Avatar>
|
||||
|
||||
<CapturedPieces fen={fen} color={color} />
|
||||
<Stack marginLeft={1}>
|
||||
<Stack direction="row">
|
||||
<Typography fontSize="0.9rem">{player.name}</Typography>
|
||||
|
||||
{player.rating && (
|
||||
<Typography marginLeft={0.5} fontSize="0.9rem" fontWeight="200">
|
||||
({player.rating})
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<CapturedPieces fen={gameFen} color={color} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{clock && (
|
||||
<Typography
|
||||
align="center"
|
||||
sx={{
|
||||
backgroundColor: color === Color.White ? "white" : "black",
|
||||
color: color === Color.White ? "black" : "white",
|
||||
}}
|
||||
borderRadius="5px"
|
||||
padding={0.8}
|
||||
border="1px solid #424242"
|
||||
width="5rem"
|
||||
textAlign="right"
|
||||
>
|
||||
{clock.hours ? `${clock.hours}:` : ""}
|
||||
{getPaddedNumber(clock.minutes)}:{getPaddedNumber(clock.seconds)}
|
||||
{clock.hours || clock.minutes || clock.seconds > 20
|
||||
? ""
|
||||
: `.${clock.tenths}`}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const getClock = (comment: string | undefined) => {
|
||||
if (!comment) return undefined;
|
||||
|
||||
const match = comment.match(/\[%clk (\d+):(\d+):(\d+)(?:\.(\d*))?\]/);
|
||||
if (!match) return undefined;
|
||||
|
||||
return {
|
||||
hours: parseInt(match[1]),
|
||||
minutes: parseInt(match[2]),
|
||||
seconds: parseInt(match[3]),
|
||||
tenths: match[4] ? parseInt(match[4]) : 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { setGameHeaders } from "@/lib/chess";
|
||||
import { getGameFromPgn, setGameHeaders } from "@/lib/chess";
|
||||
import {
|
||||
playGameEndSound,
|
||||
playIllegalMoveSound,
|
||||
playSoundFromMove,
|
||||
} from "@/lib/sounds";
|
||||
import { Player } from "@/types/game";
|
||||
import { Chess, Move } from "chess.js";
|
||||
import { Chess, Move, DEFAULT_POSITION } from "chess.js";
|
||||
import { PrimitiveAtom, useAtom } from "jotai";
|
||||
import { useCallback } from "react";
|
||||
|
||||
@@ -43,8 +43,9 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||
if (game.history().length === 0) {
|
||||
const pgnSplitted = game.pgn().split("]");
|
||||
if (
|
||||
pgnSplitted.at(-1)?.includes("1-0") ||
|
||||
pgnSplitted.at(-1) === "\n *"
|
||||
["1-0", "0-1", "1/2-1/2", "*"].includes(
|
||||
pgnSplitted.at(-1)?.trim() ?? ""
|
||||
)
|
||||
) {
|
||||
newGame.loadPgn(pgnSplitted.slice(0, -1).join("]") + "]");
|
||||
return newGame;
|
||||
@@ -55,11 +56,31 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||
return newGame;
|
||||
}, [game]);
|
||||
|
||||
const resetToStartingPosition = useCallback(
|
||||
(pgn?: string) => {
|
||||
const newGame = pgn ? getGameFromPgn(pgn) : copyGame();
|
||||
newGame.load(newGame.getHeaders().FEN || DEFAULT_POSITION, {
|
||||
preserveHeaders: true,
|
||||
});
|
||||
setGame(newGame);
|
||||
},
|
||||
[copyGame, setGame]
|
||||
);
|
||||
|
||||
const makeMove = useCallback(
|
||||
(move: { from: string; to: string; promotion?: string }): Move | null => {
|
||||
(params: {
|
||||
from: string;
|
||||
to: string;
|
||||
promotion?: string;
|
||||
comment?: string;
|
||||
}): Move | null => {
|
||||
const newGame = copyGame();
|
||||
|
||||
try {
|
||||
const { comment, ...move } = params;
|
||||
const result = newGame.move(move);
|
||||
if (comment) newGame.setComment(comment);
|
||||
|
||||
setGame(newGame);
|
||||
playSoundFromMove(result);
|
||||
return result;
|
||||
@@ -103,5 +124,12 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||
[setGame]
|
||||
);
|
||||
|
||||
return { setPgn, reset, makeMove, undoMove, goToMove };
|
||||
return {
|
||||
setPgn,
|
||||
reset,
|
||||
makeMove,
|
||||
undoMove,
|
||||
goToMove,
|
||||
resetToStartingPosition,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -280,17 +280,6 @@ const getPieceValue = (piece: PieceSymbol): number => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getStartingFen = (
|
||||
params: { pgn: string } | { game: Chess }
|
||||
): string => {
|
||||
const game = "game" in params ? params.game : getGameFromPgn(params.pgn);
|
||||
|
||||
const history = game.history({ verbose: true });
|
||||
if (!history.length) return game.fen();
|
||||
|
||||
return history[0].before;
|
||||
};
|
||||
|
||||
export const isCheck = (fen: string): boolean => {
|
||||
const game = new Chess(fen);
|
||||
return game.inCheck();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChessComGame } from "@/types/chessCom";
|
||||
import { getPaddedMonth } from "./helpers";
|
||||
import { getPaddedNumber } from "./helpers";
|
||||
|
||||
export const getChessComUserRecentGames = async (
|
||||
username: string
|
||||
@@ -7,7 +7,7 @@ export const getChessComUserRecentGames = async (
|
||||
const date = new Date();
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth() + 1;
|
||||
const paddedMonth = getPaddedMonth(month);
|
||||
const paddedMonth = getPaddedNumber(month);
|
||||
|
||||
const res = await fetch(
|
||||
`https://api.chess.com/pub/player/${username}/games/${year}/${paddedMonth}`
|
||||
@@ -21,7 +21,7 @@ export const getChessComUserRecentGames = async (
|
||||
|
||||
if (games.length < 50) {
|
||||
const previousMonth = month === 1 ? 12 : month - 1;
|
||||
const previousPaddedMonth = getPaddedMonth(previousMonth);
|
||||
const previousPaddedMonth = getPaddedNumber(previousMonth);
|
||||
const yearToFetch = previousMonth === 12 ? year - 1 : year;
|
||||
|
||||
const resPreviousMonth = await fetch(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const getPaddedMonth = (month: number) => {
|
||||
export const getPaddedNumber = (month: number) => {
|
||||
return month < 10 ? `0${month}` : month;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export const getLichessUserRecentGames = async (
|
||||
username: string
|
||||
): Promise<LichessGame[]> => {
|
||||
const res = await fetch(
|
||||
`https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc`,
|
||||
`https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc&clocks=true`,
|
||||
{ method: "GET", headers: { accept: "application/x-ndjson" } }
|
||||
);
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function GameReview() {
|
||||
maxWidth: "1200px",
|
||||
}}
|
||||
rowGap={2}
|
||||
maxHeight={{ lg: "calc(95vh - 130px)", xs: "900px" }}
|
||||
maxHeight={{ lg: "calc(95vh - 80px)", xs: "900px" }}
|
||||
display="grid"
|
||||
gridTemplateRows="repeat(3, auto) fit-content(100%)"
|
||||
marginTop={isLgOrGreater && window.innerHeight > 780 ? 4 : 0}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function BoardContainer() {
|
||||
return Math.min(width, height - 150);
|
||||
}
|
||||
|
||||
return Math.min(width - 700, height * 0.95);
|
||||
return Math.min(width - 700, height * 0.92);
|
||||
}, [screenSize]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,9 +42,20 @@ export const useCurrentPosition = (engineName?: EngineName) => {
|
||||
if (gameEval) {
|
||||
const evalIndex = boardHistory.length;
|
||||
|
||||
position.eval = gameEval.positions[evalIndex];
|
||||
position.eval = {
|
||||
...gameEval.positions[evalIndex],
|
||||
lines: gameEval.positions[evalIndex].lines.slice(0, multiPv),
|
||||
};
|
||||
position.lastEval =
|
||||
evalIndex > 0 ? gameEval.positions[evalIndex - 1] : undefined;
|
||||
evalIndex > 0
|
||||
? {
|
||||
...gameEval.positions[evalIndex - 1],
|
||||
lines: gameEval.positions[evalIndex - 1].lines.slice(
|
||||
0,
|
||||
multiPv
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +81,12 @@ export const useCurrentPosition = (engineName?: EngineName) => {
|
||||
(savedEval.lines?.length ?? 0) >= multiPv &&
|
||||
(savedEval.lines[0].depth ?? 0) >= depth
|
||||
) {
|
||||
setPartialEval?.({
|
||||
const positionEval: PositionEval = {
|
||||
...savedEval,
|
||||
lines: savedEval.lines.slice(0, multiPv),
|
||||
});
|
||||
return savedEval;
|
||||
};
|
||||
setPartialEval?.(positionEval);
|
||||
return positionEval;
|
||||
}
|
||||
|
||||
const rawPositionEval = await engine.evaluatePositionWithUpdate({
|
||||
|
||||
@@ -12,7 +12,10 @@ export default function Opening() {
|
||||
}
|
||||
if (!lastMove) return null;
|
||||
|
||||
const opening = position?.eval?.opening || lastOpening;
|
||||
const opening =
|
||||
position?.eval?.opening && !position?.eval?.opening.includes("Unknown")
|
||||
? position.eval.opening
|
||||
: lastOpening;
|
||||
if (opening && opening !== lastOpening) {
|
||||
setLastOpening(opening);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,12 @@ import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { Chess } from "chess.js";
|
||||
import { useRouter } from "next/router";
|
||||
import { getStartingFen } from "@/lib/chess";
|
||||
|
||||
export default function LoadGame() {
|
||||
const router = useRouter();
|
||||
const game = useAtomValue(gameAtom);
|
||||
const { setPgn: setGamePgn } = useChessActions(gameAtom);
|
||||
const { reset: resetBoard } = useChessActions(boardAtom);
|
||||
const { resetToStartingPosition: resetBoard } = useChessActions(boardAtom);
|
||||
const { gameFromUrl } = useGameDatabase();
|
||||
const setEval = useSetAtom(gameEvalAtom);
|
||||
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
||||
@@ -26,7 +25,7 @@ export default function LoadGame() {
|
||||
|
||||
const resetAndSetGamePgn = useCallback(
|
||||
(pgn: string) => {
|
||||
resetBoard({ fen: getStartingFen({ pgn }) });
|
||||
resetBoard(pgn);
|
||||
setEval(undefined);
|
||||
setGamePgn(pgn);
|
||||
},
|
||||
|
||||
@@ -8,11 +8,10 @@ import NextMoveButton from "./nextMoveButton";
|
||||
import GoToLastPositionButton from "./goToLastPositionButton";
|
||||
import SaveButton from "./saveButton";
|
||||
import { useEffect } from "react";
|
||||
import { getStartingFen } from "@/lib/chess";
|
||||
|
||||
export default function PanelToolBar() {
|
||||
const board = useAtomValue(boardAtom);
|
||||
const { reset: resetBoard, undoMove: undoBoardMove } =
|
||||
const { resetToStartingPosition: resetBoard, undoMove: undoBoardMove } =
|
||||
useChessActions(boardAtom);
|
||||
|
||||
const boardHistory = board.history();
|
||||
@@ -24,7 +23,7 @@ export default function PanelToolBar() {
|
||||
if (e.key === "ArrowLeft") {
|
||||
undoBoardMove();
|
||||
} else if (e.key === "ArrowDown") {
|
||||
resetBoard({ fen: getStartingFen({ game: board }) });
|
||||
resetBoard();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,7 +41,7 @@ export default function PanelToolBar() {
|
||||
<Tooltip title="Reset board">
|
||||
<Grid>
|
||||
<IconButton
|
||||
onClick={() => resetBoard({ fen: getStartingFen({ game: board }) })}
|
||||
onClick={() => resetBoard()}
|
||||
disabled={boardHistory.length === 0}
|
||||
sx={{ paddingX: 1.2, paddingY: 0.5 }}
|
||||
>
|
||||
|
||||
@@ -22,12 +22,16 @@ export default function NextMoveButton() {
|
||||
|
||||
const nextMoveIndex = boardHistory.length;
|
||||
const nextMove = game.history({ verbose: true })[nextMoveIndex];
|
||||
const comment = game
|
||||
.getComments()
|
||||
.find((c) => c.fen === nextMove.after)?.comment;
|
||||
|
||||
if (nextMove) {
|
||||
makeBoardMove({
|
||||
from: nextMove.from,
|
||||
to: nextMove.to,
|
||||
promotion: nextMove.promotion,
|
||||
comment,
|
||||
});
|
||||
}
|
||||
}, [isButtonEnabled, boardHistory, game, makeBoardMove]);
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function Layout({ children }: PropsWithChildren) {
|
||||
darkMode={useDarkMode}
|
||||
switchDarkMode={() => setDarkMode((val) => !val)}
|
||||
/>
|
||||
<main style={{ margin: "2em 2vw" }}>{children}</main>
|
||||
<main style={{ margin: "3vh 2vw" }}>{children}</main>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function BoardContainer() {
|
||||
return Math.min(width, height - 150);
|
||||
}
|
||||
|
||||
return Math.min(width - 300, height * 0.85);
|
||||
return Math.min(width - 300, height * 0.83);
|
||||
}, [screenSize]);
|
||||
|
||||
useGameData(gameAtom, gameDataAtom);
|
||||
|
||||
Reference in New Issue
Block a user