import { nip19 } from 'nostr-tools'; import { useParams, Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { ZapButton } from '@/components/ZapButton'; import { ClinkPayButton } from '@/components/ClinkPayButton'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUserSkateSpots } from '@/hooks/useSkateSpots'; import { useUserShops } from '@/hooks/useShops'; import { useUserCrews } from '@/hooks/useCrews'; import { useUserPublicStash } from '@/hooks/useStashShowcase'; import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; import { genUserName } from '@/lib/genUserName'; import NotFound from './NotFound'; import { MapPin, Store, MessageCircle, Globe, Zap, Calendar, ExternalLink, ArrowLeft, Copy, Check, Users, Crown, Bot, Pencil, Package, Tag, ArrowRightLeft, Bitcoin, Award, MessageSquareLock, } from 'lucide-react'; import { useState } from 'react'; import { UserStatusIndicator } from '@/components/UserStatusIndicator'; function ProfilePage({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const { user } = useCurrentUser(); const { data: spots, isLoading: spotsLoading } = useUserSkateSpots(pubkey); const { data: shops, isLoading: shopsLoading } = useUserShops(pubkey); const { memberships: crews, isLoading: crewsLoading } = useUserCrews(pubkey); const { data: publicStash, isLoading: publicStashLoading } = useUserPublicStash(pubkey); const [copied, setCopied] = useState(false); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(pubkey); const avatar = metadata?.picture; const banner = metadata?.banner; const about = metadata?.about; const website = metadata?.website; const nip05 = metadata?.nip05; const lud16 = metadata?.lud16; const isBot = metadata?.bot === true; // Custom CLINK offer code stored in the kind 0 content JSON. const raw = metadata as Record | undefined; const noffer = raw?.noffer; const nofferCode = typeof noffer === 'string' ? noffer : undefined; // Extra custom profile fields. const silentPayment = typeof raw?.bitcoin_silent_payment === 'string' ? raw.bitcoin_silent_payment : undefined; const sponsors = typeof raw?.sponsors === 'string' ? raw.sponsors.trim() : undefined; const cordnUrl = typeof raw?.cordn === 'string' ? raw.cordn : undefined; const [spCopied, setSpCopied] = useState(false); const npub = nip19.npubEncode(pubkey); const isOwnProfile = user?.pubkey === pubkey; const handleCopyNpub = () => { navigator.clipboard.writeText(npub); setCopied(true); setTimeout(() => setCopied(false), 2000); }; // Create a mock event for zapping const profileEvent = author.data?.event; if (author.isLoading) { return (
); } return (
{/* Back Button */} Back to Leaderboard {/* Banner */}
{banner && ( Banner )}
{/* Profile Header */}
{displayName.slice(0, 2).toUpperCase()} {/* User Status Indicator */}

{displayName}

{isBot && ( Bot )}
{nip05 && (

{nip05}

)} {/* Crew membership summary — links straight to the crews */} {crews.length > 0 && (

{crews.slice(0, 2).map((m, i) => ( {i > 0 && ', '} {m.crew.name} {m.role === 'founder' && ( )} ))} {crews.length > 2 && ( +{crews.length - 2} more )}

)}
{/* Actions */}
{isOwnProfile && ( )} {!isOwnProfile && user && ( )} {profileEvent && !isOwnProfile && ( )}
{/* Profile Info */}
{/* Main Content */}
{/* About */} {about && (

{about}

)} {/* Spots by this user */}

Spots Added {spots && spots.length > 0 && ( {spots.length} )}

{spotsLoading ? (
{Array.from({ length: 2 }).map((_, i) => ( ))}
) : spots && spots.length > 0 ? (
{spots.slice(0, 4).map((spot) => ( ))}
) : (

No spots added yet

)}
{/* Shops by this user */}

Shops Added {shops && shops.length > 0 && ( {shops.length} )}

{shopsLoading ? (
{Array.from({ length: 2 }).map((_, i) => ( ))}
) : shops && shops.length > 0 ? (
{shops.slice(0, 4).map((shop) => (

{shop.name}

{shop.location && (

{shop.location}

)}
))}
) : (

No shops added yet

)}
{/* Public Stash — gear this skater chose to show publicly */} {(publicStashLoading || (publicStash && publicStash.length > 0)) && (

Stash {publicStash && publicStash.length > 0 && ( {publicStash.length} )}

{isOwnProfile && ( Manage )}
{publicStashLoading ? (
{Array.from({ length: 3 }).map((_, i) => ( ))}
) : (
{publicStash!.map((g) => (
{g.images[0] ? ( {g.title} ) : ( )} {g.forSale && ( {g.willingToTrade && !g.priceSats ? ( <>Trade ) : ( <>For Sale )} )}

{g.title}

{g.brand ? (

{g.brand}

) : (

{g.category}

)} {g.forSale && g.priceSats ? ( {g.priceSats > 999 ? `${Math.floor(g.priceSats / 1000)}k` : g.priceSats} ) : null}
))}
)}
)}
{/* Sidebar */}
{/* Quick Info Card */} Info {website && ( {website.replace(/^https?:\/\//, '')} )} {lud16 && (

Lightning

{lud16}
)} {/* Bitcoin Silent Payment address — copyable */} {silentPayment && ( )} {/* Cordn contact link */} {cordnUrl && (

Cordn

Message privately
)} {/* Npub Copy */}
{/* Sponsors Card */} {sponsors && ( Sponsors & Support

{sponsors}

)} {/* Crews Card */} {(crewsLoading || crews.length > 0) && ( Crews {crews.length > 0 && ( {crews.length} )} {crewsLoading ? ( <> ) : ( crews.map((m) => ( {m.crew.name.slice(0, 2).toUpperCase()}

{m.crew.name}

{m.role === 'founder' ? ( <> Founder ) : ( <>Member )}

)) )}
)} {/* Stats Card */}

Contributions

{spots?.length || 0}

Spots

{shops?.length || 0}

Shops

{crews.length}

Crews

{publicStash?.length || 0}

Gear

{/* Tip Card */} {!isOwnProfile && (profileEvent || nofferCode) && (

Support this skater

Send a tip to thank them for their contributions

{profileEvent && ( )} {nofferCode && ( )}
)}
); } export function NIP19Page() { const { nip19: identifier } = useParams<{ nip19: string }>(); if (!identifier) { return ; } let decoded; try { decoded = nip19.decode(identifier); } catch { return ; } const { type, data } = decoded; switch (type) { case 'npub': return ; case 'nprofile': return ; case 'note': // AI agent should implement note view here return ; case 'nevent': // AI agent should implement event view here return ; case 'naddr': // AI agent should implement addressable event view here return ; default: return ; } }