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;
}