From 034ba1a2064e2524e901163c51a7ba0991d8691b Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sun, 12 Mar 2023 19:54:22 +0800 Subject: [PATCH] move history navigation (closes #9) --- client/src/components/game/GamePage.tsx | 203 +++++++++++++++++---- client/src/components/game/socketEvents.ts | 4 +- client/src/components/game/utils.ts | 6 +- 3 files changed, 175 insertions(+), 38 deletions(-) diff --git a/client/src/components/game/GamePage.tsx b/client/src/components/game/GamePage.tsx index 8648cdc..128756b 100644 --- a/client/src/components/game/GamePage.tsx +++ b/client/src/components/game/GamePage.tsx @@ -1,7 +1,13 @@ "use client"; -// TODO: restructure +// TODO: restructure, i could use some help with this :> -import { IconCopy } from "@tabler/icons-react"; +import { + IconChevronLeft, + IconChevronRight, + IconCopy, + IconPlayerSkipBack, + IconPlayerSkipForward +} from "@tabler/icons-react"; import type { FormEvent, KeyboardEvent } from "react"; @@ -13,6 +19,7 @@ import type { Game } from "@chessu/types"; import type { Move, Square } from "chess.js"; import { Chess } from "chess.js"; +import type { ClearPremoves } from "react-chessboard"; import { Chessboard } from "react-chessboard"; import { API_URL } from "@/config"; @@ -41,14 +48,17 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { }); const [moveFrom, setMoveFrom] = useState(null); - - const [chatMessages, setChatMessages] = useState([]); const [boardWidth, setBoardWidth] = useState(480); + const chessboardRef = useRef(null); + + const [navFen, setNavFen] = useState(null); + const [navIndex, setNavIndex] = useState(null); const [playBtnLoading, setPlayBtnLoading] = useState(false); const [copiedLink, setCopiedLink] = useState(false); - - const chatlistRef = useRef(null); + const [chatMessages, setChatMessages] = useState([]); + const chatListRef = useRef(null); + const moveListRef = useRef(null); useEffect(() => { if (!session?.user || !session.user?.id) return; @@ -58,7 +68,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { handleResize(); if (lobby.pgn && lobby.actualGame.pgn() !== lobby.pgn) { - syncPgn(lobby.pgn, lobby, { updateCustomSquares }); + syncPgn(lobby.pgn, lobby, { updateCustomSquares, setNavFen, setNavIndex }); } syncSide(session.user, undefined, lobby, { updateLobby }); @@ -67,7 +77,9 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { updateLobby, addMessage, updateCustomSquares, - makeMove + makeMove, + setNavFen, + setNavIndex }); return () => { @@ -80,11 +92,18 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { // auto scroll down when new message is added useEffect(() => { - const chatlist = chatlistRef.current; - if (!chatlist) return; - chatlist.scrollTop = chatlist.scrollHeight; + const chatList = chatListRef.current; + if (!chatList) return; + chatList.scrollTop = chatList.scrollHeight; }, [chatMessages]); + // auto scroll for moves + useEffect(() => { + const activeMoveEl = document.getElementById("activeNavMove"); + if (!activeMoveEl) return; + activeMoveEl.scrollIntoView(); + }); + useEffect(() => { updateTurnTitle(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -148,6 +167,8 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { const result = lobby.actualGame.move(m); if (result) { + setNavFen(null); + setNavIndex(null); updateLobby({ type: "updateLobby", payload: { pgn: lobby.actualGame.pgn() } @@ -193,7 +214,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { } function onDrop(sourceSquare: Square, targetSquare: Square) { - if (lobby.side === "s") return false; + if (lobby.side === "s" || navFen) return false; // premove if (lobby.side !== lobby.actualGame.turn()) return true; @@ -240,7 +261,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { } function onPieceDragBegin(_piece: string, sourceSquare: Square) { - if (lobby.side !== lobby.actualGame.turn()) return; + if (lobby.side !== lobby.actualGame.turn() || navFen) return; getMoveOptions(sourceSquare); } @@ -251,7 +272,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { function onSquareClick(square: Square) { updateCustomSquares({ rightClicked: {} }); - if (lobby.side !== lobby.actualGame.turn()) return; + if (lobby.side !== lobby.actualGame.turn() || navFen) return; function resetFirstMove(square: Square) { setMoveFrom(square); @@ -347,6 +368,93 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { }, 5000); } + function getMoveListHtml() { + const history = lobby.actualGame.history({ verbose: true }); + const movePairs = history + .slice(history.length / 2) + .map((_, i) => history.slice((i *= 2), i + 2)); + + return movePairs.map((moves, i) => { + return ( + + {i + 1}. + navigateMove(history.indexOf(moves[0]))} + > + {moves[0].san} + + {moves[1] && ( + navigateMove(history.indexOf(moves[1]))} + > + {moves[1].san} + + )} + + ); + }); + } + + function navigateMove(index: number | null | "prev") { + const history = lobby.actualGame.history({ verbose: true }); + + if (index === null || index >= history.length - 1 || !history.length) { + // last move + setNavIndex(null); + setNavFen(null); + return; + } + + if (index === "prev") { + index = history.length - 2; + } else if (index < 0) { + index = 0; + } + + chessboardRef.current?.clearPremoves(false); + + setNavIndex(index); + setNavFen(history[index + 1].fen); + } + + function getNavMoveSquares() { + if (navIndex === null) return; + const history = lobby.actualGame.history({ verbose: true }); + + if (!history.length) return; + + return { + [history[navIndex].from]: { background: "rgba(255, 255, 0, 0.4)" }, + [history[navIndex].to]: { background: "rgba(255, 255, 0, 0.4)" } + }; + } + return (
@@ -371,7 +479,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { boardWidth={boardWidth} customDarkSquareStyle={{ backgroundColor: "#4b7399" }} customLightSquareStyle={{ backgroundColor: "#eae9d2" }} - position={lobby.actualGame.fen()} + position={navFen || lobby.actualGame.fen()} boardOrientation={lobby.side === "b" ? "black" : "white"} isDraggablePiece={isDraggablePiece} onPieceDragBegin={onPieceDragBegin} @@ -379,13 +487,14 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { onPieceDrop={onDrop} onSquareClick={onSquareClick} onSquareRightClick={onSquareRightClick} - arePremovesAllowed={true} + arePremovesAllowed={!navFen} customSquareStyles={{ - ...customSquares.lastMove, - ...customSquares.check, + ...(navIndex === null ? customSquares.lastMove : getNavMoveSquares()), + ...(navIndex === null ? customSquares.check : {}), ...customSquares.rightClicked, - ...customSquares.options + ...(navIndex === null ? customSquares.options : {}) }} + ref={chessboardRef} />
@@ -418,25 +527,47 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
-
+
- - {(lobby.actualGame.pgn() || "") - .split(/\d+\./) - .filter((move) => move.trim() !== "") - .map((moveSet, i) => { - const moves = moveSet.trim().split(" "); - return ( - - - - - - ); - })} - + {getMoveListHtml()}
{i + 1}.{moves[0]}{moves[1]}
+
+ + + + +
@@ -444,7 +575,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
    {chatMessages.map((m, i) => (
  • diff --git a/client/src/components/game/socketEvents.ts b/client/src/components/game/socketEvents.ts index 4911116..a8fb926 100644 --- a/client/src/components/game/socketEvents.ts +++ b/client/src/components/game/socketEvents.ts @@ -1,6 +1,6 @@ import type { Action, CustomSquares, Lobby, Message } from "@/types"; import type { Game, User } from "@chessu/types"; -import type { Dispatch } from "react"; +import type { Dispatch, SetStateAction } from "react"; import type { Socket } from "socket.io-client"; import { syncPgn, syncSide } from "./utils"; @@ -13,6 +13,8 @@ export function initSocket( addMessage: Function; updateCustomSquares: Dispatch>; makeMove: Function; + setNavFen: Dispatch>; + setNavIndex: Dispatch>; } ) { socket.on("connect", () => { diff --git a/client/src/components/game/utils.ts b/client/src/components/game/utils.ts index 21813ad..793b1d7 100644 --- a/client/src/components/game/utils.ts +++ b/client/src/components/game/utils.ts @@ -1,14 +1,18 @@ import type { Action, CustomSquares, Lobby } from "@/types"; import type { Game, User } from "@chessu/types"; -import type { Dispatch } from "react"; +import type { Dispatch, SetStateAction } from "react"; export const syncPgn = ( latestPgn: string, lobby: Lobby, actions: { updateCustomSquares: Dispatch>; + setNavFen: Dispatch>; + setNavIndex: Dispatch>; } ) => { + actions.setNavFen(null); + actions.setNavIndex(null); lobby.actualGame.loadPgn(latestPgn as string); const lastMove = lobby.actualGame.history({ verbose: true }).pop();