/** * PublicCardPage — /card/:npub * * A read-only view of a skater's Rider Card, shareable and scannable. The QR on * the card opens /verify so anyone can confirm the card is genuine. */ import { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { useSeoMeta } from '@unhead/react'; import { ArrowLeft, Sparkles, RefreshCw } from 'lucide-react'; import { useRiderStats } from '@/hooks/useRiderStats'; import { useRiderCard, useRiderBadges, useRiderRoles, useCardProfile } from '@/hooks/useRiderCard'; import { useUserCrews } from '@/hooks/useCrews'; import { useUserSkaterProfile } from '@/hooks/useSkaters'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { Button } from '@/components/ui/button'; import { CardPreview, type CardData, type BadgeAward } from '@/components/RiderCard'; import { RIDER_CARD_KIND, RIDER_CARD_D, DEFAULT_RIDER_CARD, cardVerifyUrl, memberSince } from '@/lib/riderCard'; function decodePubkey(value: string): string | null { if (/^[0-9a-f]{64}$/i.test(value)) return value.toLowerCase(); try { const d = nip19.decode(value); return d.type === 'npub' ? d.data : null; } catch { return null; } } export default function PublicCardPage() { const { t } = useTranslation(); const { npub = '' } = useParams<{ npub: string }>(); const pubkey = decodePubkey(npub); const { user } = useCurrentUser(); const cardProfile = useCardProfile(pubkey ?? undefined); const metadata = cardProfile.data; const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : ''); const { data: stats } = useRiderStats(pubkey ?? undefined); const { data: savedCard } = useRiderCard(pubkey ?? undefined); const { data: badges = [] } = useRiderBadges(pubkey ?? undefined); const { data: roles = [] } = useRiderRoles(pubkey ?? undefined); const { memberships } = useUserCrews(pubkey ?? undefined); const { data: skaterProfile } = useUserSkaterProfile(pubkey ?? undefined); const [side, setSide] = useState<'front' | 'back'>('front'); useSeoMeta({ title: `${displayName || 'Rider'} card — SkateSpots`, description: `A SkateSpots Rider Card${displayName ? ` for ${displayName}` : ''}.`, }); if (!pubkey) { return (

{t('cardUi.public.invalid')}

); } const config = savedCard?.config ?? DEFAULT_RIDER_CARD; const receivedBadges: BadgeAward[] = badges.map(b => ({ skill: b.skill, witnessName: genUserName(b.witnessPubkey), witnessPubkey: b.witnessPubkey, spotName: b.spotName, timestamp: b.timestamp, })); const cardData: CardData = { type: 'rider', theme: config.theme as CardData['theme'], holographic: true, name: displayName, nip05: metadata?.nip05, avatar: metadata?.picture, pubkey, npub: nip19.npubEncode(pubkey), style: config.style, skills: config.skills, sponsors: config.sponsors, ridesFor: config.ridesFor, showSpots: config.showSpots, showZaps: config.showZaps, showEvents: config.showEvents, spotsAdded: stats?.spotsAdded ?? 0, zapsReceived: stats?.zapsReceived ?? 0, eventsCreated: stats?.eventsCreated ?? 0, memberYear: metadata?.created_at ? memberSince(metadata.created_at as number) : new Date().getFullYear().toString(), receivedBadges, verifyUrl: cardVerifyUrl(RIDER_CARD_KIND, pubkey, RIDER_CARD_D), accent: config.accent || undefined, logo: config.logo || undefined, side, crews: memberships.map(m => ({ name: m.crew.name, image: m.crew.image })), local: skaterProfile?.location || skaterProfile?.homeSpot || undefined, roles, issuerName: '', recipientName: '', accessLevel: '', validThru: '', }; const isMe = user?.pubkey === pubkey; return (
{isMe ? t('cardUi.public.backToEditor') : t('cardUi.public.backHome')}
{config.back && (
)} {!savedCard?.config && (

{t('cardUi.public.notCustomised')}

)}

{t('cardUi.public.verifyNote')}

); }