nerdware/apps/web/src/pages/lobby.tsx
AyoubBenziza 29faa18731 feat(server,web): first real-time cycle — room create/join + lobby
Server (@nerdware/server):
- RoomManager: in-memory rooms (Map), code generation, create/join,
  disconnect handling, public snapshot projection (no secrets)
- Socket.IO handlers: room:create / room:join with typed acks,
  room:state broadcast on every change, lobby:updateSettings (host-only),
  player:left on disconnect

Web (web):
- typed Socket.IO client + Zustand store mirroring room:state
- wouter routing: home (create/join) + minimal lobby (live player list)
- @nerdware/shared wired into the client

Roadmap V1 step 2. Verified end-to-end (create/join/broadcast/disconnect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:16:26 +02:00

71 lines
2.5 KiB
TypeScript

import { Link } from "wouter"
import { Button } from "@workspace/ui/components/button"
import { useRoomStore } from "@/store/room"
export function LobbyPage({ code }: { code: string }) {
const snapshot = useRoomStore((s) => s.snapshot)
const playerId = useRoomStore((s) => s.playerId)
const connected = useRoomStore((s) => s.connected)
// Pas de snapshot pour ce code (accès direct / refresh) : on renvoie à l'accueil.
if (!snapshot || snapshot.code !== code) {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-4 p-6 text-center">
<p className="text-muted-foreground text-sm">
Room introuvable ou session perdue.
</p>
<Link href="/">
<Button variant="secondary">Retour à l'accueil</Button>
</Link>
</div>
)
}
return (
<div className="flex min-h-svh items-center justify-center p-6">
<div className="flex w-full max-w-md flex-col gap-6">
<header className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-xs uppercase">Code room</p>
<p className="font-heading text-3xl font-bold tracking-widest">
{snapshot.code}
</p>
</div>
<span
className={`size-2.5 rounded-full ${connected ? "bg-green-500" : "bg-red-500"}`}
title={connected ? "Connecté" : "Déconnecté"}
/>
</header>
<section className="flex flex-col gap-2">
<h2 className="text-muted-foreground text-sm font-medium">
Joueurs ({snapshot.players.length})
</h2>
<ul className="flex flex-col gap-1">
{snapshot.players.map((p) => (
<li
key={p.id}
className="bg-muted/40 flex items-center justify-between rounded-md px-3 py-2 text-sm"
>
<span className={p.connected ? "" : "opacity-40"}>
{p.name}
{p.id === playerId && (
<span className="text-muted-foreground"> (toi)</span>
)}
</span>
<span className="text-muted-foreground text-xs">
{p.id === snapshot.hostId ? "Hôte" : ""}
{!p.connected && " · hors ligne"}
</span>
</li>
))}
</ul>
</section>
<p className="text-muted-foreground text-center text-xs">
En attente du lancement de la partie par l'hôte
</p>
</div>
</div>
)
}