Files
aurorabot/panel/src/games/GameLobby.tsx
syntaxbullet 9c4da51cfb
Some checks failed
Deploy to Production / test (push) Failing after 32s
fix(chess): send game updates to move sender + responsive mobile redesign
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>
2026-04-02 14:58:40 +02:00

164 lines
8.0 KiB
TypeScript

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useWebSocket } from "../lib/useWebSocket";
import { gameUIRegistry } from "./registry";
import "./chess";
interface RoomSummary {
id: string;
gameSlug: string;
gameName: string;
host: string;
playerCount: number;
maxPlayers: number;
spectatorCount: number;
status: "waiting" | "playing" | "finished";
}
export function GameLobby() {
const { send, subscribe, connected } = useWebSocket();
const navigate = useNavigate();
const [rooms, setRooms] = useState<RoomSummary[]>([]);
const [filter, setFilter] = useState<string | null>(null);
const [showCreate, setShowCreate] = useState(false);
const gameTypes = gameUIRegistry.list();
useEffect(() => {
if (!connected) return;
const unsubscribe = subscribe((msg: any) => {
if (msg.type === "ROOM_LIST_UPDATE") {
setRooms(msg.rooms);
}
if (msg.type === "ROOM_CREATED") {
navigate(`/${msg.gameSlug}/${msg.roomId}`);
}
});
return unsubscribe;
}, [connected, subscribe, navigate]);
const filteredRooms = filter ? rooms.filter(r => r.gameSlug === filter) : rooms;
const activeRooms = filteredRooms.filter(r => r.status !== "finished");
function createRoom(gameSlug: string) {
send({ type: "CREATE_ROOM", gameType: gameSlug });
setShowCreate(false);
}
return (
<div>
<div className="flex items-center justify-between gap-3 mb-4 md:mb-6">
<div className="min-w-0">
<h1 className="font-display text-lg font-semibold">Games</h1>
<p className="text-sm text-text-tertiary hidden sm:block">Browse and create game rooms</p>
</div>
<button
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 shrink-0"
>
+ Create Room
</button>
</div>
<div className="flex gap-2 mb-4 overflow-x-auto pb-1">
<button
onClick={() => setFilter(null)}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
filter === null ? "bg-primary/15 text-primary" : "bg-card text-text-tertiary hover:text-foreground"
}`}
>
All Games
</button>
{gameTypes.map(g => (
<button
key={g.slug}
onClick={() => setFilter(g.slug)}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
filter === g.slug ? "bg-primary/15 text-primary" : "bg-card text-text-tertiary hover:text-foreground"
}`}
>
{g.icon} {g.name}
</button>
))}
</div>
<div className="bg-card rounded-lg border border-border">
<div className="flex items-center gap-2 px-5 py-3 border-b border-border">
<span className="text-sm font-semibold">Active Rooms</span>
<span className="text-xs text-text-disabled">({activeRooms.length})</span>
</div>
{activeRooms.length === 0 ? (
<div className="px-5 py-8 text-center text-sm text-text-tertiary">
No active rooms. Create one to get started!
</div>
) : (
<div className="divide-y divide-border">
{activeRooms.map(room => {
const plugin = gameUIRegistry.get(room.gameSlug);
return (
<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 min-w-0">
<span className="text-lg shrink-0">{plugin?.icon ?? "🎮"}</span>
<div className="min-w-0">
<div className="text-sm font-medium truncate">{room.gameName}</div>
<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 ${
room.status === "waiting"
? "bg-warning/15 text-warning"
: "bg-success/15 text-success"
}`}>
{room.status === "waiting" ? "Waiting" : "Playing"}
</span>
<span>{room.playerCount}/{room.maxPlayers} players</span>
{room.spectatorCount > 0 && <span>· 👁 {room.spectatorCount}</span>}
</div>
</div>
</div>
<button
onClick={() => navigate(`/${room.gameSlug}/${room.id}`)}
className={`rounded-md px-3 py-1.5 text-xs font-semibold transition-colors shrink-0 ${
room.status === "waiting"
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "bg-card border border-border text-text-tertiary hover:text-foreground"
}`}
>
{room.status === "waiting" ? "Join" : "Spectate"}
</button>
</div>
);
})}
</div>
)}
</div>
{showCreate && (
<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-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>
<div className="space-y-2">
{gameTypes.map(g => (
<button
key={g.slug}
onClick={() => createRoom(g.slug)}
className="w-full flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm font-medium hover:bg-raised/40 transition-colors"
>
<span className="text-lg">{g.icon}</span>
<span>{g.name}</span>
</button>
))}
</div>
<button
onClick={() => setShowCreate(false)}
className="mt-4 w-full text-center text-sm text-text-tertiary hover:text-foreground transition-colors"
>
Cancel
</button>
</div>
</div>
)}
</div>
);
}