diff --git a/AGENTS.md b/AGENTS.md
index eea5ffb..e52e81b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -95,6 +95,15 @@ session with **no other memory of this project**. So:
Shows title/date/venue/cover + **who it admits** + going count.
**Claims tab (2026-10-02):** `/review` has a **Claims** tab (pending
private-spot ownership claims; was a banner above the tabs).
+ **Card Studio (2026-10-02):** `/card`'s "My Card" tab is now a two-pane studio
+ — **sticky live preview** (lg) + inspector; actions collapsed to **Share** +
+ a `⋯` menu (PNG/print/copy); a **dirty/saved** state with a manual **Save
+ changes** (no autosave — an interactive signer must not prompt per keystroke);
+ card **type = compact chips**; the two toggle grids merged into **one "Show on
+ card" list** with live values. **Self-claimed skills removed** (were marginal/
+ unverifiable) — the card shows **witnessed badges** (kind 1293) only, and
+ **style/level are DERIVED** from the Skater profile (kind 35000). The canvas
+ export + `CardData` dropped the self-claimed skills row and gained `level`.
- **Marketplace**: `GearSwapPage` (grid) + `MarketplaceItemCard` and a listing
detail page at **`/listing/:pubkey/:identifier`**. Trade/escrow actions live in
the shared **`ListingTradeActions`** (used by BOTH the card and the detail page
diff --git a/NIP.md b/NIP.md
index 07aa016..331e9a1 100644
--- a/NIP.md
+++ b/NIP.md
@@ -1560,8 +1560,11 @@ author's kind 0; on-chain stats are derived live:
- `v:2` marks the `show` flags as explicit, so hiding a facet persists. An event
with **no** `v`/`show` tags defaults every facet to visible; `v:1` cards (which
never had facet flags) also default the three facets ON rather than hiding them.
-- The card shows **self-claimed** skills (`skill` tags) in the theme colour,
- **witnessed** badges (kind 1293) in gold ⭐, and **Lab roles** as blue chips.
+- The card shows **witnessed** badges (kind 1293) in gold ⭐ and **Lab roles** as
+ blue chips. Self-claimed `skill` tags are **no longer written** (unverifiable,
+ marginal) and `style` is no longer stored — **style + level are DERIVED from the
+ Skater profile** (kind 35000). `parseRiderCard` still reads legacy `style`/
+ `skill` tags so old cards don't break.
- **Identity facets are derived LIVE, never stored on the card:** crews come from
the author's crew memberships (kind 35006), the local from their skater profile
(kind 35000 `location`/`home_spot`), and the roles from their own Lab profiles
diff --git a/src/components/RiderCard.tsx b/src/components/RiderCard.tsx
index 3631ae4..2b81812 100644
--- a/src/components/RiderCard.tsx
+++ b/src/components/RiderCard.tsx
@@ -27,6 +27,8 @@ export interface CardData {
pubkey: string;
npub: string;
style: string;
+ /** Derived level from the Skater profile (kind 35000). */
+ level?: string;
skills: string[];
sponsors: string;
ridesFor: string;
@@ -201,15 +203,20 @@ function RiderFront({ d }: { d: CardData }) {
{d.style}
)}
+ {d.level && (
+
+ {d.level}
+
+ )}
{d.ridesFor && (
- {d.showRoles !== false && (d.roles ?? []).slice(0, 2).map(r => {
+ {d.showRoles !== false && (d.roles ?? []).slice(0, 3).map(r => {
const m = roleMeta(r);
return m ? (
@@ -217,14 +224,6 @@ function RiderFront({ d }: { d: CardData }) {
) : null;
})}
- {d.skills.slice(0, 3).map(sk => {
- const s = SKILL_OPTIONS.find(o => o.id === sk);
- return s ? (
-
- {s.icon} {s.label}
-
- ) : null;
- })}
{witnessedBadges.map((b, i) => {
const s = SKILL_OPTIONS.find(o => o.id === b.skill);
return s ? (
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index f6ad58c..54951d0 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -6373,6 +6373,16 @@ walletUi: {
verifiedEvent: 'Verified event',
viewEvent: 'View the event',
},
+ studio: {
+ unsaved: 'Unsaved changes',
+ saved: 'Saved',
+ saveChanges: 'Save changes',
+ allSaved: 'Saved — up to date',
+ showOnCard: 'Show on card',
+ derived: 'From your Skater profile',
+ noSkaterProfile: 'No Skater profile yet —',
+ editSkaterProfile: 'add your styles & level',
+ },
verify: {
checking: 'Checking the card…',
notFound: 'Card not found',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index b99894d..ffbb53f 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -6373,6 +6373,16 @@ walletUi: {
verifiedEvent: 'Evento verificado',
viewEvent: 'Ver el evento',
},
+ studio: {
+ unsaved: 'Cambios sin guardar',
+ saved: 'Guardado',
+ saveChanges: 'Guardar cambios',
+ allSaved: 'Guardado — al día',
+ showOnCard: 'Mostrar en el carné',
+ derived: 'Desde tu perfil de skater',
+ noSkaterProfile: 'Aún no tienes perfil de skater —',
+ editSkaterProfile: 'añade tus estilos y nivel',
+ },
verify: {
checking: 'Comprobando el carné…',
notFound: 'Carné no encontrado',
diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx
index 4f3aa3d..63ce1f1 100644
--- a/src/pages/RiderCardPage.tsx
+++ b/src/pages/RiderCardPage.tsx
@@ -21,12 +21,13 @@ import { nip19 } from 'nostr-tools';
import { LoginArea } from '@/components/auth/LoginArea';
import { RiderCardFront, type CardData, type BadgeAward } from '@/components/RiderCard';
-import { THEMES, SKILL_OPTIONS, STYLES, fullNpub, memberSince, roleMeta, formatCardDate, type ThemeId } from '@/lib/riderCard';
+import { THEMES, SKILL_OPTIONS, fullNpub, memberSince, roleMeta, formatCardDate, type ThemeId } from '@/lib/riderCard';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
@@ -47,9 +48,10 @@ import { genUserName } from '@/lib/genUserName';
import { useTranslation } from '@/contexts/language';
import { cn } from '@/lib/utils';
import { RIDER_CARD_KIND, RIDER_CARD_D, PARK_ACCESS_CARD_KIND, SKILL_BADGE_KIND, cardVerifyUrl, riderCardTags, type RiderCardConfig } from '@/lib/riderCard';
-import { SKATE_SPOT_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND } from '@/lib/types';
+import { SKATE_SPOT_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND, SKATER_STYLES, SKATER_LEVELS } from '@/lib/types';
import type { NostrEvent } from '@nostrify/nostrify';
-import { Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, ChevronUp, ChevronDown, User, Building2, Users, Ticket } from 'lucide-react';
+import { Link } from 'react-router-dom';
+import { Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, ChevronUp, ChevronDown, User, Building2, Users, Ticket, MoreHorizontal, Pencil, CheckCircle2 } from 'lucide-react';
// ─── Card canvas renderer ─────────────────────────────────────────────────────
// Draws the card entirely via Canvas 2D API — 100% reliable export,
// no CSS capture, no scaling issues, no background-stripping.
@@ -67,6 +69,7 @@ interface CanvasCardData {
pubkey: string;
npub: string;
style: string;
+ level?: string;
skills: string[];
sponsors: string;
ridesFor: string;
@@ -346,6 +349,22 @@ async function drawCardToCanvas(data: CanvasCardData): Promise
{
ry += 22;
}
+ // Level chip (derived from the Skater profile)
+ if (data.level) {
+ ctx.font = 'bold 16px system-ui, sans-serif';
+ const lvlTxt = data.level.toUpperCase();
+ const lvlW = ctx.measureText(lvlTxt).width + 24;
+ roundRect(ctx, RX, ry - 18, lvlW, 26, 6);
+ ctx.fillStyle = 'rgba(255,102,0,0.18)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(255,102,0,0.45)';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ ctx.fillStyle = '#ff9944';
+ ctx.fillText(lvlTxt, RX + 12, ry - 1);
+ ry += 22;
+ }
+
// Local + crew crests (identity facets)
if (data.local) {
ctx.fillStyle = p.accent;
@@ -397,11 +416,7 @@ async function drawCardToCanvas(data: CanvasCardData): Promise {
const m = roleMeta(r);
return m ? { label: `${m.emoji} ${m.label}`, gold: false } : null;
}).filter(Boolean),
- ...data.skills.slice(0, 4).map(id => {
- const s = SKILL_OPTIONS.find(o => o.id === id);
- return s ? { label: s.label, gold: false } : null;
- }).filter(Boolean),
- ...data.receivedBadges.slice(0, 3).map(b => {
+ ...data.receivedBadges.slice(0, 4).map(b => {
const s = SKILL_OPTIONS.find(o => o.id === b.skill);
return s ? { label: `★ ${s.label}`, gold: true } : null;
}).filter(Boolean),
@@ -824,6 +839,14 @@ export default function RiderCardPage() {
[memberships],
);
const local = skaterProfile?.location || skaterProfile?.homeSpot || '';
+ // Style + level are DERIVED from the Skater profile — no manual typing.
+ const derivedStyle = useMemo(
+ () => (skaterProfile?.styles ?? []).map(s => SKATER_STYLES[s]?.label ?? s).join(' · '),
+ [skaterProfile],
+ );
+ const derivedLevel = skaterProfile?.level
+ ? (SKATER_LEVELS[skaterProfile.level]?.label ?? '')
+ : (skaterProfile?.customLevel ?? '');
const { mutate: publishEvent } = useNostrPublish();
const { toast } = useToast();
const printRef = useRef(null);
@@ -853,8 +876,6 @@ export default function RiderCardPage() {
const [holographic, setHolographic] = useState(true);
// Rider fields
- const [style, setStyle] = useState('Street');
- const [skills, setSkills] = useState([]);
const [sponsors, setSponsors] = useState('');
const [ridesFor, setRidesFor] = useState('');
const [showSpots, setShowSpots] = useState(true);
@@ -904,8 +925,6 @@ export default function RiderCardPage() {
hydratedRef.current = true;
const c = savedCard.config;
setTheme(c.theme as ThemeId);
- setStyle(c.style);
- setSkills(c.skills);
setSponsors(c.sponsors);
setRidesFor(c.ridesFor);
setShowSpots(c.showSpots);
@@ -916,6 +935,18 @@ export default function RiderCardPage() {
setShowRoles(c.showRoles);
}, [savedCard]);
+ // Unsaved-changes tracking for the personal card (manual save — an
+ // interactive signer must not be prompted on every keystroke).
+ const isDirty = useMemo(() => {
+ const c = savedCard?.config;
+ if (!c) return true;
+ return (
+ c.theme !== theme || c.sponsors !== sponsors || c.ridesFor !== ridesFor ||
+ c.showSpots !== showSpots || c.showZaps !== showZaps || c.showEvents !== showEvents ||
+ c.showCrews !== showCrews || c.showLocal !== showLocal || c.showRoles !== showRoles
+ );
+ }, [savedCard, theme, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles]);
+
const recipientPubkey = (() => {
if (!recipientNpub.trim()) return user?.pubkey ?? '';
try {
@@ -939,7 +970,10 @@ export default function RiderCardPage() {
avatar: cardType === 'crew' ? activeCrew?.image : cardType === 'event' ? activeEvent?.image : metadata?.picture,
pubkey: cardPubkey,
npub: cardPubkey ? fullNpub(cardPubkey) : '',
- style, skills, sponsors, ridesFor,
+ style: derivedStyle,
+ level: derivedLevel || undefined,
+ skills: [],
+ sponsors, ridesFor,
showSpots, showZaps, showEvents,
crews: crewCrests,
local: local || undefined,
@@ -987,7 +1021,7 @@ export default function RiderCardPage() {
}),
[
cardType, theme, holographic, displayName, recipientName, metadata,
- cardPubkey, style, skills, sponsors, ridesFor, showSpots, showZaps,
+ cardPubkey, derivedStyle, derivedLevel, sponsors, ridesFor, showSpots, showZaps,
showEvents, stats, receivedBadges, issuerName, accessLevel, validThru, savedParkD, user?.pubkey,
crewCrests, local, roles, showCrews, showLocal, showRoles, activeCrew, activeEvent, eventRsvp.goingCount,
],
@@ -1020,7 +1054,7 @@ export default function RiderCardPage() {
const handleSaveCard = useCallback(() => {
if (!user) return;
- const config: RiderCardConfig = { theme, style, skills, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles };
+ const config: RiderCardConfig = { theme, style: '', skills: [], sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles };
publishEvent(
{ kind: RIDER_CARD_KIND, content: '', tags: riderCardTags(config) },
{
@@ -1028,7 +1062,7 @@ export default function RiderCardPage() {
onError: (e) => toast({ title: t('cardUi.saveFailed'), description: String(e), variant: 'destructive' }),
},
);
- }, [user, theme, style, skills, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles, publishEvent, toast, t]);
+ }, [user, theme, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles, publishEvent, toast, t]);
const handlePrint = useCallback(async () => {
const dataUrl = await renderCardToImage();
@@ -1089,9 +1123,6 @@ export default function RiderCardPage() {
});
}, [user, recipientPubkey, issuerName, accessLevel, validThru, theme, venueSpot, publishEvent, toast, t]);
- const toggleSkill = (id: string) =>
- setSkills(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]);
-
// ── Logged out ────────────────────────────────────────────────────────────
if (!user) {
return (
@@ -1120,7 +1151,7 @@ export default function RiderCardPage() {
return (
<>
-
+
{/* ── Page header ── */}
@@ -1131,8 +1162,8 @@ export default function RiderCardPage() {
{t('labUi.rider.headerTagline')}
- {/* ── Top tabs: My Card / My Badges / Award Badge ── */}
-
+ {/* ── Tabs: My Card / Badges / Award ── */}
+
{([
{ id: 'card', label: `🪪 ${t('labUi.rider.tabCard')}` },
{ id: 'badges', label: `🏅 ${t('labUi.rider.tabBadges')}${receivedBadges.length > 0 ? ` (${receivedBadges.length})` : ''}` },
@@ -1143,9 +1174,7 @@ export default function RiderCardPage() {
onClick={() => setPageTab(tab.id)}
className={cn(
'flex-1 py-3 text-xs font-black uppercase tracking-widest transition-all',
- pageTab === tab.id
- ? 'bg-[#ff0040] text-white'
- : 'text-gray-500 hover:text-gray-300 hover:bg-white/5'
+ pageTab === tab.id ? 'bg-[#ff0040] text-white' : 'text-gray-500 hover:bg-white/5 hover:text-gray-300'
)}
>
{tab.label}
@@ -1153,398 +1182,347 @@ export default function RiderCardPage() {
))}
- {/* ══════════════════════════════════════════════
- TAB: MY CARD
- ══════════════════════════════════════════════ */}
{pageTab === 'card' && (
-
+
- {/* ── Hero card preview ── */}
-
- {/* Stage */}
+ {/* ══ LEFT: sticky preview + actions ══ */}
+
- {/* Subtle glow behind card */}
-
- {/* Primary CTA */}
-
- {rendering ? : }
- {rendering ? t('labUi.rider.rendering') : t('labUi.rider.shareCard')}
-
+ {/* Saved state */}
+ {cardType === 'rider' && (
+
+ {isDirty
+ ?
{t('cardUi.studio.unsaved')}
+ :
{t('cardUi.studio.saved')}}
+
+ )}
- {/* Secondary actions row */}
-
+ {/* Primary action: Share + More */}
+
-
- {t('labUi.rider.savePng')}
-
-
-
- {t('labUi.rider.print')}
-
-
- {copied ? : }
- {copied ? t('common.copied') : t('labUi.rider.copyId')}
+ {rendering ? : }
+ {rendering ? t('labUi.rider.rendering') : t('labUi.rider.shareCard')}
+
+
+
+
+
+
+
+
+ {t('labUi.rider.savePng')}
+
+
+ {t('labUi.rider.print')}
+
+
+ {copied ? : }
+ {copied ? t('common.copied') : t('labUi.rider.copyId')}
+
+
+
- {/* Save the card to Nostr so it persists & verifies */}
+ {/* Save (personal card only) */}
{cardType === 'rider' && (
- {t('cardUi.save')}
+ {isDirty ? t('cardUi.studio.saveChanges') : t('cardUi.studio.allSaved')}
)}
- {/* ── Card type toggle ── */}
-
-
{t('labUi.rider.cardType')}
-
- {([
- { id: 'rider', label: t('labUi.rider.licenseLabel'), sub: t('labUi.rider.licenseSub'), icon: User },
- { id: 'park', label: t('labUi.rider.parkLabel'), sub: t('labUi.rider.parkSub'), icon: Building2 },
- { id: 'crew', label: t('cardUi.crew.crewCard'), sub: t('cardUi.crew.crewSub'), icon: Users },
- { id: 'event', label: t('cardUi.event.pass'), sub: t('cardUi.event.passSub'), icon: Ticket },
- ] as const).map(({ id, label, sub, icon: Icon }) => (
-
setCardType(id)}
- className={cn(
- 'p-4 rounded-xl border text-left transition-all',
- cardType === id
- ? 'bg-[#ff0040]/10 border-[#ff0040]/60 text-white'
- : 'bg-black/30 border-white/8 text-gray-500 hover:border-white/20 hover:text-gray-300'
- )}
- >
-
- {label}
- {sub}
-
- ))}
-
-
+ {/* ══ RIGHT: inspector ══ */}
+
- {/* ── Theme picker ── */}
-
-
- {t('labUi.rider.theme')}
-
-
- {THEMES.map((th) => (
- setTheme(th.id)}
- className={cn(
- 'aspect-square rounded-xl border-2 transition-all flex flex-col items-center justify-center gap-0.5',
- `card-theme-${th.id}`,
- theme === th.id
- ? 'ring-2 ring-white/50 ring-offset-1 ring-offset-black scale-105 border-white/30'
- : 'border-white/10 opacity-60 hover:opacity-90'
- )}
- >
- {th.emoji}
- {th.label}
-
- ))}
-
-
-
- {/* ── Holographic toggle ── */}
-
+ {/* Card type */}
-
✨ {t('labUi.rider.holoTitle')}
-
{t('labUi.rider.holoDesc')}
-
-
-
-
- {/* ── RIDER fields ── */}
- {cardType === 'rider' && (
-
-
{t('labUi.rider.cardDetails')}
-
- {/* Style */}
-
- {t('labUi.rider.skateStyle')}
-
-
-
-
-
- {STYLES.map(s => {s} )}
-
-
-
-
- {/* Rides For + Sponsors — side by side */}
-
-
- {/* Stats toggles */}
-
-
{t('labUi.rider.showOnCard')}
-
- {[
- { label: t('labUi.pyramid.spots'), value: stats?.spotsAdded ?? 0, state: showSpots, set: setShowSpots },
- { label: t('labUi.rider.zaps'), value: stats?.zapsReceived ?? 0, state: showZaps, set: setShowZaps },
- { label: t('labUi.pyramid.events'), value: stats?.eventsCreated ?? 0, state: showEvents, set: setShowEvents },
- ].map(({ label, value, state, set }) => (
-
set(!state)}
- className={cn(
- 'p-3 rounded-xl border text-center transition-all',
- state
- ? 'bg-[#ff0040]/10 border-[#ff0040]/40 text-white'
- : 'bg-black/30 border-white/8 text-gray-600'
- )}
- >
- {value}
- {label}
-
- ))}
-
-
-
- {/* Identity facets */}
-
-
{t('cardUi.facets.title')}
-
- {[
- { label: t('cardUi.facets.crews'), value: crewCrests.length, state: showCrews, set: setShowCrews },
- { label: t('cardUi.facets.local'), value: local ? '📍' : '—', state: showLocal, set: setShowLocal },
- { label: t('cardUi.facets.roles'), value: roles.length, state: showRoles, set: setShowRoles },
- ].map(({ label, value, state, set }) => (
-
set(!state)}
- className={cn(
- 'p-3 rounded-xl border text-center transition-all',
- state
- ? 'bg-[#ff0040]/10 border-[#ff0040]/40 text-white'
- : 'bg-black/30 border-white/8 text-gray-600'
- )}
- >
- {value}
- {label}
-
- ))}
-
-
-
- {/* Skills */}
-
-
- {t('labUi.rider.skills')}
- {t('labUi.rider.selected', { count: skills.length })}
-
-
- {SKILL_OPTIONS.map(s => (
- toggleSkill(s.id)}
- disabled={!skills.includes(s.id) && skills.length >= 6}
- className={cn(
- 'px-2.5 py-1.5 rounded-lg text-xs font-bold border transition-all',
- skills.includes(s.id)
- ? 'bg-[#ff0040]/15 border-[#ff0040]/50 text-white'
- : 'bg-black/30 border-white/8 text-gray-500 hover:border-white/20 disabled:opacity-25 disabled:cursor-not-allowed'
- )}
- >
- {s.icon} {s.label}
-
- ))}
-
-
- ⭐ {t('labUi.rider.goldBadgesHint1')} setPageTab('badges')} className="text-yellow-500 underline">{t('labUi.rider.tabBadges')} {t('labUi.rider.goldBadgesHint2')}
-
+
{t('labUi.rider.cardType')}
+
+ {([
+ { id: 'rider', label: t('labUi.rider.licenseLabel'), icon: User },
+ { id: 'park', label: t('labUi.rider.parkLabel'), icon: Building2 },
+ { id: 'crew', label: t('cardUi.crew.crewCard'), icon: Users },
+ { id: 'event', label: t('cardUi.event.pass'), icon: Ticket },
+ ] as const).map(({ id, label, icon: Icon }) => (
+ setCardType(id)}
+ className={cn(
+ 'flex flex-col items-center gap-1.5 rounded-xl border px-2 py-3 text-center transition-all',
+ cardType === id
+ ? 'border-[#ff0040]/60 bg-[#ff0040]/10 text-white'
+ : 'border-white/8 bg-black/30 text-gray-500 hover:border-white/20 hover:text-gray-300'
+ )}
+ >
+
+ {label}
+
+ ))}
- )}
- {/* ── PARK fields ── */}
- {cardType === 'park' && (
-
-
-
-
- {t('labUi.rider.parkOwnersOnly')} {' '}
- {t('labUi.rider.parkOwnersDesc')}
-
-
-
-
{t('labUi.rider.parkDetails')}
-
-
-
- {t('labUi.rider.parkName')}
- setIssuerName(e.target.value)}
- placeholder={t('labUi.rider.parkNamePlaceholder')} className="bg-black/50 border-white/10 h-11" />
-
-
- {t('labUi.rider.riderName')}
- setRecipientName(e.target.value)}
- placeholder={t('labUi.rider.riderNamePlaceholder')} className="bg-black/50 border-white/10 h-11" />
-
+ {/* Theme */}
+
+
+ {t('labUi.rider.theme')}
+
+
+ {THEMES.map((th) => (
+ setTheme(th.id)}
+ className={cn(
+ 'flex aspect-square flex-col items-center justify-center gap-0.5 rounded-xl border-2 transition-all',
+ `card-theme-${th.id}`,
+ theme === th.id ? 'scale-105 border-white/30 ring-2 ring-white/50 ring-offset-1 ring-offset-black' : 'border-white/10 opacity-60 hover:opacity-90'
+ )}
+ >
+ {th.emoji}
+ {th.label}
+
+ ))}
+
+ {/* Holographic */}
+
-
{t('labUi.rider.riderNpub')}
-
setRecipientNpub(e.target.value)}
- placeholder="npub1…"
- className="bg-black/50 border-white/10 font-mono text-xs h-11"
- />
+
✨ {t('labUi.rider.holoTitle')}
+
{t('labUi.rider.holoDesc')}
+
+
-
+ {/* Show on card — ONE clear list */}
+ {cardType === 'rider' && (
+ <>
-
{t('labUi.rider.accessLevel')}
-
-
+ {t('cardUi.studio.showOnCard')}
+
+ {[
+ { icon: '🛹', label: t('labUi.pyramid.spots'), value: String(stats?.spotsAdded ?? 0), state: showSpots, set: setShowSpots },
+ { icon: '⚡', label: t('labUi.rider.zaps'), value: String(stats?.zapsReceived ?? 0), state: showZaps, set: setShowZaps },
+ { icon: '📅', label: t('labUi.pyramid.events'), value: String(stats?.eventsCreated ?? 0), state: showEvents, set: setShowEvents },
+ { icon: '🏕️', label: t('cardUi.facets.crews'), value: String(crewCrests.length), state: showCrews, set: setShowCrews },
+ { icon: '📍', label: t('cardUi.facets.local'), value: local || '—', state: showLocal, set: setShowLocal },
+ { icon: '🎓', label: t('cardUi.facets.roles'), value: String(roles.length), state: showRoles, set: setShowRoles },
+ ].map((row) => (
+
+
+
{row.icon}
+
+
{row.label}
+
{row.value}
+
+
+
+
+ ))}
+
+
+
+ {/* Details */}
+
+
{t('labUi.rider.cardDetails')}
+
+
+
{t('cardUi.studio.derived')}
+
+ {derivedStyle && {derivedStyle} }
+ {derivedLevel && {derivedLevel} }
+ {!derivedStyle && !derivedLevel && (
+
+ {t('cardUi.studio.noSkaterProfile')}{' '}
+ {t('cardUi.studio.editSkaterProfile')}
+
+ )}
+
+
+
+
+
+
+ ⭐ {t('labUi.rider.goldBadgesHint1')}{' '}
+ setPageTab('badges')} className="text-yellow-500 underline">{t('labUi.rider.tabBadges')}
+ {t('labUi.rider.goldBadgesHint2')}
+
+
+ >
+ )}
+
+ {/* ── PARK fields ── */}
+ {cardType === 'park' && (
+
+
+
+
+ {t('labUi.rider.parkOwnersOnly')} {' '}
+ {t('labUi.rider.parkOwnersDesc')}
+
+
+
+
+
+
+ {t('labUi.rider.riderNpub')}
+ setRecipientNpub(e.target.value)}
+ placeholder="npub1…"
+ className="h-11 border-white/10 bg-black/50 font-mono text-xs"
+ />
+
+
+
+
+ {t('labUi.rider.accessLevel')}
+
+
+
+ {['Full Access','Supervised','Beginner Zone','Street Only','Bowl Only','Members Only'].map(l =>
+ {l} )}
+
+
+
+
+ {t('labUi.rider.validThru')}
+ setValidThru(e.target.value)}
+ placeholder="2026-12-31" className="h-11 border-white/10 bg-black/50" />
+
+
+
+
+
{t('cardUi.venue')}
+
setVenueId(v === '__none' ? '' : v)}>
+
- {['Full Access','Supervised','Beginner Zone','Street Only','Bowl Only','Members Only'].map(l =>
- {l} )}
+ {t('cardUi.venueNone')}
+ {myVenues.map(s => {s.title} )}
+
{t('cardUi.venueHint')}
-
- {t('labUi.rider.validThru')}
- setValidThru(e.target.value)}
- placeholder="2026-12-31" className="bg-black/50 border-white/10 h-11" />
-
+
+
+
+ {t('labUi.rider.publishToNostr')}
+
+ )}
-
-
{t('cardUi.venue')}
-
setVenueId(v === '__none' ? '' : v)}>
-
-
- {t('cardUi.venueNone')}
- {myVenues.map(s => {s.title} )}
-
-
-
{t('cardUi.venueHint')}
+ {/* ── CREW fields ── */}
+ {cardType === 'crew' && (
+
+
{t('cardUi.crew.pick')}
+ {memberships.length > 0 ? (
+
+
+
+ {memberships.map(m => (
+ {m.crew.name} · {m.role}
+ ))}
+
+
+ ) : (
+
{t('cardUi.crew.none')}
+ )}
+
{t('cardUi.crew.hint')}
+ )}
-
-
- {t('labUi.rider.publishToNostr')}
-
-
- )}
-
- {/* ── CREW fields ── */}
- {cardType === 'crew' && (
-
-
{t('cardUi.crew.pick')}
- {memberships.length > 0 ? (
-
-
-
- {memberships.map(m => (
- {m.crew.name} · {m.role}
- ))}
-
-
- ) : (
-
{t('cardUi.crew.none')}
- )}
-
{t('cardUi.crew.hint')}
-
- )}
-
- {/* ── EVENT fields (pass / wristband) ── */}
- {cardType === 'event' && (
-
-
{t('cardUi.event.pick')}
- {myEvents.length > 0 ? (
-
-
-
- {myEvents.map(ev => (
- {ev.title}
- ))}
-
-
- ) : (
-
{t('cardUi.event.none')}
- )}
-
{t('cardUi.event.hint')}
-
- )}
+ {/* ── EVENT fields (pass / wristband) ── */}
+ {cardType === 'event' && (
+
+
{t('cardUi.event.pick')}
+ {myEvents.length > 0 ? (
+
+
+
+ {myEvents.map(ev => (
+ {ev.title}
+ ))}
+
+
+ ) : (
+
{t('cardUi.event.none')}
+ )}
+
{t('cardUi.event.hint')}
+
+ )}
+
)}
- {/* ══════════════════════════════════════════════
- TAB: MY BADGES
- ══════════════════════════════════════════════ */}
+ {/* TAB: MY BADGES */}
{pageTab === 'badges' && (
-
-
-
- {t('labUi.rider.badgesIntro')}
-
+
+
+
{t('labUi.rider.badgesIntro')}
-
-
{t('labUi.rider.earnMore')}
+
+
{t('labUi.rider.earnMore')}
setPageTab('award')}
- className="w-full border-yellow-500/30 text-yellow-400 hover:bg-yellow-500/10 gap-2"
+ className="w-full gap-2 border-yellow-500/30 text-yellow-400 hover:bg-yellow-500/10"
>
{t('labUi.rider.awardSomeone')}
@@ -1553,20 +1531,17 @@ export default function RiderCardPage() {
)}
- {/* ══════════════════════════════════════════════
- TAB: AWARD BADGE (inline, no dialog)
- ══════════════════════════════════════════════ */}
+ {/* TAB: AWARD BADGE */}
{pageTab === 'award' && (
-
-
-
+
+
+
{t('labUi.rider.youSawIt')}
-
{t('labUi.rider.youSawItDesc')}
+
{t('labUi.rider.youSawItDesc')}
- {/* Inline award form — the dialog was removed as dead code */}
setPageTab('badges')} />
)}