A game can now carry a list of items the host puts on the scoreboard one at a time: a question (shown above the standings, with an optional picture and an answer to reveal), a sound (played through the scoreboard's speakers) or a video such as a karaoke track (takes over the screen). - Media is a YouTube link, an https link to a file, or an upload to the private game-media bucket. The scoreboard API signs a URL only for the item that is on screen. - What is on screen is state on the event, like the timer, so the ping still carries no data. An unrevealed answer and a sound's or video's name never reach the scoreboard payload. - Control room: click an item to show it, or "Random" to have one picked (prefers items not played yet). Show/hide answer, pause, take off screen; shortcuts Q / A / X. - Duplicating a game or event copies its items and files; deleting removes the files. Needs supabase/migrations/0006_screen_items.sql applied before deploying. database.types.ts was edited by hand to match; re-run gen:types after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
7.0 KiB
Markdown
105 lines
7.0 KiB
Markdown
# Build plan
|
||
|
||
Work one phase at a time. At the end of each phase: run lint + tests, commit, and give the
|
||
human a short "how to test this manually" checklist. Don't start the next phase until told.
|
||
|
||
## Phase 0 — Scaffold
|
||
- Next.js + TS strict + Tailwind + ESLint + Vitest. `src/` layout per ARCHITECTURE.md.
|
||
- Fonts: Big Shoulders (Google's new name for Big Shoulders Display) + Atkinson Hyperlegible via `next/font/google`. Tokens from UI.md as CSS variables + Tailwind theme.
|
||
- Supabase clients (`browser`, `server`, `admin` with `server-only`).
|
||
- `.env.local` loading; fail fast with a clear error if vars are missing.
|
||
**Done when:** `npm run dev` shows a placeholder home; `npm run test` runs a dummy test.
|
||
|
||
## Phase 1 — Database + types
|
||
- Apply `supabase/migrations/0001_init.sql`. Generate TS types into `src/lib/database.types.ts`.
|
||
- `src/lib/tokens.ts` zod schemas (32-char hex).
|
||
**Done when:** tables visible in Supabase; anon key cannot `select` from any table (write a quick script/test proving it).
|
||
|
||
## Phase 2 — Scoring engine (TDD)
|
||
- Implement SCORING.md in `src/lib/scoring/` as pure functions.
|
||
- All listed test cases + edge cases.
|
||
**Done when:** tests green, 100% branch coverage on the scoring module.
|
||
|
||
## Phase 3 — Admin auth + setup screens
|
||
- `/login` magic link; `proxy.ts` guards `/admin/*` with `ADMIN_EMAILS`.
|
||
- Events list + create/duplicate/delete.
|
||
- Event setup: teams CRUD (colour picker, reorder), games builder with criteria + presets, notes.
|
||
**Done when:** I can build a "Worship Game Night" with 4 teams and a Karaoke game with 5 criteria (1–5) and a Guess the Lyric game with 1 number criterion.
|
||
|
||
## Phase 4 — Scoreboard + realtime
|
||
- `src/lib/realtime.ts` `notify()` (server broadcast) + `useRefreshChannel(channel, refetch)` hook (debounce, reconnect, 10s poll, refetch on focus).
|
||
- `GET /api/public/scoreboard/[token]` building the payload via the scoring engine.
|
||
- `/s/[token]` page per UI.md: FLIP rank animation, count-up totals, reduced motion, fullscreen, cursor hide, connection dot.
|
||
**Done when:** changing anything in the DB via admin (or SQL + a manual notify) updates an open scoreboard tab in < 1s.
|
||
|
||
## Phase 5 — Control room (direct scoring)
|
||
- `/admin/events/[id]/live`: game list, prev/next, set current (→ live), mark done.
|
||
- Scoring grid with block + number inputs, autosave, optimistic UI.
|
||
- Bonus/penalty panel with quick buttons, custom, reason, undo.
|
||
- Keyboard shortcuts.
|
||
**Done when:** I run a full direct-scored game and the scoreboard tracks every change.
|
||
|
||
## Phase 6 — Visibility + reveal
|
||
- Visible / Frozen / Hidden toggle. Freeze stores snapshot server-side. Hidden strips scores in the API.
|
||
- Reveal mode: start, reveal next (bottom-up), finish → visible.
|
||
**Done when:** in Hidden mode, the network tab on the scoreboard contains NO totals. Reveal works end-to-end.
|
||
|
||
## Phase 7 — Judges
|
||
- Nominate judges, copy link, QR codes (use a small QR lib), revoke.
|
||
- Open link toggle + `/jo/[token]` name flow + localStorage.
|
||
- `/j/[token]` mobile page: team cards, spotlight pinned, block rows, autosave status, read-only states.
|
||
- `PUT /api/judge/[token]/scores` with all checks from ARCHITECTURE.md + rate limit. Clearing = DELETE or `value: null`.
|
||
- Control room in judged mode: averages, judge progress matrix, per-judge breakdown, override cells, spotlight select, lock toggle.
|
||
**Done when:** 2 phones judging Karaoke at once, admin sees progress live, scoreboard updates with averages, override works, lock makes judges read-only.
|
||
|
||
## Phase 8 — Polish + deploy
|
||
- Share panel (all links + QR in one place, "copy all judge links for this game" as text for WhatsApp).
|
||
- Empty states + error copy per UI.md.
|
||
- Accessibility pass (focus, contrast, tap targets).
|
||
- Deploy to Vercel; set env vars; set Supabase auth redirect URLs to the Vercel domain.
|
||
**Done when:** full dry run on the deployed URL: laptop admin + projector scoreboard + 2 phone judges.
|
||
|
||
## Phase 9 — Timer + sound effects
|
||
- Apply `supabase/migrations/0002_timer.sql`, then `npm run gen:types`.
|
||
- Timer rules in `src/lib/timer.ts` (tested); control-room panel (set, start/pause/reset, show/hide, `T`); scoreboard display.
|
||
- Scoreboard sound effects: reveal drum-roll + crash/fanfare, game-change chime, last-10-seconds ticks + buzzer; "Turn sound on" button.
|
||
**Done when:** a timer started in the control room counts down on the scoreboard with ticks and a buzzer, and a reveal plays its drum-roll.
|
||
|
||
## Phase 10 — Team buzzers
|
||
- Apply `supabase/migrations/0003_buzzer.sql`, then `npm run gen:types` and `npm run check:rls`.
|
||
- Buzzer rules in `src/lib/buzzer.ts` (tested); `/b/[token]` phone page + `GET`/`POST` buzzer API.
|
||
- "Teams buzz in" checkbox per game; buzzer links per team in the share panel.
|
||
- Control room: buzz order + "Clear buzzers" (`B`). Scoreboard: "{Team} buzzed!" banner + ding.
|
||
**Done when:** two phones on different teams buzz in a buzzer game; the control room and scoreboard
|
||
show who was first within a second, and "Clear buzzers" resets both phones.
|
||
|
||
## Phase 11 — Buzzer answers
|
||
- Apply `supabase/migrations/0004_buzz_result.sql`, then `npm run gen:types`.
|
||
- `checkMark` / `bumpScore` / `answeringTeamId` in `src/lib/buzzer.ts` (tested); `markBuzz` server action.
|
||
- Control room: "Got it" (`G`), "Wrong" (`W`), "Undo", "Next question" (`B`).
|
||
- Phone and scoreboard follow whose turn it is; "correct" and "wrong" sounds.
|
||
**Done when:** team A buzzes then team B; "Wrong" hands the turn to B; "Got it" gives B one point
|
||
in the scoring grid and on the scoreboard; "Undo" takes it back; "Next question" resets the phones.
|
||
|
||
## Phase 12 — Co-hosts
|
||
- Apply `supabase/migrations/0005_cohosts.sql`, then `npm run gen:types` and `npm run check:rls`.
|
||
- `event_cohosts` table; RLS widened from owner to owner-or-co-host, with delete and sharing owner-only.
|
||
- "Co-hosts" section on the setup page (`addCohost` / `removeCohost` server actions); "Shared with you" in the events list.
|
||
**Done when:** admin A shares an event with admin B's email; B sees it in their list, can edit it and
|
||
run the control room alongside A, but has no Delete button and can't change the co-hosts; removing B
|
||
makes the event disappear from B's list.
|
||
|
||
## Phase 13 — Questions, sounds and videos on the scoreboard
|
||
- Apply `supabase/migrations/0006_screen_items.sql` (table, event columns, the `game-media`
|
||
bucket and its policies), then `npm run gen:types` and `npm run check:rls`.
|
||
- Rules in `src/lib/screen.ts` (tested); setup editor per game with links and uploads;
|
||
control-room panel (show, random, answer, pause, take off screen; `Q` / `A` / `X`).
|
||
- Scoreboard: question above the standings, sound banner, video takeover.
|
||
**Done when:** a quiz game shows its questions one by one with `Q` and reveals an answer with `A`
|
||
(the answer is not in the scoreboard's network tab before that); an uploaded mp3 plays on the
|
||
scoreboard; a karaoke game with several YouTube links plays the one I click full screen, "Random
|
||
video" picks one that hasn't been played, and "Take off screen" brings the standings back.
|
||
|
||
## Stretch (only if asked)
|
||
Participants within teams · CSV export of results.
|