- Countdown: under 10s it ramps into a "bomb" — white→red color and an escalating scale/wiggle pulse (faster & wider as it nears 0) via Framer Motion - RoomCode: click-to-copy the room code with visual feedback - PlayerCards: always-on score HUD during the game (cards per player, animated reorder, crown on the leader) so scores are tracked continuously instead of only appearing at reveal - quiz view: drop the reveal-only scoreboard (now covered by PlayerCards) - add framer-motion dependency Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import type {
|
|
QuizPerPlayerResult,
|
|
QuizQuestionPayload,
|
|
QuizRevealTruth,
|
|
RoomSnapshot,
|
|
} from "@nerdware/shared"
|
|
import { useRoomStore } from "@/store/room"
|
|
import { Countdown } from "@/components/countdown"
|
|
|
|
export function QuizView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|
const round = useRoomStore((s) => s.round)
|
|
const reveal = useRoomStore((s) => s.reveal)
|
|
const voteProgress = useRoomStore((s) => s.voteProgress)
|
|
const myChoiceIndex = useRoomStore((s) => s.myChoiceIndex)
|
|
const playerId = useRoomStore((s) => s.playerId)
|
|
const vote = useRoomStore((s) => s.vote)
|
|
|
|
if (!round) {
|
|
return (
|
|
<p className="text-muted-foreground text-sm">Préparation de la manche…</p>
|
|
)
|
|
}
|
|
|
|
const question = round.payload as QuizQuestionPayload
|
|
const truth = reveal ? (reveal.truth as QuizRevealTruth) : null
|
|
const showReveal = truth !== null
|
|
const myResult = reveal
|
|
? (reveal.perPlayerResult as QuizPerPlayerResult)[playerId ?? ""]
|
|
: undefined
|
|
|
|
function choiceClass(index: number): string {
|
|
const base =
|
|
"w-full rounded-lg border px-4 py-3 text-left text-sm transition-colors"
|
|
if (showReveal) {
|
|
if (index === truth?.correctIndex) {
|
|
return `${base} border-green-500 bg-green-500/15 font-medium`
|
|
}
|
|
if (index === myChoiceIndex) {
|
|
return `${base} border-red-500 bg-red-500/10`
|
|
}
|
|
return `${base} border-input opacity-60`
|
|
}
|
|
if (index === myChoiceIndex) {
|
|
return `${base} border-primary bg-primary/10 font-medium`
|
|
}
|
|
return `${base} border-input hover:bg-muted/60`
|
|
}
|
|
|
|
return (
|
|
<div className="flex w-full max-w-md flex-col gap-5">
|
|
<header className="flex items-center justify-between">
|
|
<span className="text-muted-foreground text-xs uppercase">
|
|
Question {snapshot.currentRound + 1} / {snapshot.totalRounds}
|
|
{question.category ? ` · ${question.category}` : ""}
|
|
</span>
|
|
{!showReveal && (
|
|
<Countdown key={round.endsAt} endsAt={round.endsAt} />
|
|
)}
|
|
</header>
|
|
|
|
<h2 className="font-heading text-xl font-semibold text-balance">
|
|
{question.prompt}
|
|
</h2>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
{question.choices.map((choice, index) => (
|
|
<button
|
|
key={index}
|
|
className={choiceClass(index)}
|
|
disabled={showReveal || myChoiceIndex !== null}
|
|
onClick={() => vote(index)}
|
|
>
|
|
{choice}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{!showReveal &&
|
|
(myChoiceIndex !== null ? (
|
|
<p className="text-muted-foreground text-center text-xs">
|
|
Réponse envoyée — en attente des autres
|
|
{voteProgress ? ` (${voteProgress.count}/${voteProgress.total})` : ""}
|
|
</p>
|
|
) : (
|
|
voteProgress && (
|
|
<p className="text-muted-foreground text-center text-xs">
|
|
{voteProgress.count}/{voteProgress.total} ont répondu
|
|
</p>
|
|
)
|
|
))}
|
|
|
|
{showReveal && (
|
|
<p
|
|
className={`text-center text-sm font-medium ${myResult?.correct ? "text-green-500" : "text-red-500"}`}
|
|
>
|
|
{myResult?.correct
|
|
? "Bonne réponse ! 🎉"
|
|
: myResult?.choiceIndex == null
|
|
? "Pas de réponse 😴"
|
|
: "Raté 💥"}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|