7.7 KiB
Issue #1 Fixes — Design Spec
Goal: Address all items in Gitea issue #1 — rework predictions to use full ESC entries with tap-to-assign UI, remove Dish of the Nation, rename acts, add player submission indicators, and make lobby code copyable.
Source: #1
1. Entry Data Model
Replace the current country-only lineup with full ESC entries (shown as Zod schemas, matching codebase conventions):
const entrySchema = z.object({
country: z.object({ code: z.string(), name: z.string(), flag: z.string() }),
artist: z.string(),
song: z.string(),
})
const lineupSchema = z.object({
year: z.number(),
entries: z.array(entrySchema),
})
Note: The existing countrySchema gains a flag field — this is a breaking change to the country object shape throughout the codebase.
The data file changes from esc-2026.json to esc-2025.json using real ESC 2025 entries for testing. Each entry includes the flag emoji in the data file.
Display format everywhere: 🇩🇪 Abor — Süden
2. Prediction Model
Replace the current { predictedWinner, top3[], nulPointsPick } with 4 ordered picks:
type Prediction = {
playerId: string
first: string // country code
second: string // country code
third: string // country code
last: string // country code
}
Validation: All 4 picks must be distinct country codes from the lineup.
Scoring model (for future implementation):
- Any of 1st/2nd/3rd picks landing in the actual top 3 → points
- 1st pick matching actual winner → bonus points
- Last pick matching actual last place → bonus points
3. Prediction UI — Tap-to-Assign
Top section — 4 slot cards:
- "1st Place", "2nd Place", "3rd Place", "Last Place"
- Empty slots show placeholder text
- Filled slots show the entry (flag + artist + song) with a tap-to-remove action
Bottom section — scrollable entry list:
- All entries from the lineup
- Already-assigned entries are dimmed/disabled
- Tapping an unassigned entry shows a popover with only unfilled slot options (1st/2nd/3rd/Last)
- After selecting a slot, the entry fills that slot, the popover closes, and the entry dims in the list
Submit button: Appears when all 4 slots are filled. After submission or when predictions are locked, the UI becomes read-only showing assigned entries in their slots.
Locked state: When advancing past Pre-Show, predictions lock. The form shows the player's submitted picks (or "Not submitted" if they missed the window).
4. Remove Dish of the Nation
Strip the entire Dish of the Nation feature:
Server:
- Remove dish WS message handlers from
handler.ts - Remove dish methods from
GameManager - Remove dish persistence from
GameService - Remove dish DB tables (
dishes,dish_guesses) from schema
Client:
- Delete
dish-list.tsx,dish-host.tsx,dish-results.tsx - Remove dish state slices and actions from
room-store.ts - Remove dish WS message handlers from
use-websocket.ts - Remove dish UI from routes (
play,host,display)
Shared:
- Remove dish schemas from
game-types.ts - Remove dish WS message types from
ws-messages.ts
5. Player List — Prediction Checkmark
Add prediction submission status to the game state broadcast:
- Shared: Add
predictionSubmitted: Map<playerId, boolean>(or equivalent) togameStateSchemaingame-types.ts. This lives on the game state, not the player schema, since it's game-specific data. - Server: When building game state in
GameManager.getGameStateForPlayer()/getGameStateForDisplay(), include which players have submitted predictions. - Client:
player-list.tsxreads from game state and renders a checkmark icon (✓) next to player names that have submitted predictions. - Visible on all views (play, host, display).
6. Acts Naming
Rename internal act identifiers and add display names:
const ACTS = ["lobby", "pre-show", "live-event", "scoring", "ended"] as const
| Internal ID | Display Name | Timing Intent |
|---|---|---|
lobby |
Lobby | Waiting room, players join |
pre-show |
Pre-Show | Before broadcast, predictions |
live-event |
Live Event | During broadcast |
scoring |
Scoring | After results, leaderboard |
ended |
Ended | Party over |
Host control buttons: "Start Pre-Show", "Start Live Event", "Start Scoring", "End Party".
Predictions lock when advancing from Pre-Show to Live Event (previously act1 → act2).
DB migration: The Postgres actEnum must be updated from ["lobby", "act1", "act2", "act3", "ended"] to ["lobby", "pre-show", "live-event", "scoring", "ended"]. Since there is no production data to preserve, drop and recreate the enum (via Drizzle push or a migration). The predictions table columns also change from predictedWinner/top3/nulPointsPick to first/second/third/last — same approach, drop and recreate.
7. Lobby Code — Copy to Clipboard
On the display view (and anywhere the room code is shown prominently):
- Wrap the room code in a tappable/clickable element
- On click:
navigator.clipboard.writeText(roomCode) - Show brief "Copied!" feedback (tooltip or temporary text swap)
- Style to indicate interactivity (cursor pointer, subtle hover state)
Data Flow Changes
Prediction Flow (updated)
- Client taps entry → selects slot → slot fills in UI
- Client fills all 4 slots → submits
submit_predictionwith{ first, second, third, last } - Server validates: all 4 distinct, all valid country codes
- Server stores in GameManager, persists to DB
- Server broadcasts updated game state (includes
hasSubmittedPredictionper player) - All clients update player list checkmarks
Act Progression (updated names)
lobby → pre-show → live-event → scoring → ended
Predictions lock on pre-show → live-event transition.
Files Affected
Modified
packages/shared/src/game-types.ts— entry/lineup schemas, prediction model, add predictionSubmitted to game statepackages/shared/src/ws-messages.ts— remove dish messages, update prediction messagepackages/shared/src/constants.ts— act namespackages/server/data/— replaceesc-2026.jsonwithesc-2025.jsonpackages/server/src/games/game-manager.ts— remove dish logic, update prediction logicpackages/server/src/games/game-service.ts— remove dish persistence, update prediction columnspackages/server/src/rooms/room-manager.ts— act name referencespackages/server/src/ws/handler.ts— remove dish handlers, update prediction handlerpackages/server/src/db/schema.ts— remove dish tables, update prediction columns, update actEnum valuespackages/server/tests/game-manager.test.ts— rewrite for new modelpackages/server/tests/ws-handler.test.ts— update for changed messagespackages/client/src/stores/room-store.ts— remove dish state, update game state shapepackages/client/src/hooks/use-websocket.ts— remove dish handlerspackages/client/src/components/predictions-form.tsx— rewrite as tap-to-assignpackages/client/src/components/player-list.tsx— add prediction checkmarkpackages/client/src/routes/play.$roomCode.tsx— remove dish UIpackages/client/src/routes/host.$roomCode.tsx— remove dish UIpackages/client/src/routes/display.$roomCode.tsx— remove dish UI, add copy-to-clipboard
Deleted
packages/client/src/components/dish-list.tsxpackages/client/src/components/dish-host.tsxpackages/client/src/components/dish-results.tsx
Created
packages/server/data/esc-2025.json— full ESC 2025 entry data