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... +

+ )} +
+
+ ); }