diff --git a/apps/web/src/routes/multiplayer/lobby.$code.tsx b/apps/web/src/routes/multiplayer/lobby.$code.tsx
index 32d44d9..dcebfa2 100644
--- a/apps/web/src/routes/multiplayer/lobby.$code.tsx
+++ b/apps/web/src/routes/multiplayer/lobby.$code.tsx
@@ -1,9 +1,159 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect, useState, useCallback } from "react";
+import {
+ useWsClient,
+ useWsConnect,
+ useWsDisconnect,
+} from "../../lib/ws-hooks.js";
+import type { Lobby, WsLobbyState, WsError } from "@lila/shared";
-export const Route = createFileRoute('/multiplayer/lobby/$code')({
- component: RouteComponent,
-})
+const API_URL = (import.meta.env["VITE_API_URL"] as string) || "";
-function RouteComponent() {
- return
Hello "/multiplayer/lobby/$code"!
+export const Route = createFileRoute("/multiplayer/lobby/$code")({
+ component: LobbyPage,
+});
+
+function LobbyPage() {
+ const { code } = Route.useParams();
+ const { session } = Route.useRouteContext();
+ const currentUserId = session.user.id;
+ const navigate = useNavigate();
+ const client = useWsClient();
+ const connect = useWsConnect();
+ const disconnect = useWsDisconnect();
+
+ const [lobby, setLobby] = useState(null);
+ const [error, setError] = useState(null);
+ const [isStarting, setIsStarting] = useState(false);
+
+ const handleLobbyState = useCallback((msg: WsLobbyState) => {
+ setLobby(msg.lobby);
+ setError(null);
+ }, []);
+
+ const handleGameQuestion = useCallback(() => {
+ void navigate({ to: "/multiplayer/game/$code", params: { code } });
+ }, [navigate, code]);
+
+ const handleWsError = useCallback((msg: WsError) => {
+ setError(msg.message);
+ setIsStarting(false);
+ }, []);
+
+ useEffect(() => {
+ client.on("lobby:state", handleLobbyState);
+ client.on("game:question", handleGameQuestion);
+ client.on("error", handleWsError);
+
+ void connect(API_URL)
+ .then(() => {
+ client.send({ type: "lobby:join", code });
+ })
+ .catch((err) => {
+ console.error("Failed to connect to WebSocket:", err);
+ setError("Could not connect to server. Please try again.");
+ });
+
+ return () => {
+ client.off("lobby:state", handleLobbyState);
+ client.off("game:question", handleGameQuestion);
+ client.off("error", handleWsError);
+ client.send({ type: "lobby:leave", lobbyId: lobby?.id ?? "" });
+ disconnect();
+ };
+ // Effect runs once on mount. All referenced values are stable:
+ // client/connect/disconnect from context (useCallback), handlers
+ // wrapped in useCallback, code is a URL param. lobbyIdRef is a ref.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleStart = useCallback(() => {
+ if (!lobby) return;
+ setIsStarting(true);
+ client.send({ type: "lobby:start", lobbyId: lobby.id });
+ }, [lobby, client]);
+
+ if (!lobby) {
+ return (
+
+
+ {error ?? "Connecting..."}
+
+
+ );
+ }
+
+ const isHost = lobby.hostUserId === currentUserId;
+ const canStart = isHost && lobby.players.length >= 2 && !isStarting;
+
+ return (
+
+
+ {/* Lobby code */}
+
+
Lobby code
+
+
Click to copy
+
+
+
+
+ {/* Player list */}
+
+
+ Players ({lobby.players.length})
+
+
+ {lobby.players.map((player) => (
+ -
+
+ {player.user.name}
+ {player.userId === lobby.hostUserId && (
+
+ host
+
+ )}
+
+ ))}
+
+
+
+ {/* Error */}
+ {error &&
{error}
}
+
+ {/* Start button — host only */}
+ {isHost && (
+
+ )}
+
+ {/* Non-host waiting message */}
+ {!isHost && (
+
+ Waiting for host to start the game...
+
+ )}
+
+
+ );
}