diff --git a/src/pages/EventDetailPage.tsx b/src/pages/EventDetailPage.tsx index 3342c72..463c98c 100644 --- a/src/pages/EventDetailPage.tsx +++ b/src/pages/EventDetailPage.tsx @@ -39,6 +39,7 @@ import { Megaphone, } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useToast } from '@/hooks/useToast'; function formatTime(timestamp: number): string { const date = new Date(timestamp * 1000); @@ -80,12 +81,53 @@ export default function EventDetailPage() { const { data: event, isLoading, error } = useSkateEvent(identifier || '', pubkey); const author = useAuthor(event?.pubkey); const { user } = useCurrentUser(); + const { toast } = useToast(); const isOwner = user && event && user.pubkey === event.pubkey; + // Canonical, shareable URL for this event (HashRouter โ†’ keep the /#/ prefix). + const shareUrl = event + ? `https://skate-spots.org/#/event/${event.pubkey}/${encodeURIComponent(event.identifier)}` + : 'https://skate-spots.org'; + + // og:image must be an absolute URL. Fall back to the site social card when the + // event has no cover so shared links never render blank. + const ogImage = event?.image || 'https://skate-spots.org/images/icon-1024.png'; + + // Compose a punchy share description: date + location, then the blurb. + const shareDescription = (() => { + if (!event) return 'View this skateboarding event on SkateSpots.'; + const bits: string[] = []; + if (event.start) { + bits.push( + new Date(event.start * 1000).toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', + day: 'numeric', + }), + ); + } + if (event.location) bits.push(`๐Ÿ“ ${event.location}`); + const prefix = bits.length ? `${bits.join(' ยท ')} โ€” ` : ''; + const body = event.description?.trim() || 'A skateboarding event on SkateSpots.'; + return `${prefix}${body}`.slice(0, 200); + })(); + + const shareTitle = event ? `${event.title} ยท SkateSpots` : 'Skate Event ยท SkateSpots'; + useSeoMeta({ - title: event ? `${event.title} - SkateSpots` : 'Skate Event - SkateSpots', - description: event?.description || 'View this skateboarding event on SkateSpots.', + title: shareTitle, + description: shareDescription, + ogTitle: shareTitle, + ogDescription: shareDescription, + ogType: 'article', + ogUrl: shareUrl, + ogImage, + ogSiteName: 'SkateSpots', + twitterCard: 'summary_large_image', + twitterTitle: shareTitle, + twitterDescription: shareDescription, + twitterImage: ogImage, }); const metadata = author.data?.metadata; @@ -132,18 +174,35 @@ export default function EventDetailPage() { }, [event]); const handleShare = async () => { + const shareText = event + ? `${event.title} โ€” a skate event on SkateSpots` + : 'Check out this skate event on SkateSpots'; + if (navigator.share) { try { await navigator.share({ title: event?.title, - text: event?.description || `Check out this skate event: ${event?.title}`, - url: window.location.href, + text: shareText, + url: shareUrl, }); + return; } catch { - // User cancelled or share failed + // User cancelled or share failed โ€” fall through to clipboard. } - } else { - navigator.clipboard.writeText(window.location.href); + } + + try { + await navigator.clipboard.writeText(shareUrl); + toast({ + title: 'Link copied', + description: 'Event link copied to your clipboard โ€” paste it anywhere.', + }); + } catch { + toast({ + title: 'Copy failed', + description: shareUrl, + variant: 'destructive', + }); } };