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.
This commit is contained in:
SkateSpots 2026-10-02 11:24:51 +07:00
parent 7cafcc0dfa
commit 9891101452
6 changed files with 389 additions and 383 deletions

View file

@ -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

7
NIP.md
View file

@ -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

View file

@ -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}
</span>
)}
{d.level && (
<span className="text-[5.5px] font-bold uppercase px-[4px] py-[1px] rounded-[3px] w-fit leading-none" style={{ background: 'rgba(255,102,0,0.18)', border: '1px solid rgba(255,102,0,0.45)', color: '#ff9944' }}>
{d.level}
</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) */}
{/* Roles + witnessed badges only — self-claimed skills are gone */}
<div className="flex flex-wrap gap-[2px] mt-[2px]">
{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 ? (
<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' }}>
@ -217,14 +224,6 @@ function RiderFront({ d }: { d: CardData }) {
</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 ? (

View file

@ -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',

View file

@ -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',

View file

@ -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<string> {
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<string> {
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<HTMLDivElement>(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<string[]>([]);
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 (
<>
<div className="max-w-2xl mx-auto px-4 pb-16">
<div className="mx-auto max-w-5xl px-4 pb-24">
{/* ── Page header ── */}
<div className="pt-8 pb-6 text-center">
@ -1131,8 +1162,8 @@ export default function RiderCardPage() {
<p className="text-gray-500 text-xs tracking-widest uppercase">{t('labUi.rider.headerTagline')}</p>
</div>
{/* ── Top tabs: My Card / My Badges / Award Badge ── */}
<div className="flex rounded-xl overflow-hidden border border-white/10 mb-8 bg-black/40">
{/* ── Tabs: My Card / Badges / Award ── */}
<div className="mx-auto mb-8 flex max-w-2xl overflow-hidden rounded-xl border border-white/10 bg-black/40">
{([
{ 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() {
))}
</div>
{/* ══════════════════════════════════════════════
TAB: MY CARD
══════════════════════════════════════════════ */}
{pageTab === 'card' && (
<div className="space-y-8">
<div className="lg:grid lg:grid-cols-[minmax(0,1fr)_400px] lg:items-start lg:gap-8">
{/* ── Hero card preview ── */}
<div className="flex flex-col items-center gap-4">
{/* Stage */}
{/* ══ LEFT: sticky preview + actions ══ */}
<div className="space-y-4 lg:sticky lg:top-20">
<div
className="w-full rounded-2xl flex items-center justify-center py-8 relative overflow-hidden"
className="relative flex w-full items-center justify-center overflow-hidden rounded-2xl py-8"
style={{ background: 'radial-gradient(ellipse at 50% 60%, rgba(255,0,64,0.08) 0%, rgba(0,0,0,0) 70%), #0a0a0a', border: '1px solid rgba(255,255,255,0.06)' }}
>
{/* Subtle glow behind card */}
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-32 blur-3xl opacity-30"
<div
className="absolute inset-x-0 top-1/2 h-32 -translate-y-1/2 opacity-30 blur-3xl"
style={{ background: `radial-gradient(ellipse, ${theme === 'lime' ? '#ccff00' : theme === 'midnight' ? '#64a0ff' : theme === 'wood' ? '#f5c842' : theme === 'chrome' ? '#888' : theme === 'sunset' ? '#ff6020' : '#ff0040'}, transparent)` }}
/>
<RiderCardFront d={cardData} printRef={printRef} />
</div>
{/* Primary CTA */}
<Button
onClick={handleShare}
disabled={rendering}
size="lg"
className="w-full h-14 bg-gradient-to-r from-[#ff0040] to-[#ff6600] hover:opacity-90 font-black uppercase text-base rounded-xl shadow-xl shadow-[#ff0040]/20 gap-2"
>
{rendering ? <Loader2 className="h-5 w-5 animate-spin" /> : <Share2 className="h-5 w-5" />}
{rendering ? t('labUi.rider.rendering') : t('labUi.rider.shareCard')}
</Button>
{/* Saved state */}
{cardType === 'rider' && (
<div className="flex items-center justify-center text-[10px] font-black uppercase tracking-widest">
{isDirty
? <span className="inline-flex items-center gap-1.5 text-amber-400"><Pencil className="h-3 w-3" /> {t('cardUi.studio.unsaved')}</span>
: <span className="inline-flex items-center gap-1.5 text-green-400"><CheckCircle2 className="h-3 w-3" /> {t('cardUi.studio.saved')}</span>}
</div>
)}
{/* Secondary actions row */}
<div className="grid grid-cols-3 gap-2 w-full">
{/* Primary action: Share + More */}
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleDownload}
onClick={handleShare}
disabled={rendering}
className="gap-1.5 border-white/10 hover:border-white/30 text-xs font-bold uppercase h-10"
size="lg"
className="h-12 flex-1 gap-2 rounded-xl bg-gradient-to-r from-[#ff0040] to-[#ff6600] font-black uppercase shadow-xl shadow-[#ff0040]/20 hover:opacity-90"
>
<Download className="h-3.5 w-3.5" />
{t('labUi.rider.savePng')}
</Button>
<Button
variant="outline"
onClick={handlePrint}
disabled={rendering}
className="gap-1.5 border-white/10 hover:border-white/30 text-xs font-bold uppercase h-10"
>
<Printer className="h-3.5 w-3.5" />
{t('labUi.rider.print')}
</Button>
<Button
variant="outline"
onClick={handleCopyNpub}
className="gap-1.5 border-white/10 hover:border-white/30 text-xs font-bold uppercase h-10"
>
{copied ? <Check className="h-3.5 w-3.5 text-green-400" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? t('common.copied') : t('labUi.rider.copyId')}
{rendering ? <Loader2 className="h-5 w-5 animate-spin" /> : <Share2 className="h-5 w-5" />}
{rendering ? t('labUi.rider.rendering') : t('labUi.rider.shareCard')}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="lg" className="h-12 w-12 rounded-xl border-white/10 px-0">
<MoreHorizontal className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={handleDownload} disabled={rendering} className="gap-2">
<Download className="h-4 w-4" /> {t('labUi.rider.savePng')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handlePrint} disabled={rendering} className="gap-2">
<Printer className="h-4 w-4" /> {t('labUi.rider.print')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopyNpub} className="gap-2">
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
{copied ? t('common.copied') : t('labUi.rider.copyId')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Save the card to Nostr so it persists & verifies */}
{/* Save (personal card only) */}
{cardType === 'rider' && (
<Button
onClick={handleSaveCard}
className="w-full h-12 bg-[#ccff00] hover:bg-[#b8e600] text-black font-black uppercase gap-2"
disabled={!isDirty}
className={cn(
'h-12 w-full gap-2 font-black uppercase',
isDirty ? 'bg-[#ccff00] text-black hover:bg-[#b8e600]' : 'bg-white/5 text-gray-500',
)}
>
<Check className="h-4 w-4" />
{t('cardUi.save')}
{isDirty ? t('cardUi.studio.saveChanges') : t('cardUi.studio.allSaved')}
</Button>
)}
</div>
{/* ── Card type toggle ── */}
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 mb-2">{t('labUi.rider.cardType')}</p>
<div className="grid grid-cols-2 gap-2">
{([
{ 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 }) => (
<button
key={id}
onClick={() => 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'
)}
>
<Icon className={cn('h-5 w-5 mb-2', cardType === id ? 'text-[#ff0040]' : 'text-gray-600')} />
<div className="text-xs font-black uppercase">{label}</div>
<div className="text-[10px] mt-0.5 opacity-60">{sub}</div>
</button>
))}
</div>
</div>
{/* ══ RIGHT: inspector ══ */}
<div className="mt-8 space-y-6 lg:mt-0">
{/* ── Theme picker ── */}
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 mb-2 flex items-center gap-1.5">
<Palette className="h-3 w-3" /> {t('labUi.rider.theme')}
</p>
<div className="grid grid-cols-6 gap-2">
{THEMES.map((th) => (
<button
key={th.id}
onClick={() => 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'
)}
>
<span className="text-xl">{th.emoji}</span>
<span className="text-[8px] font-black uppercase opacity-80">{th.label}</span>
</button>
))}
</div>
</div>
{/* ── Holographic toggle ── */}
<div className="flex items-center justify-between px-4 py-3 rounded-xl bg-white/3 border border-white/8">
{/* Card type */}
<div>
<div className="text-sm font-bold">✨ {t('labUi.rider.holoTitle')}</div>
<div className="text-xs text-gray-500">{t('labUi.rider.holoDesc')}</div>
</div>
<Switch checked={holographic} onCheckedChange={setHolographic} />
</div>
{/* ── RIDER fields ── */}
{cardType === 'rider' && (
<div className="space-y-5">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 border-t border-white/8 pt-4">{t('labUi.rider.cardDetails')}</p>
{/* Style */}
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.skateStyle')}</Label>
<Select value={style} onValueChange={setStyle}>
<SelectTrigger className="bg-black/50 border-white/10 h-11">
<SelectValue />
</SelectTrigger>
<SelectContent>
{STYLES.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}
</SelectContent>
</Select>
</div>
{/* Rides For + Sponsors — side by side */}
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.ridesFor')}</Label>
<Input
placeholder={t('labUi.rider.ridesForPlaceholder')}
value={ridesFor}
onChange={e => setRidesFor(e.target.value)}
className="bg-black/50 border-white/10 text-sm h-11"
/>
</div>
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.sponsors')}</Label>
<Input
placeholder={t('labUi.rider.sponsorsPlaceholder')}
value={sponsors}
onChange={e => setSponsors(e.target.value)}
className="bg-black/50 border-white/10 text-sm h-11"
/>
</div>
</div>
{/* Stats toggles */}
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-2 block">{t('labUi.rider.showOnCard')}</Label>
<div className="grid grid-cols-3 gap-2">
{[
{ 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 }) => (
<button
key={label}
onClick={() => 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'
)}
>
<div className={cn('text-lg font-black', state ? 'text-[#ff0040]' : 'text-gray-700')}>{value}</div>
<div className="text-[9px] uppercase tracking-wide mt-0.5">{label}</div>
</button>
))}
</div>
</div>
{/* Identity facets */}
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-2 block">{t('cardUi.facets.title')}</Label>
<div className="grid grid-cols-3 gap-2">
{[
{ 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 }) => (
<button
key={label}
onClick={() => 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'
)}
>
<div className={cn('text-lg font-black', state ? 'text-[#ff0040]' : 'text-gray-700')}>{value}</div>
<div className="text-[9px] uppercase tracking-wide mt-0.5">{label}</div>
</button>
))}
</div>
</div>
{/* Skills */}
<div>
<div className="flex items-center justify-between mb-2">
<Label className="text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.skills')}</Label>
<span className="text-[10px] text-gray-600">{t('labUi.rider.selected', { count: skills.length })}</span>
</div>
<div className="flex flex-wrap gap-1.5">
{SKILL_OPTIONS.map(s => (
<button
key={s.id}
onClick={() => 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}
</button>
))}
</div>
<p className="text-[10px] text-gray-600 mt-2">
⭐ {t('labUi.rider.goldBadgesHint1')} <button onClick={() => setPageTab('badges')} className="text-yellow-500 underline">{t('labUi.rider.tabBadges')}</button>{t('labUi.rider.goldBadgesHint2')}
</p>
<p className="mb-2 text-[10px] font-black uppercase tracking-widest text-gray-600">{t('labUi.rider.cardType')}</p>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{([
{ 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 }) => (
<button
key={id}
onClick={() => 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'
)}
>
<Icon className={cn('h-4 w-4', cardType === id ? 'text-[#ff0040]' : 'text-gray-600')} />
<span className="text-[9px] font-black uppercase leading-tight">{label}</span>
</button>
))}
</div>
</div>
)}
{/* ── PARK fields ── */}
{cardType === 'park' && (
<div className="space-y-5">
<div className="flex items-start gap-3 p-4 rounded-xl bg-blue-500/5 border border-blue-500/20 text-xs text-gray-400">
<Info className="h-4 w-4 text-blue-400 flex-shrink-0 mt-0.5" />
<div>
<span className="font-bold text-blue-300">{t('labUi.rider.parkOwnersOnly')}</span>{' '}
{t('labUi.rider.parkOwnersDesc')}
</div>
</div>
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 border-t border-white/8 pt-4">{t('labUi.rider.parkDetails')}</p>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.parkName')}</Label>
<Input value={issuerName} onChange={e => setIssuerName(e.target.value)}
placeholder={t('labUi.rider.parkNamePlaceholder')} className="bg-black/50 border-white/10 h-11" />
</div>
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.riderName')}</Label>
<Input value={recipientName} onChange={e => setRecipientName(e.target.value)}
placeholder={t('labUi.rider.riderNamePlaceholder')} className="bg-black/50 border-white/10 h-11" />
</div>
{/* Theme */}
<div>
<p className="mb-2 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-gray-600">
<Palette className="h-3 w-3" /> {t('labUi.rider.theme')}
</p>
<div className="grid grid-cols-6 gap-2">
{THEMES.map((th) => (
<button
key={th.id}
onClick={() => 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'
)}
>
<span className="text-lg">{th.emoji}</span>
<span className="text-[7px] font-black uppercase opacity-80">{th.label}</span>
</button>
))}
</div>
</div>
{/* Holographic */}
<div className="flex items-center justify-between rounded-xl border border-white/8 bg-white/3 px-4 py-3">
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.riderNpub')}</Label>
<Input
value={recipientNpub}
onChange={e => setRecipientNpub(e.target.value)}
placeholder="npub1…"
className="bg-black/50 border-white/10 font-mono text-xs h-11"
/>
<div className="text-sm font-bold">✨ {t('labUi.rider.holoTitle')}</div>
<div className="text-xs text-gray-500">{t('labUi.rider.holoDesc')}</div>
</div>
<Switch checked={holographic} onCheckedChange={setHolographic} />
</div>
<div className="grid grid-cols-2 gap-3">
{/* Show on card — ONE clear list */}
{cardType === 'rider' && (
<>
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.accessLevel')}</Label>
<Select value={accessLevel} onValueChange={setAccessLevel}>
<SelectTrigger className="bg-black/50 border-white/10 h-11"><SelectValue /></SelectTrigger>
<p className="mb-2 text-[10px] font-black uppercase tracking-widest text-gray-600">{t('cardUi.studio.showOnCard')}</p>
<div className="divide-y divide-white/5 rounded-xl border border-white/8">
{[
{ 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) => (
<div key={row.label} className="flex items-center justify-between gap-3 px-3 py-2.5">
<div className="flex min-w-0 items-center gap-2.5">
<span className="text-base">{row.icon}</span>
<div className="min-w-0">
<div className="text-xs font-bold text-white">{row.label}</div>
<div className="truncate text-[10px] text-gray-500">{row.value}</div>
</div>
</div>
<Switch checked={row.state} onCheckedChange={row.set} />
</div>
))}
</div>
</div>
{/* Details */}
<div className="space-y-4 border-t border-white/8 pt-4">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600">{t('labUi.rider.cardDetails')}</p>
<div className="rounded-xl border border-white/8 bg-white/3 p-3">
<div className="text-[10px] uppercase tracking-widest text-gray-500">{t('cardUi.studio.derived')}</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{derivedStyle && <span className="rounded-md bg-white/5 px-2 py-0.5 text-[11px] font-bold text-white">{derivedStyle}</span>}
{derivedLevel && <span className="rounded-md px-2 py-0.5 text-[11px] font-bold" style={{ background: 'rgba(255,102,0,0.15)', color: '#ff9944' }}>{derivedLevel}</span>}
{!derivedStyle && !derivedLevel && (
<span className="text-[11px] text-gray-500">
{t('cardUi.studio.noSkaterProfile')}{' '}
<Link to="/skaters" className="text-[#ccff00] underline">{t('cardUi.studio.editSkaterProfile')}</Link>
</span>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.ridesFor')}</Label>
<Input
placeholder={t('labUi.rider.ridesForPlaceholder')}
value={ridesFor}
onChange={e => setRidesFor(e.target.value)}
className="h-11 border-white/10 bg-black/50 text-sm"
/>
</div>
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.sponsors')}</Label>
<Input
placeholder={t('labUi.rider.sponsorsPlaceholder')}
value={sponsors}
onChange={e => setSponsors(e.target.value)}
className="h-11 border-white/10 bg-black/50 text-sm"
/>
</div>
</div>
<p className="text-[10px] text-gray-600">
⭐ {t('labUi.rider.goldBadgesHint1')}{' '}
<button onClick={() => setPageTab('badges')} className="text-yellow-500 underline">{t('labUi.rider.tabBadges')}</button>
{t('labUi.rider.goldBadgesHint2')}
</p>
</div>
</>
)}
{/* ── PARK fields ── */}
{cardType === 'park' && (
<div className="space-y-5 border-t border-white/8 pt-4">
<div className="flex items-start gap-3 rounded-xl border border-blue-500/20 bg-blue-500/5 p-4 text-xs text-gray-400">
<Info className="mt-0.5 h-4 w-4 flex-shrink-0 text-blue-400" />
<div>
<span className="font-bold text-blue-300">{t('labUi.rider.parkOwnersOnly')}</span>{' '}
{t('labUi.rider.parkOwnersDesc')}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.parkName')}</Label>
<Input value={issuerName} onChange={e => setIssuerName(e.target.value)}
placeholder={t('labUi.rider.parkNamePlaceholder')} className="h-11 border-white/10 bg-black/50" />
</div>
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.riderName')}</Label>
<Input value={recipientName} onChange={e => setRecipientName(e.target.value)}
placeholder={t('labUi.rider.riderNamePlaceholder')} className="h-11 border-white/10 bg-black/50" />
</div>
</div>
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.riderNpub')}</Label>
<Input
value={recipientNpub}
onChange={e => setRecipientNpub(e.target.value)}
placeholder="npub1…"
className="h-11 border-white/10 bg-black/50 font-mono text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.accessLevel')}</Label>
<Select value={accessLevel} onValueChange={setAccessLevel}>
<SelectTrigger className="h-11 border-white/10 bg-black/50"><SelectValue /></SelectTrigger>
<SelectContent>
{['Full Access','Supervised','Beginner Zone','Street Only','Bowl Only','Members Only'].map(l =>
<SelectItem key={l} value={l}>{l}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('labUi.rider.validThru')}</Label>
<Input type="text" value={validThru} onChange={e => setValidThru(e.target.value)}
placeholder="2026-12-31" className="h-11 border-white/10 bg-black/50" />
</div>
</div>
<div>
<Label className="mb-1.5 block text-[10px] uppercase tracking-widest text-gray-500">{t('cardUi.venue')}</Label>
<Select value={venueId || '__none'} onValueChange={(v) => setVenueId(v === '__none' ? '' : v)}>
<SelectTrigger className="h-11 border-white/10 bg-black/50"><SelectValue placeholder={t('cardUi.venueNone')} /></SelectTrigger>
<SelectContent>
{['Full Access','Supervised','Beginner Zone','Street Only','Bowl Only','Members Only'].map(l =>
<SelectItem key={l} value={l}>{l}</SelectItem>)}
<SelectItem value="__none">{t('cardUi.venueNone')}</SelectItem>
{myVenues.map(s => <SelectItem key={s.id} value={s.id}>{s.title}</SelectItem>)}
</SelectContent>
</Select>
<p className="mt-1.5 text-[10px] text-gray-600">{t('cardUi.venueHint')}</p>
</div>
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('labUi.rider.validThru')}</Label>
<Input type="text" value={validThru} onChange={e => setValidThru(e.target.value)}
placeholder="2026-12-31" className="bg-black/50 border-white/10 h-11" />
</div>
<Button
onClick={handlePublishParkCard}
className="h-12 w-full gap-2 bg-[#ccff00] font-black uppercase text-black hover:bg-[#b8e600]"
>
<Zap className="h-4 w-4" />
{t('labUi.rider.publishToNostr')}
</Button>
</div>
)}
<div>
<Label className="text-[10px] uppercase tracking-widest text-gray-500 mb-1.5 block">{t('cardUi.venue')}</Label>
<Select value={venueId || '__none'} onValueChange={(v) => setVenueId(v === '__none' ? '' : v)}>
<SelectTrigger className="bg-black/50 border-white/10 h-11"><SelectValue placeholder={t('cardUi.venueNone')} /></SelectTrigger>
<SelectContent>
<SelectItem value="__none">{t('cardUi.venueNone')}</SelectItem>
{myVenues.map(s => <SelectItem key={s.id} value={s.id}>{s.title}</SelectItem>)}
</SelectContent>
</Select>
<p className="text-[10px] text-gray-600 mt-1.5">{t('cardUi.venueHint')}</p>
{/* ── CREW fields ── */}
{cardType === 'crew' && (
<div className="space-y-5 border-t border-white/8 pt-4">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600">{t('cardUi.crew.pick')}</p>
{memberships.length > 0 ? (
<Select value={activeCrew?.id ?? ''} onValueChange={setCrewId}>
<SelectTrigger className="h-11 border-white/10 bg-black/50"><SelectValue /></SelectTrigger>
<SelectContent>
{memberships.map(m => (
<SelectItem key={m.crew.id} value={m.crew.id}>{m.crew.name} · {m.role}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<p className="text-xs text-gray-500">{t('cardUi.crew.none')}</p>
)}
<p className="text-[10px] text-gray-600">{t('cardUi.crew.hint')}</p>
</div>
)}
<Button
onClick={handlePublishParkCard}
className="w-full h-12 bg-[#ccff00] hover:bg-[#b8e600] text-black font-black uppercase gap-2"
>
<Zap className="h-4 w-4" />
{t('labUi.rider.publishToNostr')}
</Button>
</div>
)}
{/* ── CREW fields ── */}
{cardType === 'crew' && (
<div className="space-y-5">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 border-t border-white/8 pt-4">{t('cardUi.crew.pick')}</p>
{memberships.length > 0 ? (
<Select value={activeCrew?.id ?? ''} onValueChange={setCrewId}>
<SelectTrigger className="bg-black/50 border-white/10 h-11"><SelectValue /></SelectTrigger>
<SelectContent>
{memberships.map(m => (
<SelectItem key={m.crew.id} value={m.crew.id}>{m.crew.name} · {m.role}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<p className="text-xs text-gray-500">{t('cardUi.crew.none')}</p>
)}
<p className="text-[10px] text-gray-600">{t('cardUi.crew.hint')}</p>
</div>
)}
{/* ── EVENT fields (pass / wristband) ── */}
{cardType === 'event' && (
<div className="space-y-5">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600 border-t border-white/8 pt-4">{t('cardUi.event.pick')}</p>
{myEvents.length > 0 ? (
<Select value={activeEvent?.id ?? ''} onValueChange={setEventId}>
<SelectTrigger className="bg-black/50 border-white/10 h-11"><SelectValue /></SelectTrigger>
<SelectContent>
{myEvents.map(ev => (
<SelectItem key={ev.id} value={ev.id}>{ev.title}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<p className="text-xs text-gray-500">{t('cardUi.event.none')}</p>
)}
<p className="text-[10px] text-gray-600">{t('cardUi.event.hint')}</p>
</div>
)}
{/* ── EVENT fields (pass / wristband) ── */}
{cardType === 'event' && (
<div className="space-y-5 border-t border-white/8 pt-4">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-600">{t('cardUi.event.pick')}</p>
{myEvents.length > 0 ? (
<Select value={activeEvent?.id ?? ''} onValueChange={setEventId}>
<SelectTrigger className="h-11 border-white/10 bg-black/50"><SelectValue /></SelectTrigger>
<SelectContent>
{myEvents.map(ev => (
<SelectItem key={ev.id} value={ev.id}>{ev.title}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<p className="text-xs text-gray-500">{t('cardUi.event.none')}</p>
)}
<p className="text-[10px] text-gray-600">{t('cardUi.event.hint')}</p>
</div>
)}
</div>
</div>
)}
{/* ══════════════════════════════════════════════
TAB: MY BADGES
══════════════════════════════════════════════ */}
{/* TAB: MY BADGES */}
{pageTab === 'badges' && (
<div className="space-y-6">
<div className="text-center py-2">
<p className="text-gray-400 text-sm max-w-xs mx-auto">
{t('labUi.rider.badgesIntro')}
</p>
<div className="mx-auto max-w-2xl space-y-6">
<div className="py-2 text-center">
<p className="mx-auto max-w-xs text-sm text-gray-400">{t('labUi.rider.badgesIntro')}</p>
</div>
<ReceivedBadgesPanel pubkey={user.pubkey} />
<div className="pt-2 border-t border-white/8">
<p className="text-xs text-gray-600 text-center mb-3">{t('labUi.rider.earnMore')}</p>
<div className="border-t border-white/8 pt-2">
<p className="mb-3 text-center text-xs text-gray-600">{t('labUi.rider.earnMore')}</p>
<Button
variant="outline"
onClick={() => 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"
>
<Award className="h-4 w-4" />
{t('labUi.rider.awardSomeone')}
@ -1553,20 +1531,17 @@ export default function RiderCardPage() {
</div>
)}
{/* ══════════════════════════════════════════════
TAB: AWARD BADGE (inline, no dialog)
══════════════════════════════════════════════ */}
{/* TAB: AWARD BADGE */}
{pageTab === 'award' && (
<div className="space-y-5">
<div className="flex items-center gap-3 p-4 rounded-xl bg-yellow-500/5 border border-yellow-500/20">
<Award className="h-5 w-5 text-yellow-400 flex-shrink-0" />
<div className="mx-auto max-w-2xl space-y-5">
<div className="flex items-center gap-3 rounded-xl border border-yellow-500/20 bg-yellow-500/5 p-4">
<Award className="h-5 w-5 flex-shrink-0 text-yellow-400" />
<div>
<div className="text-sm font-bold text-yellow-300">{t('labUi.rider.youSawIt')}</div>
<div className="text-xs text-gray-400 mt-0.5">{t('labUi.rider.youSawItDesc')}</div>
<div className="mt-0.5 text-xs text-gray-400">{t('labUi.rider.youSawItDesc')}</div>
</div>
</div>
{/* Inline award form — the dialog was removed as dead code */}
<AwardBadgeInline issuerPubkey={user.pubkey} onSuccess={() => setPageTab('badges')} />
</div>
)}