- /card gains a fourth type: Event. Renders an event (kind 31923) you organise or RSVP'd going/interested to (useMyRsvpCoords, kind 31925) as a passport/ wristband: title, date/time, venue, cover, who it admits, going count. Derived from the event's own signed event — its QR verifies that event; /verify/:naddr renders kind 31923 too. React + canvas renderers; i18n. - /review gains a dedicated Claims tab (pending private-spot ownership claims), replacing the banner that sat above the tabs.
519 lines
22 KiB
TypeScript
519 lines
22 KiB
TypeScript
/**
|
||
* RiderCard — the visual card used by both the `/card` editor and the public
|
||
* `/card/:npub` view. Extracted so there is ONE React rendering of the card
|
||
* (the canvas export in `RiderCardPage` remains the print renderer).
|
||
*/
|
||
import { useEffect, useState } from 'react';
|
||
import { SKILL_OPTIONS, shortNpub, roleMeta, formatCardDate, type ThemeId } from '@/lib/riderCard';
|
||
import { useTranslation } from '@/contexts/language';
|
||
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;
|
||
// Rider
|
||
name: string;
|
||
nip05?: string;
|
||
avatar?: string;
|
||
pubkey: string;
|
||
npub: string;
|
||
style: string;
|
||
skills: string[];
|
||
sponsors: string;
|
||
ridesFor: string;
|
||
showSpots: boolean;
|
||
showZaps: boolean;
|
||
showEvents: boolean;
|
||
spotsAdded: number;
|
||
zapsReceived: number;
|
||
eventsCreated: number;
|
||
memberYear: string;
|
||
receivedBadges: BadgeAward[];
|
||
/** When set, the printed QR opens the verify page for this card. */
|
||
verifyUrl?: string;
|
||
// Identity facets (derived live, toggled on the card)
|
||
crews?: { name: string; image?: string }[];
|
||
local?: string;
|
||
roles?: string[];
|
||
showCrews?: boolean;
|
||
showLocal?: boolean;
|
||
showRoles?: boolean;
|
||
// Crew card (derived from the crew's own signed kind 35006 event)
|
||
crew?: {
|
||
name: string;
|
||
image?: string;
|
||
pubkey: string;
|
||
identifier: string;
|
||
location?: string;
|
||
founded?: string;
|
||
members: number;
|
||
focus?: string;
|
||
vibes: string[];
|
||
lookingFor: string[];
|
||
spots: number;
|
||
};
|
||
// Park
|
||
issuerName: string;
|
||
recipientName: string;
|
||
accessLevel: string;
|
||
validThru: string;
|
||
// Event pass / wristband (derived from the event's own signed kind 31923)
|
||
event?: {
|
||
title: string;
|
||
start: number;
|
||
end?: number;
|
||
location?: string;
|
||
image?: string;
|
||
pubkey: string;
|
||
identifier: string;
|
||
going?: number;
|
||
holder?: string;
|
||
};
|
||
}
|
||
|
||
/** Real QR (lazy-imported so canvas API access stays out of module load). */
|
||
function CardQr({ url }: { url?: string }) {
|
||
const [src, setSrc] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
if (!url) { setSrc(null); return; }
|
||
(async () => {
|
||
try {
|
||
const QRCode = (await import('qrcode')).default;
|
||
const dataUrl = await QRCode.toDataURL(url, {
|
||
width: 88,
|
||
margin: 0,
|
||
errorCorrectionLevel: 'M',
|
||
color: { dark: '#000000', light: '#ffffff' },
|
||
});
|
||
if (!cancelled) setSrc(dataUrl);
|
||
} catch {
|
||
if (!cancelled) setSrc(null);
|
||
}
|
||
})();
|
||
return () => { cancelled = true; };
|
||
}, [url]);
|
||
|
||
return (
|
||
<div className="rounded-[3px] flex-shrink-0" style={{ background: '#fff', padding: '3px', width: '44px', height: '44px' }}>
|
||
{src
|
||
? <img src={src} alt="QR" width={38} height={38} style={{ display: 'block', width: '38px', height: '38px' }} />
|
||
: <div style={{ width: '38px', height: '38px' }} />}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Card visual: Rider front ─────────────────────────────────────────────────
|
||
// Layout is STRICTLY pixel-fixed to match the 340×214px card container.
|
||
function RiderFront({ d }: { d: CardData }) {
|
||
const { t } = useTranslation();
|
||
const witnessedBadges = d.receivedBadges.slice(0, 2);
|
||
|
||
return (
|
||
<div className="absolute inset-0" style={{ padding: '10px 10px 20px 10px' }}>
|
||
|
||
{/* Row 1: header */}
|
||
<div className="flex items-start justify-between" style={{ height: '26px' }}>
|
||
<div>
|
||
<div className="text-[8px] font-black uppercase tracking-[0.18em] card-accent leading-none">
|
||
🛹 SkateSpots
|
||
</div>
|
||
<div className="text-[6px] card-muted uppercase tracking-widest mt-[2px]">{t('labUi.rider.previewLicense')}</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-[5px] card-muted uppercase tracking-wide leading-none">npub</div>
|
||
<div className="text-[6px] font-bold card-accent font-mono tracking-tight leading-none mt-[1px]">
|
||
{shortNpub(d.pubkey)}
|
||
</div>
|
||
<div className="text-[5px] card-muted mt-[1px]">{t('labUi.rider.previewSince', { year: d.memberYear })}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Identity facets: local + crew crests */}
|
||
{((d.showLocal !== false && d.local) || (d.showCrews !== false && (d.crews?.length ?? 0) > 0)) && (
|
||
<div className="flex items-center gap-[6px] mt-[2px] overflow-hidden" style={{ height: '13px' }}>
|
||
{d.showLocal !== false && d.local && (
|
||
<span className="text-[6px] font-bold card-accent truncate flex-shrink-0" style={{ maxWidth: '150px' }}>📍 {d.local}</span>
|
||
)}
|
||
{d.showCrews !== false && (d.crews ?? []).slice(0, 3).map((c, i) => (
|
||
<span key={i} className="flex items-center gap-[2px] min-w-0 flex-shrink-0">
|
||
{c.image
|
||
? <img src={c.image} alt="" crossOrigin="anonymous" className="w-[11px] h-[11px] rounded-full object-cover border border-white/25" />
|
||
: <span className="w-[11px] h-[11px] rounded-full bg-white/15" />}
|
||
<span className="text-[5.5px] card-muted truncate" style={{ maxWidth: '66px' }}>{c.name}</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Row 2: main body */}
|
||
<div className="flex items-stretch gap-[8px]" style={{ height: '118px', marginTop: '6px' }}>
|
||
|
||
{/* Avatar */}
|
||
<div className="flex-shrink-0 flex flex-col gap-[4px]">
|
||
<div className="w-[52px] h-[52px] rounded-[6px] overflow-hidden border-[1.5px] border-white/20">
|
||
{d.avatar ? (
|
||
<img src={d.avatar} alt={d.name} className="w-full h-full object-cover" crossOrigin="anonymous" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center card-bg-chip text-[18px]">🛹</div>
|
||
)}
|
||
</div>
|
||
<div className="space-y-[2px]">
|
||
{d.showSpots && (
|
||
<div className="flex items-center justify-between gap-1">
|
||
<span className="text-[5px] card-muted">{t('labUi.pyramid.spots')}</span>
|
||
<span className="text-[7px] font-black card-accent leading-none">{d.spotsAdded}</span>
|
||
</div>
|
||
)}
|
||
{d.showZaps && (
|
||
<div className="flex items-center justify-between gap-1">
|
||
<span className="text-[5px] card-muted">⚡</span>
|
||
<span className="text-[7px] font-black card-accent2 leading-none">{d.zapsReceived}</span>
|
||
</div>
|
||
)}
|
||
{d.showEvents && (
|
||
<div className="flex items-center justify-between gap-1">
|
||
<span className="text-[5px] card-muted">{t('labUi.rider.previewEvts')}</span>
|
||
<span className="text-[7px] font-black leading-none" style={{ color: '#ff6600' }}>{d.eventsCreated}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Identity block */}
|
||
<div className="flex-1 min-w-0 flex flex-col justify-start gap-[3px]">
|
||
<div className="text-[14px] font-black uppercase truncate leading-tight">{d.name}</div>
|
||
{d.nip05 && (
|
||
<div className="text-[6.5px] card-accent truncate leading-none">@{d.nip05.split('@')[0]}</div>
|
||
)}
|
||
{d.style && (
|
||
<span className="card-bg-chip text-[5.5px] font-bold uppercase px-[4px] py-[1px] rounded-[3px] card-accent w-fit">
|
||
{d.style}
|
||
</span>
|
||
)}
|
||
{d.ridesFor && (
|
||
<div className="text-[5.5px] card-muted truncate leading-none">🏪 {d.ridesFor}</div>
|
||
)}
|
||
{d.sponsors && (
|
||
<div className="text-[5.5px] card-muted truncate leading-none">🤝 {d.sponsors}</div>
|
||
)}
|
||
{/* Skills — roles, self-claimed, then witnessed (gold) */}
|
||
<div className="flex flex-wrap gap-[2px] mt-[2px]">
|
||
{d.showRoles !== false && (d.roles ?? []).slice(0, 2).map(r => {
|
||
const m = roleMeta(r);
|
||
return m ? (
|
||
<span key={r} className="text-[5px] font-bold px-[3px] py-[1px] rounded-[2px] leading-none" style={{ background: 'rgba(100,160,255,0.18)', border: '1px solid rgba(100,160,255,0.45)', color: '#9cc4ff' }}>
|
||
{m.emoji} {t(`cardUi.roles.${m.role}`)}
|
||
</span>
|
||
) : null;
|
||
})}
|
||
{d.skills.slice(0, 3).map(sk => {
|
||
const s = SKILL_OPTIONS.find(o => o.id === sk);
|
||
return s ? (
|
||
<span key={sk} className="card-bg-chip text-[5px] font-bold px-[3px] py-[1px] rounded-[2px] leading-none">
|
||
{s.icon} {s.label}
|
||
</span>
|
||
) : null;
|
||
})}
|
||
{witnessedBadges.map((b, i) => {
|
||
const s = SKILL_OPTIONS.find(o => o.id === b.skill);
|
||
return s ? (
|
||
<span
|
||
key={`w${i}`}
|
||
className="text-[5px] font-bold px-[3px] py-[1px] rounded-[2px] leading-none"
|
||
style={{ background: 'rgba(255,215,0,0.2)', border: '1px solid rgba(255,215,0,0.5)', color: '#ffd700' }}
|
||
>
|
||
⭐ {s.label}
|
||
</span>
|
||
) : null;
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* QR */}
|
||
<div className="flex-shrink-0 flex flex-col items-end justify-end">
|
||
<CardQr url={d.verifyUrl} />
|
||
<div className="text-[4.5px] card-muted uppercase tracking-wide mt-[2px] text-right">
|
||
{d.verifyUrl ? t('labUi.rider.previewScan') : t('cardUi.notPublished')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bottom bar */}
|
||
<div
|
||
className="absolute left-0 right-0 flex items-center justify-between px-[10px]"
|
||
style={{ bottom: '6px' }}
|
||
>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">
|
||
skate-spots.org · Powered by Nostr
|
||
</span>
|
||
<span className="card-muted text-[5px] font-mono" style={{ maxWidth: '120px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{shortNpub(d.pubkey)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Card visual: Park Access front ──────────────────────────────────────────
|
||
function ParkFront({ d }: { d: CardData }) {
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<div className="absolute inset-0" style={{ padding: '10px 10px 20px 10px' }}>
|
||
|
||
<div className="flex items-center justify-between" style={{ height: '26px' }}>
|
||
<div>
|
||
<div className="text-[8px] font-black uppercase tracking-[0.15em] card-accent leading-none">
|
||
🏟️ {d.issuerName || t('labUi.rider.previewSkatepark')}
|
||
</div>
|
||
<div className="text-[6px] card-muted uppercase tracking-widest mt-[2px]">{t('labUi.rider.previewAccessPass')}</div>
|
||
</div>
|
||
<div className="card-bg-chip px-[6px] py-[2px] rounded-[4px]">
|
||
<span className="text-[7px] font-black uppercase card-accent">{d.accessLevel || 'Full Access'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-stretch gap-[8px]" style={{ height: '118px', marginTop: '6px' }}>
|
||
|
||
<div className="flex-shrink-0">
|
||
<div className="w-[52px] h-[52px] rounded-[6px] overflow-hidden border-[1.5px] border-white/20">
|
||
{d.avatar ? (
|
||
<img src={d.avatar} alt={d.recipientName} className="w-full h-full object-cover" crossOrigin="anonymous" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center card-bg-chip text-[18px]">🛹</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0 flex flex-col justify-start gap-[3px]">
|
||
<div className="text-[13px] font-black uppercase truncate leading-tight">{d.recipientName || t('labUi.rider.previewMemberName')}</div>
|
||
<div className="text-[5.5px] card-muted font-mono leading-none truncate">{shortNpub(d.pubkey)}</div>
|
||
<div className="text-[5.5px] card-muted leading-none">{t('labUi.rider.previewIssuedBy')}</div>
|
||
{d.validThru && (
|
||
<div className="flex items-center gap-1 mt-[3px]">
|
||
<span className="text-[5.5px] card-muted uppercase">{t('labUi.rider.previewValidThru')}</span>
|
||
<span className="text-[8px] font-black card-accent leading-none">{d.validThru}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex-shrink-0 flex flex-col items-end justify-end">
|
||
<CardQr url={d.verifyUrl} />
|
||
<div className="text-[4.5px] card-muted uppercase tracking-wide mt-[2px] text-right">
|
||
{d.verifyUrl ? t('labUi.rider.previewScan') : t('cardUi.notPublished')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className="absolute left-0 right-0 flex items-center justify-between px-[10px]"
|
||
style={{ bottom: '6px' }}
|
||
>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">
|
||
skate-spots.org · Powered by Nostr
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">{t('labUi.rider.previewNostrVerified')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Card visual: Crew front ─────────────────────────────────────────────────
|
||
function CrewFront({ d }: { d: CardData }) {
|
||
const { t } = useTranslation();
|
||
const c = d.crew;
|
||
if (!c) return null;
|
||
|
||
return (
|
||
<div className="absolute inset-0" style={{ padding: '10px 10px 20px 10px' }}>
|
||
|
||
{/* Row 1: header */}
|
||
<div className="flex items-start justify-between" style={{ height: '26px' }}>
|
||
<div>
|
||
<div className="text-[8px] font-black uppercase tracking-[0.18em] card-accent leading-none">
|
||
🛹 SkateSpots
|
||
</div>
|
||
<div className="text-[6px] card-muted uppercase tracking-widest mt-[2px]">{t('cardUi.crew.crewCard')}</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-[5px] card-muted uppercase tracking-wide leading-none">npub</div>
|
||
<div className="text-[6px] font-bold card-accent font-mono tracking-tight leading-none mt-[1px]">
|
||
{shortNpub(c.pubkey)}
|
||
</div>
|
||
{c.founded && <div className="text-[5px] card-muted mt-[1px]">{t('cardUi.crew.est', { year: c.founded })}</div>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Row 2: main body */}
|
||
<div className="flex items-stretch gap-[8px]" style={{ height: '118px', marginTop: '6px' }}>
|
||
|
||
{/* Logo + stats */}
|
||
<div className="flex-shrink-0 flex flex-col gap-[4px]">
|
||
<div className="w-[52px] h-[52px] rounded-[6px] overflow-hidden border-[1.5px] border-white/20">
|
||
{c.image ? (
|
||
<img src={c.image} alt={c.name} className="w-full h-full object-cover" crossOrigin="anonymous" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center card-bg-chip text-[18px]">🏕️</div>
|
||
)}
|
||
</div>
|
||
<div className="space-y-[2px]">
|
||
<div className="flex items-center justify-between gap-1">
|
||
<span className="text-[5px] card-muted">{t('cardUi.crew.members')}</span>
|
||
<span className="text-[7px] font-black card-accent leading-none">{c.members}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between gap-1">
|
||
<span className="text-[5px] card-muted">{t('cardUi.crew.spots')}</span>
|
||
<span className="text-[7px] font-black card-accent2 leading-none">{c.spots}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Identity block */}
|
||
<div className="flex-1 min-w-0 flex flex-col justify-start gap-[3px]">
|
||
<div className="text-[14px] font-black uppercase truncate leading-tight">{c.name}</div>
|
||
{c.location && (
|
||
<div className="text-[6.5px] card-accent truncate leading-none">📍 {c.location}</div>
|
||
)}
|
||
{c.lookingFor.length > 0 && (
|
||
<span
|
||
className="text-[5px] font-black uppercase px-[4px] py-[1px] rounded-[3px] w-fit leading-none"
|
||
style={{ background: 'rgba(204,255,0,0.2)', border: '1px solid rgba(204,255,0,0.5)', color: '#ccff00' }}
|
||
>
|
||
{t('cardUi.crew.openCall')}
|
||
</span>
|
||
)}
|
||
<div className="flex flex-wrap gap-[2px] mt-[2px]">
|
||
{c.vibes.slice(0, 5).map(v => (
|
||
<span key={v} className="card-bg-chip text-[5px] font-bold px-[3px] py-[1px] rounded-[2px] leading-none">
|
||
{v}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* QR */}
|
||
<div className="flex-shrink-0 flex flex-col items-end justify-end">
|
||
<CardQr url={d.verifyUrl} />
|
||
<div className="text-[4.5px] card-muted uppercase tracking-wide mt-[2px] text-right">
|
||
{d.verifyUrl ? t('labUi.rider.previewScan') : t('cardUi.notPublished')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bottom bar */}
|
||
<div
|
||
className="absolute left-0 right-0 flex items-center justify-between px-[10px]"
|
||
style={{ bottom: '6px' }}
|
||
>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">
|
||
skate-spots.org · Powered by Nostr
|
||
</span>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">{t('cardUi.crew.verifiedCrew')}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Card visual: Event pass / wristband ─────────────────────────────────────
|
||
function EventFront({ d }: { d: CardData }) {
|
||
const { t } = useTranslation();
|
||
const e = d.event;
|
||
if (!e) return null;
|
||
const dateText = formatCardDate(e.start, e.end);
|
||
|
||
return (
|
||
<div className="absolute inset-0" style={{ padding: '10px 10px 20px 10px' }}>
|
||
|
||
{/* Row 1: header */}
|
||
<div className="flex items-start justify-between" style={{ height: '26px' }}>
|
||
<div>
|
||
<div className="text-[8px] font-black uppercase tracking-[0.18em] card-accent leading-none">
|
||
🛹 SkateSpots
|
||
</div>
|
||
<div className="text-[6px] card-muted uppercase tracking-widest mt-[2px]">{t('cardUi.event.pass')}</div>
|
||
</div>
|
||
<div className="text-right min-w-0">
|
||
<div className="text-[5px] card-muted uppercase tracking-wide leading-none">{t('cardUi.event.admits')}</div>
|
||
<div className="text-[7px] font-black card-accent uppercase leading-none mt-[1px] truncate" style={{ maxWidth: '120px' }}>
|
||
{e.holder || d.name}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Row 2: main body */}
|
||
<div className="flex items-stretch gap-[8px]" style={{ height: '118px', marginTop: '6px' }}>
|
||
|
||
<div className="flex-shrink-0">
|
||
<div className="w-[52px] h-[52px] rounded-[6px] overflow-hidden border-[1.5px] border-white/20">
|
||
{e.image ? (
|
||
<img src={e.image} alt="" className="w-full h-full object-cover" crossOrigin="anonymous" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center card-bg-chip text-[18px]">🎟️</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0 flex flex-col justify-start gap-[3px]">
|
||
<div className="text-[14px] font-black uppercase truncate leading-tight">{e.title}</div>
|
||
<div className="text-[6.5px] card-accent truncate leading-none">🗓️ {dateText}</div>
|
||
{e.location && (
|
||
<div className="text-[5.5px] card-muted truncate leading-none">📍 {e.location}</div>
|
||
)}
|
||
{typeof e.going === 'number' && e.going > 0 && (
|
||
<div className="text-[5.5px] card-muted leading-none">{t('cardUi.event.going', { count: e.going })}</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex-shrink-0 flex flex-col items-end justify-end">
|
||
<CardQr url={d.verifyUrl} />
|
||
<div className="text-[4.5px] card-muted uppercase tracking-wide mt-[2px] text-right">
|
||
{d.verifyUrl ? t('labUi.rider.previewScan') : t('cardUi.notPublished')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bottom bar */}
|
||
<div
|
||
className="absolute left-0 right-0 flex items-center justify-between px-[10px]"
|
||
style={{ bottom: '6px' }}
|
||
>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">
|
||
skate-spots.org · Powered by Nostr
|
||
</span>
|
||
<span className="card-muted text-[5px] uppercase tracking-widest">{t('cardUi.event.verifiedEvent')}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function RiderCardFront({ d, printRef }: { d: CardData; printRef?: React.RefObject<HTMLDivElement> }) {
|
||
const themeClass = `card-theme-${d.theme}`;
|
||
const holoClass = d.holographic ? 'rider-card-holo' : '';
|
||
|
||
return (
|
||
<div
|
||
ref={printRef}
|
||
className={cn('rider-card', themeClass, holoClass, 'select-none')}
|
||
style={{ fontFamily: "'Inter Variable', 'Inter', system-ui, sans-serif" }}
|
||
>
|
||
<div className="card-stripe absolute top-0 left-0 right-0 h-[4px]" />
|
||
{d.type === 'rider' && <RiderFront d={d} />}
|
||
{d.type === 'park' && <ParkFront d={d} />}
|
||
{d.type === 'crew' && <CrewFront d={d} />}
|
||
{d.type === 'event' && <EventFront d={d} />}
|
||
</div>
|
||
);
|
||
}
|