diff --git a/src/components/ActionRegistry.tsx b/src/components/ActionRegistry.tsx index f1e97a8..73c0d74 100644 --- a/src/components/ActionRegistry.tsx +++ b/src/components/ActionRegistry.tsx @@ -1,39 +1,79 @@ -import { requestJSON } from '@/client/utils' -import type { RoomData } from '@/db/model' -import type { ActionAnswer, ActionJolly } from '@/ggwp' -import { useEffect, useState } from 'preact/hooks' +import { requestJSON, sendJSON } from '@/client/utils' +import type { Room, RoomData } from '@/db/model' +import type { Action, ActionAnswer, ActionJolly } from '@/ggwp' +import { useEffect, useState, type Dispatch, type StateUpdater } from 'preact/hooks' + +type ActionCardProps = { + moveUp: () => void + moveDown: () => void + + remove: () => void + + // editing: boolean + // edit: () => void + // doneEdit: (newAction: Action) => void +} type Props = { - roomId: string + room: Room + refreshRoom: () => void } -const ActionCardAnswer = ({ action }: { action: ActionAnswer }) => { +const CardActions = ({ moveUp, moveDown, remove }: ActionCardProps) => ( + <> + + + {/* */} + + +) + +const ActionCardAnswer = ({ action, ...props }: { action: ActionAnswer } & ActionCardProps) => { return (
- {action.question} -
{action.team}
-
{action.outcome}
+
Risposta
+
+ Team: {action.team} +
+
+ Domanda: {action.question} +
+
+ Risultato: {action.outcome} +
+
+
) } -const ActionCardJolly = ({ action }: { action: ActionJolly }) => { +const ActionCardJolly = ({ action, ...props }: { action: ActionJolly } & ActionCardProps) => { return (
- {action.team} -
{action.groupId}
+
Jolly
+
+ Team: {action.team} +
+
+ Problem: {action.group} +
+
+
) } -export const ActionRegistry = ({ roomId }: Props) => { - const [room, setRoom] = useState(null) - useEffect(() => { - requestJSON(`/api/room/${roomId}`).then(room => { - setRoom(room) - }) - }, []) - +export const ActionRegistry = ({ room, refreshRoom }: Props) => { if (!room) { return
Loading...
} @@ -42,13 +82,84 @@ export const ActionRegistry = ({ roomId }: Props) => { return

Ancora nessuna azione

} + // const [editing, setEditing] = useState(null) + + const setActions: Dispatch> = async actions => { + const newActions = typeof actions === 'function' ? actions(room.actions) : actions + + await sendJSON(`/api/room/${room.id}`, { ...room, actions: newActions }) + refreshRoom() + } + + const moveActionUp = (index: number) => { + if (index === 0) return + + setActions(actions => { + const newActions = [...actions] + const [action] = newActions.splice(index, 1) + newActions.splice(index - 1, 0, action) + return newActions + }) + } + + const moveActionDown = (index: number) => { + if (index === room.actions.length - 1) return + + setActions(actions => { + const newActions = [...actions] + const [action] = newActions.splice(index, 1) + newActions.splice(index + 1, 0, action) + return newActions + }) + } + + // const editAction = (index: number) => { + // setEditing(index) + // } + + // const doneEdit = (newAction: Action) => { + // if (editing === null) return + + // setActions(actions => { + // const newActions = [...actions] + // newActions[editing] = newAction + // return newActions + // }) + + // setEditing(null) + // } + + const removeAction = (index: number) => { + setActions(actions => { + const newActions = [...actions] + newActions.splice(index, 1) + return newActions + }) + } + return ( <> - {room.actions.map(action => + {room.actions.map((action, index) => action.type === 'answer' ? ( - + moveActionUp(index)} + moveDown={() => moveActionDown(index)} + remove={() => removeAction(index)} + // editing={editing === index} + // edit={() => editAction(index)} + // doneEdit={doneEdit} + /> ) : action.type === 'jolly' ? ( - + moveActionUp(index)} + moveDown={() => moveActionDown(index)} + remove={() => removeAction(index)} + // editing={editing === index} + // edit={() => editAction(index)} + // doneEdit={doneEdit} + /> ) : (
Unknown action type: {JSON.stringify(action)} diff --git a/src/components/AdminPage.tsx b/src/components/AdminPage.tsx index 5d0e6d2..aa17001 100644 --- a/src/components/AdminPage.tsx +++ b/src/components/AdminPage.tsx @@ -1,27 +1,59 @@ import { LiveLeaderboard } from '@/components/LiveLeaderboard' import { SubmitAction } from '@/components/SubmitAction' import { ActionRegistry } from '@/components/ActionRegistry' -import { useRef, useState } from 'preact/hooks' +import { useEffect, useRef, useState } from 'preact/hooks' +import type { Room } from '@/db/model' +import { requestJSON } from '@/client/utils' +import { computeScoreboardState } from '@/ggwp' +import { Leaderboard } from './Leaderboard' -export const AdminPage = ({ room }: { room: string }) => { +export const AdminPage = ({ roomId }: { roomId: string }) => { const listeners = useRef void>>({}) + const [room, setRoom] = useState(null) + const fetchRoom = async () => { + await requestJSON(`/api/room/${roomId}`).then(room => { + setRoom({ id: roomId, ...room }) + }) + } + + useEffect(() => { + fetchRoom() + }, []) + + if (!room) { + return
Loading...
+ } + + const scoreboard = computeScoreboardState( + { + teamIds: room.teams, + questions: room.questions, + }, + room.actions + ) + return ( <>

Admin

-

{room}

- {room.id} + Object.values(listeners.current).forEach(listener => listener(teamQuestion)) } />  

Azioni

- (listeners.current[key] = listener)} /> + (listeners.current[key] = listener)} + />  

Registro Azioni

- + ) } diff --git a/src/components/LiveLeaderboard.tsx b/src/components/LiveLeaderboard.tsx index b5862b1..f652c82 100644 --- a/src/components/LiveLeaderboard.tsx +++ b/src/components/LiveLeaderboard.tsx @@ -1,35 +1,17 @@ -import type { RoomData } from '@/db/model' +import type { Room } from '@/db/model' import { Leaderboard } from './Leaderboard' -import { useEffect, useState } from 'preact/hooks' -import { requestJSON, sendJSON } from '@/client/utils' +import { type Dispatch, type StateUpdater } from 'preact/hooks' import { computeScoreboardState } from '@/ggwp' type Props = { - roomId: string + room: Room + setRoom: Dispatch> + selectTeamQuestion?: ({ team, question }: { team: string; question: string }) => void } -export const LiveLeaderboard = ({ roomId, selectTeamQuestion }: Props) => { - const [room, setRoom] = useState(null) - useEffect(() => { - requestJSON(`/api/room/${roomId}`).then(room => { - setRoom(room) - }) - }, []) - - console.log(room) - +export const LiveLeaderboard = ({ room, selectTeamQuestion }: Props) => { if (!room) { return
Loading...
} - - const scoreboard = computeScoreboardState( - { - teamIds: room.teams, - questions: room.questions, - }, - room.actions - ) - - return } diff --git a/src/components/SubmitAction.tsx b/src/components/SubmitAction.tsx index 3b880d4..79cf19f 100644 --- a/src/components/SubmitAction.tsx +++ b/src/components/SubmitAction.tsx @@ -1,20 +1,17 @@ -import { requestJSON } from '@/client/utils' -import type { RoomData } from '@/db/model' +import { requestJSON, sendJSON } from '@/client/utils' +import type { Room, RoomData } from '@/db/model' import { type Action, type ActionAnswer, type ActionJolly } from '@/ggwp' import { useEffect, useState } from 'preact/hooks' type Outcome = 'correct' | 'partial' | 'wrong' -const handleSendAnswer = (action: Action) => { - // TODO: send action to server - console.log(action) -} - export const SubmitActionAnswer = ({ room, + sendAction, onTeamQuestionIndex, }: { room: RoomData + sendAction: (action: Action) => void onTeamQuestionIndex?: Receiver<{ team: string; question: string }> }) => { const [answer, setAnswer] = useState({ @@ -31,6 +28,7 @@ export const SubmitActionAnswer = ({ return (

Invia Risposta

+ setAnswer({ ...answer, team: e.currentTarget.value })} - /> */} - - + +
) } @@ -98,9 +80,11 @@ type Receiver = (key: string, cb: (data: T) => void) => void export const SubmitActionJolly = ({ room, + sendAction, onTeamQuestionIndex, }: { room: RoomData + sendAction: (action: Action) => void onTeamQuestionIndex?: Receiver<{ team: string; question: string }> }) => { const groupsMap: Record = {} @@ -117,12 +101,12 @@ export const SubmitActionJolly = ({ const [answer, setAnswer] = useState({ team: '', - groupId: '', + group: '', }) onTeamQuestionIndex?.('SubmitActionJolly', ({ team, question }) => { console.log('onTeamQuestionIndex', team, question) - setAnswer(answer => ({ ...answer, team, groupId: questionToGroup[question] })) + setAnswer(answer => ({ ...answer, team, group: questionToGroup[question] })) }) return ( @@ -148,8 +132,8 @@ export const SubmitActionJolly = ({ - +
) } export const SubmitAction = ({ - roomId, + room, + refreshRoom, + onTeamQuestionIndex, }: { - roomId: string + room: Room + refreshRoom: () => void onTeamQuestionIndex?: Receiver<{ team: string; question: string }> }) => { - const [room, setRoom] = useState(null) - useEffect(() => { - requestJSON(`/api/room/${roomId}`).then(room => { - setRoom(room) - }) - }, []) + const sendAction = async (action: Action) => { + await sendJSON(`/api/room/${room.id}/action`, action) + refreshRoom() + } if (!room) { return
Loading...
@@ -193,8 +169,8 @@ export const SubmitAction = ({ return ( <> - - + + ) } diff --git a/src/db/index.ts b/src/db/index.ts index 126fd34..b73ebe5 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -53,6 +53,16 @@ export function createRoom(id: string, teams: string[], questions: Question[]): return password } +export function updateRoom(id: string, data: RoomData): void { + db.prepare<[string, string]>( + ` + UPDATE rooms + SET data = ? + WHERE id = ?; + ` + ).run(JSON.stringify(data), id) +} + export function getRoom(id: string): RoomData | null { const row = db .prepare< diff --git a/src/ggwp/index.ts b/src/ggwp/index.ts index 185aa9e..04dc026 100644 --- a/src/ggwp/index.ts +++ b/src/ggwp/index.ts @@ -51,7 +51,7 @@ export type ActionAnswer = { export type ActionJolly = { team: string - groupId: string + group: string } export type Action = ({ type: 'answer' } & ActionAnswer) | ({ type: 'jolly' } & ActionJolly) @@ -134,7 +134,7 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb // dict from team to jolly question group const teamsJollyGroup = Object.fromEntries( - actions.filter(action => action.type === 'jolly').map(action => [action.team, action.groupId]) + actions.filter(action => action.type === 'jolly').map(action => [action.team, action.group]) ) for (const team of game.teamIds) { diff --git a/src/pages/[room]/admin.astro b/src/pages/[room]/admin.astro index dd594d8..82cd0b1 100644 --- a/src/pages/[room]/admin.astro +++ b/src/pages/[room]/admin.astro @@ -35,5 +35,5 @@ console.log('listeners', listeners) --- - + diff --git a/src/pages/api/room/[id]/action.ts b/src/pages/api/room/[id]/action.ts new file mode 100644 index 0000000..fd88290 --- /dev/null +++ b/src/pages/api/room/[id]/action.ts @@ -0,0 +1,24 @@ +import { getRoom, updateRoom } from '@/db' +import type { Action } from '@/ggwp' +import type { APIRoute } from 'astro' + +export const POST: APIRoute = async ({ params, request }) => { + const { id: roomId } = params + if (!roomId) { + return new Response('Invalid room id', { status: 400 }) + } + + const room = getRoom(roomId) + if (!room) { + return new Response('Room not found', { status: 404 }) + } + + const action = (await request.json()) as Action + room.actions.push(action) + + updateRoom(roomId, room) + + return new Response(JSON.stringify('ok'), { + headers: { 'content-type': 'application/json' }, + }) +} diff --git a/src/pages/api/room/[id].ts b/src/pages/api/room/[id]/index.ts similarity index 54% rename from src/pages/api/room/[id].ts rename to src/pages/api/room/[id]/index.ts index e25e28b..302b516 100644 --- a/src/pages/api/room/[id].ts +++ b/src/pages/api/room/[id]/index.ts @@ -1,4 +1,5 @@ -import { getRoom } from '@/db' +import { getRoom, updateRoom } from '@/db' +import type { RoomData } from '@/db/model' import type { APIRoute } from 'astro' async function sseHandler() { @@ -27,3 +28,26 @@ export const GET: APIRoute = async ({ params, url }) => { headers: { 'content-type': 'application/json' }, }) } + +export const POST: APIRoute = async ({ params, request }) => { + const { id: roomId } = params + if (!roomId) { + return new Response('Invalid room id', { status: 400 }) + } + + const room = getRoom(roomId) + if (!room) { + return new Response('Room not found', { status: 404 }) + } + + const newRoom = (await request.json()) as RoomData + + // @ts-ignore + delete newRoom.id + + updateRoom(roomId, newRoom) + + return new Response(JSON.stringify('ok'), { + headers: { 'content-type': 'application/json' }, + }) +} diff --git a/src/styles.css b/src/styles.css index aaa8323..ed2d90f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -101,6 +101,19 @@ a { text-decoration: underline 2px solid; } +.chip { + display: inline-block; + align-content: center; + + height: 1.75rem; + padding: 0 0.5rem; + border-radius: 0.25rem; + + color: oklch(from var(--tint) 0.15 0.1 h); + background: oklch(from var(--tint) calc(l + 0.3) c h); + box-shadow: 2px 2px 0 oklch(from var(--tint) calc(l - 0.2) 0.1 h); +} + .text-large { font-size: 20px; } @@ -108,7 +121,7 @@ a { /* Components */ .card { - padding: 1rem; + padding: 0.75rem; border: 2px solid #333; border-radius: 0.5rem; @@ -116,7 +129,7 @@ a { background: linear-gradient(-15deg, var(--tint), oklch(from var(--tint) calc(l + 0.25) c h)); box-shadow: 3px 3px 0 oklch(from var(--tint) l 0.1 h); - min-width: 50ch; + min-width: 80ch; @media screen and (max-width: 768px) { min-width: 100%;