even more changes
This commit is contained in:
@@ -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) => (
|
||||
<>
|
||||
<button class="square" onClick={moveUp}>
|
||||
<div class="icon">arrow_upward</div>
|
||||
</button>
|
||||
<button class="square" onClick={moveDown}>
|
||||
<div class="icon">arrow_downward</div>
|
||||
</button>
|
||||
{/* <button class="square"
|
||||
onClick={edit}
|
||||
>
|
||||
<div class="icon">edit</div>
|
||||
</button> */}
|
||||
<button class="square" onClick={remove}>
|
||||
<div class="icon">delete</div>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
const ActionCardAnswer = ({ action, ...props }: { action: ActionAnswer } & ActionCardProps) => {
|
||||
return (
|
||||
<div class="card tint-blue stack-h center">
|
||||
<strong>{action.question}</strong>
|
||||
<div>{action.team}</div>
|
||||
<div>{action.outcome}</div>
|
||||
<div class="chip">Risposta</div>
|
||||
<div>
|
||||
<strong>Team:</strong> <code>{action.team}</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Domanda:</strong> <code>{action.question}</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Risultato:</strong> <code>{action.outcome}</code>
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<CardActions {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionCardJolly = ({ action }: { action: ActionJolly }) => {
|
||||
const ActionCardJolly = ({ action, ...props }: { action: ActionJolly } & ActionCardProps) => {
|
||||
return (
|
||||
<div class="card tint-blue stack-h center">
|
||||
<strong>{action.team}</strong>
|
||||
<div>{action.groupId}</div>
|
||||
<div class="chip">Jolly</div>
|
||||
<div>
|
||||
<strong>Team:</strong> {action.team}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Problem:</strong> {action.group}
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<CardActions {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ActionRegistry = ({ roomId }: Props) => {
|
||||
const [room, setRoom] = useState<RoomData | null>(null)
|
||||
useEffect(() => {
|
||||
requestJSON(`/api/room/${roomId}`).then(room => {
|
||||
setRoom(room)
|
||||
})
|
||||
}, [])
|
||||
|
||||
export const ActionRegistry = ({ room, refreshRoom }: Props) => {
|
||||
if (!room) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
@@ -42,13 +82,84 @@ export const ActionRegistry = ({ roomId }: Props) => {
|
||||
return <p>Ancora nessuna azione</p>
|
||||
}
|
||||
|
||||
// const [editing, setEditing] = useState<number | null>(null)
|
||||
|
||||
const setActions: Dispatch<StateUpdater<Action[]>> = 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' ? (
|
||||
<ActionCardAnswer action={action} />
|
||||
<ActionCardAnswer
|
||||
action={action}
|
||||
moveUp={() => moveActionUp(index)}
|
||||
moveDown={() => moveActionDown(index)}
|
||||
remove={() => removeAction(index)}
|
||||
// editing={editing === index}
|
||||
// edit={() => editAction(index)}
|
||||
// doneEdit={doneEdit}
|
||||
/>
|
||||
) : action.type === 'jolly' ? (
|
||||
<ActionCardJolly action={action} />
|
||||
<ActionCardJolly
|
||||
action={action}
|
||||
moveUp={() => moveActionUp(index)}
|
||||
moveDown={() => moveActionDown(index)}
|
||||
remove={() => removeAction(index)}
|
||||
// editing={editing === index}
|
||||
// edit={() => editAction(index)}
|
||||
// doneEdit={doneEdit}
|
||||
/>
|
||||
) : (
|
||||
<div class="card tint-red">
|
||||
Unknown action type: <code>{JSON.stringify(action)}</code>
|
||||
|
||||
@@ -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<Record<string, (teamQuestion: { team: string; question: string }) => void>>({})
|
||||
|
||||
const [room, setRoom] = useState<Room | null>(null)
|
||||
const fetchRoom = async () => {
|
||||
await requestJSON(`/api/room/${roomId}`).then(room => {
|
||||
setRoom({ id: roomId, ...room })
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoom()
|
||||
}, [])
|
||||
|
||||
if (!room) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const scoreboard = computeScoreboardState(
|
||||
{
|
||||
teamIds: room.teams,
|
||||
questions: room.questions,
|
||||
},
|
||||
room.actions
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Admin</h1>
|
||||
<h2>{room}</h2>
|
||||
<LiveLeaderboard
|
||||
roomId={room}
|
||||
<h2>{room.id}</h2>
|
||||
<Leaderboard
|
||||
questions={room.questions}
|
||||
scoreboard={scoreboard}
|
||||
selectTeamQuestion={teamQuestion =>
|
||||
Object.values(listeners.current).forEach(listener => listener(teamQuestion))
|
||||
}
|
||||
/>
|
||||
|
||||
<h2>Azioni</h2>
|
||||
<SubmitAction roomId={room} onTeamQuestionIndex={(key, listener) => (listeners.current[key] = listener)} />
|
||||
<SubmitAction
|
||||
room={room}
|
||||
refreshRoom={fetchRoom}
|
||||
onTeamQuestionIndex={(key, listener) => (listeners.current[key] = listener)}
|
||||
/>
|
||||
|
||||
<h2>Registro Azioni</h2>
|
||||
<ActionRegistry roomId={room} />
|
||||
<ActionRegistry room={room} refreshRoom={fetchRoom} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<StateUpdater<Room>>
|
||||
|
||||
selectTeamQuestion?: ({ team, question }: { team: string; question: string }) => void
|
||||
}
|
||||
|
||||
export const LiveLeaderboard = ({ roomId, selectTeamQuestion }: Props) => {
|
||||
const [room, setRoom] = useState<RoomData | null>(null)
|
||||
useEffect(() => {
|
||||
requestJSON(`/api/room/${roomId}`).then(room => {
|
||||
setRoom(room)
|
||||
})
|
||||
}, [])
|
||||
|
||||
console.log(room)
|
||||
|
||||
export const LiveLeaderboard = ({ room, selectTeamQuestion }: Props) => {
|
||||
if (!room) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const scoreboard = computeScoreboardState(
|
||||
{
|
||||
teamIds: room.teams,
|
||||
questions: room.questions,
|
||||
},
|
||||
room.actions
|
||||
)
|
||||
|
||||
return <Leaderboard questions={room.questions} scoreboard={scoreboard} selectTeamQuestion={selectTeamQuestion} />
|
||||
}
|
||||
|
||||
@@ -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<ActionAnswer>({
|
||||
@@ -31,6 +28,7 @@ export const SubmitActionAnswer = ({
|
||||
return (
|
||||
<div class="card tint-red stack-v center">
|
||||
<h3>Invia Risposta</h3>
|
||||
|
||||
<select
|
||||
class="fill-w"
|
||||
placeholder="Domanda..."
|
||||
@@ -47,14 +45,6 @@ export const SubmitActionAnswer = ({
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* <input
|
||||
type="text"
|
||||
class="fill-w"
|
||||
placeholder="Squadra..."
|
||||
value={answer.team}
|
||||
onInput={e => setAnswer({ ...answer, team: e.currentTarget.value })}
|
||||
/> */}
|
||||
|
||||
<select
|
||||
class="fill-w"
|
||||
placeholder="Squadra..."
|
||||
@@ -80,16 +70,8 @@ export const SubmitActionAnswer = ({
|
||||
<option value="partial">Parziale</option>
|
||||
<option value="wrong">Sbagliata</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSendAnswer({
|
||||
type: 'answer',
|
||||
...answer,
|
||||
})
|
||||
}
|
||||
>
|
||||
Invia risposta
|
||||
</button>
|
||||
|
||||
<button onClick={() => sendAction({ type: 'answer', ...answer })}>Invia risposta</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -98,9 +80,11 @@ type Receiver<T> = (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<string, boolean> = {}
|
||||
@@ -117,12 +101,12 @@ export const SubmitActionJolly = ({
|
||||
|
||||
const [answer, setAnswer] = useState<ActionJolly>({
|
||||
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 = ({
|
||||
<select
|
||||
class="fill-w"
|
||||
placeholder="Gruppo..."
|
||||
value={answer.groupId}
|
||||
onInput={e => setAnswer({ ...answer, groupId: e.currentTarget.value })}
|
||||
value={answer.group}
|
||||
onInput={e => setAnswer({ ...answer, group: e.currentTarget.value })}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Seleziona gruppo...
|
||||
@@ -159,33 +143,25 @@ export const SubmitActionJolly = ({
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSendAnswer({
|
||||
type: 'jolly',
|
||||
...answer,
|
||||
})
|
||||
}
|
||||
>
|
||||
Imposta jolly
|
||||
</button>
|
||||
<button onClick={() => sendAction({ type: 'jolly', ...answer })}>Imposta jolly</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SubmitAction = ({
|
||||
roomId,
|
||||
room,
|
||||
refreshRoom,
|
||||
|
||||
onTeamQuestionIndex,
|
||||
}: {
|
||||
roomId: string
|
||||
room: Room
|
||||
refreshRoom: () => void
|
||||
onTeamQuestionIndex?: Receiver<{ team: string; question: string }>
|
||||
}) => {
|
||||
const [room, setRoom] = useState<RoomData | null>(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 <div>Loading...</div>
|
||||
@@ -193,8 +169,8 @@ export const SubmitAction = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubmitActionAnswer room={room} onTeamQuestionIndex={onTeamQuestionIndex} />
|
||||
<SubmitActionJolly room={room} onTeamQuestionIndex={onTeamQuestionIndex} />
|
||||
<SubmitActionAnswer room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
|
||||
<SubmitActionJolly room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
@@ -35,5 +35,5 @@ console.log('listeners', listeners)
|
||||
---
|
||||
|
||||
<Base>
|
||||
<AdminPage client:load room={room} />
|
||||
<AdminPage client:load roomId={room} />
|
||||
</Base>
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
}
|
||||
@@ -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' },
|
||||
})
|
||||
}
|
||||
+15
-2
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user