291 lines
12 KiB
TypeScript
291 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Avatar from "./Avatar";
|
||
|
||
interface LeaderboardEntry {
|
||
rank: number;
|
||
username: string;
|
||
email: string;
|
||
score: number;
|
||
level: number;
|
||
avatar?: string | null;
|
||
bio?: string | null;
|
||
characterClass?: string | null;
|
||
}
|
||
|
||
interface LeaderboardSectionProps {
|
||
leaderboard: LeaderboardEntry[];
|
||
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,
|
||
backgroundImage,
|
||
}: LeaderboardSectionProps) {
|
||
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
|
||
null
|
||
);
|
||
|
||
return (
|
||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center pt-24 pb-16">
|
||
{/* Background Image */}
|
||
<div
|
||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||
style={{
|
||
backgroundImage: `url('${backgroundImage}')`,
|
||
}}
|
||
>
|
||
{/* Dark overlay for readability */}
|
||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
|
||
{/* Title Section */}
|
||
<div className="text-center mb-12 overflow-hidden">
|
||
<h1 className="text-3xl sm:text-4xl md:text-7xl font-gaming font-black mb-4 tracking-tight break-words">
|
||
<span
|
||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||
style={{
|
||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||
}}
|
||
>
|
||
LEADERBOARD
|
||
</span>
|
||
</h1>
|
||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||
<span>✦</span>
|
||
<span>Top Players</span>
|
||
<span>✦</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 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 truncate ${
|
||
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
|
||
}`}
|
||
>
|
||
{entry.username}
|
||
</span>
|
||
{entry.characterClass && (
|
||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||
[{entry.characterClass === "WARRIOR" && "⚔️"}
|
||
{entry.characterClass === "MAGE" && "🔮"}
|
||
{entry.characterClass === "ROGUE" && "🗡️"}
|
||
{entry.characterClass === "RANGER" && "🏹"}
|
||
{entry.characterClass === "PALADIN" && "🛡️"}
|
||
{entry.characterClass === "ENGINEER" && "⚙️"}
|
||
{entry.characterClass === "MERCHANT" && "💰"}
|
||
{entry.characterClass === "SCHOLAR" && "📚"}
|
||
{entry.characterClass === "BERSERKER" && "🔥"}
|
||
{entry.characterClass === "NECROMANCER" && "💀"}]
|
||
</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>
|
||
|
||
{/* 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>
|
||
</div>
|
||
|
||
{/* Character Modal */}
|
||
{selectedEntry && (
|
||
<div
|
||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||
onClick={() => setSelectedEntry(null)}
|
||
>
|
||
<div
|
||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<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>
|
||
<button
|
||
onClick={() => setSelectedEntry(null)}
|
||
className="text-gray-400 hover:text-pixel-gold text-2xl font-bold transition"
|
||
>
|
||
×
|
||
</button>
|
||
</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">
|
||
{selectedEntry.characterClass === "WARRIOR" && "⚔️"}
|
||
{selectedEntry.characterClass === "MAGE" && "🔮"}
|
||
{selectedEntry.characterClass === "ROGUE" && "🗡️"}
|
||
{selectedEntry.characterClass === "RANGER" && "🏹"}
|
||
{selectedEntry.characterClass === "PALADIN" && "🛡️"}
|
||
{selectedEntry.characterClass === "ENGINEER" && "⚙️"}
|
||
{selectedEntry.characterClass === "MERCHANT" && "💰"}
|
||
{selectedEntry.characterClass === "SCHOLAR" && "📚"}
|
||
{selectedEntry.characterClass === "BERSERKER" && "🔥"}
|
||
{selectedEntry.characterClass === "NECROMANCER" && "💀"}
|
||
</span>
|
||
<span className="text-lg font-bold text-pixel-gold uppercase tracking-wider">
|
||
{selectedEntry.characterClass === "WARRIOR" &&
|
||
"Guerrier"}
|
||
{selectedEntry.characterClass === "MAGE" && "Mage"}
|
||
{selectedEntry.characterClass === "ROGUE" && "Voleur"}
|
||
{selectedEntry.characterClass === "RANGER" && "Rôdeur"}
|
||
{selectedEntry.characterClass === "PALADIN" &&
|
||
"Paladin"}
|
||
{selectedEntry.characterClass === "ENGINEER" &&
|
||
"Ingénieur"}
|
||
{selectedEntry.characterClass === "MERCHANT" &&
|
||
"Marchand"}
|
||
{selectedEntry.characterClass === "SCHOLAR" && "Érudit"}
|
||
{selectedEntry.characterClass === "BERSERKER" &&
|
||
"Berserker"}
|
||
{selectedEntry.characterClass === "NECROMANCER" &&
|
||
"Nécromancien"}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||
<div className="bg-black/60 border border-pixel-gold/30 rounded 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>
|
||
</div>
|
||
<div className="bg-black/60 border border-pixel-gold/30 rounded 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>
|
||
</div>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|