more features

main
parent eb000f79b6
commit df92c378e7

Binary file not shown.

@ -20,6 +20,7 @@
"astro": "^4.16.13", "astro": "^4.16.13",
"better-sqlite3": "^11.5.0", "better-sqlite3": "^11.5.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"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,6 +1,7 @@
import { sendJSON } from '@/client/utils' import { sendJSON } from '@/client/utils'
import type { Room } from '@/db/model' import type { Room } from '@/db/model'
import type { Action, ActionAnswer, ActionJolly } from '@/ggwp' import type { Action, ActionAnswer, ActionJolly } from '@/ggwp'
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 = {
@ -40,35 +41,61 @@ const CardActions = ({ moveUp, moveDown, remove }: ActionCardProps) => (
const ActionCardAnswer = ({ action, ...props }: { action: ActionAnswer } & ActionCardProps) => { const ActionCardAnswer = ({ action, ...props }: { action: ActionAnswer } & ActionCardProps) => {
return ( return (
<div class="card tint-blue stack-h center"> <div class="card tint-blue stack-v start">
<div class="chip">Risposta</div> <div class="stack-h center">
<div> <div class="chip">Risposta</div>
<strong>Team:</strong> <code>{action.team}</code> <div class="stack-h center gap-sm">
</div> <div class="icon">schedule</div>
<div> <div class="text dimmed">
<strong>Domanda:</strong> <code>{action.question}</code> {action.timestamp && format(new Date(action.timestamp), 'dd/MM/yyyy HH:mm:ss')}
<span class="text smaller">
{action.timestamp && ' ' + format(new Date(action.timestamp), 'SSS')}
</span>
</div>
</div>
</div> </div>
<div> <div class="stack-h fill-w center">
<strong>Risultato:</strong> <code>{action.outcome}</code> <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> </div>
<div class="fill"></div>
<CardActions {...props} />
</div> </div>
) )
} }
const ActionCardJolly = ({ action, ...props }: { action: ActionJolly } & ActionCardProps) => { const ActionCardJolly = ({ action, ...props }: { action: ActionJolly } & ActionCardProps) => {
return ( return (
<div class="card tint-blue stack-h center"> <div class="card tint-blue stack-v start">
<div class="chip">Jolly</div> <div class="stack-h center">
<div> <div class="chip">Jolly</div>
<strong>Team:</strong> {action.team} <div class="stack-h center gap-sm">
<div class="icon">schedule</div>
<div class="text dimmed">
{action.timestamp && format(new Date(action.timestamp), 'dd/MM/yyyy HH:mm:ss')}
<span class="text smaller">
{action.timestamp && ' ' + format(new Date(action.timestamp), 'SSS')}
</span>
</div>
</div>
</div> </div>
<div> <div class="stack-h fill-w center">
<strong>Problem:</strong> {action.group} <div>
<strong>Team:</strong> {action.team}
</div>
<div>
<strong>Problem:</strong> {action.group}
</div>
<div class="fill"></div>
<CardActions {...props} />
</div> </div>
<div class="fill"></div>
<CardActions {...props} />
</div> </div>
) )
} }

@ -7,6 +7,15 @@ import { computeScoreboardState } from '@/ggwp'
import { Leaderboard } from './Leaderboard' import { Leaderboard } from './Leaderboard'
export const AdminPage = ({ roomId }: { roomId: string }) => { export const AdminPage = ({ roomId }: { roomId: string }) => {
const [password, setPassword] = useState<string | null>(null)
useEffect(() => {
const url = new URL(location.href)
setPassword(url.searchParams.get('password'))
url.searchParams.delete('password')
history.replaceState({}, '', url.toString())
}, [])
const listeners = useRef<Record<string, (teamQuestion: { team: string; question: string }) => void>>({}) const listeners = useRef<Record<string, (teamQuestion: { team: string; question: string }) => void>>({})
const [room, setRoom] = useState<Room | null>(null) const [room, setRoom] = useState<Room | null>(null)
@ -35,6 +44,28 @@ export const AdminPage = ({ roomId }: { roomId: string }) => {
return ( return (
<> <>
<h1>Admin</h1> <h1>Admin</h1>
{password && (
<>
<h2>Password</h2>
<div class="card tint-orange stack-v center">
<h2>Password Pannello Admin</h2>
<p>
Non perdere questa password, serve per accedere a questo pannello da un altro pc (altrimeni
chiedi ad Antonio)
</p>
<code>{password}</code>
<button
onClick={() => {
setPassword(null)
}}
>
Chiudi
</button>
</div>
</>
)}
<h2>{room.id}</h2> <h2>{room.id}</h2>
<Leaderboard <Leaderboard
questions={room.questions} questions={room.questions}

@ -13,15 +13,12 @@ const handleCreateRoom = async (id: string, teams: string[], questions: Question
const { password } = value const { password } = value
if (status === 'ok') {
alert(`Gara creata con successo!\n\nPassword Admin: ${password}\n(non la perdere)`)
return
}
if (status === 'error') { if (status === 'error') {
alert(value) alert(value)
return return
} }
location.href = `/${id}/admin?password=${password}`
} }
export const NewRoom = ({}) => { export const NewRoom = ({}) => {

@ -14,7 +14,7 @@ export const SubmitActionAnswer = ({
sendAction: (action: Action) => void sendAction: (action: Action) => void
onTeamQuestionIndex?: Receiver<{ team: string; question: string }> onTeamQuestionIndex?: Receiver<{ team: string; question: string }>
}) => { }) => {
const [answer, setAnswer] = useState<ActionAnswer>({ const [answer, setAnswer] = useState<Omit<ActionAnswer, 'timestamp'>>({
question: '', question: '',
team: '', team: '',
outcome: 'correct', outcome: 'correct',
@ -71,7 +71,17 @@ export const SubmitActionAnswer = ({
<option value="wrong">Sbagliata</option> <option value="wrong">Sbagliata</option>
</select> </select>
<button onClick={() => sendAction({ type: 'answer', ...answer })}>Invia risposta</button> <button
onClick={() =>
sendAction({
...answer,
type: 'answer',
timestamp: new Date().toISOString(),
})
}
>
Invia risposta
</button>
</div> </div>
) )
} }
@ -99,7 +109,7 @@ export const SubmitActionJolly = ({
questionToGroup[question.id] = question.group questionToGroup[question.id] = question.group
}) })
const [answer, setAnswer] = useState<ActionJolly>({ const [answer, setAnswer] = useState<Omit<ActionJolly, 'timestamp'>>({
team: '', team: '',
group: '', group: '',
}) })
@ -143,7 +153,17 @@ export const SubmitActionJolly = ({
))} ))}
</select> </select>
<button onClick={() => sendAction({ type: 'jolly', ...answer })}>Imposta jolly</button> <button
onClick={() =>
sendAction({
...answer,
type: 'jolly',
timestamp: new Date().toISOString(),
})
}
>
Imposta jolly
</button>
</div> </div>
) )
} }

