import { useMemo } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link } from 'react-router-dom'; import ngeohash from 'ngeohash'; import { Layout } from '@/components/Layout'; import { SpotMap } from '@/components/SpotMap'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { ZapButton } from '@/components/ZapButton'; import { CommentsSection } from '@/components/comments/CommentsSection'; import { useSkateEvent } from '@/hooks/useSkateEvents'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { EVENT_TYPE_LABELS, SKATE_CREW_KIND, SKATE_MUSICIAN_KIND } from '@/lib/types'; import { Users } from 'lucide-react'; import NotFound from './NotFound'; import { Calendar, Clock, MapPin, ArrowLeft, Share2, Navigation, CalendarPlus, Pencil, Music, } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; function formatDate(timestamp: number): string { const date = new Date(timestamp * 1000); return date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } function formatTime(timestamp: number): string { const date = new Date(timestamp * 1000); return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', }); } function EventDetailSkeleton() { return ( ); } export default function EventDetailPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const { data: event, isLoading, error } = useSkateEvent(identifier || '', pubkey); const author = useAuthor(event?.pubkey); const { user } = useCurrentUser(); const isOwner = user && event && user.pubkey === event.pubkey; useSeoMeta({ title: event ? `${event.title} - SkateSpots` : 'Skate Event - SkateSpots', description: event?.description || 'View this skateboarding event on SkateSpots.', }); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(event?.pubkey || ''); const coordinates = useMemo(() => { if (!event?.geohash) return null; const { latitude, longitude } = ngeohash.decode(event.geohash); return { lat: latitude, lng: longitude }; }, [event?.geohash]); const isUpcoming = event ? event.start > Date.now() / 1000 : false; const isPast = event ? event.start < Date.now() / 1000 : false; // Crew host — an `a` tag pointing at a crew coordinate (35006:pubkey:d). const crewHost = useMemo(() => { if (!event) return null; const ref = event.event.tags .filter(([t]) => t === 'a') .map(([, v]) => v) .find((v) => v?.startsWith(`${SKATE_CREW_KIND}:`)); if (!ref) return null; const [, hPubkey, hId] = ref.split(':'); const name = event.event.tags.find(([t]) => t === 'crew-name')?.[1]; return hPubkey && hId ? { pubkey: hPubkey, identifier: hId, name } : null; }, [event]); // Featured musician — an `a` tag pointing at a musician coordinate (35011:pubkey:d). const musicAct = useMemo(() => { if (!event) return null; const ref = event.event.tags .filter(([t]) => t === 'a') .map(([, v]) => v) .find((v) => v?.startsWith(`${SKATE_MUSICIAN_KIND}:`)); if (!ref) return null; const [, mPubkey, mId] = ref.split(':'); const name = event.event.tags.find(([t]) => t === 'music-act')?.[1]; return mPubkey && mId ? { pubkey: mPubkey, identifier: mId, name } : null; }, [event]); const handleShare = async () => { if (navigator.share) { try { await navigator.share({ title: event?.title, text: event?.description || `Check out this skate event: ${event?.title}`, url: window.location.href, }); } catch { // User cancelled or share failed } } else { navigator.clipboard.writeText(window.location.href); } }; const handleDirections = () => { if (coordinates) { window.open( `https://www.google.com/maps/dir/?api=1&destination=${coordinates.lat},${coordinates.lng}`, '_blank' ); } }; const handleAddToCalendar = () => { if (!event) return; const startDate = new Date(event.start * 1000); const endDate = event.end ? new Date(event.end * 1000) : new Date(event.start * 1000 + 3600000); // Default 1 hour const formatICSDate = (date: Date) => { return date.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; }; const icsContent = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'BEGIN:VEVENT', `DTSTART:${formatICSDate(startDate)}`, `DTEND:${formatICSDate(endDate)}`, `SUMMARY:${event.title}`, `DESCRIPTION:${event.description || ''}`, `LOCATION:${event.location || ''}`, 'END:VEVENT', 'END:VCALENDAR', ].join('\n'); const blob = new Blob([icsContent], { type: 'text/calendar' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${event.title.replace(/[^a-z0-9]/gi, '-')}.ics`; link.click(); URL.revokeObjectURL(url); }; if (isLoading) { return ; } if (error || !event) { return ; } return ( {/* Back link */} Back to Events {/* Main Content */} {/* Cover Image */} {event.image ? ( ) : ( )} {/* Event Info */} {isUpcoming && ( Upcoming )} {isPast && ( Past Event )} {event.title} {crewHost && ( Hosted by {crewHost.name || 'a crew'} )} {musicAct && ( Live: {musicAct.name || 'live music'} )} {isOwner && ( )} {isUpcoming && ( )} {/* Date & Time */} {formatDate(event.start)} {formatTime(event.start)} {event.end && ` - ${formatTime(event.end)}`} {/* Location */} {event.location && ( {event.location} )} {/* Types */} {event.types.length > 0 && ( {event.types.map((type) => ( {EVENT_TYPE_LABELS[type] || type} ))} )} {/* Description */} {event.description && ( {event.description} )} {/* Comments Section */} {event.event && } {/* Sidebar */} {/* Date Card */} {new Date(event.start * 1000).toLocaleDateString('en-US', { month: 'long' })} {new Date(event.start * 1000).getDate()} {new Date(event.start * 1000).toLocaleDateString('en-US', { weekday: 'long' })} {formatTime(event.start)} {isUpcoming && ( Add to Calendar )} {/* Map (if has location) */} {coordinates && ( Location Get Directions )} {/* Organizer Card */} Organizer {displayName.slice(0, 2).toUpperCase()} {displayName} Event Organizer ); }
{event.location}
{event.description}
{displayName}
Event Organizer