From 989110145202adb384016724b64f2952784c2091 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Fri, 2 Oct 2026 11:24:51 +0700 Subject: [PATCH] feat(card): Card Studio redesign + drop self-claimed skills - Two-pane studio: sticky live preview (desktop) + inspector column; mobile stacks preview-first. - Actions decluttered to a single primary Share + a '...' menu (Save PNG / Print / Copy ID); removed the competing big buttons. - Unsaved-changes state + manual Save changes button (no autosave, so an interactive signer like Amber isn't prompted per keystroke). - Card type is now a compact 4-up chip row; the two toggle grids merged into ONE 'Show on card' list with live values. - Skills: removed the self-claimed skill chips (marginal/unverifiable). The card shows witnessed badges (kind 1293) only; style + level are now DERIVED from the Skater profile (kind 35000) instead of a manual 'Skate Style' select. - React card + canvas export updated; i18n cardUi.studio.*; NIP.md + AGENTS. --- AGENTS.md | 9 + NIP.md | 7 +- src/components/RiderCard.tsx | 19 +- src/i18n/en.ts | 10 + src/i18n/es.ts | 10 + src/pages/RiderCardPage.tsx | 717 +++++++++++++++++------------------ 6 files changed, 389 insertions(+), 383 deletions(-) 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.ridesFor}
)} {d.sponsors && (
🤝 {d.sponsors}
)} - {/* Skills — roles, self-claimed, then witnessed (gold) */} + {/* Roles + witnessed badges only — self-claimed skills are gone */}
- {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 */} - + {/* 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')} + + +
- {/* Save the card to Nostr so it persists & verifies */} + {/* Save (personal card only) */} {cardType === 'rider' && ( )}
- {/* ── 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 }) => ( - - ))} -
-
+ {/* ══ RIGHT: inspector ══ */} +
- {/* ── Theme picker ── */} -
-

- {t('labUi.rider.theme')} -

-
- {THEMES.map((th) => ( - - ))} -
-
- - {/* ── Holographic toggle ── */} -
+ {/* Card type */}
-
✨ {t('labUi.rider.holoTitle')}
-
{t('labUi.rider.holoDesc')}
-
- -
- - {/* ── RIDER fields ── */} - {cardType === 'rider' && ( -
-

{t('labUi.rider.cardDetails')}

- - {/* Style */} -
- - -
- - {/* Rides For + Sponsors — side by side */} -
-
- - setRidesFor(e.target.value)} - className="bg-black/50 border-white/10 text-sm h-11" - /> -
-
- - setSponsors(e.target.value)} - className="bg-black/50 border-white/10 text-sm h-11" - /> -
-
- - {/* Stats toggles */} -
- -
- {[ - { 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 }) => ( - - ))} -
-
- - {/* Identity facets */} -
- -
- {[ - { 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 }) => ( - - ))} -
-
- - {/* Skills */} -
-
- - {t('labUi.rider.selected', { count: skills.length })} -
-
- {SKILL_OPTIONS.map(s => ( - - ))} -
-

- ⭐ {t('labUi.rider.goldBadgesHint1')} {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 }) => ( + + ))}
- )} - {/* ── PARK fields ── */} - {cardType === 'park' && ( -
-
- -
- {t('labUi.rider.parkOwnersOnly')}{' '} - {t('labUi.rider.parkOwnersDesc')} -
-
- -

{t('labUi.rider.parkDetails')}

- -
-
- - setIssuerName(e.target.value)} - placeholder={t('labUi.rider.parkNamePlaceholder')} className="bg-black/50 border-white/10 h-11" /> -
-
- - 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) => ( + + ))}
+
+ {/* Holographic */} +
- - 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' && ( + <>
- - setRidesFor(e.target.value)} + className="h-11 border-white/10 bg-black/50 text-sm" + /> +
+
+ + setSponsors(e.target.value)} + className="h-11 border-white/10 bg-black/50 text-sm" + /> +
+
+ +

+ ⭐ {t('labUi.rider.goldBadgesHint1')}{' '} + + {t('labUi.rider.goldBadgesHint2')} +

+
+ + )} + + {/* ── PARK fields ── */} + {cardType === 'park' && ( +
+
+ +
+ {t('labUi.rider.parkOwnersOnly')}{' '} + {t('labUi.rider.parkOwnersDesc')} +
+
+ +
+
+ + setIssuerName(e.target.value)} + placeholder={t('labUi.rider.parkNamePlaceholder')} className="h-11 border-white/10 bg-black/50" /> +
+
+ + setRecipientName(e.target.value)} + placeholder={t('labUi.rider.riderNamePlaceholder')} className="h-11 border-white/10 bg-black/50" /> +
+
+ +
+ + setRecipientNpub(e.target.value)} + placeholder="npub1…" + className="h-11 border-white/10 bg-black/50 font-mono text-xs" + /> +
+ +
+
+ + +
+
+ + setValidThru(e.target.value)} + placeholder="2026-12-31" className="h-11 border-white/10 bg-black/50" /> +
+
+ +
+ + +

{t('cardUi.venueHint')}

-
- - setValidThru(e.target.value)} - placeholder="2026-12-31" className="bg-black/50 border-white/10 h-11" /> -
+ +
+ )} -
- - -

{t('cardUi.venueHint')}

+ {/* ── CREW fields ── */} + {cardType === 'crew' && ( +
+

{t('cardUi.crew.pick')}

+ {memberships.length > 0 ? ( + + ) : ( +

{t('cardUi.crew.none')}

+ )} +

{t('cardUi.crew.hint')}

+ )} - -
- )} - - {/* ── CREW fields ── */} - {cardType === 'crew' && ( -
-

{t('cardUi.crew.pick')}

- {memberships.length > 0 ? ( - - ) : ( -

{t('cardUi.crew.none')}

- )} -

{t('cardUi.crew.hint')}

-
- )} - - {/* ── EVENT fields (pass / wristband) ── */} - {cardType === 'event' && ( -
-

{t('cardUi.event.pick')}

- {myEvents.length > 0 ? ( - - ) : ( -

{t('cardUi.event.none')}

- )} -

{t('cardUi.event.hint')}

-
- )} + {/* ── EVENT fields (pass / wristband) ── */} + {cardType === 'event' && ( +
+

{t('cardUi.event.pick')}

+ {myEvents.length > 0 ? ( + + ) : ( +

{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')}

)} - {/* ══════════════════════════════════════════════ - 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')} />
)}