Files
got-gaming/components/LeaderboardSection.tsx

320 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
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">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
{entry.avatar ? (
<img
src={entry.avatar}
alt={entry.username}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none";
e.currentTarget.nextElementSibling?.classList.remove(
"hidden"
);
}}
/>
) : null}
<div
className={`w-full h-full flex items-center justify-center text-pixel-gold text-[10px] sm:text-xs font-bold ${
entry.avatar ? "hidden" : ""
}`}
>
{entry.username.charAt(0).toUpperCase()}
</div>
</div>
<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">
<div className="w-16 h-16 sm:w-24 sm:h-24 rounded-full border-2 sm:border-4 border-pixel-gold/50 overflow-hidden bg-black/60 flex-shrink-0">
{selectedEntry.avatar ? (
<img
src={selectedEntry.avatar}
alt={selectedEntry.username}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none";
e.currentTarget.nextElementSibling?.classList.remove(
"hidden"
);
}}
/>
) : null}
<div
className={`w-full h-full flex items-center justify-center text-pixel-gold text-2xl sm:text-4xl font-bold ${
selectedEntry.avatar ? "hidden" : ""
}`}
>
{selectedEntry.username.charAt(0).toUpperCase()}
</div>
</div>
<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>
);
}