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>
106 lines
3.1 KiB
TypeScript
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>
|
|
)
|
|
}
|