nerdware/apps/web/src/pages/home.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

106 lines
3.1 KiB
TypeScript

import { useState } from "react"
import { useLocation } from "wouter"
import { Button } from "@workspace/ui/components/button"
import { useRoomStore } from "@/store/room"
const inputClass =
"border-input bg-background ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
export function HomePage() {
const [, navigate] = useLocation()
const createRoom = useRoomStore((s) => s.createRoom)
const joinRoom = useRoomStore((s) => s.joinRoom)
const connected = useRoomStore((s) => s.connected)
const [name, setName] = useState("")
const [code, setCode] = useState("")
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
const canSubmit = name.trim().length > 0 && connected && !busy
async function handleCreate() {
setError(null)
setBusy(true)
try {
const { roomCode } = await createRoom(name.trim())
navigate(`/room/${roomCode}`)
} catch (err) {
setError((err as { message?: string }).message ?? "Erreur")
} finally {
setBusy(false)
}
}
async function handleJoin() {
setError(null)
setBusy(true)
try {
const { roomCode } = await joinRoom(code.trim().toUpperCase(), name.trim())
navigate(`/room/${roomCode}`)
} catch (err) {
setError((err as { message?: string }).message ?? "Erreur")
} finally {
setBusy(false)
}
}
return (
<div className="flex min-h-svh items-center justify-center p-6">
<div className="flex w-full max-w-sm flex-col gap-6">
<header className="text-center">
<h1 className="font-heading text-3xl font-bold tracking-tight">
NerdWare
</h1>
<p className="text-muted-foreground text-sm">
Party game culture geek
</p>
</header>
<input
className={inputClass}
placeholder="Ton pseudo"
value={name}
maxLength={24}
onChange={(e) => setName(e.target.value)}
/>
<Button disabled={!canSubmit} onClick={handleCreate}>
Créer une room
</Button>
<div className="flex items-center gap-2">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground text-xs">ou rejoindre</span>
<div className="bg-border h-px flex-1" />
</div>
<div className="flex gap-2">
<input
className={`${inputClass} uppercase`}
placeholder="Code"
value={code}
maxLength={4}
onChange={(e) => setCode(e.target.value)}
/>
<Button
variant="secondary"
disabled={!canSubmit || code.trim().length === 0}
onClick={handleJoin}
>
Rejoindre
</Button>
</div>
{!connected && (
<p className="text-muted-foreground text-center text-xs">
Connexion au serveur
</p>
)}
{error && (
<p className="text-destructive text-center text-sm">{error}</p>
)}
</div>
</div>
)
}