"use client"; // TODO: restructure import { Chessboard } from "react-chessboard"; import { IconCopy } from "@tabler/icons-react"; import { useState, useEffect, useContext, useReducer, useRef } from "react"; import type { KeyboardEvent, FormEvent } from "react"; //import Image from "next/image"; import type { Game } from "@chessu/types"; import type { Message } from "@/types"; import { io } from "socket.io-client"; import { API_URL } from "@/config"; import { SessionContext } from "@/context/session"; import { Chess } from "chess.js"; import type { Square, Move } from "chess.js"; import { initSocket, lobbyReducer, squareReducer } from "./handlers"; 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(""); const [chatMessages, setChatMessages] = useState([]); const [boardWidth, setBoardWidth] = useState(480); const [playBtnLoading, setPlayBtnLoading] = useState(false); const chatlistRef = 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) { const gameCopy = new Chess(); gameCopy.loadPgn(lobby.pgn as string); updateLobby({ type: "setGame", payload: gameCopy }); const lastMove = gameCopy.history({ verbose: true }).pop(); let lastMoveSquares = undefined; let kingSquare = undefined; if (lastMove) { lastMoveSquares = { [lastMove.from]: { background: "rgba(255, 255, 0, 0.4)" }, [lastMove.to]: { background: "rgba(255, 255, 0, 0.4)" } }; } if (gameCopy.inCheck()) { const kingPos = gameCopy.board().reduce((acc, row, index) => { const squareIndex = row.findIndex( (square) => square && square.type === "k" && square.color === gameCopy.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: lastMoveSquares, check: kingSquare }); } if (lobby.black?.id === session?.user?.id) { if (lobby.side !== "b") updateLobby({ type: "setSide", payload: "b" }); } else if (lobby.white?.id === session?.user?.id) { if (lobby.side !== "w") updateLobby({ type: "setSide", payload: "w" }); } else if (lobby.side !== "s") { updateLobby({ type: "setSide", payload: "s" }); } initSocket(session.user, socket, lobby, { updateLobby, addMessage, updateCustomSquares, makeMove }); 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]); 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) { 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: {} }); console.log(m); console.log(err); return false; } } function isDraggablePiece({ piece }: { piece: string }) { if (lobby.side === "s") return true; return piece.startsWith(lobby.side); } function onDrop(sourceSquare: Square, targetSquare: Square) { if (lobby.side !== lobby.actualGame.turn()) return false; 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()) return; getMoveOptions(sourceSquare); } function onPieceDragEnd() { updateCustomSquares({ options: {} }); } function onSquareClick(square: Square) { updateCustomSquares({ rightClicked: {} }); if (lobby.side !== lobby.actualGame.turn()) return; function resetFirstMove(square: Square) { setMoveFrom(square); getMoveOptions(square); } // from square if (!moveFrom) { resetFirstMove(square); return; } const moveDetails = { from: moveFrom, to: square, promotion: "q" }; const move = makeMove(moveDetails); if (!move) { resetFirstMove(square); } else { setMoveFrom(""); 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; } } 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:
ches.su/game/{initialLobby.code} {/* TODO */}
{(lobby.actualGame.pgn() || "") .split(/\d+\./) .filter((move) => move.trim() !== "") .map((moveSet, i) => { const moves = moveSet.trim().split(" "); return ( ); })}
{i + 1}. {moves[0]} {moves[1]}
    {chatMessages.map((m, i) => (
  • {m.author.name}: {m.message}
  • ))}
{lobby.observers && lobby.observers.length > 0 && (
Spectators: {lobby.observers?.map((o) => o.name).join(", ")}
)}
); }