diff --git a/frontend/components/chess/BlindfoldToggle.tsx b/frontend/components/chess/BlindfoldToggle.tsx new file mode 100644 index 00000000..ca800522 --- /dev/null +++ b/frontend/components/chess/BlindfoldToggle.tsx @@ -0,0 +1,39 @@ +"use client"; + +import React from "react"; + +interface BlindfoldToggleProps { + blindfoldMode: boolean; + onToggle: (enabled: boolean) => void; +} + +/** + * FE-51: blindfold mode toggle. Consumers apply `blindfoldMode` to piece + * rendering (e.g. `opacity: 0` on piece SVGs) while keeping hitboxes and + * legal-move logic untouched — this component only owns the on/off state. + */ +const BlindfoldToggle: React.FC = ({ + blindfoldMode, + onToggle, +}) => { + return ( + + ); +}; + +export const pieceStyleForBlindfold = ( + blindfoldMode: boolean +): React.CSSProperties => ({ + opacity: blindfoldMode ? 0 : 1, + pointerEvents: "auto", +}); + +export default BlindfoldToggle; diff --git a/frontend/components/chess/GameReviewSummary.tsx b/frontend/components/chess/GameReviewSummary.tsx new file mode 100644 index 00000000..0ce5fd06 --- /dev/null +++ b/frontend/components/chess/GameReviewSummary.tsx @@ -0,0 +1,61 @@ +"use client"; + +import React from "react"; + +export type MoveClass = + | "brilliant" + | "great" + | "best" + | "inaccuracy" + | "mistake" + | "blunder"; + +interface GameReviewSummaryProps { + whiteAccuracy: number; + blackAccuracy: number; + counts: Record; + onRetryMistake?: () => void; +} + +const BADGE: Record = { + brilliant: "!!", + great: "!", + best: "★", + inaccuracy: "?!", + mistake: "?", + blunder: "??", +}; + +/** + * FE-50: game review summary card. Accuracy % and per-move classification + * are computed upstream (engine analysis) and passed in as props. + */ +const GameReviewSummary: React.FC = ({ + whiteAccuracy, + blackAccuracy, + counts, + onRetryMistake, +}) => { + return ( +
+
+ White accuracy: {whiteAccuracy.toFixed(1)}% + Black accuracy: {blackAccuracy.toFixed(1)}% +
+
    + {(Object.keys(BADGE) as MoveClass[]).map((cls) => ( +
  • + {BADGE[cls]} {cls}: {counts[cls] ?? 0} +
  • + ))} +
+ {counts.blunder > 0 && onRetryMistake && ( + + )} +
+ ); +}; + +export default GameReviewSummary; diff --git a/frontend/components/chess/OfflinePlayView.tsx b/frontend/components/chess/OfflinePlayView.tsx new file mode 100644 index 00000000..447ae6a1 --- /dev/null +++ b/frontend/components/chess/OfflinePlayView.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +interface OfflinePlayViewProps { + onStartOfflineGame: (elo: number) => void; +} + +/** + * FE-49: minimal offline play-vs-Stockfish entry point. + * Actual Stockfish WASM worker + ServiceWorker caching are separate + * follow-up work — this wires the "Play Offline" affordance and Elo pick. + */ +const OfflinePlayView: React.FC = ({ + onStartOfflineGame, +}) => { + const [isOffline, setIsOffline] = useState(false); + const [elo, setElo] = useState(1200); + + useEffect(() => { + setIsOffline(!navigator.onLine); + const goOffline = () => setIsOffline(true); + const goOnline = () => setIsOffline(false); + window.addEventListener("offline", goOffline); + window.addEventListener("online", goOnline); + return () => { + window.removeEventListener("offline", goOffline); + window.removeEventListener("online", goOnline); + }; + }, []); + + if (!isOffline) return null; + + return ( +
+

You're offline. Practice vs Stockfish instead?

+ setElo(Number(e.target.value))} + aria-label="Stockfish Elo" + /> + {elo} Elo + +
+ ); +}; + +export default OfflinePlayView; diff --git a/frontend/components/tournament/BerserkButton.tsx b/frontend/components/tournament/BerserkButton.tsx new file mode 100644 index 00000000..5fa7bcab --- /dev/null +++ b/frontend/components/tournament/BerserkButton.tsx @@ -0,0 +1,34 @@ +"use client"; + +import React from "react"; + +interface BerserkButtonProps { + isArenaMatch: boolean; + moveNumber: number; + onBerserk: () => void; +} + +/** + * FE-52: Berserk button for Arena tournaments. Only active before the + * player's first move; dispatches a `BerserkMove` event upstream via + * `onBerserk`, which the game socket layer is responsible for sending. + */ +const BerserkButton: React.FC = ({ + isArenaMatch, + moveNumber, + onBerserk, +}) => { + if (!isArenaMatch || moveNumber > 0) return null; + + return ( + + ); +}; + +export default BerserkButton;