Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions frontend/components/chess/BlindfoldToggle.tsx
Original file line number Diff line number Diff line change
@@ -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<BlindfoldToggleProps> = ({
blindfoldMode,
onToggle,
}) => {
return (
<label className="blindfold-toggle flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={blindfoldMode}
onChange={(e) => onToggle(e.target.checked)}
aria-label="Blindfold mode"
/>
Blindfold mode (hide pieces)
</label>
);
};

export const pieceStyleForBlindfold = (
blindfoldMode: boolean
): React.CSSProperties => ({
opacity: blindfoldMode ? 0 : 1,
pointerEvents: "auto",
});

export default BlindfoldToggle;
61 changes: 61 additions & 0 deletions frontend/components/chess/GameReviewSummary.tsx
Original file line number Diff line number Diff line change
@@ -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<MoveClass, number>;
onRetryMistake?: () => void;
}

const BADGE: Record<MoveClass, string> = {
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<GameReviewSummaryProps> = ({
whiteAccuracy,
blackAccuracy,
counts,
onRetryMistake,
}) => {
return (
<div className="game-review-summary p-4 rounded-lg border border-white/10">
<div className="flex justify-between text-sm mb-3">
<span>White accuracy: {whiteAccuracy.toFixed(1)}%</span>
<span>Black accuracy: {blackAccuracy.toFixed(1)}%</span>
</div>
<ul className="flex flex-wrap gap-3 text-sm">
{(Object.keys(BADGE) as MoveClass[]).map((cls) => (
<li key={cls}>
{BADGE[cls]} {cls}: {counts[cls] ?? 0}
</li>
))}
</ul>
{counts.blunder > 0 && onRetryMistake && (
<button onClick={onRetryMistake} className="mt-3 px-3 py-1.5 rounded bg-red-600 text-white text-sm">
Retry Mistake
</button>
)}
</div>
);
};

export default GameReviewSummary;
57 changes: 57 additions & 0 deletions frontend/components/chess/OfflinePlayView.tsx
Original file line number Diff line number Diff line change
@@ -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<OfflinePlayViewProps> = ({
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 (
<div className="offline-play-view p-4 rounded-lg border border-yellow-500/40 bg-yellow-500/10">
<p className="text-sm mb-2">You&apos;re offline. Practice vs Stockfish instead?</p>
<input
type="range"
min={800}
max={2500}
step={100}
value={elo}
onChange={(e) => setElo(Number(e.target.value))}
aria-label="Stockfish Elo"
/>
<span className="ml-2 text-sm">{elo} Elo</span>
<button
onClick={() => onStartOfflineGame(elo)}
className="block mt-3 px-4 py-2 rounded bg-yellow-600 text-white"
>
Play Offline
</button>
</div>
);
};

export default OfflinePlayView;
34 changes: 34 additions & 0 deletions frontend/components/tournament/BerserkButton.tsx
Original file line number Diff line number Diff line change
@@ -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<BerserkButtonProps> = ({
isArenaMatch,
moveNumber,
onBerserk,
}) => {
if (!isArenaMatch || moveNumber > 0) return null;

return (
<button
onClick={onBerserk}
aria-label="Berserk — halve your clock for bonus tournament points"
className="berserk-button px-4 py-2 rounded-md font-bold text-white bg-red-700 border-2 border-orange-400 animate-pulse shadow-[0_0_12px_rgba(255,80,0,0.8)]"
>
🔥 BERSERK
</button>
);
};

export default BerserkButton;