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 */}
);
}
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.
);
}