Files
got-gaming/components/leaderboard/LeaderboardSection.tsx

456 lines
17 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Avatar,
Modal,
CloseButton,
Card,
BackgroundSection,
SectionTitle,
} from "@/components/ui";
import {
getCharacterClassIcon,
getCharacterClassName,
type CharacterClass,
} from "@/lib/character-classes";
interface LeaderboardEntry {
rank: number;
username: string;
email: string;
score: number;
level: number;
avatar?: string | null;
bio?: string | null;
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;
}
// Format number with consistent locale to avoid hydration mismatch
const formatScore = (score: number): string => {
return score.toLocaleString("en-US");
};
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}>
{/* Title Section */}
<SectionTitle
variant="gradient"
size="xl"
subtitle="Top Players"
className="mb-16"
>
LEADERBOARD
</SectionTitle>
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
Consultez le classement des meilleurs joueurs et des maisons les plus
performantes. Montez dans les rangs en participant aux événements et en
relevant des défis
</p>
{/* 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">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Player</div>
<div className="col-span-3 text-right">Score</div>
<div className="col-span-2 text-right">Level</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{leaderboard.map((entry) => (
<div
key={entry.rank}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative ${
entry.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
>
{/* 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 ${
entry.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: entry.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: entry.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{entry.rank}
</span>
</div>
{/* Player */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<Avatar
src={entry.avatar}
username={entry.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:opacity-80 transition min-w-0"
onClick={() => setSelectedEntry(entry)}
>
<span
className={`font-bold text-xs sm:text-sm break-words ${
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{entry.username}
</span>
{entry.characterClass && (
<span className="text-xs text-gray-400 uppercase tracking-wider">
[{getCharacterClassIcon(entry.characterClass)}]
</span>
)}
{entry.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(entry.score)}
</span>
</div>
{/* Level */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
Lv.{entry.level}
</span>
</div>
</div>
))}
</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">
Compete with players worldwide and climb the ranks!
</p>
<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
isOpen={!!selectedEntry}
onClose={() => setSelectedEntry(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">
{selectedEntry.username}
</h2>
<CloseButton onClick={() => setSelectedEntry(null)} size="md" />
</div>
{/* Avatar and Class */}
<div className="flex items-center gap-4 sm:gap-6 mb-6">
<Avatar
src={selectedEntry.avatar}
username={selectedEntry.username}
size="lg"
className="flex-shrink-0"
borderClassName="border-2 sm:border-4 border-pixel-gold/50"
/>
<div>
<div className="text-xs text-gray-400 uppercase tracking-widest mb-2">
Rank #{selectedEntry.rank}
</div>
<div className="text-sm text-gray-300 mb-2">
{selectedEntry.email}
</div>
{selectedEntry.characterClass && (
<div className="flex items-center gap-2">
<span className="text-2xl">
{getCharacterClassIcon(selectedEntry.characterClass)}
</span>
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
{getCharacterClassName(selectedEntry.characterClass)}
</span>
</div>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-6">
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedEntry.score)}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Niveau
</div>
<div className="text-2xl font-bold text-pixel-gold">
Lv.{selectedEntry.level}
</div>
</Card>
</div>
{/* Bio */}
{selectedEntry.bio && (
<div className="border-t border-pixel-gold/30 pt-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
Bio
</div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedEntry.bio}
</p>
</div>
)}
</div>
</Modal>
)}
</BackgroundSection>
);
}