diff --git a/frontend/src/components/chess/CapturedPieces.tsx b/frontend/src/components/chess/CapturedPieces.tsx new file mode 100644 index 0000000..0a17611 --- /dev/null +++ b/frontend/src/components/chess/CapturedPieces.tsx @@ -0,0 +1,40 @@ +"use client"; + +import Piece from "./board/Piece"; + +interface Props { + capturedPieces: string[]; + advantage: number; + capturedColor: "w" | "b"; +} + +export default function CapturedPieces({ + capturedPieces, + advantage, + capturedColor, +}: Props) { + if (capturedPieces.length === 0 && advantage <= 0) return null; + + return ( +
+ {/* Removed the negative spacing and added flex-wrap so they flow nicely */} +
+ {capturedPieces.map((p, i) => { + const pieceType = capturedColor === "w" ? p.toUpperCase() : p; + return ( + // Bumped size from w-3.5 to w-4 so they are easier to read +
+ +
+ ); + })} +
+ + {advantage > 0 && ( + + +{advantage} + + )} +
+ ); +} diff --git a/frontend/src/components/chess/ChessBoard.tsx b/frontend/src/components/chess/ChessBoard.tsx index 5e9ffa7..9301e4d 100644 --- a/frontend/src/components/chess/ChessBoard.tsx +++ b/frontend/src/components/chess/ChessBoard.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from "react"; import { Chess } from "chess.js"; -import type { ValidationResult, GameStatus } from "@/store/chess/useChessStore"; +import type { ValidationResult, GameStatus } from "@/types/chess.type"; import { cn } from "@/lib/utils"; import TurnIndicator from "./board/TurnIndicator"; @@ -14,177 +14,259 @@ type Square = string; type Color = "w" | "b"; interface Props { - position: string; - activeColor: Color; - status: GameStatus; - onMove: (from: Square, to: Square, promotion?: string) => void; - validationResult: ValidationResult | null; - orientation?: Color; - disabled?: boolean; + position: string; + activeColor: Color; + status: GameStatus; + onMove: (from: Square, to: Square, promotion?: string) => void; + validationResult: ValidationResult | null; + orientation?: Color; + disabled?: boolean; } const FILES = ["a", "b", "c", "d", "e", "f", "g", "h"] as const; const RANKS = [8, 7, 6, 5, 4, 3, 2, 1] as const; function parseFen(fen: string): Record { - const pieces: Record = {}; - const rows = fen.split(" ")[0].split("/"); - rows.forEach((row, rankIdx) => { - let fileIdx = 0; - for (const char of row) { - if (/\d/.test(char)) { - fileIdx += parseInt(char, 10); - } else { - pieces[`${FILES[fileIdx]}${8 - rankIdx}`] = char; - fileIdx++; - } - } - }); - return pieces; + const pieces: Record = {}; + const rows = fen.split(" ")[0].split("/"); + rows.forEach((row, rankIdx) => { + let fileIdx = 0; + for (const char of row) { + if (/\d/.test(char)) { + fileIdx += parseInt(char, 10); + } else { + pieces[`${FILES[fileIdx]}${8 - rankIdx}`] = char; + fileIdx++; + } + } + }); + return pieces; } export default function ChessBoard({ - position, - activeColor, - status, - onMove, - validationResult, - orientation = "w", - disabled = false, + position, + activeColor, + status, + onMove, + validationResult, + orientation = "w", + disabled = false, }: Props) { - const [selected, setSelected] = useState(null); - const [legalSquares, setLegalSquares] = useState>(new Set()); - const [promotionPending, setPromotionPending] = useState<{ from: Square; to: Square } | null>(null); - const [isFlashing, setIsFlashing] = useState(false); - - const pieceMap = parseFen(position); - const files = orientation === "w" ? FILES : [...FILES].reverse(); - const ranks = orientation === "w" ? RANKS : [...RANKS].reverse(); - - useEffect(() => { - if (validationResult && !validationResult.valid) { - setIsFlashing(true); - const t = setTimeout(() => setIsFlashing(false), 400); - return () => clearTimeout(t); - } - }, [validationResult]); + const [selected, setSelected] = useState(null); + const [legalSquares, setLegalSquares] = useState>(new Set()); + const [promotionPending, setPromotionPending] = useState<{ + from: Square; + to: Square; + } | null>(null); + const [isFlashing, setIsFlashing] = useState(false); + + const pieceMap = parseFen(position); + const files = orientation === "w" ? FILES : [...FILES].reverse(); + const ranks = orientation === "w" ? RANKS : [...RANKS].reverse(); + + useEffect(() => { + if (validationResult && !validationResult.valid) { + setIsFlashing(true); + const t = setTimeout(() => setIsFlashing(false), 400); + return () => clearTimeout(t); + } + }, [validationResult]); + + useEffect(() => { + setSelected(null); + setLegalSquares(new Set()); + }, [activeColor]); + + const getLegalSquares = useCallback( + (square: Square, fen: string): Set => { + try { + const engine = new Chess(fen); + const moves = engine.moves({ square: square as any, verbose: true }); + return new Set(moves.map((m: any) => m.to)); + } catch { + return new Set(); + } + }, + [], + ); + + // --- DRAG AND DROP HANDLERS --- + + const handleDragStart = useCallback( + (e: React.DragEvent, square: Square) => { + if (disabled || promotionPending) { + e.preventDefault(); + return; + } + + // Save the starting square in the drag event + e.dataTransfer.setData("text/plain", square); + e.dataTransfer.effectAllowed = "move"; + + // Instantly select the piece and show legal moves while dragging! + if (pieceMap[square]) { + setSelected(square); + setLegalSquares(getLegalSquares(square, position)); + } + }, + [disabled, promotionPending, pieceMap, position, getLegalSquares], + ); - useEffect(() => { + const handleDragOver = useCallback((e: React.DragEvent) => { + // We MUST prevent default here to tell the browser "yes, you can drop here" + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent, targetSquare: Square) => { + e.preventDefault(); + const sourceSquare = e.dataTransfer.getData("text/plain") as Square; + + // If they dropped it somewhere weird or on the exact same square, cancel + if (!sourceSquare || sourceSquare === targetSquare) { setSelected(null); setLegalSquares(new Set()); - }, [activeColor]); - - const getLegalSquares = useCallback((square: Square, fen: string): Set => { - try { - const engine = new Chess(fen); - const moves = engine.moves({ square: square as any, verbose: true }); - return new Set(moves.map((m: any) => m.to)); - } catch { - return new Set(); + return; + } + + // If they dropped it on a legal square, execute the move! + if (legalSquares.has(targetSquare)) { + const piece = pieceMap[sourceSquare]; + const isPromotion = + (piece === "P" && targetSquare[1] === "8") || + (piece === "p" && targetSquare[1] === "1"); + + if (isPromotion) { + setPromotionPending({ from: sourceSquare, to: targetSquare }); + setSelected(null); + setLegalSquares(new Set()); + return; } - }, []); - - const handleSquareClick = useCallback( - (square: Square) => { - if (disabled || promotionPending) return; - - if (selected && legalSquares.has(square)) { - const piece = pieceMap[selected]; - const isPromotion = - (piece === "P" && square[1] === "8") || - (piece === "p" && square[1] === "1"); - - if (isPromotion) { - setPromotionPending({ from: selected, to: square }); - setSelected(null); - setLegalSquares(new Set()); - return; - } - - onMove(selected, square); - setSelected(null); - setLegalSquares(new Set()); - return; - } - - if (selected === square) { - setSelected(null); - setLegalSquares(new Set()); - return; - } - - if (pieceMap[square]) { - setSelected(square); - setLegalSquares(getLegalSquares(square, position)); - return; - } - - setSelected(null); - setLegalSquares(new Set()); - }, - [disabled, selected, legalSquares, pieceMap, promotionPending, position, onMove, getLegalSquares] - ); - - const handlePromotion = useCallback( - (piece: string) => { - if (!promotionPending) return; - onMove(promotionPending.from, promotionPending.to, piece); - setPromotionPending(null); - }, - [promotionPending, onMove] - ); - - return ( -
- - - - - -
- - -
- {ranks.map((rank) => - files.map((file) => { - const square = `${file}${rank}`; - const piece = pieceMap[square]; - const isLight = (FILES.indexOf(file) + rank) % 2 === 0; - - return ( - handleSquareClick(square)} - /> - ); - }) - )} -
- - -
- - - - {promotionPending && ( - - )} + + onMove(sourceSquare, targetSquare); + } + + // Reset the board selection after the drop + setSelected(null); + setLegalSquares(new Set()); + }, + [legalSquares, pieceMap, onMove], + ); + + // --- CLICK HANDLER (Kept as a fallback for accessibility!) --- + const handleSquareClick = useCallback( + (square: Square) => { + if (disabled || promotionPending) return; + + if (selected && legalSquares.has(square)) { + const piece = pieceMap[selected]; + const isPromotion = + (piece === "P" && square[1] === "8") || + (piece === "p" && square[1] === "1"); + + if (isPromotion) { + setPromotionPending({ from: selected, to: square }); + setSelected(null); + setLegalSquares(new Set()); + return; + } + + onMove(selected, square); + setSelected(null); + setLegalSquares(new Set()); + return; + } + + if (selected === square) { + setSelected(null); + setLegalSquares(new Set()); + return; + } + + if (pieceMap[square]) { + setSelected(square); + setLegalSquares(getLegalSquares(square, position)); + return; + } + + setSelected(null); + setLegalSquares(new Set()); + }, + [ + disabled, + selected, + legalSquares, + pieceMap, + promotionPending, + position, + onMove, + getLegalSquares, + ], + ); + + const handlePromotion = useCallback( + (piece: string) => { + if (!promotionPending) return; + onMove(promotionPending.from, promotionPending.to, piece); + setPromotionPending(null); + }, + [promotionPending, onMove], + ); + + return ( +
+ + + +
+ + +
+ {ranks.map((rank) => + files.map((file) => { + const square = `${file}${rank}`; + const piece = pieceMap[square]; + const isLight = (FILES.indexOf(file) + rank) % 2 === 0; + + return ( + handleSquareClick(square)} + // Wire up our new Drag functions! + onDragStart={(e) => handleDragStart(e, square)} + onDragOver={handleDragOver} + onDrop={(e) => handleDrop(e, square)} + /> + ); + }), + )}
- ); -} \ No newline at end of file + + +
+ + + + {promotionPending && ( + + )} +
+ ); +} diff --git a/frontend/src/components/chess/GameAlerts.tsx b/frontend/src/components/chess/GameAlerts.tsx index c98141a..287e82b 100644 --- a/frontend/src/components/chess/GameAlerts.tsx +++ b/frontend/src/components/chess/GameAlerts.tsx @@ -1,101 +1,84 @@ "use client"; -import { useEffect, useState } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import type { GameStatus, Color } from "@/store/chess/useChessStore"; // Types interface Props { - status: GameStatus; - activeColor: Color; + status: GameStatus; + activeColor: Color; } interface AlertConfig { - variant: "default" | "destructive"; - title: string; - description: string; - transient: boolean; // auto-dismiss after 3 s if true + variant: "default" | "destructive"; + title: string; + description: string; } // Alert Map -function resolveAlert(status: GameStatus, activeColor: Color): AlertConfig | null { - const side = activeColor === "w" ? "White" : "Black"; +function resolveAlert(status: GameStatus, activeColor: Color): AlertConfig { + const side = activeColor === "w" ? "White" : "Black"; - switch (status) { - case "check": - return { - variant: "destructive", - title: "Check", - description: `${side} is in check.`, - transient: true, - }; - case "checkmate": - return { - variant: "destructive", - title: "Checkmate", - description: `${side} has been checkmated. Game over.`, - transient: false, - }; - case "stalemate": - return { - variant: "default", - title: "Stalemate", - description: "No legal moves — the game is a draw.", - transient: false, - }; - case "draw": - return { - variant: "default", - title: "Draw", - description: "The game has ended in a draw.", - transient: false, - }; - case "resigned": - return { - variant: "default", - title: "Resigned", - description: `${side} has resigned. Game over.`, - transient: false, - }; - default: - return null; - } + switch (status) { + case "check": + return { + variant: "destructive", + title: "⚠️ Check", + description: `${side} is in check!`, + }; + case "checkmate": + return { + variant: "destructive", + title: "Checkmate", + description: `${side} has been checkmated. Game over.`, + }; + case "stalemate": + return { + variant: "default", + title: "Stalemate", + description: "No legal moves — the game is a draw.", + }; + case "draw": + return { + variant: "default", + title: "Draw", + description: "The game has ended in a draw.", + }; + case "resigned": + return { + variant: "default", + title: "Resigned", + description: `${side} has resigned. Game over.`, + }; + case "playing": + default: + return { + variant: "default", + title: "Game in Progress", + description: `${side} to move.`, + }; + } } // Component export default function GameAlerts({ status, activeColor }: Props) { - const [dismissed, setDismissed] = useState(false); - const [prevStatus, setPrevStatus] = useState(status); + const config = resolveAlert(status, activeColor); - // Reset on every status change - useEffect(() => { - if (status !== prevStatus) { - setDismissed(false); - setPrevStatus(status); - } - }, [status, prevStatus]); - - const config = resolveAlert(status, activeColor); - - // Auto-dismiss transient alerts (check) - useEffect(() => { - if (!config?.transient) return; - const t = setTimeout(() => setDismissed(true), 3000); - return () => clearTimeout(t); - }, [config]); - - if (!config || dismissed) return null; - - return ( - - {config.title} - {config.description} - - ); -} \ No newline at end of file + return ( + + + {config.title} + + + {config.description} + + + ); +} diff --git a/frontend/src/components/chess/GameClock.tsx b/frontend/src/components/chess/GameClock.tsx index 33cb9f4..3ed77a3 100644 --- a/frontend/src/components/chess/GameClock.tsx +++ b/frontend/src/components/chess/GameClock.tsx @@ -6,86 +6,91 @@ import { cn } from "@/lib/utils"; // Types interface Props { - timeLeft: number; // seconds remaining - isActive: boolean; - onTick?: () => void; // parent owns state; called each second - onExpire?: () => void; + timeLeft: number; // seconds remaining + isActive: boolean; + onTick?: () => void; // parent owns state; called each second + onExpire?: () => void; } // Helpers function formatTime(seconds: number): string { - const s = Math.max(0, Math.floor(seconds)); - const m = Math.floor(s / 60); - const ss = s % 60; - return `${String(m).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; + const s = Math.max(0, Math.floor(seconds)); + const m = Math.floor(s / 60); + const ss = s % 60; + return `${String(m).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; } // Component -export default function GameClock({ timeLeft, isActive, onTick, onExpire }: Props) { - const intervalRef = useRef | null>(null); +export default function GameClock({ + timeLeft, + isActive, + onTick, + onExpire, +}: Props) { + const intervalRef = useRef | null>(null); - useEffect(() => { - if (!isActive) { - if (intervalRef.current) clearInterval(intervalRef.current); - return; - } + useEffect(() => { + if (!isActive) { + if (intervalRef.current) clearInterval(intervalRef.current); + return; + } - intervalRef.current = setInterval(() => { - if (timeLeft <= 1) { - clearInterval(intervalRef.current!); - onTick?.(); - onExpire?.(); - } else { - onTick?.(); - } - }, 1000); + intervalRef.current = setInterval(() => { + if (timeLeft <= 1) { + clearInterval(intervalRef.current!); + onTick?.(); + onExpire?.(); + } else { + onTick?.(); + } + }, 1000); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isActive, timeLeft, onTick, onExpire]); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [isActive, timeLeft, onTick, onExpire]); - const isCritical = timeLeft <= 10; - const isLow = timeLeft <= 30 && !isCritical; + const isCritical = timeLeft <= 10; + const isLow = timeLeft <= 30 && !isCritical; - return ( -
- {/* Active indicator */} -
+ ); +} diff --git a/frontend/src/components/chess/MoveHistory.tsx b/frontend/src/components/chess/MoveHistory.tsx index 364e557..4888a08 100644 --- a/frontend/src/components/chess/MoveHistory.tsx +++ b/frontend/src/components/chess/MoveHistory.tsx @@ -8,100 +8,100 @@ import type { MoveEntry } from "@/store/chess/useChessStore"; // Types interface Props { - moves: MoveEntry[]; + moves: MoveEntry[]; } interface MovePair { - moveNumber: number; - white: string | null; - black: string | null; + moveNumber: number; + white: string | null; + black: string | null; } // Helpers function pairMoves(moves: MoveEntry[]): MovePair[] { - const pairs: MovePair[] = []; - for (let i = 0; i < moves.length; i += 2) { - pairs.push({ - moveNumber: moves[i].moveNumber, - white: moves[i]?.san ?? null, - black: moves[i + 1]?.san ?? null, - }); - } - return pairs; + const pairs: MovePair[] = []; + for (let i = 0; i < moves.length; i += 2) { + pairs.push({ + moveNumber: moves[i].moveNumber, + white: moves[i]?.san ?? null, + black: moves[i + 1]?.san ?? null, + }); + } + return pairs; } // Component export default function MoveHistory({ moves }: Props) { - const bottomRef = useRef(null); + const bottomRef = useRef(null); - useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [moves]); + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [moves]); - const pairs = pairMoves(moves); + const pairs = pairMoves(moves); - return ( - - -
- - Moves - - - {moves.length > 0 ? `${moves.length} ply` : "—"} - -
-
- - - {pairs.length === 0 ? ( -

- No moves yet. -

- ) : ( - - - - - - - - - - {pairs.map((pair, idx) => ( - - - - - - ))} - -
- # - - White - - Black -
- {pair.moveNumber}. - - {pair.white ?? ""} - - {pair.black ?? ""} -
- )} -
- - - - ); -} \ No newline at end of file + return ( + + +
+ + Moves + + + {moves.length > 0 ? `${moves.length} ply` : "—"} + +
+
+ + + {pairs.length === 0 ? ( +

+ No moves yet. +

+ ) : ( + + + + + + + + + + {pairs.map((pair, idx) => ( + + + + + + ))} + +
+ # + + White + + Black +
+ {pair.moveNumber}. + + {pair.white ?? ""} + + {pair.black ?? ""} +
+ )} +
+ + + + ); +} diff --git a/frontend/src/components/chess/MoveValidationToast.tsx b/frontend/src/components/chess/MoveValidationToast.tsx index 09974c1..bf818fa 100644 --- a/frontend/src/components/chess/MoveValidationToast.tsx +++ b/frontend/src/components/chess/MoveValidationToast.tsx @@ -5,29 +5,28 @@ import { toast } from "sonner"; import type { ValidationResult } from "@/store/chess/useChessStore"; interface Props { - result: ValidationResult | null; + result: ValidationResult | null; } export default function MoveValidationToast({ result }: Props) { - const lastResultRef = useRef(null); + const lastResultRef = useRef(null); - useEffect(() => { - if (!result) return; - // Deduplicate — only fire when result reference changes - if (result === lastResultRef.current) return; - lastResultRef.current = result; + useEffect(() => { + if (!result) return; + // Deduplicate — only fire when result reference changes + if (result === lastResultRef.current) return; + lastResultRef.current = result; - if (!result.valid) { - toast.error(result.reason ?? "Illegal move", { - duration: 2500, - position: "bottom-center", - classNames: { - toast: "font-roboto text-xs", - }, - }); - } - }, [result]); + if (!result.valid) { + toast.error(result.reason ?? "Illegal move", { + duration: 2500, + position: "bottom-center", + classNames: { + toast: "font-roboto text-xs", + }, + }); + } + }, [result]); - - return null; -} \ No newline at end of file + return null; +} diff --git a/frontend/src/components/chess/board/BoardLabels.tsx b/frontend/src/components/chess/board/BoardLabels.tsx index 0464154..12f9f9b 100644 --- a/frontend/src/components/chess/board/BoardLabels.tsx +++ b/frontend/src/components/chess/board/BoardLabels.tsx @@ -1,38 +1,46 @@ "use client"; interface Props { - files: readonly string[]; - ranks: readonly number[]; + files: readonly string[]; + ranks: readonly number[]; } export function FileLabelRow({ files }: Pick) { - return ( -
- {files.map((f) => ( - - ))} -
- ); + return ( +
+ {files.map((f) => ( + + ))} +
+ ); } -export function RankLabelCol({ ranks, side }: { ranks: readonly number[]; side: "left" | "right" }) { - return ( -
- {ranks.map((r) => ( - - ))} -
- ); -} \ No newline at end of file +export function RankLabelCol({ + ranks, + side, +}: { + ranks: readonly number[]; + side: "left" | "right"; +}) { + return ( +
+ {ranks.map((r) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/chess/board/BoardSquare.tsx b/frontend/src/components/chess/board/BoardSquare.tsx index f3059e3..e28eacc 100644 --- a/frontend/src/components/chess/board/BoardSquare.tsx +++ b/frontend/src/components/chess/board/BoardSquare.tsx @@ -1,80 +1,93 @@ -// Single square on the board which renders the piece glyph -// Highlight legal moves - to be improved -// capture and selection highlight - - - "use client"; import { cn } from "@/lib/utils"; - -// Glyphs are temporary placeholders -const T = "\uFE0E"; -export const PIECE_GLYPHS: Record = { - K: `♔${T}`, Q: `♕${T}`, R: `♖${T}`, B: `♗${T}`, N: `♘${T}`, P: `♙${T}`, - k: `♚${T}`, q: `♛${T}`, r: `♜${T}`, b: `♝${T}`, n: `♞${T}`, p: `♟${T}`, -}; +import Piece from "./Piece"; +import { motion } from "framer-motion"; interface Props { - square: string; - piece: string | undefined; - isLight: boolean; - isSelected: boolean; - isLegal: boolean; - isCapture: boolean; - disabled: boolean; - onClick: () => void; + square: string; + piece: string | undefined; + isLight: boolean; + isSelected: boolean; + isLegal: boolean; + isCapture: boolean; + disabled: boolean; + onClick: () => void; + // New Drag Events + onDragStart: (e: React.DragEvent) => void; + onDragOver: (e: React.DragEvent) => void; + onDrop: (e: React.DragEvent) => void; } export default function BoardSquare({ - square, - piece, - isLight, - isSelected, - isLegal, - isCapture, - disabled, - onClick, + square, + piece, + isLight, + isSelected, + isLegal, + isCapture, + disabled, + onClick, + onDragStart, + onDragOver, + onDrop, }: Props) { - const isWhitePiece = piece === piece?.toUpperCase(); + return ( + - ); -} \ No newline at end of file + {/* Native HTML5 Drag wrapper */} +
+ +
+ + )} + + ); +} diff --git a/frontend/src/components/chess/board/Piece.tsx b/frontend/src/components/chess/board/Piece.tsx new file mode 100644 index 0000000..21e2a6c --- /dev/null +++ b/frontend/src/components/chess/board/Piece.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { + FaChessKing, + FaChessQueen, + FaChessRook, + FaChessBishop, + FaChessKnight, + FaChessPawn, +} from "react-icons/fa6"; + +interface PieceProps { + type: string; + className?: string; +} + +export default function Piece({ type, className }: PieceProps) { + const isWhite = type === type.toUpperCase(); + const normalizedType = type.toLowerCase(); + + const Icon = { + k: FaChessKing, + q: FaChessQueen, + r: FaChessRook, + b: FaChessBishop, + n: FaChessKnight, + p: FaChessPawn, + }[normalizedType]; + + if (!Icon) return null; + + return ( + + ); +} diff --git a/frontend/src/components/chess/board/PromotionPicker.tsx b/frontend/src/components/chess/board/PromotionPicker.tsx index fd3234e..0642469 100644 --- a/frontend/src/components/chess/board/PromotionPicker.tsx +++ b/frontend/src/components/chess/board/PromotionPicker.tsx @@ -1,35 +1,41 @@ -// Pick a promotion piece - "use client"; -import { PIECE_GLYPHS } from "./BoardSquare"; +import Piece from "./Piece"; interface Props { - onSelect: (piece: string) => void; + color?: "w" | "b"; + onSelect: (piece: string) => void; } -export default function PromotionPicker({ onSelect }: Props) { - return ( -
-

- Promote to -

-
- {["q", "r", "b", "n"].map((p) => ( - - ))} -
-
- ); -} \ No newline at end of file +export default function PromotionPicker({ color = "w", onSelect }: Props) { + return ( +
+

+ Promote to +

+
+ {["q", "r", "b", "n"].map((p) => { + const pieceType = color === "w" ? p.toUpperCase() : p; + + return ( + + ); + })} +
+
+ ); +} diff --git a/frontend/src/components/chess/board/TurnIndicator.tsx b/frontend/src/components/chess/board/TurnIndicator.tsx index b19a4ee..12f3cfd 100644 --- a/frontend/src/components/chess/board/TurnIndicator.tsx +++ b/frontend/src/components/chess/board/TurnIndicator.tsx @@ -6,27 +6,27 @@ import { cn } from "@/lib/utils"; import type { Color, GameStatus } from "@/store/chess/useChessStore"; interface Props { - activeColor: Color; - status: GameStatus; + activeColor: Color; + status: GameStatus; } export default function TurnIndicator({ activeColor, status }: Props) { - const isGameActive = status === "playing" || status === "check"; + const isGameActive = status === "playing" || status === "check"; - if (!isGameActive) return