restructure host routes: layout with nested game, bingo, board, host children

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 11:54:32 +01:00
parent 3ec8803711
commit d056647bbd
6 changed files with 78 additions and 202 deletions

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router"
import { useRoomStore } from "@/stores/room-store"
import { BingoTab } from "@/components/bingo-tab"
export const Route = createFileRoute("/host/$roomCode/bingo")({
component: HostBingo,
})
function HostBingo() {
const { room, gameState, send } = useRoomStore()
if (!room || !gameState) return null
return <BingoTab currentAct={room.currentAct} gameState={gameState} send={send} />
}

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router"
import { useRoomStore } from "@/stores/room-store"
import { BoardTab } from "@/components/board-tab"
export const Route = createFileRoute("/host/$roomCode/board")({
component: HostBoard,
})
function HostBoard() {
const { room, gameState } = useRoomStore()
if (!room || !gameState) return null
return <BoardTab currentAct={room.currentAct} gameState={gameState} />
}

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router"
import { useRoomStore } from "@/stores/room-store"
import { GameTab } from "@/components/game-tab"
export const Route = createFileRoute("/host/$roomCode/game")({
component: HostGame,
})
function HostGame() {
const { room, gameState, send } = useRoomStore()
if (!room || !gameState) return null
return <GameTab currentAct={room.currentAct} gameState={gameState} send={send} />
}

View File

@@ -0,0 +1,16 @@
import { createFileRoute } from "@tanstack/react-router"
import { useRoomStore } from "@/stores/room-store"
import { HostTab } from "@/components/host-tab"
export const Route = createFileRoute("/host/$roomCode/host")({
component: HostControls,
})
function HostControls() {
const { roomCode } = Route.useParams()
const { room, gameState, send } = useRoomStore()
if (!room || !gameState) return null
return <HostTab roomCode={roomCode} currentAct={room.currentAct} gameState={gameState} send={send} />
}

View File

@@ -0,0 +1,7 @@
import { createFileRoute, redirect } from "@tanstack/react-router"
export const Route = createFileRoute("/host/$roomCode/")({
beforeLoad: ({ params }) => {
throw redirect({ to: "/host/$roomCode/game", params })
},
})

View File

