diff --git a/package-lock.json b/package-lock.json index 82073c6..b286bd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "next": "14.2.5", "react": "18.2.0", "react-chessboard": "^4.6.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "recharts": "^2.12.7" }, "devDependencies": { "@types/node": "20.3.0", @@ -1699,6 +1700,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2430,6 +2494,127 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2503,6 +2688,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -3405,6 +3596,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3416,6 +3613,15 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -3947,6 +4153,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -4507,6 +4722,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -5155,6 +5376,21 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-smooth": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -5170,6 +5406,44 @@ "react-dom": ">=16.6.0" } }, + "node_modules/recharts": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -5738,6 +6012,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5941,6 +6221,28 @@ "punycode": "^2.1.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/package.json b/package.json index 9a1a49a..b9b3b6a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "next": "14.2.5", "react": "18.2.0", "react-chessboard": "^4.6.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "recharts": "^2.12.7" }, "devDependencies": { "@types/node": "20.3.0", diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index 2396c88..6fb72d9 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -11,10 +11,11 @@ import { useChessActions } from "@/hooks/useChessActions"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Color, MoveClassification } from "@/types/enums"; import { Chess } from "chess.js"; -import { getSquareRenderer, moveClassificationColors } from "./squareRenderer"; +import { getSquareRenderer } from "./squareRenderer"; import { CurrentPosition } from "@/types/eval"; import EvaluationBar from "./evaluationBar"; import CapturedPieces from "./capturedPieces"; +import { moveClassificationColors } from "@/lib/chess"; export interface Props { id: string; diff --git a/src/components/board/squareRenderer.tsx b/src/components/board/squareRenderer.tsx index 6991b29..0f47c92 100644 --- a/src/components/board/squareRenderer.tsx +++ b/src/components/board/squareRenderer.tsx @@ -7,6 +7,7 @@ import { CustomSquareProps, Square, } from "react-chessboard/dist/chessboard/types"; +import { moveClassificationColors } from "@/lib/chess"; export interface Props { currentPositionAtom: PrimitiveAtom; @@ -74,18 +75,6 @@ export function getSquareRenderer({ return squareRenderer; } -export const moveClassificationColors: Record = { - [MoveClassification.Book]: "#d5a47d", - [MoveClassification.Brilliant]: "#26c2a3", - [MoveClassification.Great]: "#4099ed", - [MoveClassification.Best]: "#3aab18", - [MoveClassification.Excellent]: "#3aab18", - [MoveClassification.Good]: "#81b64c", - [MoveClassification.Inaccuracy]: "#f7c631", - [MoveClassification.Mistake]: "#ffa459", - [MoveClassification.Blunder]: "#fa412d", -}; - const rightClickSquareStyle: CSSProperties = { position: "absolute", width: "100%", diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index 763a517..702e34c 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -37,6 +37,15 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { const copyGame = useCallback(() => { const newGame = new Chess(); + + if (game.history().length === 0) { + const pgnSplitted = game.pgn().split("]"); + if (pgnSplitted.at(-1)?.includes("1-0")) { + newGame.loadPgn(pgnSplitted.slice(0, -1).join("]") + "]"); + return newGame; + } + } + newGame.loadPgn(game.pgn()); return newGame; }, [game]); diff --git a/src/lib/chess.ts b/src/lib/chess.ts index d953c18..a1fb8b3 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -1,8 +1,8 @@ -import { EvaluateGameParams, PositionEval } from "@/types/eval"; +import { EvaluateGameParams, LineEval, PositionEval } from "@/types/eval"; import { Game } from "@/types/game"; import { Chess, PieceSymbol, Square } from "chess.js"; import { getPositionWinPercentage } from "./engine/helpers/winPercentage"; -import { Color } from "@/types/enums"; +import { Color, MoveClassification } from "@/types/enums"; export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => { const history = game.history({ verbose: true }); @@ -315,3 +315,29 @@ export const getCapturedPieces = ( return capturedPieces; }; + +export const getLineEvalLabel = ( + line: Pick +): string => { + if (line.cp !== undefined) { + return `${line.cp > 0 ? "+" : ""}${(line.cp / 100).toFixed(2)}`; + } + + if (line.mate) { + return `${line.mate > 0 ? "+" : "-"}M${Math.abs(line.mate)}`; + } + + return "?"; +}; + +export const moveClassificationColors: Record = { + [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Brilliant]: "#26c2a3", + [MoveClassification.Great]: "#4099ed", + [MoveClassification.Best]: "#3aab18", + [MoveClassification.Excellent]: "#3aab18", + [MoveClassification.Good]: "#81b64c", + [MoveClassification.Inaccuracy]: "#f7c631", + [MoveClassification.Mistake]: "#ffa459", + [MoveClassification.Blunder]: "#fa412d", +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b2b3dbf..14dbcda 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,28 +1,42 @@ import { useChessActions } from "@/hooks/useChessActions"; import Board from "@/sections/analysis/board"; -import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; -import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; -import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; +import PanelHeader from "@/sections/analysis/panelHeader"; +import PanelToolBar from "@/sections/analysis/panelToolbar"; +import AnalysisTab from "@/sections/analysis/panelBody/analysisTab"; +import ClassificationTab from "@/sections/analysis/panelBody/classificationTab"; import { boardAtom, boardOrientationAtom, gameAtom, gameEvalAtom, } from "@/sections/analysis/states"; -import { Divider, Grid, useMediaQuery, useTheme } from "@mui/material"; +import { + Box, + Divider, + Grid, + Tab, + Tabs, + useMediaQuery, + useTheme, +} from "@mui/material"; import { Chess } from "chess.js"; -import { useAtom, useSetAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useRouter } from "next/router"; -import { useEffect } from "react"; -import ClassificationPanel from "@/sections/analysis/reviewPanelBody/classificationPanel"; +import { useEffect, useState } from "react"; +import { Icon } from "@iconify/react"; +import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; +import GraphTab from "@/sections/analysis/panelBody/graphTab"; -export default function GameReport() { +export default function GameReview() { const theme = useTheme(); + const [tab, setTab] = useState(0); const isLgOrGreater = useMediaQuery(theme.breakpoints.up("lg")); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const { reset: resetBoard } = useChessActions(boardAtom); const { setPgn: setGamePgn } = useChessActions(gameAtom); const [gameEval, setGameEval] = useAtom(gameEvalAtom); + const game = useAtomValue(gameAtom); const setBoardOrientation = useSetAtom(boardOrientationAtom); const router = useRouter(); @@ -37,13 +51,22 @@ export default function GameReport() { } }, [gameId, setGameEval, setBoardOrientation, resetBoard, setGamePgn]); + const isGameLoaded = game.history().length > 0; + + useEffect(() => { + if (tab === 1 && !isGameLoaded) setTab(0); + if (tab === 2 && !gameEval) setTab(0); + }, [isGameLoaded, gameEval, tab]); + return ( - + 780 ? 4 : 0} > - + ) : ( + + )} + + {!isLgOrGreater && !gameEval && } + {!isLgOrGreater && !gameEval && ( + + )} + + - {isLgOrGreater ? ( - <> - + setTab(newValue)} + aria-label="basic tabs example" + variant="fullWidth" + > + + } + iconPosition="start" + sx={{ textTransform: "none", minHeight: 20, paddingX: 0 }} + disableFocusRipple + /> - + + } + iconPosition="start" + sx={{ + textTransform: "none", + minHeight: 20, + display: isGameLoaded ? undefined : "none", + paddingX: 0, + }} + disableFocusRipple + /> - + + } + iconPosition="start" + sx={{ + textTransform: "none", + minHeight: 20, + display: gameEval ? undefined : "none", + paddingX: 0, + }} + disableFocusRipple + /> + + - + + {!isLgOrGreater && gameEval && } + {!isLgOrGreater && gameEval && ( + + )} + + ); } diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index ca47bd8..96447b1 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -50,7 +50,13 @@ export const useCurrentPosition = (engineName?: EngineName) => { setCurrentPosition(position); - if (!position.eval && engine?.isReady() && engineName) { + if ( + !position.eval && + engine?.isReady() && + engineName && + !board.isCheckmate() && + !board.isStalemate() + ) { const getFenEngineEval = async ( fen: string, setPartialEval?: (positionEval: PositionEval) => void diff --git a/src/sections/analysis/reviewPanelBody/accuracies.tsx b/src/sections/analysis/panelBody/analysisTab/accuracies.tsx similarity index 95% rename from src/sections/analysis/reviewPanelBody/accuracies.tsx rename to src/sections/analysis/panelBody/analysisTab/accuracies.tsx index 8175e14..fb377b4 100644 --- a/src/sections/analysis/reviewPanelBody/accuracies.tsx +++ b/src/sections/analysis/panelBody/analysisTab/accuracies.tsx @@ -1,6 +1,6 @@ import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { gameEvalAtom } from "../states"; +import { gameEvalAtom } from "../../states"; export default function Accuracies() { const gameEval = useAtomValue(gameEvalAtom); diff --git a/src/sections/analysis/reviewPanelBody/index.tsx b/src/sections/analysis/panelBody/analysisTab/index.tsx similarity index 61% rename from src/sections/analysis/reviewPanelBody/index.tsx rename to src/sections/analysis/panelBody/analysisTab/index.tsx index 769520b..334b420 100644 --- a/src/sections/analysis/reviewPanelBody/index.tsx +++ b/src/sections/analysis/panelBody/analysisTab/index.tsx @@ -1,21 +1,19 @@ -import { Icon } from "@iconify/react"; -import { Grid, List, Typography } from "@mui/material"; +import { Grid, GridProps, List, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, engineMultiPvAtom, engineNameAtom, gameAtom, -} from "../states"; +} from "../../states"; import LineEvaluation from "./lineEvaluation"; -import { useCurrentPosition } from "../hooks/useCurrentPosition"; +import { useCurrentPosition } from "../../hooks/useCurrentPosition"; import { LineEval } from "@/types/eval"; -import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; import Accuracies from "./accuracies"; import MoveInfo from "./moveInfo"; import Opening from "./opening"; -export default function ReviewPanelBody() { +export default function AnalysisTab(props: GridProps) { const linesNumber = useAtomValue(engineMultiPvAtom); const engineName = useAtomValue(engineNameAtom); const position = useCurrentPosition(engineName); @@ -45,41 +43,16 @@ export default function ReviewPanelBody() { container xs={12} justifyContent="center" - alignItems="center" + alignItems="start" + height="100%" rowGap={1.2} + {...props} + sx={ + props.hidden + ? { display: "none" } + : { overflow: "hidden", overflowY: "auto", ...props.sx } + } > - - - - - - - Engine evaluation - - - - - - - - diff --git a/src/sections/analysis/reviewPanelBody/lineEvaluation.tsx b/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx similarity index 84% rename from src/sections/analysis/reviewPanelBody/lineEvaluation.tsx rename to src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx index aff8827..d4cbdab 100644 --- a/src/sections/analysis/reviewPanelBody/lineEvaluation.tsx +++ b/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx @@ -1,8 +1,8 @@ import { LineEval } from "@/types/eval"; import { ListItem, Skeleton, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { boardAtom } from "../states"; -import { moveLineUciToSan } from "@/lib/chess"; +import { boardAtom } from "../../states"; +import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess"; interface Props { line: LineEval; @@ -10,12 +10,7 @@ interface Props { export default function LineEvaluation({ line }: Props) { const board = useAtomValue(boardAtom); - const lineLabel = - line.cp !== undefined - ? `${line.cp > 0 ? "+" : ""}${(line.cp / 100).toFixed(2)}` - : line.mate - ? `${line.mate > 0 ? "+" : "-"}M${Math.abs(line.mate)}` - : "?"; + const lineLabel = getLineEvalLabel(line); const isBlackCp = (line.cp !== undefined && line.cp < 0) || diff --git a/src/sections/analysis/reviewPanelBody/moveInfo.tsx b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx similarity index 97% rename from src/sections/analysis/reviewPanelBody/moveInfo.tsx rename to src/sections/analysis/panelBody/analysisTab/moveInfo.tsx index 21eaf2c..c94eec2 100644 --- a/src/sections/analysis/reviewPanelBody/moveInfo.tsx +++ b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx @@ -1,6 +1,6 @@ import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { boardAtom, currentPositionAtom } from "../states"; +import { boardAtom, currentPositionAtom } from "../../states"; import { useMemo } from "react"; import { moveLineUciToSan } from "@/lib/chess"; import { MoveClassification } from "@/types/enums"; diff --git a/src/sections/analysis/reviewPanelBody/opening.tsx b/src/sections/analysis/panelBody/analysisTab/opening.tsx similarity index 83% rename from src/sections/analysis/reviewPanelBody/opening.tsx rename to src/sections/analysis/panelBody/analysisTab/opening.tsx index 76106c7..97c4f4d 100644 --- a/src/sections/analysis/reviewPanelBody/opening.tsx +++ b/src/sections/analysis/panelBody/analysisTab/opening.tsx @@ -1,4 +1,4 @@ -import { useCurrentPosition } from "../hooks/useCurrentPosition"; +import { useCurrentPosition } from "../../hooks/useCurrentPosition"; import { Grid, Typography } from "@mui/material"; export default function Opening() { diff --git a/src/sections/analysis/panelBody/classificationTab/index.tsx b/src/sections/analysis/panelBody/classificationTab/index.tsx new file mode 100644 index 0000000..558c58b --- /dev/null +++ b/src/sections/analysis/panelBody/classificationTab/index.tsx @@ -0,0 +1,24 @@ +import { Grid, GridProps } from "@mui/material"; +import MovesPanel from "./movesPanel"; +import MovesClassificationsRecap from "./movesClassificationsRecap"; + +export default function ClassificationTab(props: GridProps) { + return ( + + + + + + ); +} diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/movesClassificationsRecap/classificationRow.tsx b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx similarity index 97% rename from src/sections/analysis/reviewPanelBody/classificationPanel/movesClassificationsRecap/classificationRow.tsx rename to src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx index da3c664..dfc3b14 100644 --- a/src/sections/analysis/reviewPanelBody/classificationPanel/movesClassificationsRecap/classificationRow.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx @@ -3,10 +3,10 @@ import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, gameAtom, gameEvalAtom } from "../../../states"; import { useMemo } from "react"; -import { moveClassificationColors } from "@/components/board/squareRenderer"; import Image from "next/image"; import { capitalize } from "@/lib/helpers"; import { useChessActions } from "@/hooks/useChessActions"; +import { moveClassificationColors } from "@/lib/chess"; interface Props { classification: MoveClassification; diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/movesClassificationsRecap/index.tsx b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx similarity index 100% rename from src/sections/analysis/reviewPanelBody/classificationPanel/movesClassificationsRecap/index.tsx rename to src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/index.tsx b/src/sections/analysis/panelBody/classificationTab/movesPanel/index.tsx similarity index 100% rename from src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/index.tsx rename to src/sections/analysis/panelBody/classificationTab/movesPanel/index.tsx diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/moveItem.tsx b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx similarity index 97% rename from src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/moveItem.tsx rename to src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx index c9370b2..17af1a1 100644 --- a/src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/moveItem.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx @@ -1,12 +1,12 @@ import { MoveClassification } from "@/types/enums"; import { Grid, Typography } from "@mui/material"; -import { moveClassificationColors } from "@/components/board/squareRenderer"; import Image from "next/image"; import { useAtomValue } from "jotai"; import { boardAtom, currentPositionAtom, gameAtom } from "../../../states"; import { useChessActions } from "@/hooks/useChessActions"; import { useEffect } from "react"; import { isInViewport } from "@/lib/helpers"; +import { moveClassificationColors } from "@/lib/chess"; interface Props { san: string; diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/movesLine.tsx b/src/sections/analysis/panelBody/classificationTab/movesPanel/movesLine.tsx similarity index 100% rename from src/sections/analysis/reviewPanelBody/classificationPanel/movesPanel/movesLine.tsx rename to src/sections/analysis/panelBody/classificationTab/movesPanel/movesLine.tsx diff --git a/src/sections/analysis/panelBody/graphTab/dot.tsx b/src/sections/analysis/panelBody/graphTab/dot.tsx new file mode 100644 index 0000000..7d91083 --- /dev/null +++ b/src/sections/analysis/panelBody/graphTab/dot.tsx @@ -0,0 +1,39 @@ +import { DotProps } from "recharts"; +import { ChartItemData } from "./types"; +import { useAtomValue } from "jotai"; +import { boardAtom, gameAtom } from "../../states"; +import { useChessActions } from "@/hooks/useChessActions"; +import { moveClassificationColors } from "@/lib/chess"; + +export default function CustomDot({ + cx, + cy, + r, + payload, +}: DotProps & { payload?: ChartItemData }) { + const { goToMove } = useChessActions(boardAtom); + const game = useAtomValue(gameAtom); + + const handleDotClick = () => { + if (!payload) return; + goToMove(payload.moveNb, game); + }; + + const moveColor = payload?.moveClassification + ? moveClassificationColors[payload.moveClassification] + : "grey"; + + return ( + + ); +} diff --git a/src/sections/analysis/panelBody/graphTab/index.tsx b/src/sections/analysis/panelBody/graphTab/index.tsx new file mode 100644 index 0000000..141aa3d --- /dev/null +++ b/src/sections/analysis/panelBody/graphTab/index.tsx @@ -0,0 +1,135 @@ +import { Box, Grid, GridProps } from "@mui/material"; +import { useAtomValue } from "jotai"; +import { + Area, + AreaChart, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { currentPositionAtom, gameEvalAtom } from "../../states"; +import { useMemo } from "react"; +import CustomTooltip from "./tooltip"; +import { ChartItemData } from "./types"; +import { PositionEval } from "@/types/eval"; +import { moveClassificationColors } from "@/lib/chess"; +import CustomDot from "./dot"; + +export default function GraphTab(props: GridProps) { + const gameEval = useAtomValue(gameEvalAtom); + const currentPosition = useAtomValue(currentPositionAtom); + + const chartData: ChartItemData[] = useMemo( + () => gameEval?.positions.map(formatEvalToChartData) ?? [], + [gameEval] + ); + + const boardMoveColor = currentPosition.eval?.moveClassification + ? moveClassificationColors[currentPosition.eval.moveClassification] + : "grey"; + + if (!gameEval) return null; + + return ( + + + + + + + } + isAnimationActive={false} + cursor={{ + stroke: "grey", + strokeWidth: 2, + strokeOpacity: 0.3, + }} + /> + } + isAnimationActive={false} + /> + + + + + + + ); +} + +const formatEvalToChartData = ( + position: PositionEval, + index: number +): ChartItemData => { + const line = position.lines[0]; + + const chartItem: ChartItemData = { + moveNb: index, + value: 10, + cp: line.cp, + mate: line.mate, + moveClassification: position.moveClassification, + }; + + if (line.mate) { + return { + ...chartItem, + value: line.mate > 0 ? 20 : 0, + }; + } + + if (line.cp) { + return { + ...chartItem, + value: Math.max(Math.min(line.cp / 100, 10), -10) + 10, + }; + } + + return chartItem; +}; diff --git a/src/sections/analysis/panelBody/graphTab/tooltip.tsx b/src/sections/analysis/panelBody/graphTab/tooltip.tsx new file mode 100644 index 0000000..8fe5172 --- /dev/null +++ b/src/sections/analysis/panelBody/graphTab/tooltip.tsx @@ -0,0 +1,27 @@ +import { TooltipProps } from "recharts"; +import { ChartItemData } from "./types"; +import { getLineEvalLabel } from "@/lib/chess"; + +export default function CustomTooltip({ + active, + payload, +}: TooltipProps) { + if (!active || !payload?.length) return null; + + const data = payload[0].payload as ChartItemData; + + return ( +
+ {getLineEvalLabel(data)} +
+ ); +} diff --git a/src/sections/analysis/panelBody/graphTab/types.ts b/src/sections/analysis/panelBody/graphTab/types.ts new file mode 100644 index 0000000..587e4c5 --- /dev/null +++ b/src/sections/analysis/panelBody/graphTab/types.ts @@ -0,0 +1,9 @@ +import { MoveClassification } from "@/types/enums"; + +export interface ChartItemData { + moveNb: number; + value: number; + cp?: number; + mate?: number; + moveClassification?: MoveClassification; +} diff --git a/src/sections/analysis/reviewPanelHeader/analyzeButton.tsx b/src/sections/analysis/panelHeader/analyzeButton.tsx similarity index 100% rename from src/sections/analysis/reviewPanelHeader/analyzeButton.tsx rename to src/sections/analysis/panelHeader/analyzeButton.tsx diff --git a/src/sections/analysis/reviewPanelHeader/gamePanel.tsx b/src/sections/analysis/panelHeader/gamePanel.tsx similarity index 100% rename from src/sections/analysis/reviewPanelHeader/gamePanel.tsx rename to src/sections/analysis/panelHeader/gamePanel.tsx diff --git a/src/sections/analysis/reviewPanelHeader/index.tsx b/src/sections/analysis/panelHeader/index.tsx similarity index 94% rename from src/sections/analysis/reviewPanelHeader/index.tsx rename to src/sections/analysis/panelHeader/index.tsx index faa43a9..0cc7bf7 100644 --- a/src/sections/analysis/reviewPanelHeader/index.tsx +++ b/src/sections/analysis/panelHeader/index.tsx @@ -7,7 +7,7 @@ import LinearProgressBar from "@/components/LinearProgressBar"; import { useAtomValue } from "jotai"; import { evaluationProgressAtom } from "../states"; -export default function ReviewPanelHeader() { +export default function PanelHeader() { const evaluationProgress = useAtomValue(evaluationProgressAtom); return ( @@ -30,7 +30,7 @@ export default function ReviewPanelHeader() { - Game Analysis + Game Review
diff --git a/src/sections/analysis/reviewPanelHeader/loadGame.tsx b/src/sections/analysis/panelHeader/loadGame.tsx similarity index 100% rename from src/sections/analysis/reviewPanelHeader/loadGame.tsx rename to src/sections/analysis/panelHeader/loadGame.tsx diff --git a/src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx b/src/sections/analysis/panelToolbar/flipBoardButton.tsx similarity index 100% rename from src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx rename to src/sections/analysis/panelToolbar/flipBoardButton.tsx diff --git a/src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx b/src/sections/analysis/panelToolbar/goToLastPositionButton.tsx similarity index 100% rename from src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx rename to src/sections/analysis/panelToolbar/goToLastPositionButton.tsx diff --git a/src/sections/analysis/reviewPanelToolbar/index.tsx b/src/sections/analysis/panelToolbar/index.tsx similarity index 97% rename from src/sections/analysis/reviewPanelToolbar/index.tsx rename to src/sections/analysis/panelToolbar/index.tsx index 2f978de..a649a06 100644 --- a/src/sections/analysis/reviewPanelToolbar/index.tsx +++ b/src/sections/analysis/panelToolbar/index.tsx @@ -10,7 +10,7 @@ import SaveButton from "./saveButton"; import { useEffect } from "react"; import { getStartingFen } from "@/lib/chess"; -export default function ReviewPanelToolBar() { +export default function PanelToolBar() { const board = useAtomValue(boardAtom); const { reset: resetBoard, undoMove: undoBoardMove } = useChessActions(boardAtom); diff --git a/src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx b/src/sections/analysis/panelToolbar/nextMoveButton.tsx similarity index 100% rename from src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx rename to src/sections/analysis/panelToolbar/nextMoveButton.tsx diff --git a/src/sections/analysis/reviewPanelToolbar/saveButton.tsx b/src/sections/analysis/panelToolbar/saveButton.tsx similarity index 100% rename from src/sections/analysis/reviewPanelToolbar/saveButton.tsx rename to src/sections/analysis/panelToolbar/saveButton.tsx diff --git a/src/sections/analysis/reviewPanelBody/classificationPanel/index.tsx b/src/sections/analysis/reviewPanelBody/classificationPanel/index.tsx deleted file mode 100644 index a9611e1..0000000 --- a/src/sections/analysis/reviewPanelBody/classificationPanel/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Divider, Grid } from "@mui/material"; -import MovesPanel from "./movesPanel"; -import MovesClassificationsRecap from "./movesClassificationsRecap"; -import { useAtomValue } from "jotai"; -import { gameAtom } from "../../states"; - -export default function ClassificationPanel() { - const game = useAtomValue(gameAtom); - - if (!game.history().length) return null; - - return ( - <> - - - - - - - - - ); -} diff --git a/src/sections/engineSettings/engineSettingsButton.tsx b/src/sections/engineSettings/engineSettingsButton.tsx index 282bc1d..8c832be 100644 --- a/src/sections/engineSettings/engineSettingsButton.tsx +++ b/src/sections/engineSettings/engineSettingsButton.tsx @@ -1,4 +1,4 @@ -import { IconButton, Tooltip } from "@mui/material"; +import { Fab } from "@mui/material"; import { useState } from "react"; import EngineSettingsDialog from "./engineSettingsDialog"; import { Icon } from "@iconify/react"; @@ -8,11 +8,21 @@ export default function EngineSettingsButton() { return ( <> - - setOpenDialog(true)} sx={{ paddingY: 0.3 }}> - - - + setOpenDialog(true)} + > + + Stockfish 16 Lite (HCE) is the default engine. It offers the best balance between speed and strength. Stockfish 16 is the strongest - engine available, but please note that it requires a one time download - of 40MB. + engine available, note that it requires a one time download of 40MB.