feat : mui & state refacto
This commit is contained in:
951
package-lock.json
generated
951
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,16 @@
|
|||||||
"deploy": "firebase deploy --project=freechess --only hosting"
|
"deploy": "firebase deploy --project=freechess --only hosting"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@fontsource/roboto": "^5.0.3",
|
||||||
|
"@iconify/react": "^4.1.0",
|
||||||
|
"@mui/material": "^5.13.4",
|
||||||
"chess.js": "^1.0.0-beta.7",
|
"chess.js": "^1.0.0-beta.7",
|
||||||
"jotai": "^2.6.4",
|
"jotai": "^2.6.4",
|
||||||
"next": "13.5.6",
|
"next": "13.5.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-chessboard": "^4.4.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
23
src/components/NavLink.tsx
Normal file
23
src/components/NavLink.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Link as MuiLink } from "@mui/material";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export default function NavLink({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MuiLink
|
||||||
|
component={NextLink}
|
||||||
|
href={href}
|
||||||
|
underline="none"
|
||||||
|
color="inherit"
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MuiLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/hooks/useChess.ts
Normal file
36
src/hooks/useChess.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Chess } from "chess.js";
|
||||||
|
import { PrimitiveAtom, useAtom } from "jotai";
|
||||||
|
|
||||||
|
export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||||
|
const [game, setGame] = useAtom(chessAtom);
|
||||||
|
|
||||||
|
const setPgn = (pgn: string) => {
|
||||||
|
const newGame = new Chess();
|
||||||
|
newGame.loadPgn(pgn);
|
||||||
|
setGame(newGame);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setGame(new Chess());
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyGame = () => {
|
||||||
|
const newGame = new Chess();
|
||||||
|
newGame.loadPgn(game.pgn());
|
||||||
|
return newGame;
|
||||||
|
};
|
||||||
|
|
||||||
|
const move = (move: { from: string; to: string; promotion?: string }) => {
|
||||||
|
const newGame = copyGame();
|
||||||
|
newGame.move(move);
|
||||||
|
setGame(newGame);
|
||||||
|
};
|
||||||
|
|
||||||
|
const undo = () => {
|
||||||
|
const newGame = copyGame();
|
||||||
|
newGame.undo();
|
||||||
|
setGame(newGame);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { setPgn, reset, move, undo };
|
||||||
|
};
|
||||||
29
src/hooks/useLocalStorage.ts
Normal file
29
src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type SetValue<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T
|
||||||
|
): [T, SetValue<T>] {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
if (item) {
|
||||||
|
setStoredValue(parseJSON<T>(item));
|
||||||
|
}
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
const setValue: SetValue<T> = (value) => {
|
||||||
|
const newValue = value instanceof Function ? value(storedValue) : value;
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(newValue));
|
||||||
|
setStoredValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJSON<T>(value: string): T {
|
||||||
|
return value === "undefined" ? undefined : JSON.parse(value);
|
||||||
|
}
|
||||||
268
src/lib/board.ts
268
src/lib/board.ts
@@ -1,268 +0,0 @@
|
|||||||
const boardFlipped = false;
|
|
||||||
const currentMoveIndex = 0;
|
|
||||||
const reportResults: any = undefined;
|
|
||||||
|
|
||||||
export async function drawBoard(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
fen = startingPositionFen
|
|
||||||
) {
|
|
||||||
// Draw surface of board
|
|
||||||
let colours = ["#f6dfc0", "#b88767"];
|
|
||||||
|
|
||||||
for (let y = 0; y < 8; y++) {
|
|
||||||
for (let x = 0; x < 8; x++) {
|
|
||||||
ctx.fillStyle = colours[(x + y) % 2];
|
|
||||||
|
|
||||||
ctx.fillRect(x * 90, y * 90, 90, 90);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw coordinates
|
|
||||||
ctx.font = "20px Arial";
|
|
||||||
|
|
||||||
let files = "abcdefgh".split("");
|
|
||||||
for (let x = 0; x < 8; x++) {
|
|
||||||
ctx.fillStyle = colours[x % 2];
|
|
||||||
ctx.fillText(boardFlipped ? files[7 - x] : files[x], x * 90 + 5, 715);
|
|
||||||
}
|
|
||||||
for (let y = 0; y < 8; y++) {
|
|
||||||
ctx.fillStyle = colours[(y + 1) % 2];
|
|
||||||
ctx.fillText(
|
|
||||||
boardFlipped ? (y + 1).toString() : (8 - y).toString(),
|
|
||||||
5,
|
|
||||||
y * 90 + 22
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw last move highlight and top move arrows
|
|
||||||
let lastMove = reportResults?.positions[currentMoveIndex];
|
|
||||||
|
|
||||||
let lastMoveCoordinates = {
|
|
||||||
from: { x: 0, y: 0 },
|
|
||||||
to: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentMoveIndex > 0 && lastMove) {
|
|
||||||
let lastMoveUCI = lastMove.move?.uci;
|
|
||||||
if (!lastMoveUCI) return;
|
|
||||||
|
|
||||||
lastMoveCoordinates.from = getBoardCoordinates(lastMoveUCI.slice(0, 2));
|
|
||||||
lastMoveCoordinates.to = getBoardCoordinates(lastMoveUCI.slice(2, 4));
|
|
||||||
|
|
||||||
ctx.globalAlpha = 0.7;
|
|
||||||
ctx.fillStyle =
|
|
||||||
classificationColours[
|
|
||||||
reportResults?.positions[currentMoveIndex].classification ?? "book"
|
|
||||||
];
|
|
||||||
ctx.fillRect(
|
|
||||||
lastMoveCoordinates.from.x * 90,
|
|
||||||
lastMoveCoordinates.from.y * 90,
|
|
||||||
90,
|
|
||||||
90
|
|
||||||
);
|
|
||||||
ctx.fillRect(
|
|
||||||
lastMoveCoordinates.to.x * 90,
|
|
||||||
lastMoveCoordinates.to.y * 90,
|
|
||||||
90,
|
|
||||||
90
|
|
||||||
);
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw pieces
|
|
||||||
let fenBoard = fen.split(" ")[0];
|
|
||||||
let x = boardFlipped ? 7 : 0,
|
|
||||||
y = x;
|
|
||||||
|
|
||||||
for (let character of fenBoard) {
|
|
||||||
if (character == "/") {
|
|
||||||
x = boardFlipped ? 7 : 0;
|
|
||||||
y += boardFlipped ? -1 : 1;
|
|
||||||
} else if (/\d/g.test(character)) {
|
|
||||||
x += parseInt(character) * (boardFlipped ? -1 : 1);
|
|
||||||
} else {
|
|
||||||
const pieceSrc = pieceImagesSrc[character];
|
|
||||||
if (!pieceSrc) throw new Error(`No image source for piece ${character}`);
|
|
||||||
const pieceImage = await loadImage(pieceSrc);
|
|
||||||
|
|
||||||
ctx.drawImage(pieceImage, x * 90, y * 90, 90, 90);
|
|
||||||
x += boardFlipped ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw last move classification
|
|
||||||
if (currentMoveIndex > 0 && reportResults) {
|
|
||||||
let classification =
|
|
||||||
reportResults?.positions[currentMoveIndex]?.classification;
|
|
||||||
|
|
||||||
if (!classification) return;
|
|
||||||
|
|
||||||
const iconSrc = classificationIconsSrc[classification];
|
|
||||||
if (!iconSrc)
|
|
||||||
throw new Error(`No image source for classification ${classification}`);
|
|
||||||
const classificationIcon = await loadImage(iconSrc);
|
|
||||||
|
|
||||||
ctx.drawImage(
|
|
||||||
classificationIcon,
|
|
||||||
lastMoveCoordinates.to.x * 90 + 68,
|
|
||||||
lastMoveCoordinates.to.y * 90 - 10,
|
|
||||||
32,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw engine suggestion arrows
|
|
||||||
if (true) {
|
|
||||||
let arrowAttributes = [
|
|
||||||
{
|
|
||||||
width: 20,
|
|
||||||
opacity: 0.8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
width: 12,
|
|
||||||
opacity: 0.55,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let topLineIndex = -1;
|
|
||||||
for (let topLine of lastMove?.topLines ?? []) {
|
|
||||||
topLineIndex++;
|
|
||||||
|
|
||||||
let from = getBoardCoordinates(topLine.moveUCI.slice(0, 2));
|
|
||||||
let to = getBoardCoordinates(topLine.moveUCI.slice(2, 4));
|
|
||||||
|
|
||||||
let arrow = drawArrow(
|
|
||||||
from.x * 90 + 45,
|
|
||||||
from.y * 90 + 45,
|
|
||||||
to.x * 90 + 45,
|
|
||||||
to.y * 90 + 45,
|
|
||||||
arrowAttributes[topLineIndex].width,
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
if (!arrow) continue;
|
|
||||||
|
|
||||||
ctx.globalAlpha = arrowAttributes[topLineIndex].opacity;
|
|
||||||
ctx.drawImage(arrow, 0, 0);
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBoardCoordinates(square: string): { x: number; y: number } {
|
|
||||||
if (boardFlipped) {
|
|
||||||
return {
|
|
||||||
x: 7 - "abcdefgh".split("").indexOf(square.slice(0, 1)),
|
|
||||||
y: parseInt(square.slice(1)) - 1,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
x: "abcdefgh".split("").indexOf(square.slice(0, 1)),
|
|
||||||
y: 8 - parseInt(square.slice(1)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawArrow(
|
|
||||||
fromX: number,
|
|
||||||
fromY: number,
|
|
||||||
toX: number,
|
|
||||||
toY: number,
|
|
||||||
width: number,
|
|
||||||
arrowCtx: CanvasRenderingContext2D
|
|
||||||
) {
|
|
||||||
if (!arrowCtx) return;
|
|
||||||
|
|
||||||
arrowCtx.canvas.width = 720;
|
|
||||||
arrowCtx.canvas.height = 720;
|
|
||||||
|
|
||||||
let headlen = 15;
|
|
||||||
let angle = Math.atan2(toY - fromY, toX - fromX);
|
|
||||||
toX -= Math.cos(angle) * (width * 1.15);
|
|
||||||
toY -= Math.sin(angle) * (width * 1.15);
|
|
||||||
|
|
||||||
arrowCtx.beginPath();
|
|
||||||
arrowCtx.moveTo(fromX, fromY);
|
|
||||||
arrowCtx.lineTo(toX, toY);
|
|
||||||
arrowCtx.strokeStyle = classificationColours.best;
|
|
||||||
arrowCtx.lineWidth = width;
|
|
||||||
arrowCtx.stroke();
|
|
||||||
|
|
||||||
arrowCtx.beginPath();
|
|
||||||
arrowCtx.moveTo(toX, toY);
|
|
||||||
arrowCtx.lineTo(
|
|
||||||
toX - headlen * Math.cos(angle - Math.PI / 7),
|
|
||||||
toY - headlen * Math.sin(angle - Math.PI / 7)
|
|
||||||
);
|
|
||||||
|
|
||||||
arrowCtx.lineTo(
|
|
||||||
toX - headlen * Math.cos(angle + Math.PI / 7),
|
|
||||||
toY - headlen * Math.sin(angle + Math.PI / 7)
|
|
||||||
);
|
|
||||||
|
|
||||||
arrowCtx.lineTo(toX, toY);
|
|
||||||
arrowCtx.lineTo(
|
|
||||||
toX - headlen * Math.cos(angle - Math.PI / 7),
|
|
||||||
toY - headlen * Math.sin(angle - Math.PI / 7)
|
|
||||||
);
|
|
||||||
|
|
||||||
arrowCtx.strokeStyle = classificationColours.best;
|
|
||||||
arrowCtx.lineWidth = width;
|
|
||||||
arrowCtx.stroke();
|
|
||||||
arrowCtx.fillStyle = classificationColours.best;
|
|
||||||
arrowCtx.fill();
|
|
||||||
|
|
||||||
return arrowCtx.canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startingPositionFen =
|
|
||||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
||||||
|
|
||||||
const classificationColours: { [key: string]: string } = {
|
|
||||||
brilliant: "#1baaa6",
|
|
||||||
great: "#5b8baf",
|
|
||||||
best: "#98bc49",
|
|
||||||
excellent: "#98bc49",
|
|
||||||
good: "#97af8b",
|
|
||||||
inaccuracy: "#f4bf44",
|
|
||||||
mistake: "#e28c28",
|
|
||||||
blunder: "#c93230",
|
|
||||||
forced: "#97af8b",
|
|
||||||
book: "#a88764",
|
|
||||||
};
|
|
||||||
|
|
||||||
const pieceImagesSrc: Record<string, string | undefined> = {
|
|
||||||
P: "white_pawn.svg",
|
|
||||||
N: "white_knight.svg",
|
|
||||||
B: "white_bishop.svg",
|
|
||||||
R: "white_rook.svg",
|
|
||||||
Q: "white_queen.svg",
|
|
||||||
K: "white_king.svg",
|
|
||||||
p: "black_pawn.svg",
|
|
||||||
n: "black_knight.svg",
|
|
||||||
b: "black_bishop.svg",
|
|
||||||
r: "black_rook.svg",
|
|
||||||
q: "black_queen.svg",
|
|
||||||
k: "black_king.svg",
|
|
||||||
};
|
|
||||||
|
|
||||||
const classificationIconsSrc: Record<string, string | undefined> = {
|
|
||||||
brilliant: "brilliant.png",
|
|
||||||
great: "great.png",
|
|
||||||
best: "best.png",
|
|
||||||
excellent: "excellent.png",
|
|
||||||
good: "good.png",
|
|
||||||
inaccuracy: "inaccuracy.png",
|
|
||||||
mistake: "mistake.png",
|
|
||||||
blunder: "blunder.png",
|
|
||||||
forced: "forced.png",
|
|
||||||
book: "book.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadImage(filename: string): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((res) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.src = filename;
|
|
||||||
|
|
||||||
image.addEventListener("load", () => res(image));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,64 +1,7 @@
|
|||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
|
|
||||||
export const initPgn = new Chess().pgn();
|
export const pgnToFens = (pgn: string): string[] => {
|
||||||
|
|
||||||
export const getGameFens = (pgn: string): string[] => {
|
|
||||||
const game = new Chess();
|
const game = new Chess();
|
||||||
game.loadPgn(pgn);
|
game.loadPgn(pgn);
|
||||||
return game.history({ verbose: true }).map((move) => move.before);
|
return game.history({ verbose: true }).map((move) => move.before);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLastFen = (pgn: string): string => {
|
|
||||||
const game = new Chess();
|
|
||||||
game.loadPgn(pgn);
|
|
||||||
return game.fen();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const undoLastMove = (pgn: string): string => {
|
|
||||||
if (pgn === initPgn) return pgn;
|
|
||||||
const game = new Chess();
|
|
||||||
game.loadPgn(pgn);
|
|
||||||
game.undo();
|
|
||||||
return game.pgn();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addMove = (
|
|
||||||
pgn: string,
|
|
||||||
move: {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
promotion?: string;
|
|
||||||
}
|
|
||||||
): string => {
|
|
||||||
const game = new Chess();
|
|
||||||
game.loadPgn(pgn);
|
|
||||||
game.move(move);
|
|
||||||
return game.pgn();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addNextMove = (boardPgn: string, gamePgn: string): string => {
|
|
||||||
const board = new Chess();
|
|
||||||
board.loadPgn(boardPgn);
|
|
||||||
|
|
||||||
const game = new Chess();
|
|
||||||
game.loadPgn(gamePgn);
|
|
||||||
|
|
||||||
const nextMoveIndex = board.history().length;
|
|
||||||
const nextMove = game.history({ verbose: true })[nextMoveIndex];
|
|
||||||
|
|
||||||
if (nextMove) {
|
|
||||||
board.move({
|
|
||||||
from: nextMove.from,
|
|
||||||
to: nextMove.to,
|
|
||||||
promotion: nextMove.promotion,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return board.pgn();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNextMoveIndex = (pgn: string): number => {
|
|
||||||
const game = new Chess();
|
|
||||||
game.loadPgn(pgn);
|
|
||||||
return game.history().length;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { GameEval, LineEval, MoveEval } from "@/types/eval";
|
|||||||
|
|
||||||
export class Stockfish {
|
export class Stockfish {
|
||||||
private worker: Worker;
|
private worker: Worker;
|
||||||
private ready: boolean = false;
|
private ready = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.worker = new Worker(
|
this.worker = new Worker(
|
||||||
@@ -14,7 +14,7 @@ export class Stockfish {
|
|||||||
console.log("Stockfish created");
|
console.log("Stockfish created");
|
||||||
}
|
}
|
||||||
|
|
||||||
public isWasmSupported(): boolean {
|
public isWasmSupported() {
|
||||||
return (
|
return (
|
||||||
typeof WebAssembly === "object" &&
|
typeof WebAssembly === "object" &&
|
||||||
WebAssembly.validate(
|
WebAssembly.validate(
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
export async function drawEvaluationBar(
|
|
||||||
evaluationBarCtx: CanvasRenderingContext2D,
|
|
||||||
evaluation = initialEvaluation
|
|
||||||
) {
|
|
||||||
evaluationBarCtx.clearRect(0, 0, 30, 720);
|
|
||||||
evaluationBarCtx.font = "16px Arial";
|
|
||||||
evaluationBarCtx.fillStyle = "#1e1e1e";
|
|
||||||
|
|
||||||
if (evaluation.type == "cp") {
|
|
||||||
let height = Math.max(Math.min(360 - evaluation.value / 3, 680), 40);
|
|
||||||
evaluationBarCtx.fillRect(0, 0, 30, height);
|
|
||||||
|
|
||||||
let evaluationText = Math.abs(evaluation.value / 100).toFixed(1);
|
|
||||||
let evaluationTextWidth =
|
|
||||||
evaluationBarCtx.measureText(evaluationText).width;
|
|
||||||
|
|
||||||
let evaluationTextY = evaluation.value >= 0 ? 710 : 20;
|
|
||||||
evaluationBarCtx.fillStyle = evaluation.value >= 0 ? "#1e1e1e" : "#ffffff";
|
|
||||||
evaluationBarCtx.fillText(
|
|
||||||
evaluationText,
|
|
||||||
15 - evaluationTextWidth / 2,
|
|
||||||
evaluationTextY,
|
|
||||||
30
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let evaluationText = "M" + Math.abs(evaluation.value).toString();
|
|
||||||
let evaluationTextWidth =
|
|
||||||
evaluationBarCtx.measureText(evaluationText).width;
|
|
||||||
|
|
||||||
if (evaluation.value > 0) {
|
|
||||||
evaluationBarCtx.fillStyle = "#1e1e1e";
|
|
||||||
evaluationBarCtx.fillText(
|
|
||||||
evaluationText,
|
|
||||||
15 - evaluationTextWidth / 2,
|
|
||||||
710,
|
|
||||||
30
|
|
||||||
);
|
|
||||||
} else if (evaluation.value < 0) {
|
|
||||||
evaluationBarCtx.fillRect(0, 0, 30, 720);
|
|
||||||
|
|
||||||
evaluationBarCtx.fillStyle = "#ffffff";
|
|
||||||
evaluationBarCtx.fillText(
|
|
||||||
evaluationText,
|
|
||||||
15 - evaluationTextWidth / 2,
|
|
||||||
20,
|
|
||||||
30
|
|
||||||
);
|
|
||||||
} else if (evaluation.value == 0) {
|
|
||||||
evaluationBarCtx.fillStyle = "#676767";
|
|
||||||
evaluationBarCtx.fillRect(0, 0, 30, 720);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialEvaluation = { type: "cp", value: 0 };
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import "@fontsource/roboto/300.css";
|
||||||
|
import "@fontsource/roboto/400.css";
|
||||||
|
import "@fontsource/roboto/500.css";
|
||||||
|
import "@fontsource/roboto/700.css";
|
||||||
import { AppProps } from "next/app";
|
import { AppProps } from "next/app";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import "../../styles/global.css";
|
// import "../../styles/global.css";
|
||||||
import "../../styles/index.css";
|
// import "../../styles/index.css";
|
||||||
|
import Layout from "@/sections/layout";
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
@@ -9,7 +14,9 @@ export default function MyApp({ Component, pageProps }: AppProps) {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>Free Chess</title>
|
<title>Free Chess</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
<Layout>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,53 @@
|
|||||||
import TopBar from "@/sections/index/topBar";
|
import Board from "@/sections/gameReport/board";
|
||||||
import Board from "@/sections/index/board";
|
import ReviewPanelBody from "@/sections/gameReport/reviewPanelBody";
|
||||||
import ReviewPanelBody from "@/sections/index/reviewPanelBody";
|
import ReviewPanelHeader from "@/sections/gameReport/reviewPanelHeader";
|
||||||
import ReviewPanelToolBar from "@/sections/index/reviewPanelToolbar";
|
import ReviewPanelToolBar from "@/sections/gameReport/reviewPanelToolbar";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function GameReport() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Grid
|
||||||
<TopBar />
|
container
|
||||||
|
rowGap={2}
|
||||||
<div className="center">
|
justifyContent="center"
|
||||||
<div id="review-container">
|
alignItems="center"
|
||||||
|
marginTop={1}
|
||||||
|
>
|
||||||
<Board />
|
<Board />
|
||||||
|
|
||||||
<div id="review-panel">
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
rowGap={2}
|
||||||
|
paddingLeft={{ xs: 0, md: 6 }}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
item
|
||||||
|
rowGap={3}
|
||||||
|
columnGap={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius={2}
|
||||||
|
border={1}
|
||||||
|
borderColor={"secondary.main"}
|
||||||
|
xs={12}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "secondary.main",
|
||||||
|
}}
|
||||||
|
paddingY={3}
|
||||||
|
>
|
||||||
|
<ReviewPanelHeader />
|
||||||
|
|
||||||
<ReviewPanelBody />
|
<ReviewPanelBody />
|
||||||
|
|
||||||
<ReviewPanelToolBar />
|
<ReviewPanelToolBar />
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/sections/gameReport/board.tsx
Normal file
28
src/sections/gameReport/board.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Grid, Typography } from "@mui/material";
|
||||||
|
import { Chessboard } from "react-chessboard";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { boardAtom } from "./states";
|
||||||
|
|
||||||
|
export default function Board() {
|
||||||
|
const board = useAtomValue(boardAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
rowGap={2}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" align="center">
|
||||||
|
White Player (?)
|
||||||
|
</Typography>
|
||||||
|
<Chessboard id="BasicBoard" position={board.fen()} />
|
||||||
|
<Typography variant="h4" align="center">
|
||||||
|
Black Player (?)
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/sections/gameReport/reviewPanelBody.tsx
Normal file
66
src/sections/gameReport/reviewPanelBody.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import {
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { boardAtom, gameEvalAtom } from "./states";
|
||||||
|
|
||||||
|
export default function ReviewPanelBody() {
|
||||||
|
const board = useAtomValue(boardAtom);
|
||||||
|
const gameEval = useAtomValue(gameEvalAtom);
|
||||||
|
if (!gameEval) return null;
|
||||||
|
|
||||||
|
const evalIndex = board.history().length;
|
||||||
|
const moveEval = gameEval.moves[evalIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ width: "90%", marginY: 3 }} />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
xs={12}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
columnGap={1}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="pepicons-pop:star-filled-circle"
|
||||||
|
color="#27f019"
|
||||||
|
height={30}
|
||||||
|
/>
|
||||||
|
<Typography variant="h5" align="center">
|
||||||
|
Bilan de la partie
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Typography variant="h6" align="center">
|
||||||
|
{moveEval ? `${moveEval.bestMove} is the best move` : "Game is over"}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||||
|
<List>
|
||||||
|
{moveEval?.lines.map((line) => (
|
||||||
|
<ListItem disablePadding key={line.pv[0]}>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
line.cp !== undefined
|
||||||
|
? line.cp / 100
|
||||||
|
: `Mate in ${Math.abs(line.mate ?? 0)}`
|
||||||
|
}
|
||||||
|
sx={{ marginRight: 2 }}
|
||||||
|
/>
|
||||||
|
<Typography>{line.pv.slice(0, 7).join(", ")}</Typography>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/sections/gameReport/reviewPanelHeader.tsx
Normal file
58
src/sections/gameReport/reviewPanelHeader.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import SelectGameOrigin from "./selectGame/selectGameOrigin";
|
||||||
|
import { Stockfish } from "@/lib/engine/stockfish";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Button, Typography } from "@mui/material";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { boardAtom, gameAtom, gameEvalAtom } from "./states";
|
||||||
|
import { useChessActions } from "@/hooks/useChess";
|
||||||
|
import { gameInputPgnAtom } from "./selectGame/gameInput.states";
|
||||||
|
import { pgnToFens } from "@/lib/chess";
|
||||||
|
|
||||||
|
export default function ReviewPanelHeader() {
|
||||||
|
const [engine, setEngine] = useState<Stockfish | null>(null);
|
||||||
|
const setEval = useSetAtom(gameEvalAtom);
|
||||||
|
const boardActions = useChessActions(boardAtom);
|
||||||
|
const gameActions = useChessActions(gameAtom);
|
||||||
|
const pgnInput = useAtomValue(gameInputPgnAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const engine = new Stockfish();
|
||||||
|
engine.init();
|
||||||
|
setEngine(engine);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engine.shutdown();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAnalyse = async () => {
|
||||||
|
boardActions.reset();
|
||||||
|
gameActions.setPgn(pgnInput);
|
||||||
|
const gameFens = pgnToFens(pgnInput);
|
||||||
|
if (engine?.isReady() && gameFens.length) {
|
||||||
|
const newGameEval = await engine.evaluateGame(gameFens);
|
||||||
|
setEval(newGameEval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon icon="ph:file-magnifying-glass-fill" height={40} />
|
||||||
|
<Typography variant="h4" align="center">
|
||||||
|
Game Report
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<SelectGameOrigin />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
startIcon={<Icon icon="streamline:magnifying-glass-solid" />}
|
||||||
|
onClick={handleAnalyse}
|
||||||
|
>
|
||||||
|
Analyse
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/sections/gameReport/reviewPanelToolbar.tsx
Normal file
51
src/sections/gameReport/reviewPanelToolbar.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Divider, Grid, IconButton } from "@mui/material";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { boardAtom, gameAtom } from "./states";
|
||||||
|
import { useChessActions } from "@/hooks/useChess";
|
||||||
|
|
||||||
|
export default function ReviewPanelToolBar() {
|
||||||
|
const game = useAtomValue(gameAtom);
|
||||||
|
const board = useAtomValue(boardAtom);
|
||||||
|
const boardActions = useChessActions(boardAtom);
|
||||||
|
|
||||||
|
const addNextMoveToGame = () => {
|
||||||
|
const nextMoveIndex = board.history().length;
|
||||||
|
const nextMove = game.history({ verbose: true })[nextMoveIndex];
|
||||||
|
|
||||||
|
if (nextMove) {
|
||||||
|
boardActions.move({
|
||||||
|
from: nextMove.from,
|
||||||
|
to: nextMove.to,
|
||||||
|
promotion: nextMove.promotion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ width: "90%", marginY: 3 }} />
|
||||||
|
|
||||||
|
<Grid container item justifyContent="center" alignItems="center" xs={12}>
|
||||||
|
<IconButton>
|
||||||
|
<Icon icon="eva:flip-fill" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={() => boardActions.reset()}>
|
||||||
|
<Icon icon="ri:skip-back-line" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={() => boardActions.undo()}>
|
||||||
|
<Icon icon="ri:arrow-left-s-line" height={30} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={() => addNextMoveToGame()}>
|
||||||
|
<Icon icon="ri:arrow-right-s-line" height={30} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton>
|
||||||
|
<Icon icon="ri:skip-forward-line" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton>
|
||||||
|
<Icon icon="ri:save-3-line" />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/sections/gameReport/selectGame/gameInput.states.ts
Normal file
3
src/sections/gameReport/selectGame/gameInput.states.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const gameInputPgnAtom = atom("");
|
||||||
19
src/sections/gameReport/selectGame/inputGame.tsx
Normal file
19
src/sections/gameReport/selectGame/inputGame.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { TextField } from "@mui/material";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { gameInputPgnAtom } from "./gameInput.states";
|
||||||
|
|
||||||
|
export default function InputGame() {
|
||||||
|
const [pgn, setPgn] = useAtom(gameInputPgnAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Enter PGN here..."
|
||||||
|
sx={{ marginX: 4 }}
|
||||||
|
value={pgn}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPgn(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/sections/gameReport/selectGame/selectGameOrigin.tsx
Normal file
46
src/sections/gameReport/selectGame/selectGameOrigin.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { GameOrigin } from "@/types/enums";
|
||||||
|
import { FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
|
||||||
|
import InputGame from "./inputGame";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function SelectGameOrigin() {
|
||||||
|
const [gameOrigin, setGameOrigin] = useState<GameOrigin>(GameOrigin.Pgn);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
xs={12}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
rowGap={1}
|
||||||
|
>
|
||||||
|
<FormControl sx={{ m: 1, minWidth: 150 }}>
|
||||||
|
<InputLabel id="game-origin-select-label">Game Origin</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="game-origin-select-label"
|
||||||
|
id="game-origin-select"
|
||||||
|
label="Game Origin"
|
||||||
|
autoWidth
|
||||||
|
value={gameOrigin}
|
||||||
|
onChange={(e) => setGameOrigin(e.target.value as GameOrigin)}
|
||||||
|
>
|
||||||
|
{Object.values(GameOrigin).map((origin) => (
|
||||||
|
<MenuItem key={origin} value={origin}>
|
||||||
|
{gameOriginLabel[origin]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<InputGame />
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameOriginLabel: Record<GameOrigin, string> = {
|
||||||
|
[GameOrigin.Pgn]: "PGN",
|
||||||
|
[GameOrigin.ChessCom]: "Chess.com",
|
||||||
|
[GameOrigin.Lichess]: "Lichess",
|
||||||
|
[GameOrigin.Json]: "JSON",
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { initPgn } from "@/lib/chess";
|
|
||||||
import { GameEval } from "@/types/eval";
|
import { GameEval } from "@/types/eval";
|
||||||
|
import { Chess } from "chess.js";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
|
||||||
export const gameEvalAtom = atom<GameEval | undefined>(undefined);
|
export const gameEvalAtom = atom<GameEval | undefined>(undefined);
|
||||||
export const gamePgnAtom = atom(initPgn);
|
export const gameAtom = atom(new Chess());
|
||||||
export const boardPgnAtom = atom(initPgn);
|
export const boardAtom = atom(new Chess());
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { drawBoard } from "@/lib/board";
|
|
||||||
import { drawEvaluationBar } from "@/lib/evalBar";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { boardPgnAtom } from "./index.state";
|
|
||||||
import { getLastFen } from "@/lib/chess";
|
|
||||||
|
|
||||||
export default function Board() {
|
|
||||||
const boardRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const evalBarRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const boardPgn = useAtomValue(boardPgnAtom);
|
|
||||||
const boardFen = getLastFen(boardPgn);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const ctx = boardRef.current?.getContext("2d");
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
drawBoard(ctx, boardFen);
|
|
||||||
|
|
||||||
const evalCtx = evalBarRef.current?.getContext("2d");
|
|
||||||
if (!evalCtx) return;
|
|
||||||
drawEvaluationBar(evalCtx);
|
|
||||||
}, [boardFen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="board-outer-container" className="center">
|
|
||||||
<canvas id="evaluation-bar" width="30" height="720" ref={evalBarRef} />
|
|
||||||
|
|
||||||
<div id="board-inner-container" className="center">
|
|
||||||
<div id="top-player-profile" className="profile">
|
|
||||||
Black Player (?)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<canvas id="board" width="720" height="720" ref={boardRef} />
|
|
||||||
|
|
||||||
<div id="bottom-player-profile" className="profile">
|
|
||||||
White Player (?)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import ReviewResult from "./reviewResult";
|
|
||||||
import SelectDepth from "./selectDepth";
|
|
||||||
import SelectGameOrigin from "./selectGame/selectGameOrigin";
|
|
||||||
import { Stockfish } from "@/lib/engine/stockfish";
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
|
||||||
import { boardPgnAtom, gameEvalAtom, gamePgnAtom } from "./index.state";
|
|
||||||
import { getGameFens, initPgn } from "@/lib/chess";
|
|
||||||
|
|
||||||
export default function ReviewPanelBody() {
|
|
||||||
const [engine, setEngine] = useState<Stockfish | null>(null);
|
|
||||||
const setGameEval = useSetAtom(gameEvalAtom);
|
|
||||||
const setBoardPgn = useSetAtom(boardPgnAtom);
|
|
||||||
const gamePgn = useAtomValue(gamePgnAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const engine = new Stockfish();
|
|
||||||
engine.init();
|
|
||||||
setEngine(engine);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
engine.shutdown();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAnalyse = async () => {
|
|
||||||
setBoardPgn(initPgn);
|
|
||||||
const gameFens = getGameFens(gamePgn);
|
|
||||||
if (engine?.isReady() && gameFens.length) {
|
|
||||||
const newGameEval = await engine.evaluateGame(gameFens);
|
|
||||||
setGameEval(newGameEval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="review-panel-main">
|
|
||||||
<h1 id="review-panel-title" className="white">
|
|
||||||
📑 Game Report
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<SelectGameOrigin />
|
|
||||||
|
|
||||||
<button id="review-button" className="std-btn success-btn white">
|
|
||||||
<img src="analysis_icon.png" height="25" />
|
|
||||||
<b onClick={handleAnalyse}>Analyse</b>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<SelectDepth />
|
|
||||||
|
|
||||||
{false && <progress id="evaluation-progress-bar" max="100" />}
|
|
||||||
|
|
||||||
<b id="status-message" />
|
|
||||||
|
|
||||||
<b id="secondary-message" className="white" />
|
|
||||||
|
|
||||||
<ReviewResult />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useAtom, useAtomValue } from "jotai";
|
|
||||||
import { boardPgnAtom, gamePgnAtom } from "./index.state";
|
|
||||||
import { addNextMove, initPgn, undoLastMove } from "@/lib/chess";
|
|
||||||
|
|
||||||
export default function ReviewPanelToolBar() {
|
|
||||||
const [boardPgn, setBoardPgn] = useAtom(boardPgnAtom);
|
|
||||||
const gamePgn = useAtomValue(gamePgnAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="review-panel-toolbar">
|
|
||||||
<div id="review-panel-toolbar-buttons" className="center">
|
|
||||||
<img
|
|
||||||
id="flip-board-button"
|
|
||||||
src="flip.png"
|
|
||||||
alt="Flip Board"
|
|
||||||
title="Flip board"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
id="back-start-move-button"
|
|
||||||
src="back_to_start.png"
|
|
||||||
alt="Back to start"
|
|
||||||
title="Back to start"
|
|
||||||
onClick={() => setBoardPgn(initPgn)}
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
id="back-move-button"
|
|
||||||
src="back.png"
|
|
||||||
alt="Back"
|
|
||||||
title="Back"
|
|
||||||
onClick={() => {
|
|
||||||
setBoardPgn(undoLastMove(boardPgn));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
id="next-move-button"
|
|
||||||
src="next.png"
|
|
||||||
alt="Next"
|
|
||||||
title="Next"
|
|
||||||
onClick={() => {
|
|
||||||
const nextBoardPgn = addNextMove(boardPgn, gamePgn);
|
|
||||||
setBoardPgn(nextBoardPgn);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
id="go-end-move-button"
|
|
||||||
src="go_to_end.png"
|
|
||||||
alt="Go to end"
|
|
||||||
title="Go to end"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
id="save-analysis-button"
|
|
||||||
src="save.png"
|
|
||||||
alt="Save analysis"
|
|
||||||
title="Save analysis"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="white" style={{ marginBottom: "10px" }}>
|
|
||||||
<input
|
|
||||||
id="suggestion-arrows-setting"
|
|
||||||
type="checkbox"
|
|
||||||
style={{ marginRight: "0.4rem" }}
|
|
||||||
/>
|
|
||||||
<span>Suggestion Arrows</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { boardPgnAtom, gameEvalAtom } from "./index.state";
|
|
||||||
import { getNextMoveIndex } from "@/lib/chess";
|
|
||||||
|
|
||||||
export default function ReviewResult() {
|
|
||||||
const boardPgn = useAtomValue(boardPgnAtom);
|
|
||||||
const gameEval = useAtomValue(gameEvalAtom);
|
|
||||||
if (!gameEval) return null;
|
|
||||||
|
|
||||||
const evalIndex = getNextMoveIndex(boardPgn);
|
|
||||||
const moveEval = gameEval.moves[evalIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="report-cards">
|
|
||||||
<h2 id="accuracies-title" className="white">
|
|
||||||
Accuracies
|
|
||||||
</h2>
|
|
||||||
<div id="accuracies">
|
|
||||||
<b id="white-accuracy">{gameEval.whiteAccuracy.toFixed(1)}%</b>
|
|
||||||
<b id="black-accuracy">{gameEval.blackAccuracy.toFixed(1)}%</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="classification-message-container">
|
|
||||||
<img id="classification-icon" src="book.png" height="25" />
|
|
||||||
<b id="classification-message" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<b id="top-alternative-message">
|
|
||||||
{moveEval ? `${moveEval.bestMove} is best` : "Game is over"}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<div id="engine-suggestions">
|
|
||||||
<h2 id="engine-suggestions-title" className="white">
|
|
||||||
Engine
|
|
||||||
</h2>
|
|
||||||
{moveEval?.lines.map((line) => (
|
|
||||||
<div key={line.pv[0]} style={{ color: "white" }}>
|
|
||||||
<span style={{ marginRight: "2em" }}>
|
|
||||||
{line.cp !== undefined
|
|
||||||
? line.cp / 100
|
|
||||||
: `Mate in ${Math.abs(line.mate ?? 0)}`}
|
|
||||||
</span>
|
|
||||||
<span>{line.pv.slice(0, 7).join(", ")}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="opening-name" className="white" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export default function SelectDepth() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<b className="white">⚙️ Search depth</b>
|
|
||||||
<div id="depth-slider-container">
|
|
||||||
<input
|
|
||||||
id="depth-slider"
|
|
||||||
type="range"
|
|
||||||
min="14"
|
|
||||||
max="20"
|
|
||||||
defaultValue="16"
|
|
||||||
/>
|
|
||||||
<span id="depth-counter" className="white">
|
|
||||||
16 🐇
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h6 id="depth-message" className="white">
|
|
||||||
Lower depths recommended for slower devices.
|
|
||||||
</h6>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { GameOrigin } from "@/types/enums";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { gamePgnAtom } from "../index.state";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
gameOrigin: GameOrigin;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InputGame({ placeholder }: Props) {
|
|
||||||
const [gamePgn, setGamePgn] = useAtom(gamePgnAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
id="pgn"
|
|
||||||
className="white"
|
|
||||||
cols={30}
|
|
||||||
rows={10}
|
|
||||||
spellCheck="false"
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={gamePgn}
|
|
||||||
onChange={(e) => {
|
|
||||||
setGamePgn(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { GameOrigin } from "@/types/enums";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
gameOrigin: GameOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SelectGameAccount({}: Props) {
|
|
||||||
return (
|
|
||||||
<div id="chess-site-username-container">
|
|
||||||
<textarea
|
|
||||||
id="chess-site-username"
|
|
||||||
className="white"
|
|
||||||
spellCheck="false"
|
|
||||||
maxLength={48}
|
|
||||||
placeholder="Username..."
|
|
||||||
/>
|
|
||||||
<button id="fetch-account-games-button" className="std-btn success-btn">
|
|
||||||
<img src="next.png" alt=">" height="25" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import InputGame from "./inputGame";
|
|
||||||
import SelectGameAccount from "./selectGameAccount";
|
|
||||||
import { GameOrigin } from "@/types/enums";
|
|
||||||
|
|
||||||
export default function SelectGameOrigin() {
|
|
||||||
const [gameOrigin, setGameOrigin] = useState(GameOrigin.Pgn);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div id="load-type-dropdown-container" className="white">
|
|
||||||
<span style={{ marginRight: "0.3rem" }}>Load game from</span>
|
|
||||||
<select
|
|
||||||
id="load-type-dropdown"
|
|
||||||
value={gameOrigin}
|
|
||||||
onChange={(e) => setGameOrigin(e.target.value as GameOrigin)}
|
|
||||||
>
|
|
||||||
{Object.values(GameOrigin).map((origin) => (
|
|
||||||
<option key={origin} value={origin}>
|
|
||||||
{gameOriginLabel[origin]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderSelectGameInfo(gameOrigin)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameOriginLabel: Record<GameOrigin, string> = {
|
|
||||||
[GameOrigin.Pgn]: "PGN",
|
|
||||||
[GameOrigin.ChessCom]: "Chess.com",
|
|
||||||
[GameOrigin.Lichess]: "Lichess",
|
|
||||||
[GameOrigin.Json]: "JSON",
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSelectGameInfo = (gameOrigin: GameOrigin) => {
|
|
||||||
switch (gameOrigin) {
|
|
||||||
case GameOrigin.Pgn:
|
|
||||||
return (
|
|
||||||
<InputGame gameOrigin={gameOrigin} placeholder="Enter PGN here..." />
|
|
||||||
);
|
|
||||||
case GameOrigin.Json:
|
|
||||||
return (
|
|
||||||
<InputGame gameOrigin={gameOrigin} placeholder="Enter JSON here..." />
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <SelectGameAccount gameOrigin={gameOrigin} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function TopBar() {
|
|
||||||
return (
|
|
||||||
<div id="announcement">
|
|
||||||
<b>Welcome ❤️</b>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
67
src/sections/layout/NavBar.tsx
Normal file
67
src/sections/layout/NavBar.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import AppBar from "@mui/material/AppBar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import NavMenu from "./NavMenu";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import NavLink from "@/components/NavLink";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
darkMode: boolean;
|
||||||
|
switchDarkMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavBar({ darkMode, switchDarkMode }: Props) {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
}, [router.pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1, display: "flex" }}>
|
||||||
|
<AppBar
|
||||||
|
position="static"
|
||||||
|
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
onClick={() => setDrawerOpen((val) => !val)}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:menu" />
|
||||||
|
</IconButton>
|
||||||
|
<NavLink href="/">
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
Free Chess
|
||||||
|
</Typography>
|
||||||
|
</NavLink>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
onClick={() =>
|
||||||
|
window.open("https://github.com/GuillaumeSD/freechess")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:github" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton sx={{ ml: 1 }} onClick={switchDarkMode} color="inherit">
|
||||||
|
{darkMode ? (
|
||||||
|
<Icon icon="mdi:brightness-7" />
|
||||||
|
) : (
|
||||||
|
<Icon icon="mdi:brightness-4" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<NavMenu open={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/sections/layout/NavMenu.tsx
Normal file
45
src/sections/layout/NavMenu.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import NavLink from "@/components/NavLink";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Drawer,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Toolbar,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
const MenuOptions = [
|
||||||
|
{ text: "Game Report", icon: "streamline:magnifying-glass-solid", href: "/" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavMenu({ open, onClose }: Props) {
|
||||||
|
return (
|
||||||
|
<Drawer anchor="left" open={open} onClose={onClose}>
|
||||||
|
<Toolbar />
|
||||||
|
<Box sx={{ width: 250 }}>
|
||||||
|
<List>
|
||||||
|
{MenuOptions.map(({ text, icon, href }) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<NavLink href={href}>
|
||||||
|
<ListItemButton onClick={onClose}>
|
||||||
|
<ListItemIcon style={{ paddingLeft: "0.5em" }}>
|
||||||
|
<Icon icon={icon} height="1.5em" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</NavLink>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/sections/layout/index.tsx
Normal file
36
src/sections/layout/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CssBaseline, ThemeProvider, createTheme } from "@mui/material";
|
||||||
|
import { PropsWithChildren, useMemo } from "react";
|
||||||
|
import NavBar from "./NavBar";
|
||||||
|
import { red } from "@mui/material/colors";
|
||||||
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
|
|
||||||
|
export default function Layout({ children }: PropsWithChildren) {
|
||||||
|
const [useDarkMode, setDarkMode] = useLocalStorage("useDarkMode", true);
|
||||||
|
|
||||||
|
const theme = useMemo(
|
||||||
|
() =>
|
||||||
|
createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: useDarkMode ? "dark" : "light",
|
||||||
|
error: {
|
||||||
|
main: red[400],
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: useDarkMode ? "#424242" : "#90caf9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[useDarkMode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<NavBar
|
||||||
|
darkMode={useDarkMode}
|
||||||
|
switchDarkMode={() => setDarkMode((val) => !val)}
|
||||||
|
/>
|
||||||
|
<main style={{ margin: "2em 2vw" }}>{children}</main>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user