import { useSeoMeta } from '@unhead/react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { useTour } from '@/hooks/useTours'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { genUserName } from '@/lib/genUserName'; import { TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, SKATE_TOUR_KIND } from '@/lib/types'; import { ZapButton } from '@/components/ZapButton'; import { ProBadge } from '@/components/ProBadge'; import { AddToCalendar } from '@/components/AddToCalendar'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { ArrowLeft, Route, MapPin, Clock, Users, Zap, Globe, MessageCircle, Calendar, Star, Share2, Flag, Store, Percent, Pencil, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { SponsorSlot } from '@/components/SponsorSlot'; export default function TourDetailPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const navigate = useNavigate(); const { user } = useCurrentUser(); const { data: tour, isLoading, error } = useTour(identifier || '', pubkey); const author = useAuthor(tour?.pubkey || ''); const metadata = author.data?.metadata; const guideName = metadata?.name ?? genUserName(tour?.pubkey || ''); useSeoMeta({ title: tour ? `${tour.title} - ${TOUR_CATEGORIES[tour.category].label} - SkateSpots` : 'Skate Tour - SkateSpots', description: tour?.description || 'Discover skate spots and tours on SkateSpots', }); const handleBookTour = () => { if (!tour) return; // Navigate to messages/DM with the guide // For now, navigate to messages page - in future can open DM composer navigate(`/messages?to=${tour.pubkey}`); }; const handleShare = async () => { if (!tour) return; const url = window.location.href; if (navigator.share) { try { await navigator.share({ title: tour.title, text: `Check out this local skate tour: ${tour.title}`, url, }); } catch { // User cancelled or error } } else { await navigator.clipboard.writeText(url); } }; if (isLoading) { return (
); } if (error || !tour) { return (

Tour Not Found

This tour may have been removed or doesn't exist.

); } const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null; const categoryInfo = TOUR_CATEGORIES[tour.category]; const accent = categoryInfo.color; // A local tour with a fixed session uses a datetime (show time); everything else // (national/international/destruction) is a multi-day, all-day date range. const isLocalSession = tour.category === 'local'; const hasSchedule = !!tour.startDate; const formatSchedule = (): string => { if (!tour.startDate) return ''; const start = new Date(tour.startDate * 1000); if (isLocalSession) { return start.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', }); } const startStr = start.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); if (tour.endDate) { const end = new Date(tour.endDate * 1000); const endStr = end.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); return `${startStr} → ${endStr}`; } return startStr; }; const durationLabel = (() => { if (!tour.duration) return ''; const u = tour.durationUnit; if (u === 'weeks') { return tour.duration >= 4 ? `${Math.round(tour.duration / 4)} month${Math.round(tour.duration / 4) > 1 ? 's' : ''}` : `${tour.duration} week${tour.duration > 1 ? 's' : ''}`; } if (u === 'days') return `${tour.duration} day${tour.duration > 1 ? 's' : ''}`; return `${tour.duration} hour${tour.duration > 1 ? 's' : ''}`; })(); const countdownLabel = (() => { if (!tour.startDate) return null; const nowSecs = Date.now() / 1000; if (tour.startDate <= nowSecs) { if (tour.endDate && nowSecs <= tour.endDate) return { text: 'Happening now', live: true }; if (nowSecs - tour.startDate < 12 * 60 * 60) return { text: 'Happening now', live: true }; return null; } const days = Math.floor((tour.startDate - nowSecs) / 86400); const hours = Math.floor((tour.startDate - nowSecs) / 3600); if (hours < 1) return { text: 'Starting soon', live: true }; if (hours < 24) return { text: `In ${hours}h`, live: false }; if (days === 1) return { text: 'Tomorrow', live: false }; if (days < 7) return { text: `In ${days} days`, live: false }; if (days < 30) { const w = Math.round(days / 7); return { text: `In ${w} week${w > 1 ? 's' : ''}`, live: false }; } const m = Math.round(days / 30); return { text: `In ${m} month${m > 1 ? 's' : ''}`, live: false }; })(); const calendarEvent = tour.startDate ? { uid: `${SKATE_TOUR_KIND}:${tour.pubkey}:${tour.identifier}`, title: tour.title, description: tour.description || `${categoryInfo.label} on SkateSpots`, location: tour.location || tour.meetingPoint || tour.cities?.join(', ') || tour.countries?.join(', '), start: tour.startDate, end: tour.endDate, url: typeof window !== 'undefined' ? window.location.href : undefined, allDay: !isLocalSession, } : null; return (
{/* Back link + owner/admin controls */}
Back to Map
{user?.pubkey === tour.pubkey && ( )}
{/* Main card */} {/* Tour image */} {tour.image && (
{tour.title}
)}
{categoryInfo.emoji} {categoryInfo.label}
{tour.title} {tour.location && (

{tour.location}

)}
{/* Guide info */}
{guideName.slice(0, 2).toUpperCase()}

{guideName}

{tour.category === 'destruction' ? 'Organizer' : 'Tour Guide'}

{/* Schedule + Add to Calendar */} {hasSchedule && calendarEvent && (

{isLocalSession ? 'Scheduled Session' : tour.category === 'destruction' ? 'Competition Window' : 'Trip Dates'}

{countdownLabel && ( {countdownLabel.text} )}

{formatSchedule()}

{durationLabel && (

Runs about {durationLabel}

)}
)} {/* Description */} {tour.description && (

About This Tour

{tour.description}

)} {/* Tour details grid */}
{tour.duration && (
Duration

{durationLabel}

)} {tour.maxGroupSize && (
Max Group

{tour.maxGroupSize} people

)} {skillInfo && (
Skill Level

{skillInfo.emoji} {skillInfo.label}

)} {tour.price && (
Price

{tour.price.toLocaleString()} sats

per person

)} {tour.availability && (
Availability

{tour.availability}

)} {tour.languages && tour.languages.length > 0 && (
Languages

{tour.languages.map(l => l.toUpperCase()).join(', ')}

)}
{/* Spot types */} {tour.spotTypes.length > 0 && (

Types of Spots

{tour.spotTypes.map((type) => { const typeInfo = TOUR_SPOT_TYPES[type as keyof typeof TOUR_SPOT_TYPES]; return ( {typeInfo?.emoji || '🛹'} {typeInfo?.label || type} ); })}
)} {/* Partner Shop Discount */} {tour.shopDiscount && tour.shopName && (
Partner Shop Discount

{tour.shopDiscount}% OFF at {tour.shopName}

Book this tour to receive a discount at the partner skate shop. Show your booking confirmation to the shop staff.

)} {/* Tour Sponsors - invisible placeholder until sponsors are added */} {/* Meeting point */} {tour.meetingPoint && (
Meeting Point

{tour.meetingPoint}

{tour.meetingPointCoords && (

{tour.meetingPointCoords.lat.toFixed(4)}, {tour.meetingPointCoords.lon.toFixed(4)}

)}
)} {/* Actions */}
{!user && (

Login to contact the guide and book this tour

)}
); }