From f4cdafa1453b5b23ac1c60043f254b2385d8a27b Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 23 Jul 2026 20:20:26 +0700 Subject: [PATCH] =?UTF-8?q?card:=20full=20UX=20rebuild=20=E2=80=94=20hero?= =?UTF-8?q?=20preview,=203-tab=20nav,=20single-column=20layout,=20inline?= =?UTF-8?q?=20award=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Logged-out state: proper hero with value prop copy instead of bare login form - Hero card preview: full-width dark stage with theme-matched glow behind card - Card slightly larger (360x227) with depth shadow - Top tab bar: My Card / Badges (with count) / Award — replaces buried sections - Single-column max-w-2xl layout — no more awkward 2-col grid on mobile - Primary CTA: one big Share button, Print/Save/Copy as secondary 3-col row - Card type as prominent 2-up tile choice, not a buried dropdown-style control - Theme picker: 6-up square grid with emoji + label, ring on active - Rides For + Sponsors side by side (saves vertical space) - Stats as interactive tiles showing the actual numbers - Skills as pill grid with live counter (X/6 selected) - Badges tab: shows ReceivedBadgesPanel (was previously never rendered) - Award tab: inline form, no dialog — cleaner flow, redirects to Badges on success - AwardBadgeDialog kept for backward compat but Award tab uses AwardBadgeInline Co-authored-by: shakespeare.diy --- src/index.css | 7 +- src/pages/RiderCardPage.tsx | 867 +++++++++++++++++++----------------- 2 files changed, 459 insertions(+), 415 deletions(-) diff --git a/src/index.css b/src/index.css index c405367..b5d84b8 100644 --- a/src/index.css +++ b/src/index.css @@ -556,13 +556,14 @@ select { /* Credit-card aspect ratio: 85.6mm × 54mm = 1.586:1 */ .rider-card { - width: 340px; - height: 214px; - border-radius: 12px; + width: 360px; + height: 227px; + border-radius: 14px; position: relative; overflow: hidden; flex-shrink: 0; font-family: 'Inter Variable', 'Inter', system-ui, sans-serif; + box-shadow: 0 8px 40px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4); } /* Holographic shimmer overlay — appears on hover/focus for screen, diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx index 0a1c245..8282bd1 100644 --- a/src/pages/RiderCardPage.tsx +++ b/src/pages/RiderCardPage.tsx @@ -1106,23 +1106,23 @@ export default function RiderCardPage() { const displayName = metadata?.name ?? genUserName(user?.pubkey ?? ''); const npub = user?.pubkey ? nip19.npubEncode(user.pubkey) : ''; - // Received badges for the logged-in user const { data: receivedBadgeEvents = [] } = useReceivedBadges(user?.pubkey ?? ''); - - // Parse badge events into BadgeAward objects const receivedBadges: BadgeAward[] = receivedBadgeEvents.flatMap(event => { const skills = event.tags.filter(([k]) => k === 'skill').map(([, v]) => v); const spot = event.tags.find(([k]) => k === 'spot')?.[1]; return skills.map(skill => ({ skill, - witnessName: genUserName(event.pubkey), // simplified for card visual + witnessName: genUserName(event.pubkey), witnessPubkey: event.pubkey, spotName: spot, timestamp: event.created_at, })); }); - // ── Form state ────────────────────────────────────────────────────────────── + // ── Page tab: 'card' | 'badges' | 'award' ──────────────────────────────── + const [pageTab, setPageTab] = useState<'card' | 'badges' | 'award'>('card'); + + // ── Card state ──────────────────────────────────────────────────────────── const [cardType, setCardType] = useState<'rider' | 'park'>('rider'); const [theme, setTheme] = useState('thug'); const [holographic, setHolographic] = useState(true); @@ -1147,32 +1147,23 @@ export default function RiderCardPage() { const [copied, setCopied] = useState(false); const [rendering, setRendering] = useState(false); - // ── Parse recipientNpub → pubkey ───────────────────────────────────────── const recipientPubkey = (() => { if (!recipientNpub.trim()) return user?.pubkey ?? ''; try { const d = nip19.decode(recipientNpub.trim()); return d.type === 'npub' ? d.data : (user?.pubkey ?? ''); - } catch { - return user?.pubkey ?? ''; - } + } catch { return user?.pubkey ?? ''; } })(); - // ── Computed card data ─────────────────────────────────────────────────────── const cardPubkey = cardType === 'rider' ? (user?.pubkey ?? '') : recipientPubkey; const cardData: CardData = { - type: cardType, - theme, - holographic, + type: cardType, theme, holographic, name: cardType === 'rider' ? displayName : recipientName, nip05: metadata?.nip05, avatar: metadata?.picture, pubkey: cardPubkey, npub: cardPubkey ? fullNpub(cardPubkey) : '', - style, - skills, - sponsors, - ridesFor, + style, skills, sponsors, ridesFor, showSpots, showZaps, showEvents, spotsAdded: stats?.spotsAdded ?? 0, zapsReceived: stats?.zapsReceived ?? 0, @@ -1181,103 +1172,47 @@ export default function RiderCardPage() { ? memberSince(metadata.created_at as number) : new Date().getFullYear().toString(), receivedBadges, - issuerName, - recipientName, - accessLevel, - validThru, + issuerName, recipientName, accessLevel, validThru, }; - // ── Render card via Canvas 2D API → returns a PNG data URL ────────────── const renderCardToImage = useCallback(async (): Promise => { setRendering(true); try { - const dataUrl = await drawCardToCanvas({ - type: cardData.type, - themeId: cardData.theme, - name: cardData.name, - nip05: cardData.nip05, - avatarUrl: cardData.avatar, - pubkey: cardData.pubkey, - npub: cardData.npub, - style: cardData.style, - skills: cardData.skills, - sponsors: cardData.sponsors, - ridesFor: cardData.ridesFor, - showSpots: cardData.showSpots, - showZaps: cardData.showZaps, - showEvents: cardData.showEvents, - spotsAdded: cardData.spotsAdded, - zapsReceived: cardData.zapsReceived, - eventsCreated: cardData.eventsCreated, - memberYear: cardData.memberYear, - receivedBadges: cardData.receivedBadges, - issuerName: cardData.issuerName, - recipientName: cardData.recipientName, - accessLevel: cardData.accessLevel, - validThru: cardData.validThru, + return await drawCardToCanvas({ + type: cardData.type, themeId: cardData.theme, + name: cardData.name, nip05: cardData.nip05, + avatarUrl: cardData.avatar, pubkey: cardData.pubkey, npub: cardData.npub, + style: cardData.style, skills: cardData.skills, + sponsors: cardData.sponsors, ridesFor: cardData.ridesFor, + showSpots: cardData.showSpots, showZaps: cardData.showZaps, showEvents: cardData.showEvents, + spotsAdded: cardData.spotsAdded, zapsReceived: cardData.zapsReceived, eventsCreated: cardData.eventsCreated, + memberYear: cardData.memberYear, receivedBadges: cardData.receivedBadges, + issuerName: cardData.issuerName, recipientName: cardData.recipientName, + accessLevel: cardData.accessLevel, validThru: cardData.validThru, }); - return dataUrl; - } catch (err) { - console.error('Card render failed:', err); - return null; - } finally { - setRendering(false); - } + } catch (err) { console.error('Card render failed:', err); return null; } + finally { setRendering(false); } }, [cardData]); - // ── Print: render to image, open print window with the image ───────────── const handlePrint = useCallback(async () => { const dataUrl = await renderCardToImage(); - if (!dataUrl) { - toast({ title: 'Could not render card', description: 'Try again in a moment.', variant: 'destructive' }); - return; - } - - // 85.6mm × 54mm in points (1mm ≈ 2.835pt) + if (!dataUrl) { toast({ title: 'Could not render card', variant: 'destructive' }); return; } const w = Math.round(85.6 * 2.835); const h = Math.round(54 * 2.835); - const win = window.open('', '_blank', `width=${w * 2},height=${h * 2}`); - if (!win) { - toast({ title: 'Popup blocked', description: 'Allow popups for this site then try again.', variant: 'destructive' }); - return; - } - - win.document.write(` - - -SkateSpots Rider Card - - - - - - -`); + if (!win) { toast({ title: 'Popup blocked', description: 'Allow popups for this site.', variant: 'destructive' }); return; } + win.document.write(`SkateSpots Rider Card