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' import { type Dispatch, type StateUpdater } from 'preact/hooks'
type ActionCardProps = { type ActionCardProps = {
moveUp: () => void // moveUp: () => void
moveDown: () => void // moveDown: () => void
remove: () => void remove: () => void
@ -20,14 +20,17 @@ type Props = {
refreshRoom: () => void 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> <div class="icon">arrow_upward</div>
</button> </button>
<button class="square" onClick={moveDown}> <button class="square" onClick={moveDown}>
<div class="icon">arrow_downward</div> <div class="icon">arrow_downward</div>
</button> </button> */}
{/* <button class="square" {/* <button class="square"
onClick={edit} onClick={edit}
> >
@ -118,27 +121,27 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
refreshRoom() refreshRoom()
} }
const moveActionUp = (index: number) => { // const moveActionUp = (index: number) => {
if (index === 0) return // if (index === 0) return
setActions(actions => { // setActions(actions => {
const newActions = [...actions] // const newActions = [...actions]
const [action] = newActions.splice(index, 1) // const [action] = newActions.splice(index, 1)
newActions.splice(index - 1, 0, action) // newActions.splice(index - 1, 0, action)
return newActions // return newActions
}) // })
} // }
const moveActionDown = (index: number) => { // const moveActionDown = (index: number) => {
if (index === room.actions.length - 1) return // if (index === room.actions.length - 1) return
setActions(actions => { // setActions(actions => {
const newActions = [...actions] // const newActions = [...actions]
const [action] = newActions.splice(index, 1) // const [action] = newActions.splice(index, 1)
newActions.splice(index + 1, 0, action) // newActions.splice(index + 1, 0, action)
return newActions // return newActions
}) // })
} // }
// const editAction = (index: number) => { // const editAction = (index: number) => {
// setEditing(index) // setEditing(index)
@ -170,8 +173,8 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
action.type === 'answer' ? ( action.type === 'answer' ? (
<ActionCardAnswer <ActionCardAnswer
action={action} action={action}
moveUp={() => moveActionUp(index)} // moveUp={() => moveActionUp(index)}
moveDown={() => moveActionDown(index)} // moveDown={() => moveActionDown(index)}
remove={() => removeAction(index)} remove={() => removeAction(index)}
// editing={editing === index} // editing={editing === index}
// edit={() => editAction(index)} // edit={() => editAction(index)}
@ -180,8 +183,8 @@ export const ActionRegistry = ({ room, refreshRoom }: Props) => {
) : action.type === 'jolly' ? ( ) : action.type === 'jolly' ? (
<ActionCardJolly <ActionCardJolly
action={action} action={action}
moveUp={() => moveActionUp(index)} // moveUp={() => moveActionUp(index)}
moveDown={() => moveActionDown(index)} // moveDown={() => moveActionDown(index)}
remove={() => removeAction(index)} remove={() => removeAction(index)}
// editing={editing === index} // editing={editing === index}
// edit={() => editAction(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 { Leaderboard } from './Leaderboard'
import { computeScoreboardState } from '@/ggwp' import { computeScoreboardState } from '@/ggwp'
import clsx from 'clsx' import clsx from 'clsx'
import { formatDate } from 'date-fns'
import { Clock } from './Clock'
type Props = { type Props = {
roomId: string roomId: string
@ -51,50 +53,62 @@ export const LiveLeaderboard = ({ roomId }: Props) => {
room.actions room.actions
) )
const [autoscroll, setAutoscroll] = useState<false | 'up' | 'down'>(false) const [page, setPage] = useState(0)
const timerRef = useRef<Timer | null>(null) const timerRef = useRef<Timer | null>(null)
useEffect(() => { useEffect(() => {
const PIXELS_PER_MILLISECOND = [1, 1000 / 30] // e.g. [1, 10] means 1px every 10ms
if (autoscroll !== false) {
timerRef.current = setInterval(() => { timerRef.current = setInterval(() => {
console.log('scrolling') setPage(page => (page + 1) % 3)
}, 5000)
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 () => { return () => {
if (timerRef.current) { if (timerRef.current) {
clearInterval(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 ( return (
<> <>
<Leaderboard questions={room.questions} scoreboard={scoreboard} /> <h3>
<Clock /> &mdash; {room.id} (pag. {page + 1}/3)
<div class="position-fixed bottom right"> </h3>
<button <Leaderboard
class={clsx('square', autoscroll !== false && 'active')} questions={room.questions}
onClick={() => setAutoscroll(v => (v === false ? 'down' : false))} scoreboard={{
> teamJollyGroup: scoreboard.teamJollyGroup,
<div class="icon">swap_vert</div> board: scoreboard.board.slice(page * 20, (page + 1) * 20),
</button> }}
</div> />
</> </>
) )
} }

@ -53,12 +53,22 @@ export const NewRoom = ({}) => {
<button <button
class="square" class="square"
onClick={() => { 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 if (!teamsSyntax) return
setTeams(teams => { 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 { sendJSON } from '@/client/utils'
import type { Room, RoomData } from '@/db/model' import type { Room, RoomData } from '@/db/model'
import { type Action, type ActionAnswer, type ActionJolly } from '@/ggwp' import { type Action, type ActionAnswer, type ActionJolly } from '@/ggwp'
import { formatDate, parse } from 'date-fns'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { Clock } from './Clock'
type Outcome = 'correct' | 'partial' | 'wrong' type Outcome = 'correct' | 'partial' | 'wrong'
@ -27,7 +29,9 @@ export const SubmitActionAnswer = ({
return ( return (
<div class="card tint-green stack-v center"> <div class="card tint-green stack-v center">
<h3>Invia Risposta</h3> <h3>
Invia Risposta Immediata (<Clock />)
</h3>
<select <select
class="fill-w" class="fill-w"
@ -71,6 +75,7 @@ export const SubmitActionAnswer = ({
<option value="wrong">Sbagliata</option> <option value="wrong">Sbagliata</option>
</select> </select>
<div class="stack-h">
<button <button
onClick={() => onClick={() =>
sendAction({ sendAction({
@ -83,6 +88,110 @@ export const SubmitActionAnswer = ({
Invia risposta Invia risposta
</button> </button>
</div> </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 })}
>
<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 ( return (
<> <>
<SubmitActionAnswer room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} /> <SubmitActionAnswer room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
<SubmitActionAnswerAtTime room={room} sendAction={sendAction} onTeamQuestionIndex={onTeamQuestionIndex} />
<SubmitActionJolly 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 const action = (await request.json()) as Action
room.actions.push(action) room.actions.push(action)
room.actions.sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
updateRoom(roomId, room) updateRoom(roomId, room)
return new Response(JSON.stringify('ok'), { return new Response(JSON.stringify('ok'), {

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

Loading…
Cancel
Save