"use client"; // TODO: restructure, i could use some help with this :> import { IconChevronLeft, IconChevronRight, IconCopy, IconPlayerSkipBack, IconPlayerSkipForward } from "@tabler/icons-react"; import type { FormEvent, KeyboardEvent } from "react"; import { SessionContext } from "@/context/session"; import { useContext, useEffect, useReducer, useRef, useState } from "react"; import type { Message } from "@/types"; 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"; import { io } from "socket.io-client"; import { lobbyReducer, squareReducer } from "./reducers"; import { initSocket } from "./socketEvents"; import { syncPgn, syncSide } from "./utils"; const socket = io(API_URL, { withCredentials: true, autoConnect: false }); export default function GamePage({ initialLobby }: { initialLobby: Game }) { const session = useContext(SessionContext); const [lobby, updateLobby] = useReducer(lobbyReducer, { ...initialLobby, actualGame: new Chess(), side: "s" }); const [customSquares, updateCustomSquares] = useReducer(squareReducer, { options: {}, lastMove: {}, rightClicked: {}, check: {} }); const [moveFrom, setMoveFrom] = useState(null); 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 [chatMessages, setChatMessages] = useState([ { author: {}, message: `Welcome! You can invite friends to watch or play by sharing the link above. Have fun!` } ]); const chatListRef = useRef(null); const moveListRef = useRef(null); useEffect(() => { if (!session?.user || !session.user?.id) return; socket.connect(); window.addEventListener("resize", handleResize); handleResize(); if (lobby.pgn && lobby.actualGame.pgn() !== lobby.pgn) { syncPgn(lobby.pgn, lobby, { updateCustomSquares, setNavFen, setNavIndex }); } syncSide(session.user, undefined, lobby, { updateLobby }); initSocket(session.user, socket, lobby, { updateLobby, addMessage, updateCustomSquares, makeMove, setNavFen, setNavIndex }); return () => { window.removeEventListener("resize", handleResize); socket.removeAllListeners(); socket.disconnect(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // auto scroll down when new message is added useEffect(() => { 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({ inline: "nearest", block: "nearest" }); }); useEffect(() => { updateTurnTitle(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [lobby]); function updateTurnTitle() { if (lobby.side === "s" || !lobby.white?.id || !lobby.black?.id) return; if (lobby.side === lobby.actualGame.turn()) { document.title = "(your turn) chessu"; } else { document.title = "chessu"; } } function handleResize() { if (window.innerWidth >= 1920) { setBoardWidth(580); } else if (window.innerWidth >= 1536) { setBoardWidth(540); } else if (window.innerWidth >= 768) { setBoardWidth(480); } else { setBoardWidth(350); } } function addMessage(message: Message) { setChatMessages((prev) => [...prev, message]); } function sendChat(message: string) { if (!session?.user) return; socket.emit("chat", message); addMessage({ author: session.user, message }); } function chatKeyUp(e: KeyboardEvent) { e.preventDefault(); if (e.key === "Enter") { const input = e.target as HTMLInputElement; if (!input.value || input.value.length == 0) return; sendChat(input.value); input.value = ""; } } function chatClickSend(e: FormEvent) { e.preventDefault(); const target = e.target as HTMLFormElement; const input = target.elements.namedItem("chatInput") as HTMLInputElement; if (!input.value || input.value.length == 0) return; sendChat(input.value); input.value = ""; } function makeMove(m: { from: string; to: string; promotion?: string }) { try { const result = lobby.actualGame.move(m); if (result) { setNavFen(null); setNavIndex(null); updateLobby({ type: "updateLobby", payload: { pgn: lobby.actualGame.pgn() } }); updateTurnTitle(); let kingSquare = undefined; if (lobby.actualGame.inCheck()) { const kingPos = lobby.actualGame.board().reduce((acc, row, index) => { const squareIndex = row.findIndex( (square) => square && square.type === "k" && square.color === lobby.actualGame.turn() ); return squareIndex >= 0 ? `${String.fromCharCode(squareIndex + 97)}${8 - index}` : acc; }, ""); kingSquare = { [kingPos]: { background: "radial-gradient(red, rgba(255,0,0,.4), transparent 70%)", borderRadius: "50%" } }; } updateCustomSquares({ lastMove: { [result.from]: { background: "rgba(255, 255, 0, 0.4)" }, [result.to]: { background: "rgba(255, 255, 0, 0.4)" } }, options: {}, check: kingSquare }); return true; } else { throw new Error("Invalid move"); } } catch (err) { updateCustomSquares({ options: {} }); return false; } } function isDraggablePiece({ piece }: { piece: string }) { return piece.startsWith(lobby.side); } function onDrop(sourceSquare: Square, targetSquare: Square) { if (lobby.side === "s" || navFen) return false; // premove if (lobby.side !== lobby.actualGame.turn()) return true; const moveDetails = { from: sourceSquare, to: targetSquare, promotion: "q" }; const move = makeMove(moveDetails); if (!move) return false; // illegal move socket.emit("sendMove", moveDetails); return true; } function getMoveOptions(square: Square) { const moves = lobby.actualGame.moves({ square, verbose: true }) as Move[]; if (moves.length === 0) { return; } const newSquares: { [square: string]: { background: string; borderRadius?: string }; } = {}; moves.map((move) => { newSquares[move.to] = { background: lobby.actualGame.get(move.to as Square) && lobby.actualGame.get(move.to as Square)?.color !== lobby.actualGame.get(square)?.color ? "radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)" : "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", borderRadius: "50%" }; return move; }); newSquares[square] = { background: "rgba(255, 255, 0, 0.4)" }; updateCustomSquares({ options: newSquares }); } function onPieceDragBegin(_piece: string, sourceSquare: Square) { if (lobby.side !== lobby.actualGame.turn() || navFen) return; getMoveOptions(sourceSquare); } function onPieceDragEnd() { updateCustomSquares({ options: {} }); } function onSquareClick(square: Square) { updateCustomSquares({ rightClicked: {} }); if (lobby.side !== lobby.actualGame.turn() || navFen) return; function resetFirstMove(square: Square) { setMoveFrom(square); getMoveOptions(square); } // from square if (moveFrom === null) { resetFirstMove(square); return; } const moveDetails = { from: moveFrom, to: square, promotion: "q" }; const move = makeMove(moveDetails); if (!move) { resetFirstMove(square); } else { setMoveFrom(null); socket.emit("sendMove", moveDetails); } } function onSquareRightClick(square: Square) { const colour = "rgba(0, 0, 255, 0.4)"; updateCustomSquares({ rightClicked: { ...customSquares.rightClicked, [square]: customSquares.rightClicked[square] && customSquares.rightClicked[square]?.backgroundColor === colour ? undefined : { backgroundColor: colour } } }); } function clickPlay(e: FormEvent) { setPlayBtnLoading(true); e.preventDefault(); socket.emit("joinAsPlayer"); } function getPlayerHtml(side: "top" | "bottom") { const blackHtml = (
{lobby.black?.name || "(no one)"} black {lobby.black?.connected === false && ( disconnected )}
); const whiteHtml = (
{lobby.white?.name || "(no one)"} white {lobby.white?.connected === false && ( disconnected )}
); if (lobby.black?.id === session?.user?.id) { return side === "top" ? whiteHtml : blackHtml; } else { return side === "top" ? blackHtml : whiteHtml; } } function copyInvite() { const text = `https://ches.su/game/${initialLobby.code}`; if ("clipboard" in navigator) { navigator.clipboard.writeText(text); } else { document.execCommand("copy", true, text); } setCopiedLink(true); setTimeout(() => { setCopiedLink(false); }, 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 (
{/* overlay */} {(!lobby.white?.id || !lobby.black?.id) && (
Waiting for opponent. {session?.user?.id !== lobby.white?.id && session?.user?.id !== lobby.black?.id && ( )}
)}
{getPlayerHtml("top")}
vs
{getPlayerHtml("bottom")}
Invite friends:
copied to clipboard
{getMoveListHtml()}
    {chatMessages.map((m, i) => (
  • {m.author.id && ( {m.author.name}:{" "} )} {m.message}
  • ))}
{lobby.observers && lobby.observers.length > 0 && (
Spectators: {lobby.observers?.map((o) => o.name).join(", ")}
)}
); }