Phase 10: team buzzers
Each team gets a buzzer link (/b/[token]); a game opts in with a
"Teams buzz in" checkbox. The database's clock orders the buzzes, so
the result doesn't depend on phone clocks. The control room lists the
order and clears it per question (B); the scoreboard shows who buzzed
first with a ding.
Buzzes follow the existing rules: token-gated route handlers with the
service-role client, RLS on the new table, and data-free realtime pings
on a new buzzer:{eventId} channel.
Needs supabase/migrations/0003_buzzer.sql applied before deploying.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { clearBuzzes } from "@/app/admin/live-actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatBuzzGap, type BuzzerView } from "@/lib/buzzer";
|
||||
|
||||
import { ErrorText } from "./error-text";
|
||||
import { useAction } from "./use-action";
|
||||
|
||||
/** Who buzzed for the current question, in order, and the button that clears them for the next. */
|
||||
export function BuzzerPanel({ eventId, gameId, buzzer }: { eventId: string; gameId: string; buzzer: BuzzerView }) {
|
||||
const { pending, error, run } = useAction();
|
||||
const { buzzes } = buzzer;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-ink/15 p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="text-lg font-bold">Buzzers</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={pending || buzzes.length === 0}
|
||||
onClick={() => run(() => clearBuzzes(eventId, gameId))}
|
||||
>
|
||||
Clear buzzers (B)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{buzzes.length === 0 ? (
|
||||
<p role="status" className="mt-1">
|
||||
{buzzer.open ? "Waiting for a buzz…" : "Closed — the game is done."}
|
||||
</p>
|
||||
) : (
|
||||
<ol role="status" className="mt-2 flex flex-col gap-1">
|
||||
{buzzes.map((entry, index) => (
|
||||
<li
|
||||
key={entry.teamId}
|
||||
className={`flex items-center gap-2 rounded-md px-2 ${index === 0 ? "min-h-11 bg-floodlight text-xl font-bold" : ""}`}
|
||||
>
|
||||
<span className="w-5 tabular-nums">{index + 1}</span>
|
||||
<span aria-hidden className="inline-block size-3 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
||||
<span className="text-sm tabular-nums">{index === 0 ? "first" : formatBuzzGap(entry.afterFirstMs)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
<ErrorText>{error}</ErrorText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user