Stats : records (meilleur score par jeu, selon le sens du jeu)
Build and Publish Docker Image / build-and-push-image (push) Successful in 58s

Nouvelle section "Meilleurs scores" : par jeu, le meilleur score en une
partie et le joueur qui l'a réalisé — le plus haut pour les jeux positifs,
le plus bas pour les jeux négatifs (Skyjo, Cabo, Odin). Respecte les filtres.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zed
2026-07-31 00:28:52 +02:00
parent ab39a8e922
commit 0616866108
+94 -1
View File
@@ -3,10 +3,13 @@ import { useLiveQuery } from "dexie-react-hooks";
import { db } from "../../database/db"; import { db } from "../../database/db";
import { useProfileStore } from "../../stores/profileStore"; import { useProfileStore } from "../../stores/profileStore";
import { useAllGames } from "../../games"; import { useAllGames } from "../../games";
import { calculatePlayerTotalScore } from "../../utils/scoring";
import { GameConfig } from "../../types";
import { Card, CardContent } from "../../components/ui/card"; import { Card, CardContent } from "../../components/ui/card";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../components/ui/badge";
import { PageHeader } from "../../components/PageHeader"; import { PageHeader } from "../../components/PageHeader";
import { Trophy, Clock, Gamepad2, Medal, Filter } from "lucide-react"; import * as Icons from "lucide-react";
import { Trophy, Clock, Gamepad2, Medal, Filter, Crown } from "lucide-react";
import { Avatar } from "../../components/ui/avatar"; import { Avatar } from "../../components/ui/avatar";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -200,6 +203,37 @@ export default function Statistics() {
? leaderboard.filter((stat) => selectedPlayerIds.includes(stat.id)) ? leaderboard.filter((stat) => selectedPlayerIds.includes(stat.id))
: leaderboard; : leaderboard;
// 5. Records: best single-game score per game, according to the game's
// scoring direction (highest for "positive", lowest for "negative").
type GameRecord = {
game: GameConfig;
score: number;
name: string;
avatar?: string;
};
const records: GameRecord[] = [];
games.forEach((game) => {
const lowerIsBetter = game.scoreType === "negative";
let best: { score: number; name: string; avatar?: string } | undefined;
finishedGames
.filter((s) => s.gameId === game.id)
.forEach((session) => {
session.players.forEach((p) => {
const score = calculatePlayerTotalScore(
p.id,
session.rounds,
game,
true,
);
const better =
best === undefined ||
(lowerIsBetter ? score < best.score : score > best.score);
if (better) best = { score, name: p.name, avatar: p.avatar };
});
});
if (best) records.push({ game, ...best });
});
return ( return (
<div className="p-5 space-y-5 relative z-10"> <div className="p-5 space-y-5 relative z-10">
<PageHeader title="Stats" /> <PageHeader title="Stats" />
@@ -427,6 +461,65 @@ export default function Statistics() {
</div> </div>
)} )}
</section> </section>
{/* Records: highest score per game */}
{records.length > 0 && (
<section className="space-y-3">
<h2 className="text-lg font-black tracking-tight px-1 flex items-center">
<Crown className="w-5 h-5 mr-2 text-primary" />
Meilleurs scores
</h2>
<div className="rounded-[1.75rem] bg-card/70 backdrop-blur-xl border border-black/5 dark:border-white/10 shadow-sm divide-y divide-border/40">
{records.map((r, index) => {
const Icon =
(Icons as any)[r.game.icon || "Box"] || Icons.Box;
return (
<motion.div
key={r.game.id}
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="flex items-center gap-3 p-3"
>
<div
className="w-10 h-10 rounded-xl flex items-center justify-center overflow-hidden shrink-0 ring-1 ring-black/5 dark:ring-white/10"
style={{
backgroundColor: r.game.imagePath
? "transparent"
: r.game.colors.primary,
}}
>
{r.game.imagePath ? (
<img
src={r.game.imagePath}
alt={r.game.name}
className="w-full h-full object-cover"
/>
) : (
<Icon className="w-5 h-5 text-white" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="font-bold truncate leading-tight">
{r.game.name}
</p>
<p className="text-xs text-muted-foreground flex items-center gap-1.5 mt-0.5">
<Avatar src={r.avatar} name={r.name} size="sm" className="w-4 h-4" />
<span className="truncate">{r.name}</span>
</p>
</div>
<span
className="font-black text-lg tabular-nums shrink-0"
style={{ color: r.game.colors.primary }}
>
{r.score}
</span>
</motion.div>
);
})}
</div>
</section>
)}
</div> </div>
); );
} }