Add house leaderboard feature: Integrate house leaderboard functionality in LeaderboardPage and LeaderboardSection components. Update userStatsService to fetch house leaderboard data, and enhance UI to display house rankings, scores, and member details. Update Prisma schema to include house-related models and relationships, and seed database with initial house data.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
This commit is contained in:
@@ -26,8 +26,29 @@ interface LeaderboardEntry {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -38,11 +59,15 @@ const formatScore = (score: number): string => {
|
||||
|
||||
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}>
|
||||
@@ -56,7 +81,7 @@ export default function LeaderboardSection({
|
||||
LEADERBOARD
|
||||
</SectionTitle>
|
||||
|
||||
{/* Leaderboard Table */}
|
||||
{/* 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">
|
||||
@@ -143,6 +168,90 @@ export default function LeaderboardSection({
|
||||
</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">
|
||||
@@ -151,6 +260,112 @@ export default function LeaderboardSection({
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user