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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user