import { useMemo } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useSkateShops } from '@/hooks/useSkateShops'; import { useSkateSpots } from '@/hooks/useSkateSpots'; import { genUserName } from '@/lib/genUserName'; import { nip19 } from 'nostr-tools'; import { Trophy, Medal, Crown, Store, MapPin, Zap, Camera, FileText, Users, ChevronRight, Sparkles, } from 'lucide-react'; import { cn } from '@/lib/utils'; // Leaderboard entry component function LeaderboardEntry({ rank, pubkey, count, type, isCurrentUser }: { rank: number; pubkey: string; count: number; type: 'shops' | 'spots' | 'total'; isCurrentUser: boolean; }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(pubkey); const avatar = metadata?.picture; // Rank styling const getRankIcon = () => { if (rank === 1) return ; if (rank === 2) return ; if (rank === 3) return ; return #{rank}; }; const getRankBg = () => { if (rank === 1) return 'bg-gradient-to-r from-yellow-950/50 to-orange-950/30 border-yellow-500/50'; if (rank === 2) return 'bg-gradient-to-r from-gray-900/50 to-gray-800/30 border-gray-500/50'; if (rank === 3) return 'bg-gradient-to-r from-amber-950/50 to-orange-950/30 border-amber-600/50'; return 'bg-black/30 border-gray-800'; }; const getTypeLabel = () => { if (type === 'shops') return 'shops'; if (type === 'spots') return 'spots'; return 'contributions'; }; return (
{/* Rank */}
{getRankIcon()}
{/* Avatar */} {displayName.slice(0, 2).toUpperCase()} {/* Info */}

{displayName}

{isCurrentUser && ( You )}

{count} {getTypeLabel()}

{/* Count */}

{count}

); } function LeaderboardSkeleton() { return (
{Array.from({ length: 10 }).map((_, i) => (
))}
); } export default function BountyLeaderboardPage() { useSeoMeta({ title: 'Map & Earn - SkateSpots', description: 'Map skate spots and shops, earn zaps from the community. See the top contributors.', }); const { user } = useCurrentUser(); const { data: shops, isLoading: shopsLoading } = useSkateShops(500); const { data: spots, isLoading: spotsLoading } = useSkateSpots(500); // Calculate shop leaderboard const shopLeaderboard = useMemo(() => { if (!shops) return []; const countByPubkey: Record = {}; shops.forEach(shop => { countByPubkey[shop.pubkey] = (countByPubkey[shop.pubkey] || 0) + 1; }); return Object.entries(countByPubkey) .map(([pubkey, count]) => ({ pubkey, count })) .sort((a, b) => b.count - a.count) .slice(0, 50); }, [shops]); // Calculate spot leaderboard const spotLeaderboard = useMemo(() => { if (!spots) return []; const countByPubkey: Record = {}; spots.forEach(spot => { countByPubkey[spot.pubkey] = (countByPubkey[spot.pubkey] || 0) + 1; }); return Object.entries(countByPubkey) .map(([pubkey, count]) => ({ pubkey, count })) .sort((a, b) => b.count - a.count) .slice(0, 50); }, [spots]); // Combined leaderboard (total contributions) const combinedLeaderboard = useMemo(() => { const combined: Record = {}; shops?.forEach(shop => { if (!combined[shop.pubkey]) combined[shop.pubkey] = { shops: 0, spots: 0, total: 0 }; combined[shop.pubkey].shops++; combined[shop.pubkey].total++; }); spots?.forEach(spot => { if (!combined[spot.pubkey]) combined[spot.pubkey] = { shops: 0, spots: 0, total: 0 }; combined[spot.pubkey].spots++; combined[spot.pubkey].total++; }); return Object.entries(combined) .map(([pubkey, data]) => ({ pubkey, ...data })) .sort((a, b) => b.total - a.total) .slice(0, 50); }, [shops, spots]); // Find user's rank const userCombinedRank = combinedLeaderboard.findIndex(e => e.pubkey === user?.pubkey) + 1; const userContributions = combinedLeaderboard.find(e => e.pubkey === user?.pubkey); // Stats const totalShops = shops?.length || 0; const totalSpots = spots?.length || 0; const totalMappers = combinedLeaderboard.length; return (
{/* Header */}
Map & Earn

HELP MAP THE WORLD

Add spots and shops, get zapped by grateful skaters. Top contributors may receive bonus rewards!

{/* How It Works */}

How It Works

1. Map

Add spots & shops with photos and details

2. Earn Zaps

Skaters who find your spots useful will tip you

3. Bonus Rewards

Top mappers may get bonus rewards from our fund

{/* Quality Tips */}

💡 Pro Tips for More Zaps:

Add 3+ photos Write detailed descriptions Accurate location
{/* Quick Actions */}

Map a Shop

Skate shops, board stores

Map a Spot

Parks, street spots, DIY

{/* Stats */}

{totalShops}

Shops

{totalSpots}

Spots

{totalMappers}

Mappers

{/* User's position (if logged in and has contributions) */} {user && userCombinedRank > 0 && userContributions && (
#{userCombinedRank}

Your Ranking

{userContributions.spots} spots • {userContributions.shops} shops

{userContributions.total}

total

)} {/* Leaderboard */}

Top Mappers

Overall Shops Spots {shopsLoading || spotsLoading ? ( ) : combinedLeaderboard.length > 0 ? (
{combinedLeaderboard.slice(0, 20).map((entry, i) => ( ))}
) : (

No contributions yet. Be the first!

)}
{shopsLoading ? ( ) : shopLeaderboard.length > 0 ? (
{shopLeaderboard.slice(0, 20).map((entry, i) => ( ))}
) : (

No shops mapped yet

)}
{spotsLoading ? ( ) : spotLeaderboard.length > 0 ? (
{spotLeaderboard.slice(0, 20).map((entry, i) => ( ))}
) : (

No spots mapped yet

)}
{/* Community Note */}

Powered by Community Zaps

When you find a spot helpful, zap the mapper! It's the best way to say thanks and encourage more contributions.

); }