@@ -1,43 +1,17 @@
import { createFileRoute } from "@tanstack/react-router"
import { useWebSocket } from "@/hooks/use-websocket"
import { useRoomStore } from "@/stores/room-store"
import { PlayerList } from "@/components/player-list"
import { PredictionsForm } from "@/components/predictions-form"
import { JuryHost } from "@/components/jury-host"
import { JuryVoting } from "@/components/jury-voting"
import { BingoCard } from "@/components/bingo-card"
import { ActualResultsForm } from "@/components/actual-results-form"
import { QuizHost } from "@/components/quiz-host"
import { QuizBuzzer } from "@/components/quiz-buzzer"
import { Leaderboard } from "@/components/leaderboard"
import { RoomHeader } from "@/components/room-header"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import type { Act } from "@celebrate-esc/shared"
import { RoomLayout } from "@/components/room-layout"
import { BottomNav } from "@/components/bottom-nav"
export const Route = createFileRoute("/host/$roomCode")({
component: HostView,
component: HostLayout,
})
const nextActLabels: Partial<Record<Act, string>> = {
lobby: "Start Pre-Show",
"pre-show": "Start Live Event",
"live-event": "Start Scoring",
scoring: "End Party",
}
const prevActLabels: Partial<Record<Act, string>> = {
"pre-show": "Back to Lobby",
"live-event": "Back to Pre-Show",
scoring: "Back to Live Event",
ended: "Back to Scoring",
}
function HostView() {
function HostLayout() {
const { roomCode } = Route.useParams()
const { send } = useWebSocket(roomCode)
const { room, mySessionId, connectionStatus, gameState } = useRoomStore()
useWebSocket(roomCode)
const { room, connectionStatus } = useRoomStore()
if (!room) {
return (
@@ -50,175 +24,9 @@ function HostView() {
}
return (
<div className="flex min-h-screen flex-col">
<RoomHeader roomCode={roomCode} currentAct={room.currentAct} connectionStatus={connectionStatus} />
<Tabs defaultValue="host" className="flex-1">
<TabsList className="w-full rounded-none">
<TabsTrigger value="play" className="flex-1">
Play
</TabsTrigger>
<TabsTrigger value="host" className="flex-1">
Host
</TabsTrigger>
</TabsList>
<TabsContent value="play" className="p-4">
{gameState && (room.currentAct === "lobby" || room.currentAct === "pre-show") && (
<div className="flex flex-col gap-4">
<PredictionsForm
entries={gameState.lineup.entries}
existingPrediction={gameState.myPrediction}
locked={gameState.predictionsLocked}
onSubmit={(prediction) =>
send({ type: "submit_prediction", ...prediction })
}
/>
</div>
)}
{gameState && room.currentAct === "live-event" && (
<Tabs defaultValue="jury">
<TabsList className="w-full">
<TabsTrigger value="jury" className="flex-1">Jury</TabsTrigger>
<TabsTrigger value="bingo" className="flex-1">Bingo</TabsTrigger>
</TabsList>
<TabsContent value="jury" className="mt-4">
{gameState.currentJuryRound ? (
<JuryVoting
round={gameState.currentJuryRound}
myVote={gameState.myJuryVote}
onVote={(rating) => send({ type: "submit_jury_vote", rating })}
/>
) : (
<div className="py-8 text-center text-muted-foreground">
Waiting for host to open voting...
</div>
)}
</TabsContent>
<TabsContent value="bingo" className="mt-4">
{gameState.myBingoCard ? (
<BingoCard
card={gameState.myBingoCard}
onTap={(tropeId) => send({ type: "tap_bingo_square", tropeId })}
/>
) : (
<div className="py-8 text-center text-muted-foreground">
No bingo card yet
</div>
)}
</TabsContent>
</Tabs>
)}
{gameState && (room.currentAct === "scoring" || room.currentAct === "ended") && gameState.myPrediction && (
<PredictionsForm
entries={gameState.lineup.entries}
existingPrediction={gameState.myPrediction}
locked={true}
actualResults={gameState.actualResults}
onSubmit={() => {}}
/>
)}
{gameState && room.currentAct === "scoring" && gameState.currentQuizQuestion && (
<QuizBuzzer
question={gameState.currentQuizQuestion}
buzzStatus={gameState.myQuizBuzzStatus}
onBuzz={() => send({ type: "buzz" })}
/>
)}
{gameState && (room.currentAct === "scoring" || room.currentAct === "ended") && (
<Leaderboard entries={gameState.leaderboard} resultsEntered={!!gameState.actualResults} />
)}
<PlayerList
players={room.players}
mySessionId={mySessionId}
predictionSubmitted={gameState?.predictionSubmitted}
/>
</TabsContent>
<TabsContent value="host" className="p-4">
<div className="flex flex-col gap-4">
<Card>
<CardHeader>
<CardTitle>Room Controls</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{room.currentAct !== "ended" && (
<div className="flex gap-2">
{room.currentAct !== "lobby" && (
<Button
variant="outline"
onClick={() => send({ type: "revert_act" })}
className="flex-1"
>
{prevActLabels[room.currentAct] ?? "Back"}
</Button>
)}
<Button onClick={() => send({ type: "advance_act" })} className="flex-1">
{nextActLabels[room.currentAct] ?? "Next"}
</Button>
</div>
)}
{room.currentAct !== "ended" && (
<Button
variant="destructive"
onClick={() => send({ type: "end_room" })}
className="w-full"
>
End Party
</Button>
)}
{room.currentAct === "ended" && (
<div className="flex flex-col gap-2">
<p className="text-center text-muted-foreground">
The party has ended. Thanks for playing!
</p>
<Button
variant="outline"
onClick={() => send({ type: "revert_act" })}
>
{prevActLabels[room.currentAct] ?? "Back"}
</Button>
</div>
)}
{room.currentAct === "live-event" && gameState && (
<JuryHost
entries={gameState.lineup.entries}
currentRound={gameState.currentJuryRound}
results={gameState.juryResults}
onOpenVote={(countryCode) => send({ type: "open_jury_vote", countryCode })}
onCloseVote={() => send({ type: "close_jury_vote" })}
/>
)}
{gameState && room.currentAct === "scoring" && (
<QuizHost
question={gameState.currentQuizQuestion}
onStartQuestion={() => send({ type: "start_quiz_question" })}
onJudge={(correct) => send({ type: "judge_quiz_answer", correct })}
onSkip={() => send({ type: "skip_quiz_question" })}
/>
)}
{gameState && (room.currentAct === "scoring" || room.currentAct === "ended") && (
<ActualResultsForm
entries={gameState.lineup.entries}
existingResults={gameState.actualResults}
onSubmit={(results) => send({ type: "submit_actual_results", ...results })}
/>
)}
{gameState && (room.currentAct === "scoring" || room.currentAct === "ended") && (
<Leaderboard entries={gameState.leaderboard} resultsEntered={!!gameState.actualResults} />
)}
</CardContent>
</Card>
<PlayerList
players={room.players}
mySessionId={mySessionId}
predictionSubmitted={gameState?.predictionSubmitted}
/>
</div>
</TabsContent>
</Tabs>
</div>
<>
<RoomLayout roomCode={roomCode} connectionStatus={connectionStatus} />
<BottomNav basePath="/host/$roomCode" roomCode={roomCode} isHost={true} />
</>
)
}