more features
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"astro": "^4.16.13",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"nanoid": "^5.0.8",
|
||||
"preact": "^10.24.3",
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sendJSON } from '@/client/utils'
|
||||
import type { Room } from '@/db/model'
|
||||
import type { Action, ActionAnswer, ActionJolly } from '@/ggwp'
|
||||
import { format } from 'date-fns'
|
||||
import { type Dispatch, type StateUpdater } from 'preact/hooks'
|
||||
|
||||
type ActionCardProps = {
|
||||
@@ -40,35 +41,61 @@ const CardActions = ({ moveUp, moveDown, remove }: ActionCardProps) => (
|
||||
|
||||
const ActionCardAnswer = ({ action, ...props }: { action: ActionAnswer } & ActionCardProps) => {
|
||||
return (
|
||||
<div class="card tint-blue stack-h center">
|
||||
<div class="chip">Risposta</div>
|
||||
<div>
|
||||
<strong>Team:</strong> <code>{action.team}</code>
|
||||
<div class="card tint-blue stack-v start">
|
||||
<div class="stack-h center">
|
||||
<div class="chip">Risposta</div>
|
||||
<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>
|
||||
<strong>Domanda:</strong> <code>{action.question}</code>
|
||||
<div class="stack-h fill-w center">
|
||||
<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>
|
||||
<strong>Risultato:</strong> <code>{action.outcome}</code>
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<CardActions {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionCardJolly = ({ action, ...props }: { action: ActionJolly } & ActionCardProps) => {
|
||||
return (
|
||||
<div class="card tint-blue stack-h center">
|
||||
<div class="chip">Jolly</div>
|
||||
<div>
|
||||
<strong>Team:</strong> {action.team}
|
||||
<div class="card tint-blue stack-v start">
|
||||
<div class="stack-h center">
|
||||
<div class="chip">Jolly</div>
|
||||
<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>
|
||||
<strong>Problem:</strong> {action.group}
|
||||
<div class="stack-h fill-w center">
|
||||
<div>
|
||||
<strong>Team:</strong> {action.team}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Problem:</strong> {action.group}
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<CardActions {...props} />
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<CardActions {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ import { computeScoreboardState } from '@/ggwp'
|
||||
import { Leaderboard } from './Leaderboard'
|
||||
|
||||
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 [room, setRoom] = useState<Room | null>(null)
|
||||
@@ -35,6 +44,28 @@ export const AdminPage = ({ roomId }: { roomId: string }) => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<Leaderboard
|
||||
questions={room.questions}
|
||||
|
||||
@@ -13,15 +13,12 @@ const handleCreateRoom = async (id: string, teams: string[], questions: Question
|
||||
|
||||
const { password } = value
|
||||
|
||||
if (status === 'ok') {
|
||||
alert(`Gara creata con successo!\n\nPassword Admin: ${password}\n(non la perdere)`)
|
||||
return
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
alert(value)
|
||||
return
|
||||
}
|
||||
|
||||
location.href = `/${id}/admin?password=${password}`
|
||||
}
|
||||
|
||||
export const NewRoom = ({}) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const SubmitActionAnswer = ({
|
||||
sendAction: (action: Action) => void
|
||||
onTeamQuestionIndex?: Receiver<{ team: string; question: string }>
|
||||
}) => {
|
||||
const [answer, setAnswer] = useState<ActionAnswer>({
|
||||
const [answer, setAnswer] = useState<Omit<ActionAnswer, 'timestamp'>>({
|
||||
question: '',
|
||||
team: '',
|
||||
outcome: 'correct',
|
||||
@@ -71,7 +71,17 @@ export const SubmitActionAnswer = ({
|
||||
<option value="wrong">Sbagliata</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => sendAction({ type: 'answer', ...answer })}>Invia risposta</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
sendAction({
|
||||
...answer,
|
||||
type: 'answer',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
>
|
||||
Invia risposta
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -99,7 +109,7 @@ export const SubmitActionJolly = ({
|
||||
questionToGroup[question.id] = question.group
|
||||
})
|
||||
|
||||
const [answer, setAnswer] = useState<ActionJolly>({
|
||||
const [answer, setAnswer] = useState<Omit<ActionJolly, 'timestamp'>>({
|
||||
team: '',
|
||||
group: '',
|
||||
})
|
||||
@@ -143,7 +153,17 @@ export const SubmitActionJolly = ({
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button onClick={() => sendAction({ type: 'jolly', ...answer })}>Imposta jolly</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
sendAction({
|
||||
...answer,
|
||||
type: 'jolly',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
>
|
||||
Imposta jolly
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+12
-1
@@ -47,17 +47,27 @@ function getTimeBonus(index: number): number {
|
||||
}
|
||||
|
||||
export type ActionAnswer = {
|
||||
timestamp: string
|
||||
|
||||
team: string
|
||||
question: string
|
||||
outcome: 'correct' | 'partial' | 'wrong'
|
||||
}
|
||||
|
||||
export type ActionJolly = {
|
||||
timestamp: string
|
||||
|
||||
team: 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 {
|
||||
// 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: {
|
||||
timestamp: string
|
||||
type: 'answer'
|
||||
team: string
|
||||
question: string
|
||||
|
||||
+35
-2
@@ -105,15 +105,32 @@ a {
|
||||
display: inline-block;
|
||||
align-content: center;
|
||||
|
||||
height: 1.75rem;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
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);
|
||||
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 {
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -157,6 +174,14 @@ a {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
&.gap-sm {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&.start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -175,6 +200,14 @@ a {
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
|
||||
&.gap-sm {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&.start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user