fix(chess): send game updates to move sender + responsive mobile redesign
Some checks failed
Deploy to Production / test (push) Failing after 32s
Some checks failed
Deploy to Production / test (push) Failing after 32s
Bun's ws.publish() excludes the sender, so the player making a move never received the GAME_UPDATE with the new FEN — causing pieces to snap back. Added ctx.send() alongside ctx.publish() for GAME_UPDATE and GAME_ENDED. Also redesigned the panel for mobile: hamburger drawer sidebar, responsive chess board sizing via ResizeObserver, stacked layouts on small screens, and touch-friendly modals/controls across lobby and game pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,15 +77,19 @@ export function handleGameMessage(msg: GameWsClientMessage, ctx: WsContext): voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const spectatorView = roomManager.getSpectatorView(msg.roomId);
|
const spectatorView = roomManager.getSpectatorView(msg.roomId);
|
||||||
ctx.publish(`room:${msg.roomId}`, JSON.stringify({ type: "GAME_UPDATE", roomId: msg.roomId, state: spectatorView }));
|
const updateMsg = JSON.stringify({ type: "GAME_UPDATE", roomId: msg.roomId, state: spectatorView });
|
||||||
|
ctx.publish(`room:${msg.roomId}`, updateMsg);
|
||||||
|
ctx.send(updateMsg);
|
||||||
|
|
||||||
if (result.gameOver) {
|
if (result.gameOver) {
|
||||||
ctx.publish(`room:${msg.roomId}`, JSON.stringify({
|
const endedMsg = JSON.stringify({
|
||||||
type: "GAME_ENDED",
|
type: "GAME_ENDED",
|
||||||
roomId: msg.roomId,
|
roomId: msg.roomId,
|
||||||
winner: result.gameOver.winner,
|
winner: result.gameOver.winner,
|
||||||
reason: result.gameOver.reason,
|
reason: result.gameOver.reason,
|
||||||
}));
|
});
|
||||||
|
ctx.publish(`room:${msg.roomId}`, endedMsg);
|
||||||
|
ctx.send(endedMsg);
|
||||||
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
|
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
Trophy,
|
Trophy,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import type { AuthUser } from "../lib/useAuth";
|
import type { AuthUser } from "../lib/useAuth";
|
||||||
@@ -54,6 +56,7 @@ export default function Layout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -63,6 +66,11 @@ export default function Layout({
|
|||||||
? `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png?size=64`
|
? `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png?size=64`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Close mobile drawer on route change
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileOpen(false);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
function isActive(path: string): boolean {
|
function isActive(path: string): boolean {
|
||||||
if (path === "/admin" && location.pathname === "/admin") return true;
|
if (path === "/admin" && location.pathname === "/admin") return true;
|
||||||
if (path === "/dashboard" && location.pathname === "/dashboard") return true;
|
if (path === "/dashboard" && location.pathname === "/dashboard") return true;
|
||||||
@@ -70,75 +78,123 @@ export default function Layout({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNav(path: string) {
|
||||||
|
navigate(path);
|
||||||
|
setMobileOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarContent = (
|
||||||
|
<>
|
||||||
|
<nav className="flex-1 py-3 px-2 space-y-1 overflow-y-auto">
|
||||||
|
{navItems.map(({ path, label, icon: Icon }) => (
|
||||||
|
<button
|
||||||
|
key={path}
|
||||||
|
onClick={() => handleNav(path)}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",
|
||||||
|
isActive(path)
|
||||||
|
? "bg-primary/15 text-primary border-l-4 border-primary"
|
||||||
|
: "text-text-tertiary hover:bg-primary/8 hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className={cn("w-5 h-5 shrink-0", isActive(path) && "text-primary")} />
|
||||||
|
{(!collapsed || mobileOpen) && <span>{label}</span>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="border-t border-border p-3 space-y-2">
|
||||||
|
{(!collapsed || mobileOpen) && (
|
||||||
|
<div className="flex items-center gap-3 px-2 py-1.5">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img src={avatarUrl} alt={user.username} className="w-8 h-8 rounded-full" />
|
||||||
|
) : (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-surface flex items-center justify-center text-xs font-medium">
|
||||||
|
{user.username[0]?.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium truncate">{user.username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={cn("flex", collapsed && !mobileOpen ? "flex-col items-center gap-2" : "items-center justify-between px-2")}>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-tertiary hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||||
|
title="Sign out"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
{(!collapsed || mobileOpen) && <span>Sign out</span>}
|
||||||
|
</button>
|
||||||
|
{/* Collapse toggle only on desktop */}
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed((c) => !c)}
|
||||||
|
className="hidden md:block p-1.5 rounded-md text-text-tertiary hover:text-foreground hover:bg-primary/10 transition-colors"
|
||||||
|
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
|
>
|
||||||
|
{collapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex">
|
<div className="min-h-screen flex">
|
||||||
|
{/* Mobile header bar */}
|
||||||
|
<div className="fixed top-0 left-0 right-0 z-40 flex items-center h-14 px-4 bg-background border-b border-border md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(true)}
|
||||||
|
className="p-2 -ml-2 rounded-md text-text-tertiary hover:text-foreground hover:bg-primary/10 transition-colors"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<div className="font-display text-lg font-bold tracking-tight ml-3">Aurora</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile overlay */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/50 md:hidden"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar - mobile drawer + desktop fixed */}
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-y-0 left-0 z-50 flex flex-col bg-background border-r border-border transition-all duration-200",
|
"fixed inset-y-0 left-0 z-50 flex flex-col bg-background border-r border-border transition-all duration-200",
|
||||||
collapsed ? "w-16" : "w-60"
|
// Mobile: off-screen drawer, shown when mobileOpen
|
||||||
|
"w-60 -translate-x-full md:translate-x-0",
|
||||||
|
mobileOpen && "translate-x-0",
|
||||||
|
// Desktop: respect collapsed state
|
||||||
|
!mobileOpen && collapsed && "md:w-16"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center h-16 px-4 border-b border-border">
|
<div className="flex items-center justify-between h-14 md:h-16 px-4 border-b border-border">
|
||||||
<div className="font-display text-xl font-bold tracking-tight">
|
<div className="font-display text-xl font-bold tracking-tight">
|
||||||
{collapsed ? "A" : "Aurora"}
|
{collapsed && !mobileOpen ? "A" : "Aurora"}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Close button on mobile */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className="p-1.5 rounded-md text-text-tertiary hover:text-foreground md:hidden"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 py-3 px-2 space-y-1 overflow-y-auto">
|
{sidebarContent}
|
||||||
{navItems.map(({ path, label, icon: Icon }) => (
|
|
||||||
<button
|
|
||||||
key={path}
|
|
||||||
onClick={() => navigate(path)}
|
|
||||||
className={cn(
|
|
||||||
"w-full flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",
|
|
||||||
isActive(path)
|
|
||||||
? "bg-primary/15 text-primary border-l-4 border-primary"
|
|
||||||
: "text-text-tertiary hover:bg-primary/8 hover:text-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className={cn("w-5 h-5 shrink-0", isActive(path) && "text-primary")} />
|
|
||||||
{!collapsed && <span>{label}</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="border-t border-border p-3 space-y-2">
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="flex items-center gap-3 px-2 py-1.5">
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img src={avatarUrl} alt={user.username} className="w-8 h-8 rounded-full" />
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 rounded-full bg-surface flex items-center justify-center text-xs font-medium">
|
|
||||||
{user.username[0]?.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-sm font-medium truncate">{user.username}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cn("flex", collapsed ? "flex-col items-center gap-2" : "items-center justify-between px-2")}>
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-tertiary hover:text-destructive hover:bg-destructive/10 transition-colors"
|
|
||||||
title="Sign out"
|
|
||||||
>
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
{!collapsed && <span>Sign out</span>}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCollapsed((c) => !c)}
|
|
||||||
className="p-1.5 rounded-md text-text-tertiary hover:text-foreground hover:bg-primary/10 transition-colors"
|
|
||||||
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
||||||
>
|
|
||||||
{collapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className={cn("flex-1 transition-all duration-200", collapsed ? "ml-16" : "ml-60")}>
|
<main className={cn(
|
||||||
<div className="max-w-[1600px] mx-auto px-6 py-8">
|
"flex-1 transition-all duration-200",
|
||||||
|
// Mobile: no margin, pad for top header bar
|
||||||
|
"mt-14 md:mt-0",
|
||||||
|
collapsed ? "md:ml-16" : "md:ml-60"
|
||||||
|
)}>
|
||||||
|
<div className="max-w-[1600px] mx-auto px-4 py-6 md:px-6 md:py-8">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -49,20 +49,20 @@ export function GameLobby() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between gap-3 mb-4 md:mb-6">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h1 className="font-display text-lg font-semibold">Games</h1>
|
<h1 className="font-display text-lg font-semibold">Games</h1>
|
||||||
<p className="text-sm text-text-tertiary">Browse and create game rooms</p>
|
<p className="text-sm text-text-tertiary hidden sm:block">Browse and create game rooms</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreate(true)}
|
onClick={() => setShowCreate(true)}
|
||||||
className="rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/90 transition-colors"
|
className="rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/90 transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
+ Create Room
|
+ Create Room
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4 overflow-x-auto pb-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter(null)}
|
onClick={() => setFilter(null)}
|
||||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||||
@@ -98,11 +98,11 @@ export function GameLobby() {
|
|||||||
{activeRooms.map(room => {
|
{activeRooms.map(room => {
|
||||||
const plugin = gameUIRegistry.get(room.gameSlug);
|
const plugin = gameUIRegistry.get(room.gameSlug);
|
||||||
return (
|
return (
|
||||||
<div key={room.id} className="flex items-center justify-between px-5 py-3 hover:bg-raised/40 transition-colors">
|
<div key={room.id} className="flex items-center justify-between gap-3 px-4 py-3 md:px-5 hover:bg-raised/40 transition-colors">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<span className="text-lg">{plugin?.icon ?? "🎮"}</span>
|
<span className="text-lg shrink-0">{plugin?.icon ?? "🎮"}</span>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium">{room.gameName}</div>
|
<div className="text-sm font-medium truncate">{room.gameName}</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${
|
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${
|
||||||
room.status === "waiting"
|
room.status === "waiting"
|
||||||
@@ -118,7 +118,7 @@ export function GameLobby() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/${room.gameSlug}/${room.id}`)}
|
onClick={() => navigate(`/${room.gameSlug}/${room.id}`)}
|
||||||
className={`rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
|
className={`rounded-md px-3 py-1.5 text-xs font-semibold transition-colors shrink-0 ${
|
||||||
room.status === "waiting"
|
room.status === "waiting"
|
||||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
: "bg-card border border-border text-text-tertiary hover:text-foreground"
|
: "bg-card border border-border text-text-tertiary hover:text-foreground"
|
||||||
@@ -134,8 +134,8 @@ export function GameLobby() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowCreate(false)}>
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/50" onClick={() => setShowCreate(false)}>
|
||||||
<div className="bg-card border border-border rounded-lg p-6 w-full max-w-sm" onClick={e => e.stopPropagation()}>
|
<div className="bg-card border border-border rounded-t-xl sm:rounded-lg p-6 w-full sm:max-w-sm" onClick={e => e.stopPropagation()}>
|
||||||
<h2 className="font-display text-base font-semibold mb-4">Create a Room</h2>
|
<h2 className="font-display text-base font-semibold mb-4">Create a Room</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{gameTypes.map(g => (
|
{gameTypes.map(g => (
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ export function GameRoom({ userId }: { userId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between gap-3 mb-4 md:mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<span className="text-xl">{plugin.icon}</span>
|
<span className="text-xl shrink-0">{plugin.icon}</span>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h1 className="font-display text-base font-semibold">{plugin.name}</h1>
|
<h1 className="font-display text-base font-semibold truncate">{plugin.name}</h1>
|
||||||
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${
|
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold ${
|
||||||
roomStatus === "waiting" ? "bg-warning/15 text-warning"
|
roomStatus === "waiting" ? "bg-warning/15 text-warning"
|
||||||
@@ -70,7 +70,7 @@ export function GameRoom({ userId }: { userId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => { leaveRoom(); navigate("/games"); }}
|
onClick={() => { leaveRoom(); navigate("/games"); }}
|
||||||
className="rounded-md px-3 py-1.5 text-sm font-medium bg-card border border-border text-text-tertiary hover:text-foreground transition-colors"
|
className="rounded-md px-3 py-1.5 text-sm font-medium bg-card border border-border text-text-tertiary hover:text-foreground transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
Leave
|
Leave
|
||||||
</button>
|
</button>
|
||||||
@@ -94,12 +94,13 @@ export function GameRoom({ userId }: { userId: string }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{roomStatus === "waiting" && (
|
{roomStatus === "waiting" && (
|
||||||
<div className="bg-card rounded-lg border border-border p-8 text-center">
|
<div className="bg-card rounded-lg border border-border p-5 md:p-8 text-center">
|
||||||
<div className="text-sm text-text-tertiary mb-2">
|
<div className="text-sm text-text-tertiary mb-2">
|
||||||
Waiting for players ({players.length}/2)
|
Waiting for players ({players.length}/2)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-text-disabled">
|
<div className="text-xs text-text-disabled">
|
||||||
Share this URL to invite: <span className="font-mono bg-surface px-2 py-0.5 rounded select-all">{window.location.href}</span>
|
Share this URL to invite:
|
||||||
|
<span className="block mt-1 font-mono bg-surface px-2 py-1 rounded select-all text-[11px] break-all">{window.location.href}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useRef, useEffect } from "react";
|
||||||
import { Chessboard } from "react-chessboard";
|
import { Chessboard } from "react-chessboard";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
import type { GameUIProps } from "../registry";
|
import type { GameUIProps } from "../registry";
|
||||||
@@ -15,6 +15,22 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
|
|||||||
const chess = state as ChessState;
|
const chess = state as ChessState;
|
||||||
const [promotionFrom, setPromotionFrom] = useState<string | null>(null);
|
const [promotionFrom, setPromotionFrom] = useState<string | null>(null);
|
||||||
const [promotionTo, setPromotionTo] = useState<string | null>(null);
|
const [promotionTo, setPromotionTo] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [boardWidth, setBoardWidth] = useState(400);
|
||||||
|
|
||||||
|
// Responsive board sizing
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
const width = entries[0]?.contentRect.width ?? 400;
|
||||||
|
// Cap board at 400px, floor at 280px
|
||||||
|
setBoardWidth(Math.max(280, Math.min(400, width)));
|
||||||
|
});
|
||||||
|
observer.observe(container);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const game = useMemo(() => {
|
const game = useMemo(() => {
|
||||||
if (!chess?.fen) return null;
|
if (!chess?.fen) return null;
|
||||||
@@ -89,7 +105,6 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
|
|||||||
// Highlight king in check
|
// Highlight king in check
|
||||||
const customSquareStyles: Record<string, React.CSSProperties> = {};
|
const customSquareStyles: Record<string, React.CSSProperties> = {};
|
||||||
if (game.inCheck()) {
|
if (game.inCheck()) {
|
||||||
// Find the king square of the player in check
|
|
||||||
const board = game.board();
|
const board = game.board();
|
||||||
const kingColor = game.turn();
|
const kingColor = game.turn();
|
||||||
for (let r = 0; r < 8; r++) {
|
for (let r = 0; r < 8; r++) {
|
||||||
@@ -108,8 +123,9 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div>
|
{/* Board column */}
|
||||||
|
<div ref={containerRef} className="w-full max-w-[400px]">
|
||||||
{/* Opponent info */}
|
{/* Opponent info */}
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="w-6 h-6 rounded-full bg-surface flex items-center justify-center text-xs font-medium">
|
<div className="w-6 h-6 rounded-full bg-surface flex items-center justify-center text-xs font-medium">
|
||||||
@@ -126,7 +142,7 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
|
|||||||
onPromotionPieceSelect={handlePromotion}
|
onPromotionPieceSelect={handlePromotion}
|
||||||
boardOrientation={boardOrientation}
|
boardOrientation={boardOrientation}
|
||||||
isDraggablePiece={isDraggablePiece}
|
isDraggablePiece={isDraggablePiece}
|
||||||
boardWidth={400}
|
boardWidth={boardWidth}
|
||||||
showPromotionDialog={promotionFrom !== null}
|
showPromotionDialog={promotionFrom !== null}
|
||||||
promotionToSquare={promotionTo as any}
|
promotionToSquare={promotionTo as any}
|
||||||
animationDuration={200}
|
animationDuration={200}
|
||||||
@@ -153,11 +169,11 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar - stacks below on mobile */}
|
||||||
<div className="flex flex-col gap-3 min-w-[180px]">
|
<div className="flex flex-col gap-3 w-full md:w-auto md:min-w-[180px]">
|
||||||
<div className="bg-card rounded-lg border border-border">
|
<div className="bg-card rounded-lg border border-border">
|
||||||
<div className="px-4 py-2.5 border-b border-border text-xs font-semibold">Move History</div>
|
<div className="px-4 py-2.5 border-b border-border text-xs font-semibold">Move History</div>
|
||||||
<div className="px-4 py-2 max-h-64 overflow-y-auto">
|
<div className="px-4 py-2 max-h-48 md:max-h-64 overflow-y-auto">
|
||||||
{chess.moveHistory.length === 0 ? (
|
{chess.moveHistory.length === 0 ? (
|
||||||
<div className="text-xs text-text-disabled">No moves yet</div>
|
<div className="text-xs text-text-disabled">No moves yet</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user