/** * TripItineraryCard — Sasquatch's day-by-day skate trip plan. * ───────────────────────────────────────────────────────────────────────────── * Renders a structured itinerary (not a wall of text): weather header, days * with spots/events/coaches, local crews, and honest actions — * • Save all spots to a Collection (Nostr-native, logged-in only) * • Book / message a coach (links to their profile → zap/DM there) * • Deep-link out to flights/stays (we assist, we don't book) */ import { useState } from 'react'; import { Loader2, Check, MapPin, Calendar, GraduationCap, Users, CloudRain, Bookmark, Plane } from 'lucide-react'; import type { Itinerary } from '@/lib/tripPlanner'; import { travelLinks } from '@/lib/tripPlanner'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCreateSpotCollection } from '@/hooks/useSpotCollections'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; const KIND_ICON = { spot: MapPin, event: Calendar, coach: GraduationCap, } as const; export function TripItineraryCard({ itinerary }: { itinerary: Itinerary }) { const { user } = useCurrentUser(); const { createCollection, isPending } = useCreateSpotCollection(); const { toast } = useToast(); const [saved, setSaved] = useState(false); const w = itinerary.weather; const rainy = (w?.precipitation_mm ?? 0) > 0.5 || (w?.skateability_score ?? 100) < 40; const handleSave = async () => { if (!user) { toast({ title: 'Log in to save', description: 'Sign in to keep this trip as a Collection.' }); return; } try { await createCollection({ title: `${itinerary.destination} skate trip 🛹`, description: `Trip plan by The Sasquatch — ${itinerary.days.length} days in ${itinerary.destination}.`, spotRefs: itinerary.allSpotRefs, }); setSaved(true); toast({ title: 'Saved to Collections 🦧', description: `Your ${itinerary.destination} trip is bookmarked.` }); } catch (err) { toast({ title: "Couldn't save", description: err instanceof Error ? err.message : 'Try again in a sec.', variant: 'destructive', }); } }; return (
{/* Header */}
🛹 {itinerary.days.length} days in {itinerary.destination}
{w && (
{w.weatherIcon} {w.condition} {typeof w.temperature_c === 'number' && · {Math.round(w.temperature_c)}°C} {w.skateability_label && ( = 60 ? 'bg-[#ccff00]/15 text-[#ccff00]' : 'bg-orange-500/15 text-orange-300')}> {w.skateability_label} )}
)} {rainy && (
Wet forecast — hit a local crew for covered/indoor options.
)}
{/* Days */}
{itinerary.days.map((day) => (
📅 {day.label}
{day.stops.length === 0 ? (
Free skate — explore & film. 🎥
) : (
{day.stops.map((stop, i) => { const Icon = KIND_ICON[stop.kind]; const inner = (
{stop.title}
{stop.subtitle && (
{stop.subtitle}
)} {stop.kind === 'coach' && (
tap to book / zap ⚡
)}
); return stop.url ? ( {inner} ) : (
{inner}
); })}
)}
))}
{/* Local crews */} {itinerary.crews.length > 0 && (
Local crews
{itinerary.crews.map((c, i) => ( 👥 {c.name} ))}
)} {/* Actions */}
{itinerary.allSpotRefs.length > 0 && ( )} {/* External travel — assist, don't book */}
{travelLinks(itinerary.destination).map((link) => ( {link.emoji} {link.label} ))}

I plan the skating — flights & stays open on the real sites (no KYC, no middleman).

); }