fix(card): audit fixes — share link, dup passes, dead stats, public style, holo persist
- B1: Share's clipboard fallback copied https://…/<npub> (a path HashRouter ignores → homepage). Now /#/card/<npub> (native share text too). - B4: useIssuedPasses didn't dedupe by d, so a revoked pass showed twice; now newest-per-d like useReceivedPasses. - B2/L5: the redesign dropped the stats row, so Spots/Zaps/Events toggles + useRiderStats + spotsAdded/zapsReceived/eventsCreated/memberYear were dead. Removed across lib/riderCard, CardData, CanvasCardData, both pages. - B3: PublicCardPage ignored derived style/level (config.style is '' now) — it now derives them from the Skater profile like the studio; holographic honors the saved config. - B6: holographic wasn't persisted — added to the card config/tags/parse. - B5: 'Save PNG' is front-only while Print is both sides → label it 'Save front (PNG)' when a back exists.
This commit is contained in:
parent
8dcb19eba6
commit
ec02e4fe71
8 changed files with 48 additions and 93 deletions
|
|
@ -33,13 +33,9 @@ export interface CardData {
|
|||
skills: string[];
|
||||
sponsors: string;
|
||||
ridesFor: string;
|
||||
showSpots: boolean;
|
||||
showZaps: boolean;
|
||||
showEvents: boolean;
|
||||
spotsAdded: number;
|
||||
zapsReceived: number;
|
||||
eventsCreated: number;
|
||||
memberYear: string;
|
||||
showCrews?: boolean;
|
||||
showLocal?: boolean;
|
||||
showRoles?: boolean;
|
||||
receivedBadges: BadgeAward[];
|
||||
verifyUrl?: string;
|
||||
accent?: string;
|
||||
|
|
@ -48,11 +44,9 @@ export interface CardData {
|
|||
crews?: { name: string; image?: string }[];
|
||||
local?: string;
|
||||
roles?: string[];
|
||||
showCrews?: boolean;
|
||||
showLocal?: boolean;
|
||||
showRoles?: boolean;
|
||||
crew?: CanvasCardData['crew'];
|
||||
event?: CanvasCardData['event']; issuerName: string;
|
||||
event?: CanvasCardData['event'];
|
||||
issuerName: string;
|
||||
recipientName: string;
|
||||
accessLevel: string;
|
||||
validThru: string;
|
||||
|
|
@ -73,16 +67,9 @@ function toCanvasData(d: CardData): CanvasCardData {
|
|||
level: d.level,
|
||||
sponsors: d.sponsors,
|
||||
ridesFor: d.ridesFor,
|
||||
showSpots: d.showSpots,
|
||||
showZaps: d.showZaps,
|
||||
showEvents: d.showEvents,
|
||||
showCrews: d.showCrews ?? true,
|
||||
showLocal: d.showLocal ?? true,
|
||||
showRoles: d.showRoles ?? true,
|
||||
spotsAdded: d.spotsAdded,
|
||||
zapsReceived: d.zapsReceived,
|
||||
eventsCreated: d.eventsCreated,
|
||||
memberYear: d.memberYear,
|
||||
receivedBadges: d.receivedBadges,
|
||||
verifyUrl: d.verifyUrl,
|
||||
local: d.local,
|
||||
|
|
|
|||
|
|
@ -179,7 +179,15 @@ export function useIssuedPasses(pubkey?: string) {
|
|||
[{ kinds: [PARK_ACCESS_CARD_KIND], authors: [pubkey], limit: 100 }],
|
||||
{ signal },
|
||||
);
|
||||
return events
|
||||
// Newest version per d-tag — a revoke re-publishes the SAME d, so without
|
||||
// this the issued list would show both the original and the revoked pass.
|
||||
const byD = new Map<string, NostrEvent>();
|
||||
for (const ev of events) {
|
||||
const d = ev.tags.find(([n]) => n === 'd')?.[1] ?? ev.id;
|
||||
const cur = byD.get(d);
|
||||
if (!cur || ev.created_at > cur.created_at) byD.set(d, ev);
|
||||
}
|
||||
return [...byD.values()]
|
||||
.map((ev) => ({
|
||||
id: ev.id,
|
||||
d: ev.tags.find(([n]) => n === 'd')?.[1] ?? '',
|
||||
|
|
|
|||
|
|
@ -6325,6 +6325,7 @@ walletUi: {
|
|||
savedDesc: 'Your card is now signed and verifiable.',
|
||||
saveFailed: 'Could not save the card',
|
||||
notPublished: 'Not published',
|
||||
saveFrontPng: 'Save front (PNG)',
|
||||
expired: 'EXPIRED',
|
||||
logoUploaded: 'Logo uploaded',
|
||||
logoUploadFailed: 'Could not upload the logo',
|
||||
|
|
|
|||
|
|
@ -6325,6 +6325,7 @@ walletUi: {
|
|||
savedDesc: 'Tu carné está firmado y se puede verificar.',
|
||||
saveFailed: 'No se pudo guardar el carné',
|
||||
notPublished: 'Sin publicar',
|
||||
saveFrontPng: 'Guardar anverso (PNG)',
|
||||
expired: 'CADUCADO',
|
||||
logoUploaded: 'Logo subido',
|
||||
logoUploadFailed: 'No se pudo subir el logo',
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ export interface RiderCardConfig {
|
|||
skills: string[];
|
||||
sponsors: string;
|
||||
ridesFor: string;
|
||||
showSpots: boolean;
|
||||
showZaps: boolean;
|
||||
showEvents: boolean;
|
||||
showCrews: boolean;
|
||||
showLocal: boolean;
|
||||
showRoles: boolean;
|
||||
|
|
@ -39,6 +36,8 @@ export interface RiderCardConfig {
|
|||
logo: string;
|
||||
/** Whether the printable card has a back side. */
|
||||
back: boolean;
|
||||
/** Holographic sheen. */
|
||||
holographic: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_RIDER_CARD: RiderCardConfig = {
|
||||
|
|
@ -47,21 +46,18 @@ export const DEFAULT_RIDER_CARD: RiderCardConfig = {
|
|||
skills: [],
|
||||
sponsors: '',
|
||||
ridesFor: '',
|
||||
showSpots: true,
|
||||
showZaps: true,
|
||||
showEvents: true,
|
||||
showCrews: true,
|
||||
showLocal: true,
|
||||
showRoles: true,
|
||||
accent: '',
|
||||
logo: '',
|
||||
back: true,
|
||||
holographic: true,
|
||||
};
|
||||
|
||||
/** Read a saved Rider Card event back into editable state. */
|
||||
export function parseRiderCard(event: NostrEvent): RiderCardConfig {
|
||||
const tag = (name: string) => event.tags.find(([n]) => n === name)?.[1] ?? '';
|
||||
const hasShow = event.tags.some(([n]) => n === 'show');
|
||||
const v2 = (Number(tag('v')) || 0) >= 2;
|
||||
const show = new Set(event.tags.filter(([n]) => n === 'show').map(([, v]) => v));
|
||||
return {
|
||||
|
|
@ -70,11 +66,7 @@ export function parseRiderCard(event: NostrEvent): RiderCardConfig {
|
|||
skills: event.tags.filter(([n]) => n === 'skill').map(([, v]) => v).filter(Boolean),
|
||||
sponsors: tag('sponsor'),
|
||||
ridesFor: tag('rides-for'),
|
||||
// Spot/zap/event flags have always been written; facets arrived in v2, so an
|
||||
// older card defaults them ON rather than silently hiding them.
|
||||
showSpots: hasShow ? show.has('spots') : true,
|
||||
showZaps: hasShow ? show.has('zaps') : true,
|
||||
showEvents: hasShow ? show.has('events') : true,
|
||||
// Facets arrived in v2; an older card defaults them ON rather than hiding them.
|
||||
showCrews: v2 ? show.has('crews') : true,
|
||||
showLocal: v2 ? show.has('local') : true,
|
||||
showRoles: v2 ? show.has('roles') : true,
|
||||
|
|
@ -82,6 +74,7 @@ export function parseRiderCard(event: NostrEvent): RiderCardConfig {
|
|||
logo: tag('logo'),
|
||||
// Back side defaults ON; an explicit ['back','0'] turns it off.
|
||||
back: !(event.tags.some(([n, v]) => n === 'back' && v === '0')),
|
||||
holographic: !(event.tags.some(([n, v]) => n === 'holo' && v === '0')),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -101,9 +94,7 @@ export function riderCardTags(c: RiderCardConfig): string[][] {
|
|||
if (c.accent.trim()) tags.push(['accent', c.accent.trim()]);
|
||||
if (c.logo.trim()) tags.push(['logo', c.logo.trim()]);
|
||||
tags.push(['back', c.back ? '1' : '0']);
|
||||
if (c.showSpots) tags.push(['show', 'spots']);
|
||||
if (c.showZaps) tags.push(['show', 'zaps']);
|
||||
if (c.showEvents) tags.push(['show', 'events']);
|
||||
tags.push(['holo', c.holographic ? '1' : '0']);
|
||||
if (c.showCrews) tags.push(['show', 'crews']);
|
||||
if (c.showLocal) tags.push(['show', 'local']);
|
||||
if (c.showRoles) tags.push(['show', 'roles']);
|
||||
|
|
|
|||
|
|
@ -41,16 +41,9 @@ export interface CanvasCardData {
|
|||
level?: string;
|
||||
sponsors: string;
|
||||
ridesFor: string;
|
||||
showSpots: boolean;
|
||||
showZaps: boolean;
|
||||
showEvents: boolean;
|
||||
showCrews: boolean;
|
||||
showLocal: boolean;
|
||||
showRoles: boolean;
|
||||
spotsAdded: number;
|
||||
zapsReceived: number;
|
||||
eventsCreated: number;
|
||||
memberYear: string;
|
||||
receivedBadges: { skill: string }[];
|
||||
verifyUrl?: string;
|
||||
local?: string;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { nip19 } from 'nostr-tools';
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ArrowLeft, Sparkles, RefreshCw } from 'lucide-react';
|
||||
|
||||
import { useRiderStats } from '@/hooks/useRiderStats';
|
||||
import { useRiderCard, useRiderBadges, useRiderRoles, useCardProfile } from '@/hooks/useRiderCard';
|
||||
import { useUserCrews } from '@/hooks/useCrews';
|
||||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
|
|
@ -19,7 +18,8 @@ import { useTranslation } from '@/contexts/language';
|
|||
import { genUserName } from '@/lib/genUserName';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardPreview, type CardData, type BadgeAward } from '@/components/RiderCard';
|
||||
import { RIDER_CARD_KIND, RIDER_CARD_D, DEFAULT_RIDER_CARD, cardVerifyUrl, memberSince } from '@/lib/riderCard';
|
||||
import { RIDER_CARD_KIND, RIDER_CARD_D, DEFAULT_RIDER_CARD, cardVerifyUrl } from '@/lib/riderCard';
|
||||
import { SKATER_STYLES, SKATER_LEVELS } from '@/lib/types';
|
||||
|
||||
function decodePubkey(value: string): string | null {
|
||||
if (/^[0-9a-f]{64}$/i.test(value)) return value.toLowerCase();
|
||||
|
|
@ -40,13 +40,17 @@ export default function PublicCardPage() {
|
|||
const cardProfile = useCardProfile(pubkey ?? undefined);
|
||||
const metadata = cardProfile.data;
|
||||
const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : '');
|
||||
const { data: stats } = useRiderStats(pubkey ?? undefined);
|
||||
const { data: savedCard } = useRiderCard(pubkey ?? undefined);
|
||||
const { data: badges = [] } = useRiderBadges(pubkey ?? undefined);
|
||||
const { data: roles = [] } = useRiderRoles(pubkey ?? undefined);
|
||||
const { memberships } = useUserCrews(pubkey ?? undefined);
|
||||
const { data: skaterProfile } = useUserSkaterProfile(pubkey ?? undefined);
|
||||
|
||||
const derivedStyle = (skaterProfile?.styles ?? []).map((s) => SKATER_STYLES[s]?.label ?? s).join(' · ');
|
||||
const derivedLevel = skaterProfile?.level
|
||||
? (SKATER_LEVELS[skaterProfile.level]?.label ?? '')
|
||||
: (skaterProfile?.customLevel ?? '');
|
||||
|
||||
const [side, setSide] = useState<'front' | 'back'>('front');
|
||||
|
||||
useSeoMeta({
|
||||
|
|
@ -75,23 +79,20 @@ export default function PublicCardPage() {
|
|||
const cardData: CardData = {
|
||||
type: 'rider',
|
||||
theme: config.theme as CardData['theme'],
|
||||
holographic: true,
|
||||
holographic: config.holographic ?? true,
|
||||
name: displayName,
|
||||
nip05: metadata?.nip05,
|
||||
avatar: metadata?.picture,
|
||||
pubkey,
|
||||
npub: nip19.npubEncode(pubkey),
|
||||
style: config.style,
|
||||
style: derivedStyle,
|
||||
level: derivedLevel || undefined,
|
||||
skills: config.skills,
|
||||
sponsors: config.sponsors,
|
||||
ridesFor: config.ridesFor,
|
||||
showSpots: config.showSpots,
|
||||
showZaps: config.showZaps,
|
||||
showEvents: config.showEvents,
|
||||
spotsAdded: stats?.spotsAdded ?? 0,
|
||||
zapsReceived: stats?.zapsReceived ?? 0,
|
||||
eventsCreated: stats?.eventsCreated ?? 0,
|
||||
memberYear: metadata?.created_at ? memberSince(metadata.created_at as number) : new Date().getFullYear().toString(),
|
||||
showCrews: config.showCrews,
|
||||
showLocal: config.showLocal,
|
||||
showRoles: config.showRoles,
|
||||
receivedBadges,
|
||||
verifyUrl: cardVerifyUrl(RIDER_CARD_KIND, pubkey, RIDER_CARD_D),
|
||||
accent: config.accent || undefined,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { LoginArea } from '@/components/auth/LoginArea';
|
|||
import { CardPreview, type CardData, type BadgeAward } from '@/components/RiderCard';
|
||||
import { NfcWriteButton } from '@/components/NfcWriteButton';
|
||||
import { drawCardToCanvas, type CanvasCardData } from '@/lib/riderCardCanvas';
|
||||
import { THEMES, SKILL_OPTIONS, fullNpub, memberSince, type ThemeId } from '@/lib/riderCard';
|
||||
import { THEMES, SKILL_OPTIONS, fullNpub, type ThemeId } from '@/lib/riderCard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
|
@ -35,7 +35,6 @@ import { Badge } from '@/components/ui/badge';
|
|||
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useRiderStats } from '@/hooks/useRiderStats';
|
||||
import { useRiderCard, useRiderRoles, useCardProfile, useIssuedPasses, useRevokePass, useReceivedPasses, type ReceivedPass } from '@/hooks/useRiderCard';
|
||||
import { useUserCrews } from '@/hooks/useCrews';
|
||||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
|
|
@ -167,7 +166,6 @@ export default function RiderCardPage() {
|
|||
const { user } = useCurrentUser();
|
||||
const cardProfile = useCardProfile(user?.pubkey);
|
||||
const metadata = cardProfile.data;
|
||||
const { data: stats } = useRiderStats(user?.pubkey);
|
||||
const { data: savedCard } = useRiderCard(user?.pubkey);
|
||||
const { data: allSpots } = useSkateSpots(200);
|
||||
const { memberships } = useUserCrews(user?.pubkey);
|
||||
|
|
@ -231,9 +229,6 @@ export default function RiderCardPage() {
|
|||
// Rider fields
|
||||
const [sponsors, setSponsors] = useState('');
|
||||
const [ridesFor, setRidesFor] = useState('');
|
||||
const [showSpots, setShowSpots] = useState(true);
|
||||
const [showZaps, setShowZaps] = useState(true);
|
||||
const [showEvents, setShowEvents] = useState(true);
|
||||
const [showCrews, setShowCrews] = useState(true);
|
||||
const [showLocal, setShowLocal] = useState(true);
|
||||
const [showRoles, setShowRoles] = useState(true);
|
||||
|
|
@ -296,15 +291,13 @@ export default function RiderCardPage() {
|
|||
setTheme(c.theme as ThemeId);
|
||||
setSponsors(c.sponsors);
|
||||
setRidesFor(c.ridesFor);
|
||||
setShowSpots(c.showSpots);
|
||||
setShowZaps(c.showZaps);
|
||||
setShowEvents(c.showEvents);
|
||||
setShowCrews(c.showCrews);
|
||||
setShowLocal(c.showLocal);
|
||||
setShowRoles(c.showRoles);
|
||||
setAccent(c.accent);
|
||||
setLogo(c.logo);
|
||||
setBack(c.back);
|
||||
setHolographic(c.holographic);
|
||||
}, [savedCard]);
|
||||
|
||||
// Unsaved-changes tracking for the personal card (manual save — an
|
||||
|
|
@ -314,11 +307,10 @@ export default function RiderCardPage() {
|
|||
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 ||
|
||||
c.accent !== accent || c.logo !== logo || c.back !== back
|
||||
c.accent !== accent || c.logo !== logo || c.back !== back || c.holographic !== holographic
|
||||
);
|
||||
}, [savedCard, theme, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles, accent, logo, back]);
|
||||
}, [savedCard, theme, sponsors, ridesFor, showCrews, showLocal, showRoles, accent, logo, back, holographic]);
|
||||
|
||||
const recipientPubkey = (() => {
|
||||
if (!recipientNpub.trim()) return user?.pubkey ?? '';
|
||||
|
|
@ -351,17 +343,10 @@ export default function RiderCardPage() {
|
|||
logo: logo || undefined,
|
||||
side: cardSide,
|
||||
sponsors, ridesFor,
|
||||
showSpots, showZaps, showEvents,
|
||||
crews: crewCrests,
|
||||
local: local || undefined,
|
||||
roles,
|
||||
showCrews, showLocal, showRoles,
|
||||
spotsAdded: stats?.spotsAdded ?? 0,
|
||||
zapsReceived: stats?.zapsReceived ?? 0,
|
||||
eventsCreated: stats?.eventsCreated ?? 0,
|
||||
memberYear: metadata?.created_at
|
||||
? memberSince(metadata.created_at as number)
|
||||
: new Date().getFullYear().toString(),
|
||||
receivedBadges,
|
||||
verifyUrl: cardType === 'rider'
|
||||
? (user?.pubkey ? cardVerifyUrl(RIDER_CARD_KIND, user.pubkey, RIDER_CARD_D) : undefined)
|
||||
|
|
@ -401,8 +386,8 @@ export default function RiderCardPage() {
|
|||
}),
|
||||
[
|
||||
cardType, theme, holographic, displayName, recipientName, metadata,
|
||||
cardPubkey, derivedStyle, derivedLevel, sponsors, ridesFor, showSpots, showZaps,
|
||||
showEvents, stats, receivedBadges, issuerName, accessLevel, validThru, savedParkD, user?.pubkey,
|
||||
cardPubkey, derivedStyle, derivedLevel, sponsors, ridesFor,
|
||||
receivedBadges, issuerName, accessLevel, validThru, savedParkD, user?.pubkey,
|
||||
crewCrests, local, roles, showCrews, showLocal, showRoles, activeCrew, activeEvent, eventRsvp.goingCount,
|
||||
accent, logo, cardSide,
|
||||
],
|
||||
|
|
@ -417,8 +402,6 @@ export default function RiderCardPage() {
|
|||
pubkey: selectedPass.event.pubkey,
|
||||
npub: '',
|
||||
style: '', skills: [], sponsors: '', ridesFor: '',
|
||||
showSpots: true, showZaps: true, showEvents: true,
|
||||
spotsAdded: 0, zapsReceived: 0, eventsCreated: 0, memberYear: '',
|
||||
receivedBadges: [],
|
||||
verifyUrl: cardVerifyUrl(PARK_ACCESS_CARD_KIND, selectedPass.event.pubkey, selectedPass.d),
|
||||
issuerName: selectedPass.issuer,
|
||||
|
|
@ -433,9 +416,9 @@ export default function RiderCardPage() {
|
|||
try {
|
||||
const dataUrl = await drawCardToCanvas({
|
||||
type: 'park', themeId: passCardData.theme, name: passCardData.name, pubkey: passCardData.pubkey, npub: '',
|
||||
style: '', sponsors: '', ridesFor: '', showSpots: true, showZaps: true, showEvents: true,
|
||||
style: '', sponsors: '', ridesFor: '',
|
||||
showCrews: true, showLocal: true, showRoles: true,
|
||||
spotsAdded: 0, zapsReceived: 0, eventsCreated: 0, memberYear: '', receivedBadges: [],
|
||||
receivedBadges: [],
|
||||
verifyUrl: passCardData.verifyUrl, holographic: true,
|
||||
issuerName: passCardData.issuerName, recipientName: passCardData.recipientName,
|
||||
accessLevel: passCardData.accessLevel, validThru: passCardData.validThru,
|
||||
|
|
@ -467,16 +450,9 @@ export default function RiderCardPage() {
|
|||
level: cardData.level,
|
||||
sponsors: cardData.sponsors,
|
||||
ridesFor: cardData.ridesFor,
|
||||
showSpots: cardData.showSpots,
|
||||
showZaps: cardData.showZaps,
|
||||
showEvents: cardData.showEvents,
|
||||
showCrews: cardData.showCrews ?? true,
|
||||
showLocal: cardData.showLocal ?? true,
|
||||
showRoles: cardData.showRoles ?? true,
|
||||
spotsAdded: cardData.spotsAdded,
|
||||
zapsReceived: cardData.zapsReceived,
|
||||
eventsCreated: cardData.eventsCreated,
|
||||
memberYear: cardData.memberYear,
|
||||
receivedBadges: cardData.receivedBadges,
|
||||
verifyUrl: cardData.verifyUrl,
|
||||
local: cardData.local,
|
||||
|
|
@ -497,7 +473,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, accent, logo, back };
|
||||
const config: RiderCardConfig = { theme, style: '', skills: [], sponsors, ridesFor, showCrews, showLocal, showRoles, accent, logo, back, holographic };
|
||||
publishEvent(
|
||||
{ kind: RIDER_CARD_KIND, content: '', tags: riderCardTags(config) },
|
||||
{
|
||||
|
|
@ -505,7 +481,7 @@ export default function RiderCardPage() {
|
|||
onError: (e) => toast({ title: t('cardUi.saveFailed'), description: String(e), variant: 'destructive' }),
|
||||
},
|
||||
);
|
||||
}, [user, theme, sponsors, ridesFor, showSpots, showZaps, showEvents, showCrews, showLocal, showRoles, accent, logo, back, publishEvent, toast, t]);
|
||||
}, [user, theme, sponsors, ridesFor, showCrews, showLocal, showRoles, accent, logo, back, holographic, publishEvent, toast, t]);
|
||||
|
||||
const handlePublishEventPass = useCallback(() => {
|
||||
if (!user || !activeEvent) return;
|
||||
|
|
@ -571,11 +547,11 @@ export default function RiderCardPage() {
|
|||
const file = new File([blob], `skatespot-card-${cardData.name.replace(/\s+/g, '-')}.png`, { type: 'image/png' });
|
||||
if (navigator.share && navigator.canShare?.({ files: [file] })) {
|
||||
try {
|
||||
await navigator.share({ title: `${cardData.name}'s SkateSpots Card`, text: `Check out my SkateSpots Skater License 🛹\nskate-spots.org/${cardData.npub}`, files: [file] });
|
||||
await navigator.share({ title: `${cardData.name}'s SkateSpots Card`, text: `Check out my SkateSpots Skater License 🛹\nskate-spots.org/#/card/${cardData.npub}`, files: [file] });
|
||||
return;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
await navigator.clipboard.writeText(`https://skate-spots.org/${cardData.npub}`);
|
||||
await navigator.clipboard.writeText(`https://skate-spots.org/#/card/${cardData.npub}`);
|
||||
toast({ title: t('labUi.rider.linkCopied') });
|
||||
}, [renderCardToImage, cardData.name, cardData.npub, toast, t]);
|
||||
|
||||
|
|
@ -717,7 +693,7 @@ export default function RiderCardPage() {
|
|||
</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')}
|
||||
<Download className="h-4 w-4" /> {back ? t('cardUi.saveFrontPng') : t('labUi.rider.savePng')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handlePrint} disabled={rendering} className="gap-2">
|
||||
<Printer className="h-4 w-4" /> {t('labUi.rider.print')}
|
||||
|
|
@ -883,9 +859,6 @@ export default function RiderCardPage() {
|
|||
<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 },
|
||||
|
|
|
|||
Loading…
Reference in a new issue