Stats : records (meilleur score par jeu, selon le sens du jeu)
Build and Publish Docker Image / build-and-push-image (push) Successful in 58s
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:
@@ -3,10 +3,13 @@ import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { db } from "../../database/db";
|
||||
import { useProfileStore } from "../../stores/profileStore";
|
||||
import { useAllGames } from "../../games";
|
||||
import { calculatePlayerTotalScore } from "../../utils/scoring";
|
||||
import { GameConfig } from "../../types";
|
||||
import { Card, CardContent } from "../../components/ui/card";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
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 { motion } from "framer-motion";
|
||||
|
||||
@@ -200,6 +203,37 @@ export default function Statistics() {
|
||||
? leaderboard.filter((stat) => selectedPlayerIds.includes(stat.id))
|
||||
: 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 (
|
||||
<div className="p-5 space-y-5 relative z-10">
|
||||
<PageHeader title="Stats" />
|
||||
@@ -427,6 +461,65 @@ export default function Statistics() {
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user