feature/round-recap #11

Merged
ayoub merged 4 commits from feature/round-recap into dev 2026-06-10 23:14:59 +00:00
Showing only changes of commit 976ebf0799 - Show all commits

View file

@ -147,6 +147,29 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
} }
}, []) }, [])
const [copied, setCopied] = useState(false)
async function copyResults() {
const lines = ["NerdWare — Résultats", "", "🏆 Classement"]
ranked.forEach((s, i) =>
lines.push(`${i + 1}. ${nameOf(s.playerId)}${s.score}`)
)
if (gameRecap && gameRecap.length > 0) {
lines.push("", "Manches")
gameRecap.forEach((r) =>
lines.push(
`${r.index + 1}. ${r.type === "blindtest" ? "🎧" : "🧠"} ${r.answer}`
)
)
}
try {
await navigator.clipboard.writeText(lines.join("\n"))
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
// presse-papier indisponible : on ignore
}
}
const scores: PlayerScore[] = finalScores ?? snapshot.scores const scores: PlayerScore[] = finalScores ?? snapshot.scores
const ranked = [...scores].sort((a, b) => b.score - a.score) const ranked = [...scores].sort((a, b) => b.score - a.score)
const nameOf = (id: string) => const nameOf = (id: string) =>
@ -227,6 +250,10 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
</ol> </ol>
)} )}
{gameRecap && gameRecap.length > 0 && (
<FunStats recap={gameRecap} snapshot={snapshot} />
)}
{gameTracks && gameTracks.length > 0 && ( {gameTracks && gameTracks.length > 0 && (
<TracksRecap tracks={gameTracks} /> <TracksRecap tracks={gameTracks} />
)} )}
@ -236,6 +263,10 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Button variant="secondary" className="w-full" onClick={copyResults}>
{copied ? <Check className="text-green-500" /> : <ClipboardList />}
{copied ? "Copié !" : "Copier les résultats"}
</Button>
{isHost ? ( {isHost ? (
<Button className="w-full" onClick={returnToLobby}> <Button className="w-full" onClick={returnToLobby}>
Rejouer Rejouer
@ -319,6 +350,106 @@ function TracksRecap({ tracks }: { tracks: BlindtestTrackInfo[] }) {
) )
} }
interface PlayerStat {
correct: number
answered: number
decoy: number
}
function FunStats({
recap,
snapshot,
}: {
recap: RoundRecap[]
snapshot: RoomSnapshot
}) {
const stats = new Map<string, PlayerStat>(
snapshot.players.map((p) => [p.id, { correct: 0, answered: 0, decoy: 0 }])
)
for (const r of recap) {
const answered = new Set(r.answers.map((a) => a.playerId))
for (const a of r.answers) {
const s = stats.get(a.playerId)
if (s) {
s.answered++
if (a.correct) s.correct++
}
}
if (r.type === "blindtest") {
for (const sc of r.scorers) {
if (!answered.has(sc.playerId)) {
const s = stats.get(sc.playerId)
if (s) s.decoy += sc.points
}
}
}
}
const players = snapshot.players
const total = recap.length
const nameOf = (id: string) => players.find((p) => p.id === id)?.name ?? "?"
const stat = (id: string) => stats.get(id) ?? { correct: 0, answered: 0, decoy: 0 }
const winners = (pick: (s: PlayerStat) => number, min = 1) => {
const max = Math.max(0, ...players.map((p) => pick(stat(p.id))))
return max >= min ? players.filter((p) => pick(stat(p.id)) === max) : []
}
const awards: { emoji: string; label: string; ids: string[] }[] = []
const perfect = players.filter(
(p) => total > 0 && stat(p.id).correct === total
)
if (perfect.length) {
awards.push({ emoji: "🎯", label: "Sans-faute", ids: perfect.map((p) => p.id) })
}
const brains = winners((s) => s.correct)
if (brains.length && brains.length < players.length) {
awards.push({ emoji: "🧠", label: "Le cerveau", ids: brains.map((p) => p.id) })
}
const decoy = winners((s) => s.decoy)
if (decoy.length) {
awards.push({
emoji: "🦹",
label: "Roi de la tromperie",
ids: decoy.map((p) => p.id),
})
}
const boulets = players.filter(
(p) => stat(p.id).answered > 0 && stat(p.id).correct === 0
)
if (boulets.length && total >= 3) {
awards.push({ emoji: "🪨", label: "Le boulet", ids: boulets.map((p) => p.id) })
}
if (awards.length === 0) {
return null
}
return (
<section className="flex flex-col gap-2">
<h3 className="text-muted-foreground text-sm font-medium">Récompenses</h3>
<div className="flex flex-wrap gap-2">
{awards.map((a) => (
<div
key={a.label}
className="bg-muted/40 flex flex-1 flex-col items-center gap-1 rounded-xl border px-3 py-2 text-center"
>
<span className="text-2xl">{a.emoji}</span>
<span className="text-[10px] font-medium uppercase">{a.label}</span>
<div className="flex flex-wrap items-center justify-center gap-1">
{a.ids.map((id) => (
<span key={id} className="flex items-center gap-1 text-xs">
<Avatar seed={nameOf(id)} className="size-5" />
{nameOf(id)}
</span>
))}
</div>
</div>
))}
</div>
</section>
)
}
function RoundsRecap({ function RoundsRecap({
recap, recap,
nameOf, nameOf,
@ -384,7 +515,11 @@ function RoundsRecap({
</li> </li>
) : ( ) : (
answers.map((a) => ( answers.map((a) => (
<li key={a.playerId} className="text-[10px]"> <li
key={a.playerId}
className="flex items-center gap-1 text-[10px]"
>
<Avatar seed={nameOf(a.playerId)} className="size-4" />
<span <span
className={ className={
a.playerId === playerId a.playerId === playerId
@ -393,7 +528,7 @@ function RoundsRecap({
} }
> >
{nameOf(a.playerId)} {nameOf(a.playerId)}
{a.playerId === playerId && " (toi)"} :{" "} {a.playerId === playerId && " (toi)"} :
</span> </span>
<span <span
className={ className={
@ -404,7 +539,6 @@ function RoundsRecap({
</span> </span>
{pointsBy.get(a.playerId) ? ( {pointsBy.get(a.playerId) ? (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{" "}
+{pointsBy.get(a.playerId)} +{pointsBy.get(a.playerId)}
</span> </span>
) : null} ) : null}