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() })} )} )}
); }