123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
interface LeaderboardEntry {
|
|
rank: number;
|
|
username: string;
|
|
score: number;
|
|
level: number;
|
|
avatar?: string | null;
|
|
bio?: string | null;
|
|
}
|
|
|
|
// Format number with consistent locale to avoid hydration mismatch
|
|
const formatScore = (score: number): string => {
|
|
return score.toLocaleString("en-US");
|
|
};
|
|
|
|
export default function Leaderboard() {
|
|
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetch("/api/leaderboard")
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
setLeaderboard(data);
|
|
setLoading(false);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error fetching leaderboard:", err);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<section className="w-full bg-black py-16 px-6 border-t-2 border-pixel-dark-purple">
|
|
<div className="max-w-4xl mx-auto text-center text-pixel-gold">
|
|
Chargement...
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
return (
|
|
<section className="w-full bg-black py-16 px-6 border-t-2 border-pixel-dark-purple">
|
|
<div className="max-w-4xl mx-auto">
|
|
<h2 className="text-4xl md:text-5xl font-bold text-center mb-12 text-pixel-gold text-pixel">
|
|
LEADERBOARD
|
|
</h2>
|
|
|
|
<div className="bg-black/80 border-2 border-pixel-gold/30 rounded-lg backdrop-blur-sm">
|
|
{/* Header */}
|
|
<div className="bg-gray-900/80 border-b-2 border-pixel-gold/30 grid grid-cols-12 gap-4 p-4 font-bold text-sm text-gray-300">
|
|
<div className="col-span-1 text-center">Rank</div>
|
|
<div className="col-span-5">Player</div>
|
|
<div className="col-span-3 text-right">Score</div>
|
|
<div className="col-span-3 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-4 p-4 hover:bg-gray-900/50 transition relative ${
|
|
entry.rank <= 3 ? "bg-gray-950/50" : "bg-black/40"
|
|
}`}
|
|
>
|
|
<div className="col-span-1 text-center">
|
|
<span
|
|
className={`inline-block w-8 h-8 rounded-full flex items-center justify-center font-bold ${
|
|
entry.rank === 1
|
|
? "bg-pixel-gold text-black"
|
|
: entry.rank === 2
|
|
? "bg-gray-600 text-white"
|
|
: entry.rank === 3
|
|
? "bg-orange-800 text-white"
|
|
: "bg-gray-900 text-gray-400 border border-gray-800"
|
|
}`}
|
|
>
|
|
{entry.rank}
|
|
</span>
|
|
</div>
|
|
<div className="col-span-5 flex items-center relative group">
|
|
<span className="font-bold text-pixel-gold cursor-pointer relative z-10">
|
|
{entry.username}
|
|
</span>
|
|
{entry.bio && (
|
|
<div className="absolute left-0 top-full mt-1 z-[100] w-72 p-4 bg-black border-2 border-pixel-gold/70 rounded-lg shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none">
|
|
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-2 border-b border-pixel-gold/30 pb-1 font-bold">
|
|
Bio
|
|
</div>
|
|
<p className="text-sm text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
|
|
{entry.bio}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="col-span-3 text-right flex items-center justify-end">
|
|
<span className="font-mono text-gray-300">
|
|
{formatScore(entry.score)}
|
|
</span>
|
|
</div>
|
|
<div className="col-span-3 text-right flex items-center justify-end">
|
|
<span className="font-bold text-gray-400">
|
|
Lv.{entry.level}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Additional info */}
|
|
<div className="mt-8 text-center text-sm text-gray-500">
|
|
<p>Compete with players worldwide and climb the ranks!</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|