even more changes
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"astro": "^4.16.13",
|
"astro": "^4.16.13",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"nanoid": "^5.0.8",
|
"nanoid": "^5.0.8",
|
||||||
"preact": "^10.24.3",
|
"preact": "^10.24.3",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Question, Scoreboard } from '@/ggwp'
|
import type { Question, Scoreboard } from '@/ggwp'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
questions: Question[]
|
questions: Question[]
|
||||||
@@ -12,7 +13,7 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props
|
|||||||
<div
|
<div
|
||||||
class="leaderboard"
|
class="leaderboard"
|
||||||
style={{
|
style={{
|
||||||
'--team-count': scoreboard.length,
|
'--team-count': scoreboard.board.length,
|
||||||
'--question-count': questions.length,
|
'--question-count': questions.length,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -24,7 +25,7 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="team-answers-container">
|
<div class="team-answers-container">
|
||||||
{scoreboard.map(({ team, totalScore, questionScores }, i) => (
|
{scoreboard.board.map(({ team, totalScore, questionScores }, i) => (
|
||||||
<div class="row" key={i}>
|
<div class="row" key={i}>
|
||||||
<div class="team">
|
<div class="team">
|
||||||
<div class="name">{team}</div>
|
<div class="name">{team}</div>
|
||||||
@@ -33,11 +34,16 @@ export const Leaderboard = ({ questions, scoreboard, selectTeamQuestion }: Props
|
|||||||
<div class="answers">
|
<div class="answers">
|
||||||
{questions.map((question, j) => (
|
{questions.map((question, j) => (
|
||||||
<div
|
<div
|
||||||
class="answer"
|
class={clsx(
|
||||||
|
'answer',
|
||||||
|
question.group === scoreboard.teamJollyGroup[team] && 'jolly'
|
||||||
|
)}
|
||||||
key={j}
|
key={j}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('emitting', { team, question: question.id })
|
if (selectTeamQuestion) {
|
||||||
return selectTeamQuestion?.({ team, question: question.id })
|
console.log('emitting', { team, question: question.id })
|
||||||
|
selectTeamQuestion({ team, question: question.id })
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{questionScores[question.id]}
|
{questionScores[question.id]}
|
||||||
|
|||||||
@@ -1,17 +1,54 @@
|
|||||||
|
import { requestJSON } from '@/client/utils'
|
||||||
import type { Room } from '@/db/model'
|
import type { Room } from '@/db/model'
|
||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { Leaderboard } from './Leaderboard'
|
import { Leaderboard } from './Leaderboard'
|
||||||
import { type Dispatch, type StateUpdater } from 'preact/hooks'
|
|
||||||
import { computeScoreboardState } from '@/ggwp'
|
import { computeScoreboardState } from '@/ggwp'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room
|
roomId: string
|
||||||
setRoom: Dispatch<StateUpdater<Room>>
|
|
||||||
|
|
||||||
selectTeamQuestion?: ({ team, question }: { team: string; question: string }) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LiveLeaderboard = ({ room, selectTeamQuestion }: Props) => {
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
es.close()
|
||||||
|
}
|
||||||
|
}, [roomId])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LiveLeaderboard = ({ roomId }: Props) => {
|
||||||
|
const [room, setRoom] = useState<Room | null>(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) {
|
if (!room) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scoreboard = computeScoreboardState(
|
||||||
|
{
|
||||||
|
teamIds: room.teams,
|
||||||
|
questions: room.questions,
|
||||||
|
},
|
||||||
|
room.actions
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Leaderboard questions={room.questions} scoreboard={scoreboard} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import cuid2 from '@paralleldrive/cuid2'
|
|||||||
import Database from 'better-sqlite3'
|
import Database from 'better-sqlite3'
|
||||||
import type { Room, RoomData } from './model'
|
import type { Room, RoomData } from './model'
|
||||||
import type { Question } from '@/ggwp'
|
import type { Question } from '@/ggwp'
|
||||||
|
import { emitRoomUpdate } from './events'
|
||||||
|
|
||||||
const db = new Database('ggwp.db')
|
const db = new Database('ggwp.db')
|
||||||
db.pragma('journal_mode = WAL')
|
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 {
|
export function updateRoom(id: string, data: RoomData): void {
|
||||||
|
emitRoomUpdate(id, data)
|
||||||
|
|
||||||
db.prepare<[string, string]>(
|
db.prepare<[string, string]>(
|
||||||
`
|
`
|
||||||
UPDATE rooms
|
UPDATE rooms
|
||||||
|
|||||||
+13
-9
@@ -12,10 +12,13 @@ export type Game = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Scoreboard = {
|
export type Scoreboard = {
|
||||||
team: string
|
teamJollyGroup: { [team: string]: string }
|
||||||
totalScore: number
|
board: {
|
||||||
questionScores: { [id: string]: number }
|
team: string
|
||||||
}[]
|
totalScore: number
|
||||||
|
questionScores: { [id: string]: number }
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
function getCorrectnessMultiplier(outcome: 'correct' | 'partial' | 'wrong'): number {
|
function getCorrectnessMultiplier(outcome: 'correct' | 'partial' | 'wrong'): number {
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
@@ -101,8 +104,6 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(actionsByQuestion)
|
|
||||||
|
|
||||||
const scoreboardByTeam: {
|
const scoreboardByTeam: {
|
||||||
[team: string]: {
|
[team: string]: {
|
||||||
totalScore: number
|
totalScore: number
|
||||||
@@ -163,17 +164,20 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb
|
|||||||
scoreboardByTeam[team].bonusJolly = teamBonus
|
scoreboardByTeam[team].bonusJolly = teamBonus
|
||||||
}
|
}
|
||||||
|
|
||||||
const scoreboard: Scoreboard = []
|
const scoreboard: Scoreboard = {
|
||||||
|
teamJollyGroup: teamsJollyGroup,
|
||||||
|
board: [],
|
||||||
|
}
|
||||||
|
|
||||||
for (const team of game.teamIds) {
|
for (const team of game.teamIds) {
|
||||||
scoreboard.push({
|
scoreboard.board.push({
|
||||||
team,
|
team,
|
||||||
totalScore: scoreboardByTeam[team].totalScore,
|
totalScore: scoreboardByTeam[team].totalScore,
|
||||||
questionScores: scoreboardByTeam[team].questionScores,
|
questionScores: scoreboardByTeam[team].questionScores,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
scoreboard.sort((a, b) => b.totalScore - a.totalScore)
|
scoreboard.board.sort((a, b) => b.totalScore - a.totalScore)
|
||||||
|
|
||||||
return scoreboard
|
return scoreboard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,35 @@
|
|||||||
import { getRoom, updateRoom } from '@/db'
|
import { getRoom, updateRoom } from '@/db'
|
||||||
|
import { addRoomUpdateListener, removeRoomUpdateListener } from '@/db/events'
|
||||||
import type { RoomData } from '@/db/model'
|
import type { RoomData } from '@/db/model'
|
||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
async function sseHandler() {
|
function sseHandler(roomId: string) {
|
||||||
// https://github.com/MicroWebStacks/astro-examples/blob/main/03_sse-counter/src/pages/api/stream.js
|
let events_listener: () => void
|
||||||
return new Response('SSE not implemented', { status: 501 })
|
|
||||||
|
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 }) => {
|
export const GET: APIRoute = async ({ params, url }) => {
|
||||||
@@ -14,7 +39,7 @@ export const GET: APIRoute = async ({ params, url }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.searchParams.has('sse')) {
|
if (url.searchParams.has('sse')) {
|
||||||
return await sseHandler()
|
return sseHandler(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = getRoom(roomId)
|
const room = getRoom(roomId)
|
||||||
|
|||||||
@@ -452,6 +452,13 @@ button {
|
|||||||
border-right: 2px solid #333;
|
border-right: 2px solid #333;
|
||||||
border-bottom: 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 {
|
&:first-child {
|
||||||
border-left: 2px solid #333;
|
border-left: 2px solid #333;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user