import { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
import { Card, CardContent } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { ErrorState } from '@/components/ErrorState';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { useZapLeaderboard } from '@/hooks/useZapLeaderboard';
import { useSkateSpots } from '@/hooks/useSkateSpots';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { useTranslation } from '@/contexts/language';
import { SKATE_SPOT_KIND, type SkateSpot } from '@/lib/types';
import { cn } from '@/lib/utils';
import { Crown, Medal, MapPin, Store, Trophy, Users, Zap } from 'lucide-react';
import type { ZapLeaderboardSender, ZapLeaderboardTarget } from '@/hooks/useZapLeaderboard';
const RANGES = {
week: 7 * 24 * 60 * 60,
month: 30 * 24 * 60 * 60,
all: null,
} as const;
type RangeKey = keyof typeof RANGES;
function RankBadge({ rank }: { rank: number }) {
if (rank === 1) return ;
if (rank === 2) return ;
if (rank === 3) return ;
return #{rank} ;
}
function rowClasses(rank: number) {
return cn(
'flex items-center gap-3 rounded-xl border-2 bg-black/60 px-3 py-2.5 transition-colors',
rank === 1
? 'border-yellow-500/40 hover:border-yellow-500/70'
: rank === 2
? 'border-gray-400/30 hover:border-gray-400/60'
: rank === 3
? 'border-amber-600/30 hover:border-amber-600/60'
: 'border-white/10 hover:border-[#ff0040]/50',
);
}
function Sats({ sats, count }: { sats: number; count: number }) {
const { t } = useTranslation();
return (
{sats.toLocaleString()}
{t('walletUi.zap.count', { count })}
);
}
function TargetRow({
rank,
target,
spot,
}: {
rank: number;
target: ZapLeaderboardTarget;
spot?: SkateSpot;
}) {
const { t } = useTranslation();
// `a` coordinate = kind:pubkey:identifier (identifier may itself contain ':').
const [kindStr, pubkey, ...rest] = target.key.split(':');
const identifier = rest.join(':');
const kind = Number(kindStr);
const isSpotCoord = kind === SKATE_SPOT_KIND && !!pubkey && identifier !== '';
const isShop = spot?.types.includes('shop') ?? false;
const to = spot
? isShop
? `/shop/${spot.pubkey}/${spot.identifier}`
: `/spot/${spot.pubkey}/${spot.identifier}`
: isSpotCoord
? `/${nip19.naddrEncode({ kind, pubkey, identifier })}`
: undefined;
const label = spot?.title ?? (isShop ? t('walletUi.leaderboard.skateShop') : t('walletUi.leaderboard.skateSpot'));
const inner = (
{isShop ? : }
{label}
{spot?.location && (
{spot.location}
)}
);
return to ? {inner} : inner;
}
function SupporterRow({ rank, entry }: { rank: number; entry: ZapLeaderboardSender }) {
const { t } = useTranslation();
const author = useAuthor(entry.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(entry.pubkey);
return (
{displayName.slice(0, 2).toUpperCase()}
{displayName}
{t('walletUi.leaderboard.supporter')}
);
}
function RowsSkeleton() {
return (
{Array.from({ length: 5 }).map((_, i) => (
))}
);
}
export default function ZapLeaderboardPage() {
const { t } = useTranslation();
useSeoMeta({
title: 'Zap Leaderboards - SkateSpots',
description: 'Top zapped skate spots and the community members supporting them.',
});
const [range, setRange] = useState('month');
const since = useMemo(() => {
const seconds = RANGES[range];
return seconds ? Math.floor(Date.now() / 1000) - seconds : undefined;
}, [range]);
const { data, isLoading, isError, refetch } = useZapLeaderboard(since);
const { data: spots } = useSkateSpots(200);
const spotByCoord = useMemo(() => {
const map = new Map();
for (const s of spots ?? []) {
map.set(`${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`, s);
}
return map;
}, [spots]);
const topTargets = useMemo(
() => (data?.targets ?? []).filter((t) => t.key.startsWith(`${SKATE_SPOT_KIND}:`)).slice(0, 10),
[data],
);
const topSenders = useMemo(() => (data?.senders ?? []).slice(0, 10), [data]);
const totalZapped = useMemo(
() => topSenders.reduce((sum, s) => sum + s.sats, 0),
[topSenders],
);
return (
<>
{/* Header */}
{t('walletUi.leaderboard.zapTitle')}
{t('walletUi.leaderboard.zapSubtitle')}
{/* Time range */}
setRange(v as RangeKey)} className="mb-6">
{t('walletUi.leaderboard.thisWeek')}
{t('walletUi.leaderboard.thisMonth')}
{t('walletUi.leaderboard.allTime')}
{/* Top spots */}
{t('walletUi.leaderboard.topZappedSpots')}
{isError ? (
refetch()} />
) : isLoading ? (
) : topTargets.length === 0 ? (
{t('walletUi.leaderboard.noZaps')}
) : (
{topTargets.map((t, i) => (
))}
)}
{/* Top supporters */}
{t('walletUi.leaderboard.topSupporters')}
{isError ? (
refetch()} />
) : isLoading ? (
) : topSenders.length === 0 ? (
{t('walletUi.leaderboard.noZapsSent')}
) : (
<>
{topSenders.map((s, i) => (
))}
{totalZapped > 0 && (
{t('walletUi.leaderboard.fromTop10', { amount: totalZapped.toLocaleString() })}
)}
>
)}
>
);
}