From f65fcf9e0eb09e849378c4b4cbc3ecf7667e4ca9 Mon Sep 17 00:00:00 2001 From: ItsOtherMauridian <165866613+ItsOtherMauridian@users.noreply.github.com> Date: Tue, 12 May 2026 12:40:14 +0000 Subject: [PATCH] feat: add guild reputation leaderboard --- .../src/app/[locale]/guilds/[id]/page.tsx | 6 + .../guilds/components/Leaderboard.tsx | 209 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 frontend/src/features/guilds/components/Leaderboard.tsx diff --git a/frontend/src/app/[locale]/guilds/[id]/page.tsx b/frontend/src/app/[locale]/guilds/[id]/page.tsx index 6f1501a3..72d4b8ef 100644 --- a/frontend/src/app/[locale]/guilds/[id]/page.tsx +++ b/frontend/src/app/[locale]/guilds/[id]/page.tsx @@ -9,6 +9,7 @@ import { useGuildStore } from '@/store/guildStore' import { GuildStats } from '@/features/guilds/components/GuildStats' import { MemberList } from '@/features/guilds/components/MemberList' import { SpotlightCard } from '@/features/guilds/components/SpotlightCard' +import { Leaderboard } from '@/features/guilds/components/Leaderboard' import { Button } from '@/components/ui/Button' import type { GuildRole } from '@/features/guilds/types' @@ -155,6 +156,11 @@ export default function GuildDetailPage() { /> + {/* Reputation Leaderboard */} +
+ +
+ {/* Tabs */}
diff --git a/frontend/src/features/guilds/components/Leaderboard.tsx b/frontend/src/features/guilds/components/Leaderboard.tsx new file mode 100644 index 00000000..ff092631 --- /dev/null +++ b/frontend/src/features/guilds/components/Leaderboard.tsx @@ -0,0 +1,209 @@ +'use client' + +import { useMemo, useState } from 'react' +import { motion } from 'framer-motion' +import { ArrowDownUp, Award, Medal, Trophy } from 'lucide-react' +import { cn } from '@/lib/utils' + +type LeaderboardUser = { + id: string + displayName: string + avatarColor: string + reputationScore: number + totalBountiesCompleted: number +} + +type SortKey = 'reputationScore' | 'totalBountiesCompleted' + +const leaderboardUsers: LeaderboardUser[] = [ + { id: 'user-01', displayName: 'Nova Patel', avatarColor: 'from-amber-400 to-orange-500', reputationScore: 9875, totalBountiesCompleted: 42 }, + { id: 'user-02', displayName: 'Orion Vega', avatarColor: 'from-slate-300 to-slate-500', reputationScore: 9340, totalBountiesCompleted: 38 }, + { id: 'user-03', displayName: 'Lyra Chen', avatarColor: 'from-yellow-700 to-orange-900', reputationScore: 8990, totalBountiesCompleted: 35 }, + { id: 'user-04', displayName: 'Mira Sol', avatarColor: 'from-indigo-500 to-purple-600', reputationScore: 8420, totalBountiesCompleted: 34 }, + { id: 'user-05', displayName: 'Kai Morgan', avatarColor: 'from-cyan-500 to-blue-600', reputationScore: 8015, totalBountiesCompleted: 31 }, + { id: 'user-06', displayName: 'Astra Quinn', avatarColor: 'from-pink-500 to-rose-600', reputationScore: 7880, totalBountiesCompleted: 29 }, + { id: 'user-07', displayName: 'Juno Vale', avatarColor: 'from-emerald-500 to-teal-600', reputationScore: 7425, totalBountiesCompleted: 27 }, + { id: 'user-08', displayName: 'Riven Atlas', avatarColor: 'from-violet-500 to-fuchsia-600', reputationScore: 7190, totalBountiesCompleted: 25 }, + { id: 'user-09', displayName: 'Sage Rivera', avatarColor: 'from-blue-500 to-sky-600', reputationScore: 6950, totalBountiesCompleted: 24 }, + { id: 'user-10', displayName: 'Echo Stone', avatarColor: 'from-lime-500 to-green-600', reputationScore: 6725, totalBountiesCompleted: 22 }, + { id: 'user-11', displayName: 'Talia Brooks', avatarColor: 'from-red-500 to-pink-600', reputationScore: 6490, totalBountiesCompleted: 21 }, + { id: 'user-12', displayName: 'Niko Frost', avatarColor: 'from-sky-400 to-cyan-600', reputationScore: 6215, totalBountiesCompleted: 20 }, + { id: 'user-13', displayName: 'Vesper Moon', avatarColor: 'from-purple-400 to-indigo-700', reputationScore: 6040, totalBountiesCompleted: 19 }, + { id: 'user-14', displayName: 'Rune Carter', avatarColor: 'from-stone-500 to-zinc-700', reputationScore: 5890, totalBountiesCompleted: 18 }, + { id: 'user-15', displayName: 'Iris Hale', avatarColor: 'from-teal-400 to-emerald-700', reputationScore: 5710, totalBountiesCompleted: 18 }, + { id: 'user-16', displayName: 'Cassian Reed', avatarColor: 'from-orange-400 to-red-600', reputationScore: 5565, totalBountiesCompleted: 17 }, + { id: 'user-17', displayName: 'Zara Knox', avatarColor: 'from-fuchsia-400 to-purple-700', reputationScore: 5390, totalBountiesCompleted: 16 }, + { id: 'user-18', displayName: 'Atlas Noor', avatarColor: 'from-blue-600 to-indigo-800', reputationScore: 5215, totalBountiesCompleted: 16 }, + { id: 'user-19', displayName: 'Pax Wilder', avatarColor: 'from-lime-400 to-emerald-600', reputationScore: 5080, totalBountiesCompleted: 15 }, + { id: 'user-20', displayName: 'Elio Park', avatarColor: 'from-amber-500 to-yellow-700', reputationScore: 4925, totalBountiesCompleted: 15 }, + { id: 'user-21', displayName: 'Rhea Kim', avatarColor: 'from-rose-400 to-red-700', reputationScore: 4760, totalBountiesCompleted: 14 }, + { id: 'user-22', displayName: 'Dax Mercer', avatarColor: 'from-cyan-600 to-teal-800', reputationScore: 4635, totalBountiesCompleted: 14 }, + { id: 'user-23', displayName: 'Mina Fox', avatarColor: 'from-indigo-400 to-blue-700', reputationScore: 4510, totalBountiesCompleted: 13 }, + { id: 'user-24', displayName: 'Theo Vale', avatarColor: 'from-violet-600 to-purple-800', reputationScore: 4385, totalBountiesCompleted: 13 }, + { id: 'user-25', displayName: 'Luca Reyes', avatarColor: 'from-green-400 to-lime-700', reputationScore: 4260, totalBountiesCompleted: 12 }, + { id: 'user-26', displayName: 'Nyx Arden', avatarColor: 'from-slate-500 to-slate-800', reputationScore: 4115, totalBountiesCompleted: 12 }, + { id: 'user-27', displayName: 'Aria Wells', avatarColor: 'from-pink-400 to-fuchsia-700', reputationScore: 3980, totalBountiesCompleted: 11 }, + { id: 'user-28', displayName: 'Kian Cross', avatarColor: 'from-yellow-500 to-amber-800', reputationScore: 3845, totalBountiesCompleted: 11 }, + { id: 'user-29', displayName: 'Nia Blake', avatarColor: 'from-sky-500 to-blue-800', reputationScore: 3720, totalBountiesCompleted: 10 }, + { id: 'user-30', displayName: 'Milo Hart', avatarColor: 'from-emerald-400 to-teal-700', reputationScore: 3595, totalBountiesCompleted: 10 }, + { id: 'user-31', displayName: 'Sora Finch', avatarColor: 'from-purple-500 to-pink-700', reputationScore: 3470, totalBountiesCompleted: 9 }, + { id: 'user-32', displayName: 'Leah Storm', avatarColor: 'from-red-400 to-orange-700', reputationScore: 3355, totalBountiesCompleted: 9 }, + { id: 'user-33', displayName: 'Owen Ash', avatarColor: 'from-zinc-500 to-stone-800', reputationScore: 3240, totalBountiesCompleted: 8 }, + { id: 'user-34', displayName: 'Vera Holt', avatarColor: 'from-blue-400 to-cyan-700', reputationScore: 3125, totalBountiesCompleted: 8 }, + { id: 'user-35', displayName: 'Remy Cole', avatarColor: 'from-lime-500 to-green-800', reputationScore: 3010, totalBountiesCompleted: 8 }, + { id: 'user-36', displayName: 'Ayla West', avatarColor: 'from-rose-500 to-pink-800', reputationScore: 2895, totalBountiesCompleted: 7 }, + { id: 'user-37', displayName: 'Idris Lane', avatarColor: 'from-indigo-500 to-violet-800', reputationScore: 2780, totalBountiesCompleted: 7 }, + { id: 'user-38', displayName: 'Lena Quinn', avatarColor: 'from-teal-500 to-cyan-800', reputationScore: 2665, totalBountiesCompleted: 7 }, + { id: 'user-39', displayName: 'Cleo North', avatarColor: 'from-orange-500 to-amber-800', reputationScore: 2550, totalBountiesCompleted: 6 }, + { id: 'user-40', displayName: 'Bryn Ellis', avatarColor: 'from-fuchsia-500 to-rose-800', reputationScore: 2435, totalBountiesCompleted: 6 }, + { id: 'user-41', displayName: 'Ezra Pike', avatarColor: 'from-sky-600 to-indigo-800', reputationScore: 2320, totalBountiesCompleted: 6 }, + { id: 'user-42', displayName: 'Tess Vale', avatarColor: 'from-green-500 to-emerald-800', reputationScore: 2205, totalBountiesCompleted: 5 }, + { id: 'user-43', displayName: 'Finn Ro', avatarColor: 'from-yellow-600 to-orange-800', reputationScore: 2090, totalBountiesCompleted: 5 }, + { id: 'user-44', displayName: 'Mae Orion', avatarColor: 'from-purple-600 to-indigo-900', reputationScore: 1975, totalBountiesCompleted: 5 }, + { id: 'user-45', displayName: 'Hugo Ray', avatarColor: 'from-red-600 to-rose-900', reputationScore: 1860, totalBountiesCompleted: 4 }, + { id: 'user-46', displayName: 'Gia Star', avatarColor: 'from-cyan-500 to-blue-900', reputationScore: 1745, totalBountiesCompleted: 4 }, + { id: 'user-47', displayName: 'Noah Drift', avatarColor: 'from-stone-400 to-slate-700', reputationScore: 1630, totalBountiesCompleted: 4 }, + { id: 'user-48', displayName: 'Skye Moss', avatarColor: 'from-lime-400 to-teal-700', reputationScore: 1515, totalBountiesCompleted: 3 }, + { id: 'user-49', displayName: 'Rosa Beam', avatarColor: 'from-pink-500 to-purple-800', reputationScore: 1400, totalBountiesCompleted: 3 }, + { id: 'user-50', displayName: 'Jett Rowan', avatarColor: 'from-slate-600 to-indigo-900', reputationScore: 1285, totalBountiesCompleted: 3 }, +] + +const podiumStyles = [ + { + label: 'Gold', + icon: Trophy, + className: 'border-amber-400/60 bg-amber-500/10 shadow-[0_0_24px_rgba(251,191,36,0.18)]', + badgeClassName: 'bg-amber-400 text-slate-950', + }, + { + label: 'Silver', + icon: Medal, + className: 'border-slate-300/60 bg-slate-300/10 shadow-[0_0_24px_rgba(203,213,225,0.14)]', + badgeClassName: 'bg-slate-300 text-slate-950', + }, + { + label: 'Bronze', + icon: Award, + className: 'border-orange-700/70 bg-orange-700/10 shadow-[0_0_24px_rgba(194,65,12,0.16)]', + badgeClassName: 'bg-orange-700 text-white', + }, +] + +const sortOptions: { key: SortKey; label: string }[] = [ + { key: 'reputationScore', label: 'Reputation Score' }, + { key: 'totalBountiesCompleted', label: 'Total Bounties Completed' }, +] + +const formatNumber = new Intl.NumberFormat('en-US').format + +export function Leaderboard() { + const [sortKey, setSortKey] = useState('reputationScore') + + const sortedUsers = useMemo(() => { + return [...leaderboardUsers] + .sort((a, b) => { + const primary = b[sortKey] - a[sortKey] + return primary !== 0 ? primary : b.reputationScore - a.reputationScore + }) + .map((user, index) => ({ ...user, rank: index + 1 })) + }, [sortKey]) + + const visibleUsers = sortedUsers.slice(0, 10) + + return ( +
+
+
+

+ Monthly reputation +

+

+ Guild Leaderboard +

+

+ Top contributors ranked from a 50-user mocked dataset. The top three receive distinct podium styling while ranks 4-10 use compact rows. +

+
+ +
+ {sortOptions.map((option) => ( + + ))} +
+
+ +
+ {visibleUsers.map((user) => { + const podiumStyle = podiumStyles[user.rank - 1] + const PodiumIcon = podiumStyle?.icon + + return ( + +
+ #{user.rank} +
+ + + +
+
+

+ {user.displayName} +

+ {PodiumIcon && ( + + + {podiumStyle.label} + + )} +
+

+ {formatNumber(user.reputationScore)} reputation points +

+
+ +
+

+ {formatNumber(user.totalBountiesCompleted)} +

+

+ Bounties +

+
+
+ ) + })} +
+
+ ) +}