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>
71 lines
2.5 KiB
TypeScript
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>
|
|
)
|
|
}
|