/** * RiderCard — the card's data shape + the ONE preview component. * * The card itself is drawn by `@/lib/riderCardCanvas` (a single canvas * renderer), so the studio preview and the public `/#/card/:npub` view are * pixel-identical to what prints. There is intentionally NO second React * implementation of the card. */ import { useEffect, useState } from 'react'; import { Maximize2 } from 'lucide-react'; import { drawCardToCanvas, type CanvasCardData } from '@/lib/riderCardCanvas'; import type { ThemeId } from '@/lib/riderCard'; import { cn } from '@/lib/utils'; export interface BadgeAward { witnessName: string; witnessPubkey: string; skill: string; spotName?: string; timestamp: number; } export interface CardData { type: 'rider' | 'park' | 'crew' | 'event'; theme: ThemeId; holographic: boolean; name: string; nip05?: string; avatar?: string; pubkey: string; npub: string; style: string; level?: string; sponsors: string; ridesFor: string; showCrews?: boolean; showLocal?: boolean; showRoles?: boolean; receivedBadges: BadgeAward[]; verifyUrl?: string; accent?: string; logo?: string; side?: 'front' | 'back'; crews?: { name: string; image?: string }[]; local?: string; roles?: string[]; crew?: CanvasCardData['crew']; event?: CanvasCardData['event']; issuerName: string; recipientName: string; accessLevel: string; validThru: string; } function toCanvasData(d: CardData): CanvasCardData { return { type: d.type, themeId: d.theme, accentOverride: d.accent, name: d.name, nip05: d.nip05, avatarUrl: d.avatar, logoUrl: d.logo, pubkey: d.pubkey, npub: d.npub, style: d.style, level: d.level, sponsors: d.sponsors, ridesFor: d.ridesFor, showCrews: d.showCrews ?? true, showLocal: d.showLocal ?? true, showRoles: d.showRoles ?? true, receivedBadges: d.receivedBadges, verifyUrl: d.verifyUrl, local: d.local, crews: (d.crews ?? []).map((c) => c.name), roles: d.roles, crew: d.crew, event: d.event, issuerName: d.issuerName, recipientName: d.recipientName, accessLevel: d.accessLevel, validThru: d.validThru, holographic: d.holographic, }; } /** * Live canvas-backed card preview. Debounced so a fast typist doesn't kick off * a canvas + QR render on every keystroke. */ export function CardPreview({ d, className, zoomable = false }: { d: CardData; className?: string; zoomable?: boolean }) { const [src, setSrc] = useState(null); const [zoom, setZoom] = useState(false); useEffect(() => { let cancelled = false; const handle = setTimeout(async () => { try { const url = await drawCardToCanvas(toCanvasData(d), d.side ?? 'front'); if (!cancelled) setSrc(url); } catch { if (!cancelled) setSrc(null); } }, 180); return () => { cancelled = true; clearTimeout(handle); }; }, [d]); return ( <> {zoom && src && (
setZoom(false)} role="dialog" aria-modal="true" > Card
)} ); }