/** * 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 { 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 } from '@/lib/riderCard'; import { SKATER_STYLES, SKATER_LEVELS } from '@/lib/types'; 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: 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 derivedStyle = (skaterProfile?.styles ?? []).map((s) => SKATER_STYLES[s]?.label ?? s).join(' · '); const derivedLevel = skaterProfile?.level ? (SKATER_LEVELS[skaterProfile.level]?.label ?? '') : (skaterProfile?.customLevel ?? ''); 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')}
{t('cardUi.public.notCustomised')}
)}{t('cardUi.public.verifyNote')}