From 335554a4f3c7ef1d925f4473a6785cc500fa31a6 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 21 Apr 2026 14:51:20 +0900 Subject: [PATCH 1/2] [#888] Add leaderboard component with top 50 and user rank Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/airdrop/page.tsx | 2 + src/components/airdrop/Leaderboard.tsx | 107 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/components/airdrop/Leaderboard.tsx diff --git a/src/app/airdrop/page.tsx b/src/app/airdrop/page.tsx index 54649432..3d4ea7d5 100644 --- a/src/app/airdrop/page.tsx +++ b/src/app/airdrop/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { CampaignHero } from "../../components/airdrop/CampaignHero"; import { UserPoints } from "../../components/airdrop/UserPoints"; +import { Leaderboard } from "../../components/airdrop/Leaderboard"; import { MilestoneTrack } from "../../components/airdrop/MilestoneTrack"; export const metadata: Metadata = { @@ -13,6 +14,7 @@ export default function AirdropPage() {
+
); diff --git a/src/components/airdrop/Leaderboard.tsx b/src/components/airdrop/Leaderboard.tsx new file mode 100644 index 00000000..21abaddf --- /dev/null +++ b/src/components/airdrop/Leaderboard.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useAccount } from "wagmi"; +import { useQuery } from "@tanstack/react-query"; + +interface LeaderboardEntry { + rank: number; + address: string; + username: string | null; + totalPoints: number; + sharePercent: number; +} + +interface LeaderboardData { + entries: LeaderboardEntry[]; + userRank: number | null; +} + +function truncateAddress(addr: string) { + return `${addr.slice(0, 6)}...${addr.slice(-4)}`; +} + +export function Leaderboard() { + const { address, isConnected } = useAccount(); + + const { data, isLoading } = useQuery({ + queryKey: ["airdrop-leaderboard", address], + queryFn: async () => { + const params = address ? `?address=${address.toLowerCase()}` : ""; + const res = await fetch(`/api/airdrop/leaderboard${params}`); + if (!res.ok) throw new Error("Failed to fetch leaderboard"); + return res.json(); + }, + staleTime: 60_000, + refetchInterval: 60_000, + }); + + if (isLoading || !data) { + return ( +
+
Loading leaderboard...
+
+ ); + } + + if (data.entries.length === 0) { + return ( +
+

Leaderboard

+
No participants yet.
+
+ ); + } + + const userAddr = address?.toLowerCase(); + const inTop50 = userAddr && data.entries.some((e) => e.address === userAddr); + + return ( +
+

Leaderboard

+ +
+ + + + + + + + + + + {data.entries.map((entry) => { + const isUser = isConnected && userAddr === entry.address; + return ( + + + + + + + ); + })} + +
#UserPLShare
{entry.rank} + {entry.username ?? truncateAddress(entry.address)} + {isUser && (you)} + + {entry.totalPoints.toLocaleString()} + + {entry.sharePercent}% +
+
+ + {/* User's rank if outside top 50 */} + {isConnected && !inTop50 && data.userRank && ( +
+ Your rank: + #{data.userRank} +
+ )} +
+ ); +} From b10fc5c47e26956cf7d485921ee57d43e0ac6987 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 21 Apr 2026 14:52:48 +0900 Subject: [PATCH 2/2] [#888] Add defensive lowercase comparison for user address matching Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/airdrop/Leaderboard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/airdrop/Leaderboard.tsx b/src/components/airdrop/Leaderboard.tsx index 21abaddf..03d04d89 100644 --- a/src/components/airdrop/Leaderboard.tsx +++ b/src/components/airdrop/Leaderboard.tsx @@ -53,7 +53,7 @@ export function Leaderboard() { } const userAddr = address?.toLowerCase(); - const inTop50 = userAddr && data.entries.some((e) => e.address === userAddr); + const inTop50 = userAddr && data.entries.some((e) => e.address.toLowerCase() === userAddr); return (
@@ -71,7 +71,7 @@ export function Leaderboard() { {data.entries.map((entry) => { - const isUser = isConnected && userAddr === entry.address; + const isUser = isConnected && userAddr === entry.address.toLowerCase(); return (