diff --git a/bun.lockb b/bun.lockb index 902a4be..e199d0b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 83a3c82..4479caf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@paralleldrive/cuid2": "^2.2.2", "astro": "^4.16.13", "better-sqlite3": "^11.5.0", + "clsx": "^2.1.1", "nanoid": "^5.0.8", "preact": "^10.24.3", "typescript": "^5.6.3" diff --git a/src/components/Leaderboard.tsx b/src/components/Leaderboard.tsx index 9d19d83..4355f4e 100644 --- a/src/components/Leaderboard.tsx +++ b/src/components/Leaderboard.tsx @@ -1,4 +1,5 @@ import type { Question, Scoreboard } from '@/ggwp' +import clsx from 'clsx' type Props = { questions: Question[] @@ -12,7 +13,7 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props
@@ -24,7 +25,7 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props ))}
- {scoreboard.map(({ team, totalScore, questionScores }, i) => ( + {scoreboard.board.map(({ team, totalScore, questionScores }, i) => (
{team}
@@ -33,11 +34,16 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props
{questions.map((question, j) => (
{ - console.log('emitting', { team, question: question.id }) - return selectTeamQuestion?.({ team, question: question.id }) + if (selectTeamQuestion) { + console.log('emitting', { team, question: question.id }) + selectTeamQuestion({ team, question: question.id }) + } }} > {questionScores[question.id]} diff --git a/src/components/LiveLeaderboard.tsx b/src/components/LiveLeaderboard.tsx index f652c82..349fb68 100644 --- a/src/components/LiveLeaderboard.tsx +++ b/src/components/LiveLeaderboard.tsx @@ -1,17 +1,54 @@ +import { requestJSON } from '@/client/utils' import type { Room } from '@/db/model' +import { useEffect, useState } from 'preact/hooks' import { Leaderboard } from './Leaderboard' -import { type Dispatch, type StateUpdater } from 'preact/hooks' import { computeScoreboardState } from '@/ggwp' type Props = { - room: Room - setRoom: Dispatch> + roomId: string +} + +const useEventSource = (roomId: string, onMessage: (data: any) => void) => { + useEffect(() => { + const es = new EventSource(`/api/room/${roomId}?sse`) + es.onmessage = e => { + onMessage(JSON.parse(e.data)) + } - selectTeamQuestion?: ({ team, question }: { team: string; question: string }) => void + return () => { + es.close() + } + }, [roomId]) } -export const LiveLeaderboard = ({ room, selectTeamQuestion }: Props) => { +export const LiveLeaderboard = ({ roomId }: Props) => { + const [room, setRoom] = useState(null) + const fetchRoom = async () => { + await requestJSON(`/api/room/${roomId}`).then(room => { + setRoom({ id: roomId, ...room }) + }) + } + + useEffect(() => { + fetchRoom() + }, []) + + useEventSource(roomId, data => { + console.log('event', data) + fetchRoom() + }) + if (!room) { return
Loading...
} + + const scoreboard = computeScoreboardState( + { + teamIds: room.teams, + questions: room.questions, + }, + room.actions + ) + + return } diff --git a/src/db/index.ts b/src/db/index.ts index b73ebe5..a7f2ae8 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,6 +2,7 @@ import cuid2 from '@paralleldrive/cuid2' import Database from 'better-sqlite3' import type { Room, RoomData } from './model' import type { Question } from '@/ggwp' +import { emitRoomUpdate } from './events' const db = new Database('ggwp.db') db.pragma('journal_mode = WAL') @@ -54,6 +55,8 @@ export function createRoom(id: string, teams: string[], questions: Question[]): } export function updateRoom(id: string, data: RoomData): void { + emitRoomUpdate(id, data) + db.prepare<[string, string]>( ` UPDATE rooms diff --git a/src/ggwp/index.ts b/src/ggwp/index.ts index 04dc026..3ec3962 100644 --- a/src/ggwp/index.ts +++ b/src/ggwp/index.ts @@ -12,10 +12,13 @@ export type Game = { } export type Scoreboard = { - team: string - totalScore: number - questionScores: { [id: string]: number } -}[] + teamJollyGroup: { [team: string]: string } + board: { + team: string + totalScore: number + questionScores: { [id: string]: number } + }[] +} function getCorrectnessMultiplier(outcome: 'correct' | 'partial' | 'wrong'): number { switch (outcome) { @@ -101,8 +104,6 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb }) } - console.log(actionsByQuestion) - const scoreboardByTeam: { [team: string]: { totalScore: number @@ -163,17 +164,20 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb scoreboardByTeam[team].bonusJolly = teamBonus } - const scoreboard: Scoreboard = [] + const scoreboard: Scoreboard = { + teamJollyGroup: teamsJollyGroup, + board: [], + } for (const team of game.teamIds) { - scoreboard.push({ + scoreboard.board.push({ team, totalScore: scoreboardByTeam[team].totalScore, questionScores: scoreboardByTeam[team].questionScores, }) } - scoreboard.sort((a, b) => b.totalScore - a.totalScore) + scoreboard.board.sort((a, b) => b.totalScore - a.totalScore) return scoreboard } diff --git a/src/pages/api/room/[id]/index.ts b/src/pages/api/room/[id]/index.ts index 302b516..ce56951 100644 --- a/src/pages/api/room/[id]/index.ts +++ b/src/pages/api/room/[id]/index.ts @@ -1,10 +1,35 @@ import { getRoom, updateRoom } from '@/db' +import { addRoomUpdateListener, removeRoomUpdateListener } from '@/db/events' import type { RoomData } from '@/db/model' import type { APIRoute } from 'astro' -async function sseHandler() { - // https://github.com/MicroWebStacks/astro-examples/blob/main/03_sse-counter/src/pages/api/stream.js - return new Response('SSE not implemented', { status: 501 }) +function sseHandler(roomId: string) { + let events_listener: () => void + + const stream = new ReadableStream({ + start(controller) { + events_listener = () => { + const data = `data: ${JSON.stringify('room')}\r\n\r\n` + controller.enqueue(data) + } + + removeRoomUpdateListener(roomId, events_listener) + addRoomUpdateListener(roomId, events_listener) + }, + cancel() { + console.log('stream.js> cancel()') + removeRoomUpdateListener(roomId, events_listener) + }, + }) + + return new Response(stream, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + }, + }) } export const GET: APIRoute = async ({ params, url }) => { @@ -14,7 +39,7 @@ export const GET: APIRoute = async ({ params, url }) => { } if (url.searchParams.has('sse')) { - return await sseHandler() + return sseHandler(roomId) } const room = getRoom(roomId) diff --git a/src/styles.css b/src/styles.css index ed2d90f..97ec932 100644 --- a/src/styles.css +++ b/src/styles.css @@ -452,6 +452,13 @@ button { border-right: 2px solid #333; border-bottom: 2px solid #333; + &.jolly { + /* background: oklch(from gold 1 0.15 h); */ + + color: oklch(from gold 0.2 0.2 h); + background: linear-gradient(-45deg, oklch(from gold 0.9 0.2 h), oklch(from gold 1 0.05 h)); + } + &:first-child { border-left: 2px solid #333; }