From 733dc5200e4032ee903acfecd396f5b086dfd0f9 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Fri, 2 Oct 2026 14:53:12 +0700 Subject: [PATCH] =?UTF-8?q?chore(card):=20audit=20cosmetics=20=E2=80=94=20?= =?UTF-8?q?zoom=20preview,=20translated=20title,=20drop=20legacy=20fields,?= =?UTF-8?q?=20clean=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - U2: CardPreview is zoomable (click for a full-screen overlay); used in the studio, 'My passes' and the public card. - U4: the page title is translated (Rider Card / Carné de Rider) — was hardcoded English. - L4: removed the legacy style/skills from RiderCardConfig + CardData (the card derives style/level and shows only witnessed badges). - I2: cardVerifyUrl/cardPublicUrl are clean (/verify/, /card/) and a startup shim rewrites those paths to the hash (old /#/ links still work). --- src/components/RiderCard.tsx | 46 ++++++++++++++++++++++++++++-------- src/i18n/en.ts | 2 ++ src/i18n/es.ts | 2 ++ src/lib/riderCard.ts | 16 ++++--------- src/main.tsx | 7 ++++++ src/pages/PublicCardPage.tsx | 3 +-- src/pages/RiderCardPage.tsx | 17 +++++++------ 7 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/components/RiderCard.tsx b/src/components/RiderCard.tsx index 1de3eb6..fef5c94 100644 --- a/src/components/RiderCard.tsx +++ b/src/components/RiderCard.tsx @@ -7,6 +7,7 @@ * implementation of the card. */ import { useEffect, useState } from 'react'; +import { Maximize2 } from 'lucide-react'; import { drawCardToCanvas, type CanvasCardData } from '@/lib/riderCardCanvas'; import type { ThemeId } from '@/lib/riderCard'; import { cn } from '@/lib/utils'; @@ -30,7 +31,6 @@ export interface CardData { npub: string; style: string; level?: string; - skills: string[]; sponsors: string; ridesFor: string; showCrews?: boolean; @@ -89,8 +89,9 @@ function toCanvasData(d: CardData): CanvasCardData { * Live canvas-backed card preview. Debounced so a fast typist doesn't kick off * a canvas + QR render on every keystroke. */ -export function CardPreview({ d, className }: { d: CardData; className?: string }) { +export function CardPreview({ d, className, zoomable = false }: { d: CardData; className?: string; zoomable?: boolean }) { const [src, setSrc] = useState(null); + const [zoom, setZoom] = useState(false); useEffect(() => { let cancelled = false; @@ -106,13 +107,38 @@ export function CardPreview({ d, className }: { d: CardData; className?: string }, [d]); return ( -
- {src - ? Card preview - :
} -
+ <> + + + {zoom && src && ( +
setZoom(false)} + role="dialog" + aria-modal="true" + > + Card +
+ )} + ); } diff --git a/src/i18n/en.ts b/src/i18n/en.ts index b90fd80..3048deb 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6325,6 +6325,8 @@ walletUi: { savedDesc: 'Your card is now signed and verifiable.', saveFailed: 'Could not save the card', notPublished: 'Not published', + titleNeon: 'Rider', + titleRest: 'Card', saveFrontPng: 'Save front (PNG)', expired: 'EXPIRED', logoUploaded: 'Logo uploaded', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 7c86b47..72bd426 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6325,6 +6325,8 @@ walletUi: { savedDesc: 'Tu carné está firmado y se puede verificar.', saveFailed: 'No se pudo guardar el carné', notPublished: 'Sin publicar', + titleNeon: 'Carné de', + titleRest: 'Rider', saveFrontPng: 'Guardar anverso (PNG)', expired: 'CADUCADO', logoUploaded: 'Logo subido', diff --git a/src/lib/riderCard.ts b/src/lib/riderCard.ts index b58b086..6b5ba5a 100644 --- a/src/lib/riderCard.ts +++ b/src/lib/riderCard.ts @@ -23,8 +23,6 @@ export const CARD_ORIGIN = 'https://skate-spots.org'; export interface RiderCardConfig { theme: string; - style: string; - skills: string[]; sponsors: string; ridesFor: string; showCrews: boolean; @@ -42,8 +40,6 @@ export interface RiderCardConfig { export const DEFAULT_RIDER_CARD: RiderCardConfig = { theme: 'thug', - style: '', - skills: [], sponsors: '', ridesFor: '', showCrews: true, @@ -62,8 +58,6 @@ export function parseRiderCard(event: NostrEvent): RiderCardConfig { const show = new Set(event.tags.filter(([n]) => n === 'show').map(([, v]) => v)); return { theme: tag('theme') || DEFAULT_RIDER_CARD.theme, - style: tag('style') || DEFAULT_RIDER_CARD.style, - skills: event.tags.filter(([n]) => n === 'skill').map(([, v]) => v).filter(Boolean), sponsors: tag('sponsor'), ridesFor: tag('rides-for'), // Facets arrived in v2; an older card defaults them ON rather than hiding them. @@ -87,8 +81,6 @@ export function riderCardTags(c: RiderCardConfig): string[][] { ['theme', c.theme], ['v', '3'], ]; - if (c.style.trim()) tags.push(['style', c.style.trim()]); - for (const s of c.skills) tags.push(['skill', s]); if (c.sponsors.trim()) tags.push(['sponsor', c.sponsors.trim()]); if (c.ridesFor.trim()) tags.push(['rides-for', c.ridesFor.trim()]); if (c.accent.trim()) tags.push(['accent', c.accent.trim()]); @@ -117,14 +109,14 @@ export function cardNaddr(kind: number, pubkey: string, d: string): string { return nip19.naddrEncode({ kind, pubkey, identifier: d }); } -/** HashRouter URL that opens the verify page for a card. */ +/** Clean URL that opens the verify page for a card (a startup shim maps it to the hash). */ export function cardVerifyUrl(kind: number, pubkey: string, d: string): string { - return `${CARD_ORIGIN}/#/verify/${cardNaddr(kind, pubkey, d)}`; + return `${CARD_ORIGIN}/verify/${cardNaddr(kind, pubkey, d)}`; } -/** HashRouter URL for a public card page. */ +/** Clean URL for a public card page. */ export function cardPublicUrl(pubkey: string): string { - return `${CARD_ORIGIN}/#/card/${nip19.npubEncode(pubkey)}`; + return `${CARD_ORIGIN}/card/${nip19.npubEncode(pubkey)}`; } // ─── View presets (shared by the editor + the public card) ──────────────────── diff --git a/src/main.tsx b/src/main.tsx index 3079840..b7c8c2c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -16,6 +16,13 @@ if (typeof window !== 'undefined') { } } +// Clean share links: a bare /verify/ or /card/ hits the Netlify SPA +// fallback (index.html), but HashRouter needs the route in the hash — rewrite it. +if (typeof window !== 'undefined' && !window.location.hash) { + const m = window.location.pathname.match(/^\/(verify|card)\/(.+)$/); + if (m) window.location.replace(`/#/${m[1]}/${m[2]}${window.location.search}`); +} + // Outfit font for body text import '@fontsource-variable/outfit'; // Bebas Neue — bold condensed display font for headlines diff --git a/src/pages/PublicCardPage.tsx b/src/pages/PublicCardPage.tsx index a914a91..dd6d21d 100644 --- a/src/pages/PublicCardPage.tsx +++ b/src/pages/PublicCardPage.tsx @@ -87,7 +87,6 @@ export default function PublicCardPage() { npub: nip19.npubEncode(pubkey), style: derivedStyle, level: derivedLevel || undefined, - skills: config.skills, sponsors: config.sponsors, ridesFor: config.ridesFor, showCrews: config.showCrews, @@ -121,7 +120,7 @@ export default function PublicCardPage() { className="w-full rounded-2xl flex items-center justify-center py-8 relative overflow-hidden" 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)' }} > - +
{config.back && ( diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx index 0bcc54d..973f517 100644 --- a/src/pages/RiderCardPage.tsx +++ b/src/pages/RiderCardPage.tsx @@ -343,7 +343,6 @@ export default function RiderCardPage() { npub: cardPubkey ? fullNpub(cardPubkey) : '', style: derivedStyle, level: derivedLevel || undefined, - skills: [], accent: accent || undefined, accent2: accent || undefined, logo: logo || undefined, @@ -407,7 +406,7 @@ export default function RiderCardPage() { name: displayName, pubkey: selectedPass.event.pubkey, npub: '', - style: '', skills: [], sponsors: '', ridesFor: '', + style: '', sponsors: '', ridesFor: '', receivedBadges: [], verifyUrl: cardVerifyUrl(PARK_ACCESS_CARD_KIND, selectedPass.event.pubkey, selectedPass.d), issuerName: selectedPass.issuer, @@ -479,7 +478,7 @@ export default function RiderCardPage() { const handleSaveCard = useCallback(() => { if (!user) return; - const config: RiderCardConfig = { theme, style: '', skills: [], sponsors, ridesFor, showCrews, showLocal, showRoles, accent, logo, back, holographic }; + const config: RiderCardConfig = { theme, sponsors, ridesFor, showCrews, showLocal, showRoles, accent, logo, back, holographic }; publishEvent( { kind: RIDER_CARD_KIND, content: '', tags: riderCardTags(config) }, { @@ -592,8 +591,8 @@ export default function RiderCardPage() {
🛹

- RIDER - CARD + {t('cardUi.titleNeon')} + {t('cardUi.titleRest')}

{t('labUi.rider.heroDesc')} @@ -614,8 +613,8 @@ export default function RiderCardPage() { {/* ── Page header ── */}

- RIDER - CARD + {t('cardUi.titleNeon')} + {t('cardUi.titleRest')}

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

@@ -654,7 +653,7 @@ export default function RiderCardPage() { 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)` }} /> - + {/* Flip front/back (personal card) */} @@ -1200,7 +1199,7 @@ export default function RiderCardPage() { {passCardData && (
- +