feat: pages and timed answers
continuous-integration/drone/push Build is passing Details

main
parent 3b3ec6df3e
commit e2a14fa340

@ -5,8 +5,8 @@ import { format } from 'date-fns'
import { type Dispatch, type StateUpdater } from 'preact/hooks'
type ActionCardProps = {
moveUp: () => void
moveDown: () => void
// moveUp: () => void
// moveDown: () => void
remove: () => void
@ -20,14 +20,17 @@ type Props = {
refreshRoom: () => void
}
const CardActions = ({ moveUp, moveDown, remove }: ActionCardProps) => (
const CardActions = ({
// moveUp, moveDown,
remove,
}: ActionCardProps) => (
<>
<button class="square" onClick={moveUp}>
{/* <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> */}
{/* <button class="square"
onClick={edit}
>
@ -118,27 +121,27 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
refreshRoom()
}
const moveActionUp = (index: number) => {
if (index === 0) return
// 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
})
}
// 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
// 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
})
}
// 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)
@ -170,8 +173,8 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
action.type === 'answer' ? (
<ActionCardAnswer
action={action}
moveUp={() => moveActionUp(index)}
moveDown={() => moveActionDown(index)}
// moveUp={() => moveActionUp(index)}
// moveDown={() => moveActionDown(index)}
remove={() => removeAction(index)}
// editing={editing === index}
// edit={() => editAction(index)}
@ -180,8 +183,8 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
) : action.type === 'jolly' ? (
<ActionCardJolly
action={action}
moveUp={() => moveActionUp(index)}
moveDown={() => moveActionDown(index)}
// moveUp={() => moveActionUp(index)}
// moveDown={() => moveActionDown(index)}
remove={() => removeAction(index)}
// editing={editing === index}
// edit={() => editAction(index)}

@ -0,0 +1,18 @@
import { formatDate } from 'date-fns'
import { useEffect, useState } from 'preact/hooks'
export const Clock = ({}) => {
const [time, setTime] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date())
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
return <>{formatDate(time, 'HH:mm:ss')}</>
}

@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'preact/hooks'
import { Leaderboard } from './Leaderboard'
import { computeScoreboardState } from '@/ggwp'
import clsx from 'clsx'
import { formatDate } from 'date-fns'
import { Clock } from './Clock'
type Props = {
roomId: string
@ -51,50 +53,62 @@ export const LiveLeaderboard = ({ roomId }: Props) => {
room.actions
)
const [autoscroll, setAutoscroll] = useState<false | 'up' | 'down'>(false)
const [page, setPage] = useState(0)
const timerRef = useRef<Timer | null>(null)
useEffect(() => {
const PIXELS_PER_MILLISECOND = [1, 1000 / 30] // e.g. [1, 10] means 1px every 10ms
if (autoscroll !== false) {
timerRef.current = setInterval(() => {
console.log('scrolling')
window.scrollBy({
top: autoscroll === 'down' ? PIXELS_PER_MILLISECOND[0] : -PIXELS_PER_MILLISECOND[0],
behavior: 'instant',
})
if (window.scrollY + window.innerHeight + 1 > document.body.scrollHeight) {
setAutoscroll('up')
}
if (window.scrollY === 0) {
setAutoscroll('down')
}
}, PIXELS_PER_MILLISECOND[1])
return () => {
if (timerRef.current) {
clearInterval(timerRef.current)
}
timerRef.current = setInterval(() => {
setPage(page => (page + 1) % 3)
}, 5000)
return () => {
if (timerRef.current) {
clearInterval(timerRef.current)
}
}
}, [autoscroll])
}, [])
// useEffect(() => {
// const PIXELS_PER_MILLISECOND = [1, 1000 / 30] // e.g. [1, 10] means 1px every 10ms
// if (autoscroll !== false) {
// timerRef.current = setInterval(() => {
// console.log('scrolling')
// window.scrollBy({
// top: autoscroll === 'down' ? PIXELS_PER_MILLISECOND[0] : -PIXELS_PER_MILLISECOND[0],
// behavior: 'instant',
// })
// if (window.scrollY + window.innerHeight + 1 > document.body.scrollHeight) {
// setAutoscroll('up')
// }
// if (window.scrollY === 0) {
// setAutoscroll('down')
// }
// }, PIXELS_PER_MILLISECOND[1])
// return () => {
// if (timerRef.current) {
// clearInterval(timerRef.current)
// }
// }
// }
// }, [autoscroll])
return (
<>
<Leaderboard questions={room.questions} scoreboard={scoreboard} />
<div class="position-fixed bottom right">
<button
class={clsx('square', autoscroll !== false && 'active')}
onClick={() => setAutoscroll(v => (v === false ? 'down' : false))}
>
<div class="icon">swap_vert</div>
</button>
</div>
<h3>
<Clock /> &mdash; {room.id} (pag. {page + 1}/3)
</h3>
<Leaderboard
questions={room.questions}
scoreboard={{
teamJollyGroup: scoreboard.teamJollyGroup,
board: scoreboard.board.slice(page * 20, (page + 1) * 20),
}}
/>
</>
)
}

@ -53,12 +53,22 @@ export const NewRoom = ({}) => {
<button
class="square"
onClick={() => {
const teamsSyntax = prompt(`Aggiungi squadre separate da virgola, e.g. "Team 1, Team 2"`)
const teamsSyntax = prompt(`Aggiungi squadre separate da virgola, e.g. "Team 1,Team 2,..."`)
if (!teamsSyntax) return
setTeams(teams => {
return [...teams, ...teamsSyntax.split(',')]
return [
...teams,
...teamsSyntax.split(',').flatMap(t => {
if (t.includes('*')) {
const [team, count] = t.split('*')
return Array.from({ length: Number(count) }, (_, i) => `${team}-${i + 1}`)
}
return [t]
}),
]
})
}}
>

@ -1,7 +1,9 @@
import { sendJSON } from '@/client/utils'
import type { Room, RoomData } from '@/db/model'
import { type Action, type ActionAnswer, type ActionJolly } from '@/ggwp'
import { formatDate, parse } from 'date-fns'
import { useState } from 'preact/hooks'
import { Clock } from './Clock'
type Outcome = 'correct' | 'partial' | 'wrong'
@ -27,7 +29,9 @@ export const SubmitActionAnswer = ({
return (
<div class="card tint-green stack-v center">
<h3>Invia Risposta</h3>
<h3>
Invia Risposta Immediata (<Clock />)
</h3>
<select
class="fill-w"
@ -71,17 +75,122 @@ export const SubmitActionAnswer = ({
<option value="wrong">Sbagliata</option>
</select>
<button
onClick={() =>
sendAction({
...answer,
type: 'answer',
timestamp: new Date().toISOString(),
})
}
<div class="stack-h">
<button
onClick={() =>
sendAction({
...answer,
type: 'answer',
timestamp: new Date().toISOString(),
})
}
>
Invia risposta
</button>
</div>
</div>
)
}
export const SubmitActionAnswerAtTime = ({
room,
sendAction,
onTeamQuestionIndex,
}: {
room: RoomData
sendAction: (action: Action) => void
onTeamQuestionIndex?: Receiver<{ team: string; question: string }>
}) => {
const [answer, setAnswer] = useState<ActionAnswer>({
timestamp: '',
question: '',
team: '',
outcome: 'correct',
})
onTeamQuestionIndex?.('SubmitActionAnswerAtTime', ({ team, question }) => {
console.log('onTeamQuestionIndex', team, question)
setAnswer(answer => ({ ...answer, team, question }))
})
return (
<div class="card tint-green stack-v center">
<h3>Invia Risposta al Tempo</h3>
<div class="stack-h fill-w">
<input
type="text"
class="fill"
placeholder={'Tempo in formato HH:mm:ss...'}
value={answer.timestamp}
onInput={e => setAnswer({ ...answer, timestamp: e.currentTarget.value })}
/>
<button onClick={() => setAnswer({ ...answer, timestamp: formatDate(new Date(), 'HH:mm:ss') })}>
Aggiorna Tempo
</button>
</div>
<select
class="fill-w"
placeholder="Domanda..."
value={answer.question}
onInput={e => setAnswer({ ...answer, question: e.currentTarget.value })}
>
Invia risposta
</button>
<option value="" disabled>
Seleziona domanda...
</option>
{room.questions.map((question, i) => (
<option value={question.id} key={i}>
{question.id}
</option>
))}
</select>
<select
class="fill-w"
placeholder="Squadra..."
value={answer.team}
onInput={e => setAnswer({ ...answer, team: e.currentTarget.value })}
>
<option value="" disabled>
Seleziona squadra...
</option>
{room.teams.map((team, i) => (
<option value={team} key={i}>
{team}
</option>
))}
</select>
<select
class="fill-w"
value={answer.outcome}
onInput={e => setAnswer({ ...answer, outcome: e.currentTarget.value as Outcome })}
>
<option value="correct">Corretta</option>
<option value="partial">Parziale</option>
<option value="wrong">Sbagliata</option>
</select>
<div class="stack-h">
<button
onClick={() => {
if (answer.timestamp.trim() === '') {
alert('Inserisci un tempo valido')
return
}
sendAction({
...answer,
type: 'answer',
timestamp: parse(answer.timestamp, 'HH:mm:ss', new Date()).toISOString(),
})
}}
>
Invia risposta
</button>
</div>
</div>
)
}
@ -190,6 +299,8 @@ export const SubmitAction = ({
return (
<>
<SubmitActionAnswer room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
<SubmitActionAnswerAtTime room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
<SubmitActionJolly room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
</>
)

@ -28,6 +28,10 @@ export const POST: APIRoute = async ({ params, request, cookies }) => {
const action = (await request.json()) as Action
room.actions.push(action)
room.actions.sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
updateRoom(roomId, room)
return new Response(JSON.stringify('ok'), {

@ -44,8 +44,8 @@ body {
justify-items: center;
align-content: start;
padding: 6rem 1rem 12rem;
gap: 3rem;
padding: 2rem 1rem 12rem;
gap: 2rem;
}
/* Typography */

Loading…
Cancel
Save