Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
251
components/leaderboard/Leaderboard.tsx
Normal file
251
components/leaderboard/Leaderboard.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
username: string;
|
||||
email: string;
|
||||
score: number;
|
||||
level: number;
|
||||
avatar?: string | null;
|
||||
bio?: string | null;
|
||||
characterClass?: 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);
|
||||
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
|
||||
null
|
||||
);
|
||||
|
||||
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 gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:text-pixel-gold transition"
|
||||
onClick={() => setSelectedEntry(entry)}
|
||||
>
|
||||
<span className="font-bold text-pixel-gold">
|
||||
{entry.username}
|
||||
</span>
|
||||
{entry.characterClass && (
|
||||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
[{entry.characterClass === "WARRIOR" && "⚔️ Guerrier"}
|
||||
{entry.characterClass === "MAGE" && "🔮 Mage"}
|
||||
{entry.characterClass === "ROGUE" && "🗡️ Voleur"}
|
||||
{entry.characterClass === "RANGER" && "🏹 Rôdeur"}
|
||||
{entry.characterClass === "PALADIN" && "🛡️ Paladin"}
|
||||
{entry.characterClass === "ENGINEER" && "⚙️ Ingénieur"}
|
||||
{entry.characterClass === "MERCHANT" && "💰 Marchand"}
|
||||
{entry.characterClass === "SCHOLAR" && "📚 Érudit"}
|
||||
{entry.characterClass === "BERSERKER" && "🔥 Berserker"}
|
||||
{entry.characterClass === "NECROMANCER" &&
|
||||
"💀 Nécromancien"}
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 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-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-bold text-pixel-gold uppercase tracking-wider">
|
||||
{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-6 mb-6">
|
||||
<Avatar
|
||||
src={selectedEntry.avatar}
|
||||
username={selectedEntry.username}
|
||||
size="xl"
|
||||
borderClassName="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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user