@ -47,17 +47,27 @@ function getTimeBonus(index: number): number {
} }
export type ActionAnswer = { export type ActionAnswer = {
timestamp: string
team: string team: string
question: string question: string
outcome: 'correct' | 'partial' | 'wrong' outcome: 'correct' | 'partial' | 'wrong'
} }
export type ActionJolly = { export type ActionJolly = {
timestamp: string
team: string team: string
group: string group: string
} }
export type Action = ({ type: 'answer' } & ActionAnswer) | ({ type: 'jolly' } & ActionJolly) export type Action =
| ({
type: 'answer'
} & ActionAnswer)
| ({
type: 'jolly'
} & ActionJolly)
export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreboard { export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreboard {
// filter out actions for the same question by the same team, keeping only the last one // filter out actions for the same question by the same team, keeping only the last one
@ -89,6 +99,7 @@ export function computeScoreboardState(game: Game, rawActions: Action[]): Scoreb
} }
const questionActions: { const questionActions: {
timestamp: string
type: 'answer' type: 'answer'
team: string team: string
question: string question: string

@ -105,15 +105,32 @@ a {
display: inline-block; display: inline-block;
align-content: center; align-content: center;
height: 1.75rem; font-size: 15px;
font-weight: 700;
height: 1.5rem;
padding: 0 0.5rem; padding: 0 0.5rem;
border-radius: 0.25rem; border-radius: 0.25rem;
color: oklch(from var(--tint) 0.15 0.1 h); color: oklch(from var(--tint) 0.3 0.1 h);
background: oklch(from var(--tint) calc(l + 0.3) c 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); box-shadow: 2px 2px 0 oklch(from var(--tint) calc(l - 0.2) 0.1 h);
} }
.text {
&.small {
font-size: 16px;
}
&.smaller {
font-size: 14px;
}
&.dimmed {
color: oklch(from var(--tint) 0.4 0.1 h);
}
}
.text-large { .text-large {
font-size: 20px; font-size: 20px;
} }
@ -157,6 +174,14 @@ a {
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
&.gap-sm {
gap: 0.25rem;
}
&.start {
align-items: start;
}
&.center { &.center {
align-items: center; align-items: center;
} }
@ -175,6 +200,14 @@ a {
flex-direction: row; flex-direction: row;
gap: 0.75rem; gap: 0.75rem;
&.gap-sm {
gap: 0.25rem;
}
&.start {
align-items: start;
}
&.center { &.center {
align-items: center; align-items: center;
} }

Loading…
Cancel
Save