move history navigation (closes #9)
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
"use client";
|
"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";
|
import type { FormEvent, KeyboardEvent } from "react";
|
||||||
|
|
||||||
@@ -13,6 +19,7 @@ import type { Game } from "@chessu/types";
|
|||||||
|
|
||||||
import type { Move, Square } from "chess.js";
|
import type { Move, Square } from "chess.js";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
|
import type { ClearPremoves } from "react-chessboard";
|
||||||
import { Chessboard } from "react-chessboard";
|
import { Chessboard } from "react-chessboard";
|
||||||
|
|
||||||
import { API_URL } from "@/config";
|
import { API_URL } from "@/config";
|
||||||
@@ -41,14 +48,17 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [moveFrom, setMoveFrom] = useState<string | Square | null>(null);
|
const [moveFrom, setMoveFrom] = useState<string | Square | null>(null);
|
||||||
|
|
||||||
const [chatMessages, setChatMessages] = useState<Message[]>([]);
|
|
||||||
const [boardWidth, setBoardWidth] = useState(480);
|
const [boardWidth, setBoardWidth] = useState(480);
|
||||||
|
const chessboardRef = useRef<ClearPremoves>(null);
|
||||||
|
|
||||||
|
const [navFen, setNavFen] = useState<string | null>(null);
|
||||||
|
const [navIndex, setNavIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const [playBtnLoading, setPlayBtnLoading] = useState(false);
|
const [playBtnLoading, setPlayBtnLoading] = useState(false);
|
||||||
const [copiedLink, setCopiedLink] = useState(false);
|
const [copiedLink, setCopiedLink] = useState(false);
|
||||||
|
const [chatMessages, setChatMessages] = useState<Message[]>([]);
|
||||||
const chatlistRef = useRef<HTMLUListElement>(null);
|
const chatListRef = useRef<HTMLUListElement>(null);
|
||||||
|
const moveListRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.user || !session.user?.id) return;
|
if (!session?.user || !session.user?.id) return;
|
||||||
@@ -58,7 +68,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
handleResize();
|
handleResize();
|
||||||
|
|
||||||
if (lobby.pgn && lobby.actualGame.pgn() !== lobby.pgn) {
|
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 });
|
syncSide(session.user, undefined, lobby, { updateLobby });
|
||||||
@@ -67,7 +77,9 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
updateLobby,
|
updateLobby,
|
||||||
addMessage,
|
addMessage,
|
||||||
updateCustomSquares,
|
updateCustomSquares,
|
||||||
makeMove
|
makeMove,
|
||||||
|
setNavFen,
|
||||||
|
setNavIndex
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -80,11 +92,18 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
|
|
||||||
// auto scroll down when new message is added
|
// auto scroll down when new message is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const chatlist = chatlistRef.current;
|
const chatList = chatListRef.current;
|
||||||
if (!chatlist) return;
|
if (!chatList) return;
|
||||||
chatlist.scrollTop = chatlist.scrollHeight;
|
chatList.scrollTop = chatList.scrollHeight;
|
||||||
}, [chatMessages]);
|
}, [chatMessages]);
|
||||||
|
|
||||||
|
// auto scroll for moves
|
||||||
|
useEffect(() => {
|
||||||
|
const activeMoveEl = document.getElementById("activeNavMove");
|
||||||
|
if (!activeMoveEl) return;
|
||||||
|
activeMoveEl.scrollIntoView();
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateTurnTitle();
|
updateTurnTitle();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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);
|
const result = lobby.actualGame.move(m);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
setNavFen(null);
|
||||||
|
setNavIndex(null);
|
||||||
updateLobby({
|
updateLobby({
|
||||||
type: "updateLobby",
|
type: "updateLobby",
|
||||||
payload: { pgn: lobby.actualGame.pgn() }
|
payload: { pgn: lobby.actualGame.pgn() }
|
||||||
@@ -193,7 +214,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(sourceSquare: Square, targetSquare: Square) {
|
function onDrop(sourceSquare: Square, targetSquare: Square) {
|
||||||
if (lobby.side === "s") return false;
|
if (lobby.side === "s" || navFen) return false;
|
||||||
|
|
||||||
// premove
|
// premove
|
||||||
if (lobby.side !== lobby.actualGame.turn()) return true;
|
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) {
|
function onPieceDragBegin(_piece: string, sourceSquare: Square) {
|
||||||
if (lobby.side !== lobby.actualGame.turn()) return;
|
if (lobby.side !== lobby.actualGame.turn() || navFen) return;
|
||||||
|
|
||||||
getMoveOptions(sourceSquare);
|
getMoveOptions(sourceSquare);
|
||||||
}
|
}
|
||||||
@@ -251,7 +272,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
|
|
||||||
function onSquareClick(square: Square) {
|
function onSquareClick(square: Square) {
|
||||||
updateCustomSquares({ rightClicked: {} });
|
updateCustomSquares({ rightClicked: {} });
|
||||||
if (lobby.side !== lobby.actualGame.turn()) return;
|
if (lobby.side !== lobby.actualGame.turn() || navFen) return;
|
||||||
|
|
||||||
function resetFirstMove(square: Square) {
|
function resetFirstMove(square: Square) {
|
||||||
setMoveFrom(square);
|
setMoveFrom(square);
|
||||||
@@ -347,6 +368,93 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
}, 5000);
|
}, 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 (
|
||||||
|
<tr className="flex w-full items-center gap-1" key={i + 1}>
|
||||||
|
<td className="">{i + 1}.</td>
|
||||||
|
<td
|
||||||
|
className={
|
||||||
|
"btn btn-ghost btn-xs h-full w-2/5 font-normal normal-case" +
|
||||||
|
((history.indexOf(moves[0]) === history.length - 1 && navIndex === null) ||
|
||||||
|
navIndex === history.indexOf(moves[0])
|
||||||
|
? " btn-active pointer-events-none rounded-none"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
id={
|
||||||
|
(history.indexOf(moves[0]) === history.length - 1 && navIndex === null) ||
|
||||||
|
navIndex === history.indexOf(moves[0])
|
||||||
|
? "activeNavMove"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(history.indexOf(moves[0]))}
|
||||||
|
>
|
||||||
|
{moves[0].san}
|
||||||
|
</td>
|
||||||
|
{moves[1] && (
|
||||||
|
<td
|
||||||
|
className={
|
||||||
|
"btn btn-ghost btn-xs h-full w-2/5 font-normal normal-case" +
|
||||||
|
((history.indexOf(moves[1]) === history.length - 1 && navIndex === null) ||
|
||||||
|
navIndex === history.indexOf(moves[1])
|
||||||
|
? " btn-active pointer-events-none rounded-none"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
id={
|
||||||
|
(history.indexOf(moves[1]) === history.length - 1 && navIndex === null) ||
|
||||||
|
navIndex === history.indexOf(moves[1])
|
||||||
|
? "activeNavMove"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(history.indexOf(moves[1]))}
|
||||||
|
>
|
||||||
|
{moves[1].san}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="flex w-full flex-wrap justify-center gap-6 px-4 py-4 lg:gap-10 2xl:gap-16">
|
<div className="flex w-full flex-wrap justify-center gap-6 px-4 py-4 lg:gap-10 2xl:gap-16">
|
||||||
<div className="relative h-min">
|
<div className="relative h-min">
|
||||||
@@ -371,7 +479,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
boardWidth={boardWidth}
|
boardWidth={boardWidth}
|
||||||
customDarkSquareStyle={{ backgroundColor: "#4b7399" }}
|
customDarkSquareStyle={{ backgroundColor: "#4b7399" }}
|
||||||
customLightSquareStyle={{ backgroundColor: "#eae9d2" }}
|
customLightSquareStyle={{ backgroundColor: "#eae9d2" }}
|
||||||
position={lobby.actualGame.fen()}
|
position={navFen || lobby.actualGame.fen()}
|
||||||
boardOrientation={lobby.side === "b" ? "black" : "white"}
|
boardOrientation={lobby.side === "b" ? "black" : "white"}
|
||||||
isDraggablePiece={isDraggablePiece}
|
isDraggablePiece={isDraggablePiece}
|
||||||
onPieceDragBegin={onPieceDragBegin}
|
onPieceDragBegin={onPieceDragBegin}
|
||||||
@@ -379,13 +487,14 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
onPieceDrop={onDrop}
|
onPieceDrop={onDrop}
|
||||||
onSquareClick={onSquareClick}
|
onSquareClick={onSquareClick}
|
||||||
onSquareRightClick={onSquareRightClick}
|
onSquareRightClick={onSquareRightClick}
|
||||||
arePremovesAllowed={true}
|
arePremovesAllowed={!navFen}
|
||||||
customSquareStyles={{
|
customSquareStyles={{
|
||||||
...customSquares.lastMove,
|
...(navIndex === null ? customSquares.lastMove : getNavMoveSquares()),
|
||||||
...customSquares.check,
|
...(navIndex === null ? customSquares.check : {}),
|
||||||
...customSquares.rightClicked,
|
...customSquares.rightClicked,
|
||||||
...customSquares.options
|
...(navIndex === null ? customSquares.options : {})
|
||||||
}}
|
}}
|
||||||
|
ref={chessboardRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,25 +527,47 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-36 w-full overflow-y-scroll">
|
<div className="h-32 w-full overflow-y-scroll" ref={moveListRef}>
|
||||||
<table className="table-compact table w-full">
|
<table className="table-compact table w-full">
|
||||||
<tbody>
|
<tbody>{getMoveListHtml()}</tbody>
|
||||||
{(lobby.actualGame.pgn() || "")
|
|
||||||
.split(/\d+\./)
|
|
||||||
.filter((move) => move.trim() !== "")
|
|
||||||
.map((moveSet, i) => {
|
|
||||||
const moves = moveSet.trim().split(" ");
|
|
||||||
return (
|
|
||||||
<tr className="flex w-full items-center gap-1" key={i + 1}>
|
|
||||||
<td className="">{i + 1}.</td>
|
|
||||||
<td className="w-1/2 text-center">{moves[0]}</td>
|
|
||||||
<td className="w-1/2 text-center">{moves[1]}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex h-4 w-full">
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"btn btn-sm flex-grow rounded-r-none" +
|
||||||
|
(navIndex === 0 || lobby.actualGame.history().length <= 1 ? " btn-disabled" : "")
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(0)}
|
||||||
|
>
|
||||||
|
<IconPlayerSkipBack size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"btn btn-sm flex-grow rounded-none" +
|
||||||
|
(navIndex === 0 || lobby.actualGame.history().length <= 1 ? " btn-disabled" : "")
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(navIndex === null ? "prev" : navIndex - 1)}
|
||||||
|
>
|
||||||
|
<IconChevronLeft size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"btn btn-sm flex-grow rounded-none" + (navIndex === null ? " btn-disabled" : "")
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(navIndex === null ? null : navIndex + 1)}
|
||||||
|
>
|
||||||
|
<IconChevronRight size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
"btn btn-sm flex-grow rounded-l-none" + (navIndex === null ? " btn-disabled" : "")
|
||||||
|
}
|
||||||
|
onClick={() => navigateMove(null)}
|
||||||
|
>
|
||||||
|
<IconPlayerSkipForward size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -444,7 +575,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
<div className="bg-base-300 flex h-full w-full min-w-[64px] flex-col rounded-lg p-4 shadow-sm">
|
<div className="bg-base-300 flex h-full w-full min-w-[64px] flex-col rounded-lg p-4 shadow-sm">
|
||||||
<ul
|
<ul
|
||||||
className="mb-4 flex h-full flex-col gap-1 overflow-y-scroll break-words"
|
className="mb-4 flex h-full flex-col gap-1 overflow-y-scroll break-words"
|
||||||
ref={chatlistRef}
|
ref={chatListRef}
|
||||||
>
|
>
|
||||||
{chatMessages.map((m, i) => (
|
{chatMessages.map((m, i) => (
|
||||||
<li className="max-w-[30rem]" key={i}>
|
<li className="max-w-[30rem]" key={i}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Action, CustomSquares, Lobby, Message } from "@/types";
|
import type { Action, CustomSquares, Lobby, Message } from "@/types";
|
||||||
import type { Game, User } from "@chessu/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 type { Socket } from "socket.io-client";
|
||||||
import { syncPgn, syncSide } from "./utils";
|
import { syncPgn, syncSide } from "./utils";
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ export function initSocket(
|
|||||||
addMessage: Function;
|
addMessage: Function;
|
||||||
updateCustomSquares: Dispatch<Partial<CustomSquares>>;
|
updateCustomSquares: Dispatch<Partial<CustomSquares>>;
|
||||||
makeMove: Function;
|
makeMove: Function;
|
||||||
|
setNavFen: Dispatch<SetStateAction<string | null>>;
|
||||||
|
setNavIndex: Dispatch<SetStateAction<number | null>>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import type { Action, CustomSquares, Lobby } from "@/types";
|
import type { Action, CustomSquares, Lobby } from "@/types";
|
||||||
import type { Game, User } from "@chessu/types";
|
import type { Game, User } from "@chessu/types";
|
||||||
import type { Dispatch } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
export const syncPgn = (
|
export const syncPgn = (
|
||||||
latestPgn: string,
|
latestPgn: string,
|
||||||
lobby: Lobby,
|
lobby: Lobby,
|
||||||
actions: {
|
actions: {
|
||||||
updateCustomSquares: Dispatch<Partial<CustomSquares>>;
|
updateCustomSquares: Dispatch<Partial<CustomSquares>>;
|
||||||
|
setNavFen: Dispatch<SetStateAction<string | null>>;
|
||||||
|
setNavIndex: Dispatch<SetStateAction<number | null>>;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
|
actions.setNavFen(null);
|
||||||
|
actions.setNavIndex(null);
|
||||||
lobby.actualGame.loadPgn(latestPgn as string);
|
lobby.actualGame.loadPgn(latestPgn as string);
|
||||||
|
|
||||||
const lastMove = lobby.actualGame.history({ verbose: true }).pop();
|
const lastMove = lobby.actualGame.history({ verbose: true }).pop();
|
||||||
|
|||||||
Reference in New Issue
Block a user