import { dateLocaleCode } from '@/lib/dateFnsI18n'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; 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 { TOUR_CATEGORY_KEYS, TOUR_SPOT_TYPE_KEYS, TOUR_SKILL_KEYS } from '@/lib/labelKeys'; 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'; import { useTranslation } from '@/contexts/language'; export default function TourDetailPage() { const { t } = useTranslation(); 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_CATEGORY_KEYS[tour.category] ? t(TOUR_CATEGORY_KEYS[tour.category]) : TOUR_CATEGORIES[tour.category].label} - SkateSpots` : 'Skate Tour - SkateSpots', description: tour?.description || 'Discover skate spots and tours on SkateSpots', }); const handleBookTour = () => { if (!tour) return; // Open a DM with the guide. Must use the same /messages/:npub route every // other entry point uses — the old `/messages?to=` param was ignored, so // the CTA silently opened an empty inbox. navigate(`/messages/${nip19.npubEncode(tour.pubkey)}`); }; const handleShare = async () => { if (!tour) return; const url = window.location.href; if (navigator.share) { try { await navigator.share({ title: tour.title, text: t('placesUi.tourDetail.shareText', { title: tour.title }), url, }); } catch { // User cancelled or error } } else { await navigator.clipboard.writeText(url); } }; if (isLoading) { return ( <>
); } if (error || !tour) { return ( <>

{t('placesUi.tourDetail.notFound')}

{t('placesUi.tourDetail.notFoundDesc')}

); } const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null; const skillKey = tour.skillLevel ? TOUR_SKILL_KEYS[tour.skillLevel] : undefined; 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(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' }); if (tour.endDate) { const end = new Date(tour.endDate * 1000); const endStr = end.toLocaleDateString(dateLocaleCode(), { 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 ? t('placesUi.tourCard.durationMonths', { count: Math.round(tour.duration / 4) }) : t('placesUi.tourCard.durationWeeks', { count: tour.duration }); } if (u === 'days') return t('placesUi.tourCard.durationDays', { count: tour.duration }); return t('placesUi.tourCard.durationHours', { count: tour.duration }); })(); const countdownLabel = (() => { if (!tour.startDate) return null; const nowSecs = Date.now() / 1000; if (tour.startDate <= nowSecs) { if (tour.endDate && nowSecs <= tour.endDate) return { text: t('placesUi.tourCard.happeningNow'), live: true }; if (nowSecs - tour.startDate < 12 * 60 * 60) return { text: t('placesUi.tourCard.happeningNow'), 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: t('placesUi.tourCard.startingSoon'), live: true }; if (hours < 24) return { text: t('placesUi.tourCard.inHours', { count: hours }), live: false }; if (days === 1) return { text: t('placesUi.tourCard.tomorrow'), live: false }; if (days < 7) return { text: t('placesUi.tourCard.inDays', { count: days }), live: false }; if (days < 30) { const w = Math.round(days / 7); return { text: t('placesUi.tourCard.inWeeks', { count: w }), live: false }; } const m = Math.round(days / 30); return { text: t('placesUi.tourCard.inMonths', { count: m }), live: false }; })(); const calendarEvent = tour.startDate ? { uid: `${SKATE_TOUR_KIND}:${tour.pubkey}:${tour.identifier}`, title: tour.title, description: tour.description || `${TOUR_CATEGORY_KEYS[tour.category] ? t(TOUR_CATEGORY_KEYS[tour.category]) : 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 */}
{t('placesUi.shop.backToMap')}
{user?.pubkey === tour.pubkey && ( )}
{/* Main card */} {/* Tour image */} {tour.image && (
{tour.title}
)}
{categoryInfo.emoji} {TOUR_CATEGORY_KEYS[tour.category] ? t(TOUR_CATEGORY_KEYS[tour.category]) : categoryInfo.label}
{tour.title} {tour.location && (

{tour.location}

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

{guideName}

{tour.category === 'destruction' ? t('placesUi.tourDetail.organizer') : t('placesUi.tourDetail.tourGuide')}

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

{isLocalSession ? t('placesUi.tourDetail.scheduledSession') : tour.category === 'destruction' ? t('placesUi.tourDetail.competitionWindow') : t('placesUi.tourDetail.tripDates')}

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

{formatSchedule()}

{durationLabel && (

{t('placesUi.tourDetail.runsAbout', { duration: durationLabel })}

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

{t('placesUi.tourDetail.about')}

{tour.description}

)} {/* Tour details grid */}
{tour.duration && (
{t('placesUi.tourDetail.duration')}

{durationLabel}

)} {tour.maxGroupSize && (
{t('placesUi.tourDetail.maxGroup')}

{t('placesUi.tourDetail.people', { count: tour.maxGroupSize })}

)} {skillInfo && (
{t('placesUi.tourDetail.skillLevel')}

{skillInfo.emoji} {skillKey ? t(skillKey) : skillInfo.label}

)} {tour.price && (
{t('placesUi.tourDetail.price')}

{t('placesUi.tourCard.price', { amount: tour.price.toLocaleString() })}

{t('placesUi.tourDetail.perPerson')}

)} {tour.availability && (
{t('placesUi.tourDetail.availability')}

{tour.availability}

)} {tour.languages && tour.languages.length > 0 && (
{t('placesUi.tourDetail.languages')}

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

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

{t('placesUi.tourDetail.spotTypes')}

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

{t('placesUi.tourDetail.discountAt', { percent: tour.shopDiscount, shop: tour.shopName ?? '' })}

{t('placesUi.tourDetail.partnerDiscountDesc')}

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

{tour.meetingPoint}

{tour.meetingPointCoords && (

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

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

{t('placesUi.tourDetail.loginToBookDesc')}

)}
); }