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
11 changes: 10 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ClientRoot from "@/components/ClientRoot";
import { AppProvider } from "@/context/walletContext";
import { MatchmakingProvider } from "@/context/matchmakingContext";
import { ToastProvider } from "@/components/ui/toast";
import { TransactionProvider } from "@/context/transactionContext";
import { ThemeProvider } from "next-themes";

export const metadata: Metadata = {
Expand All @@ -19,11 +20,19 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className="antialiased font-rowdies bg-background text-foreground">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[200] focus:px-4 focus:py-2 focus:rounded-lg focus:bg-teal-600 focus:text-white focus:shadow-lg focus:outline-none focus:ring-2 focus:ring-teal-400"
>
Skip to main content
</a>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
<AppProvider>
<MatchmakingProvider>
<ToastProvider>
<ClientRoot>{children}</ClientRoot>
<TransactionProvider>
<ClientRoot>{children}</ClientRoot>
</TransactionProvider>
</ToastProvider>
</MatchmakingProvider>
</AppProvider>
Expand Down
16 changes: 8 additions & 8 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,21 @@ export default function Home() {
};

return (
<div className="min-h-screen p-4 md:p-8">
<div className="min-h-screen p-4 md:p-8" role="region" aria-label="Home">
{/* Web3 Status Bar */}
<div className="mb-4">
<Web3StatusBar />
</div>
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row gap-8 items-center justify-center">
{/* Chessboard Section */}
<div className="w-full max-w-[600px] order-2 md:order-1">
<div className="w-full max-w-[600px] order-2 md:order-1" role="region" aria-label="Chessboard">
<div className="w-full min-w-[320px]">
<ChessboardComponent position={position} onDrop={handleMove} />
<ChessboardComponent position={position} onDrop={handleMove} aria-label="Chess board. Use arrow keys or mouse to interact." />
</div>

{gameMode && (
<div className="mt-4 flex items-center justify-between bg-gray-800/60 p-4 rounded-xl border border-gray-700/50 animate-slide-up">
<div className="mt-4 flex items-center justify-between bg-gray-800/60 p-4 rounded-xl border border-gray-700/50 animate-slide-up" role="status" aria-live="polite">
<div className="flex items-center gap-4">
<div className="bg-gradient-to-br from-teal-400/30 to-blue-500/30 p-3 rounded-xl">
{gameMode === "online" ? (
Expand All @@ -240,7 +240,7 @@ export default function Home() {
)}

{gameMode === "online" && matchmakingStatus === "searching" && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center animate-overlay-in">
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center animate-overlay-in" role="dialog" aria-modal="true" aria-label="Searching for opponent">
<div className="bg-gray-900 p-8 rounded-2xl border border-yellow-500/30 text-center animate-modal-in max-w-sm w-full mx-4">
<div className="flex flex-col items-center gap-4">
<h3 className="text-xl font-bold text-yellow-400">
Expand All @@ -251,7 +251,7 @@ export default function Home() {
<span className="relative inline-flex rounded-full h-10 w-10 border-2 border-yellow-500 bg-yellow-500/10"></span>
</span>

<p className="text-gray-300 text-sm">
<p className="text-gray-300 text-sm" aria-live="polite">
{onlinePlayerCount} Players online
</p>

Expand All @@ -267,7 +267,7 @@ export default function Home() {
)}

{gameMode === "online" && socketStatus === "reconnecting" && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center animate-overlay-in">
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center animate-overlay-in" role="dialog" aria-modal="true" aria-label="Reconnecting to game">
<div className="bg-gray-900 p-8 rounded-2xl border border-yellow-500/30 text-center animate-modal-in max-w-sm w-full mx-4">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin" />
Expand All @@ -290,7 +290,7 @@ export default function Home() {
</div>

{/* Game Modes Section */}
<div className="flex flex-col justify-center space-y-6 max-w-[500px] w-full order-1 md:order-2">
<div className="flex flex-col justify-center space-y-6 max-w-[500px] w-full order-1 md:order-2" role="region" aria-label="Game mode selection">
{!gameMode && <GameModeButtons setGameMode={handleSetGameMode} />}
</div>
</div>
Expand Down
63 changes: 52 additions & 11 deletions frontend/app/play/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useParams, useRouter } from "next/navigation";
import { useChessSocket } from "@/hook/useChessSocket";
import { FaUser, FaClock, FaSignal } from "react-icons/fa";
import { Web3StatusBar } from "@/components/Web3StatusBar";
import { useCheatDetection } from "@/hook/useCheatDetection";
import { CheatDetectionPanel } from "@/components/chess/CheatDetectionPanel";

const ChessboardComponent = dynamic(
() => import("@/components/chess/ChessboardComponent"),
Expand Down Expand Up @@ -45,6 +47,7 @@ export default function PlayOnlinePage() {
const [blackTime] = useState(600);
const [playerColor] = useState<"white" | "black">("white");
const [gameStatus, setGameStatus] = useState<GameStatus>("playing");
const [isCheatPanelExpanded, setIsCheatPanelExpanded] = useState(false);

const {
status: socketStatus,
Expand All @@ -64,10 +67,20 @@ export default function PlayOnlinePage() {
}
}, [game]);

// Cheat detection
const {
opponentAnalysis,
playerAnalysis,
recordMove: recordCheatMove,
reset: resetCheatDetection,
isActive: isCheatDetectionActive,
} = useCheatDetection(playerColor);

// Apply opponent's move to local chess state
useEffect(() => {
if (!lastOpponentMove) return;
try {
const fenBeforeOpponentMove = game.fen();
const move = game.move({
from: lastOpponentMove.from,
to: lastOpponentMove.to,
Expand All @@ -76,12 +89,19 @@ export default function PlayOnlinePage() {
if (move) {
setPosition(game.fen());
setMoveHistory((prev: string[]) => [...prev, move.san]);
recordCheatMove(
move.san,
move,
fenBeforeOpponentMove,
playerColor === "white" ? "b" : "w",
Math.ceil(game.moveNumber() / 2),
);
checkGameStatus();
}
} catch {
// illegal move from server — ignore
}
}, [lastOpponentMove, game, checkGameStatus]);
}, [lastOpponentMove, game, checkGameStatus, recordCheatMove, playerColor]);

const isMyTurn =
socketStatus === "connected" &&
Expand All @@ -99,6 +119,7 @@ export default function PlayOnlinePage() {
if (!isMyTurn || gameStatus !== "playing") return false;

try {
const fenBeforeMyMove = game.fen();
const move = game.move({
from: sourceSquare,
to: targetSquare,
Expand All @@ -109,19 +130,27 @@ export default function PlayOnlinePage() {
requestAnimationFrame(() => setPosition(game.fen()));
setMoveHistory((prev: string[]) => [...prev, move.san]);
sendMove({ from: sourceSquare, to: targetSquare, promotion: "q" });
recordCheatMove(
move.san,
move,
fenBeforeMyMove,
playerColor === "white" ? "w" : "b",
Math.ceil(game.moveNumber() / 2),
);
checkGameStatus();
return true;
} catch {
return false;
}
},
[isMyTurn, game, gameStatus, sendMove, checkGameStatus],
[isMyTurn, game, gameStatus, sendMove, checkGameStatus, recordCheatMove, playerColor],
);

const handleResign = useCallback(() => {
setGameStatus("resigned");
resetCheatDetection();
disconnect();
}, [disconnect]);
}, [disconnect, resetCheatDetection]);

const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
Expand Down Expand Up @@ -169,12 +198,13 @@ export default function PlayOnlinePage() {
);

return (
<div className="min-h-screen p-4 md:p-8">
<div className="min-h-screen p-4 md:p-8" role="region" aria-label="Online Chess Game">
<div className="max-w-7xl mx-auto">
{/* Top bar */}
<div className="flex items-center justify-between mb-4">
<button
onClick={() => router.push("/")}
aria-label="Back to lobby"
className="text-gray-400 hover:text-white transition-colors text-sm flex items-center gap-2"
>
<svg
Expand All @@ -198,9 +228,9 @@ export default function PlayOnlinePage() {

<div className="flex flex-col lg:flex-row gap-6 items-start justify-center">
{/* Chessboard Section */}
<div className="w-full max-w-[600px]">
<div className="w-full max-w-[600px]" role="region" aria-label="Chess board">
{/* Opponent info bar */}
<div className="flex items-center justify-between mb-3 px-1">
<div className="flex items-center justify-between mb-3 px-1" role="status" aria-label="Opponent info">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center">
<FaUser className="text-white text-xs" />
Expand Down Expand Up @@ -229,7 +259,7 @@ export default function PlayOnlinePage() {
</div>

{/* Player info bar */}
<div className="flex items-center justify-between mt-3 px-1">
<div className="flex items-center justify-between mt-3 px-1" role="status" aria-label="Your info">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-teal-400 to-blue-600 flex items-center justify-center">
<FaUser className="text-white text-xs" />
Expand All @@ -256,7 +286,7 @@ export default function PlayOnlinePage() {
</div>

{/* Game Sidebar - Move History & Controls */}
<div className="w-full lg:w-80 space-y-4">
<div className="w-full lg:w-80 space-y-4" role="complementary" aria-label="Game controls">
{/* Game Status Card */}
<div className="rounded-xl border border-gray-700/50 bg-gray-800/40 p-4 animate-fade-in">
<div className="flex items-center justify-between mb-3">
Expand All @@ -272,7 +302,7 @@ export default function PlayOnlinePage() {
</div>

{gameStatus !== "playing" && (
<div className="p-3 rounded-lg bg-gradient-to-r from-yellow-500/10 to-orange-500/10 border border-yellow-500/30 mb-3 animate-scale-in">
<div className="p-3 rounded-lg bg-gradient-to-r from-yellow-500/10 to-orange-500/10 border border-yellow-500/30 mb-3 animate-scale-in" role="alert" aria-live="assertive">
<p className="text-sm font-bold text-yellow-400">
{gameStatus === "checkmate" && "Checkmate!"}
{gameStatus === "stalemate" && "Stalemate!"}
Expand All @@ -283,7 +313,7 @@ export default function PlayOnlinePage() {
)}

{game.isCheck() && gameStatus === "playing" && (
<div className="p-2 rounded-lg bg-red-500/10 border border-red-500/30 mb-3 animate-scale-in">
<div className="p-2 rounded-lg bg-red-500/10 border border-red-500/30 mb-3 animate-scale-in" role="alert" aria-live="assertive">
<p className="text-sm font-bold text-red-400">Check!</p>
</div>
)}
Expand All @@ -294,7 +324,7 @@ export default function PlayOnlinePage() {
</div>

{/* Move History */}
<div className="rounded-xl border border-gray-700/50 bg-gray-800/40 p-4">
<div className="rounded-xl border border-gray-700/50 bg-gray-800/40 p-4" role="region" aria-label="Move history">
<h3 className="text-sm font-semibold text-gray-300 mb-3">
Moves
</h3>
Expand Down Expand Up @@ -325,17 +355,28 @@ export default function PlayOnlinePage() {
</div>
</div>

{/* Cheat Detection Panel */}
<CheatDetectionPanel
opponentAnalysis={opponentAnalysis}
playerAnalysis={playerAnalysis}
isActive={isCheatDetectionActive}
isExpanded={isCheatPanelExpanded}
onToggle={() => setIsCheatPanelExpanded((prev: boolean) => !prev)}
/>

{/* Controls */}
<div className="flex gap-2">
<button
onClick={() => {}}
aria-label="Flip board orientation"
className="flex-1 py-2.5 rounded-xl bg-gray-800/60 hover:bg-gray-700/60 border border-gray-700/50 text-gray-300 text-sm font-medium transition-all duration-300"
>
Flip Board
</button>
<button
onClick={handleResign}
disabled={gameStatus !== "playing"}
aria-label="Resign game"
className="flex-1 py-2.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-400 text-sm font-medium transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
Resign
Expand Down
Loading
Loading