Add house leaderboard feature: Integrate house leaderboard functionality in LeaderboardPage and LeaderboardSection components. Update userStatsService to fetch house leaderboard data, and enhance UI to display house rankings, scores, and member details. Update Prisma schema to include house-related models and relationships, and seed database with initial house data.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled

This commit is contained in:
Julien Froidefond
2025-12-17 13:35:18 +01:00
parent cb02b494f4
commit 85ee812ab1
36 changed files with 5422 additions and 13 deletions

View File

@@ -26,8 +26,29 @@ interface LeaderboardEntry {
characterClass?: CharacterClass | null;
}
interface HouseMember {
id: string;
username: string;
avatar: string | null;
score: number;
level: number;
role: string;
}
interface HouseLeaderboardEntry {
rank: number;
houseId: string;
houseName: string;
totalScore: number;
memberCount: number;
averageScore: number;
description: string | null;
members: HouseMember[];
}
interface LeaderboardSectionProps {
leaderboard: LeaderboardEntry[];
houseLeaderboard: HouseLeaderboardEntry[];
backgroundImage: string;
}
@@ -38,11 +59,15 @@ const formatScore = (score: number): string => {
export default function LeaderboardSection({
leaderboard,
houseLeaderboard,
backgroundImage,
}: LeaderboardSectionProps) {
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
null
);
const [selectedHouse, setSelectedHouse] = useState<HouseLeaderboardEntry | null>(
null
);
return (
<BackgroundSection backgroundImage={backgroundImage}>
@@ -56,7 +81,7 @@ export default function LeaderboardSection({
LEADERBOARD
</SectionTitle>
{/* Leaderboard Table */}
{/* Players Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
@@ -143,6 +168,90 @@ export default function LeaderboardSection({
</div>
</div>
{/* House Leaderboard Table */}
<div className="mt-12">
<SectionTitle
variant="gradient"
size="md"
subtitle="Top Houses"
className="mb-8 overflow-hidden"
>
MAISONS
</SectionTitle>
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Maison</div>
<div className="col-span-3 text-right">Score Total</div>
<div className="col-span-2 text-right">Membres</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{houseLeaderboard.map((house) => (
<div
key={house.houseId}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative cursor-pointer ${
house.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
onClick={() => setSelectedHouse(house)}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
house.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: house.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: house.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{house.rank}
</span>
</div>
{/* House Name */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<div className="flex items-center gap-1 sm:gap-2 min-w-0">
<span
className={`font-bold text-xs sm:text-sm break-words ${
house.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{house.houseName}
</span>
{house.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Total Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(house.totalScore)}
</span>
</div>
{/* Member Count */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
{house.memberCount}
</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Footer Info */}
<div className="mt-8 text-center">
<p className="text-gray-500 text-sm">
@@ -151,6 +260,112 @@ export default function LeaderboardSection({
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
</div>
{/* House Modal */}
{selectedHouse && (
<Modal
isOpen={!!selectedHouse}
onClose={() => setSelectedHouse(null)}
size="md"
>
<div className="p-4 sm:p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
{selectedHouse.houseName}
</h2>
<CloseButton onClick={() => setSelectedHouse(null)} size="md" />
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Rank
</div>
<div className="text-2xl font-bold text-pixel-gold">
#{selectedHouse.rank}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score Total
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedHouse.totalScore)}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Membres
</div>
<div className="text-2xl font-bold text-pixel-gold">
{selectedHouse.memberCount}
</div>
</Card>
</div>
{/* Members List */}
<div className="border-t border-pixel-gold/30 pt-6 mb-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-4 font-bold">
Membres ({selectedHouse.memberCount})
</div>
<div className="space-y-2">
{selectedHouse.members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between p-3 rounded"
style={{ backgroundColor: "var(--card-hover)" }}
>
<div className="flex items-center gap-3">
<Avatar
src={member.avatar}
username={member.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold text-sm" style={{ color: "var(--foreground)" }}>
{member.username}
</span>
<span className="text-xs uppercase" style={{ color: "var(--accent)" }}>
{member.role}
</span>
</div>
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
Niveau {member.level}
</div>
</div>
</div>
<div className="text-right">
<div className="font-mono text-sm font-bold" style={{ color: "var(--foreground)" }}>
{formatScore(member.score)}
</div>
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
points
</div>
</div>
</div>
))}
</div>
</div>
{/* Description */}
{selectedHouse.description && (
<div className="border-t border-pixel-gold/30 pt-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
Description
</div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedHouse.description}
</p>
</div>
)}
</div>
</Modal>
)}
{/* Character Modal */}
{selectedEntry && (
<Modal