fix(chess): migrate ChessBoard to react-chessboard v5 API
Some checks failed
Deploy to Production / test (push) Failing after 33s

react-chessboard v5 moved all props into an `options` object and
renamed several callbacks/style props. The v4-style props were silently
ignored, causing pieces to snap back, no legal-move highlights, and no
WS events on drop. Also adds a custom promotion dialog since v5 removed
the built-in one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 19:26:24 +02:00
parent abca1922f2
commit ebac1ad6cc

View File

@@ -325,7 +325,8 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
onAction({ type: "move", from, to, ...(promotion ? { promotion } : {}) }); onAction({ type: "move", from, to, ...(promotion ? { promotion } : {}) });
} }
function onDrop(sourceSquare: string, targetSquare: string): boolean { function onDrop({ sourceSquare, targetSquare }: { piece: any; sourceSquare: string; targetSquare: string | null }): boolean {
if (!targetSquare) return false;
if (isSpectator || !isMyTurn || isGameOver) return false; if (isSpectator || !isMyTurn || isGameOver) return false;
const testGame = new Chess(localFenRef.current); const testGame = new Chess(localFenRef.current);
const piece = testGame.get(sourceSquare as any); const piece = testGame.get(sourceSquare as any);
@@ -347,15 +348,13 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
function handlePromotion(piece: string) { function handlePromotion(piece: string) {
if (promotionFrom && promotionTo) { if (promotionFrom && promotionTo) {
const promotionPiece = piece[1]?.toLowerCase() ?? "q"; dispatchMove(promotionFrom, promotionTo, piece);
dispatchMove(promotionFrom, promotionTo, promotionPiece);
} }
setPromotionFrom(null); setPromotionFrom(null);
setPromotionTo(null); setPromotionTo(null);
return true;
} }
function onSquareClick(square: string) { function onSquareClick({ square }: { piece: any; square: string }) {
if (isSpectator || isGameOver || !isMyTurn) return; if (isSpectator || isGameOver || !isMyTurn) return;
if (promotionFrom !== null) return; if (promotionFrom !== null) return;
const testGame = new Chess(localFenRef.current); const testGame = new Chess(localFenRef.current);
@@ -397,9 +396,9 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
if (isBothSides ? color === turn : color === myColor) setSelectedSquare(square); if (isBothSides ? color === turn : color === myColor) setSelectedSquare(square);
} }
function isDraggablePiece({ piece }: { piece: string }): boolean { function canDragPiece({ piece }: { isSparePiece: boolean; piece: { pieceType: string }; square: string | null }): boolean {
if (isSpectator || !isMyTurn || isGameOver) return false; if (isSpectator || !isMyTurn || isGameOver) return false;
const pieceColor = piece[0] === "w" ? "white" : "black"; const pieceColor = piece.pieceType === piece.pieceType.toUpperCase() ? "white" : "black";
return isBothSides ? pieceColor === turn : pieceColor === myColor; return isBothSides ? pieceColor === turn : pieceColor === myColor;
} }
@@ -472,26 +471,41 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
/> />
{/* Board wrapper */} {/* Board wrapper */}
<div ref={containerRef} className="relative" style={{ lineHeight: 0 }}> <div ref={containerRef} className="relative" style={{ lineHeight: 0, width: boardWidth, maxWidth: "100%" }}>
<Chessboard <Chessboard
position={localFen} options={{
onPieceDrop={onDrop} position: localFen,
onPromotionPieceSelect={handlePromotion} onPieceDrop: onDrop,
onSquareClick={onSquareClick} onSquareClick: onSquareClick,
boardOrientation={boardOrientation} boardOrientation: boardOrientation,
isDraggablePiece={isDraggablePiece} canDragPiece: canDragPiece,
boardWidth={boardWidth} animationDurationInMs: 150,
showPromotionDialog={promotionFrom !== null} squareStyles: customSquareStyles,
promotionToSquare={promotionTo as any} darkSquareStyle: { backgroundColor: DARK_SQUARE },
animationDuration={150} lightSquareStyle: { backgroundColor: LIGHT_SQUARE },
customSquareStyles={customSquareStyles} boardStyle: { borderRadius: "0" },
customDarkSquareStyle={{ backgroundColor: DARK_SQUARE }} dropSquareStyle: { boxShadow: "inset 0 0 1px 6px rgba(20,85,30,0.7)" },
customLightSquareStyle={{ backgroundColor: LIGHT_SQUARE }} }}
customBoardStyle={{ borderRadius: "0" }}
customDropSquareStyle={{ boxShadow: "inset 0 0 1px 6px rgba(20,85,30,0.7)" }}
customPremoveDarkSquareStyle={{ backgroundColor: "#a04a4a" }}
customPremoveLightSquareStyle={{ backgroundColor: "#d08080" }}
/> />
{promotionFrom !== null && (
<div
className="absolute inset-0 flex items-center justify-center z-20"
style={{ backgroundColor: "rgba(20, 18, 15, 0.7)" }}
>
<div className="flex gap-1 p-2 rounded-lg" style={{ backgroundColor: "#1a1917", border: "1px solid #3a3733" }}>
{["q", "r", "b", "n"].map((p) => (
<button
key={p}
onClick={() => handlePromotion(p)}
className="w-12 h-12 flex items-center justify-center text-2xl rounded hover:bg-white/10 transition-colors"
style={{ color: "#f0d9b5" }}
>
{{ q: turn === "white" ? "♕" : "♛", r: turn === "white" ? "♖" : "♜", b: turn === "white" ? "♗" : "♝", n: turn === "white" ? "♘" : "♞" }[p]}
</button>
))}
</div>
</div>
)}
{isGameOver && ( {isGameOver && (
<GameOverOverlay <GameOverOverlay
status={chess.status} status={chess.status}