import { useState, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import ngeohash from 'ngeohash'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Slider } from '@/components/ui/slider'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useShops } from '@/hooks/useShops'; import { useToast } from '@/hooks/useToast'; import { LoginArea } from '@/components/auth/LoginArea'; import { SKATE_TOUR_KIND, TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, type TourCategory, type DestructionChallenge } from '@/lib/types'; import { Route, Upload, X, Loader2, ArrowLeft, MapPin, Clock, Users, Zap, Globe, Info, ChevronDown, Store, Percent, Plane, Car, Skull, Trophy, Target, Plus, Trash2, Calendar, Eye, EyeOff, } from 'lucide-react'; import { Link } from 'react-router-dom'; import { cn } from '@/lib/utils'; // Common languages shown by default const COMMON_LANGUAGES = [ { code: 'en', label: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, { code: 'es', label: 'Spanish', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, { code: 'pt', label: 'Portuguese', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, { code: 'fr', label: 'French', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, { code: 'de', label: 'German', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, { code: 'it', label: 'Italian', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, { code: 'ja', label: 'Japanese', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, { code: 'zh', label: 'Chinese', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, ]; // Extended language list (shown when expanded) const ALL_LANGUAGES = [ // Common (also in COMMON_LANGUAGES) { code: 'en', label: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, { code: 'es', label: 'Spanish', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, { code: 'pt', label: 'Portuguese', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, { code: 'fr', label: 'French', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, { code: 'de', label: 'German', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, { code: 'it', label: 'Italian', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, { code: 'ja', label: 'Japanese', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, { code: 'zh', label: 'Chinese', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, // European { code: 'nl', label: 'Dutch', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, { code: 'pl', label: 'Polish', flag: '๐Ÿ‡ต๐Ÿ‡ฑ' }, { code: 'ru', label: 'Russian', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, { code: 'uk', label: 'Ukrainian', flag: '๐Ÿ‡บ๐Ÿ‡ฆ' }, { code: 'cs', label: 'Czech', flag: '๐Ÿ‡จ๐Ÿ‡ฟ' }, { code: 'sk', label: 'Slovak', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ' }, { code: 'hu', label: 'Hungarian', flag: '๐Ÿ‡ญ๐Ÿ‡บ' }, { code: 'ro', label: 'Romanian', flag: '๐Ÿ‡ท๐Ÿ‡ด' }, { code: 'bg', label: 'Bulgarian', flag: '๐Ÿ‡ง๐Ÿ‡ฌ' }, { code: 'hr', label: 'Croatian', flag: '๐Ÿ‡ญ๐Ÿ‡ท' }, { code: 'sr', label: 'Serbian', flag: '๐Ÿ‡ท๐Ÿ‡ธ' }, { code: 'sl', label: 'Slovenian', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, { code: 'el', label: 'Greek', flag: '๐Ÿ‡ฌ๐Ÿ‡ท' }, { code: 'tr', label: 'Turkish', flag: '๐Ÿ‡น๐Ÿ‡ท' }, { code: 'sv', label: 'Swedish', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, { code: 'no', label: 'Norwegian', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, { code: 'da', label: 'Danish', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, { code: 'fi', label: 'Finnish', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ' }, { code: 'is', label: 'Icelandic', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ' }, { code: 'et', label: 'Estonian', flag: '๐Ÿ‡ช๐Ÿ‡ช' }, { code: 'lv', label: 'Latvian', flag: '๐Ÿ‡ฑ๐Ÿ‡ป' }, { code: 'lt', label: 'Lithuanian', flag: '๐Ÿ‡ฑ๐Ÿ‡น' }, { code: 'ca', label: 'Catalan', flag: '๐Ÿด๓ ฅ๓ ณ๓ ฃ๓ ด๓ ฟ' }, { code: 'eu', label: 'Basque', flag: '๐Ÿด' }, { code: 'gl', label: 'Galician', flag: '๐Ÿด' }, // Asian { code: 'ko', label: 'Korean', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, { code: 'vi', label: 'Vietnamese', flag: '๐Ÿ‡ป๐Ÿ‡ณ' }, { code: 'th', label: 'Thai', flag: '๐Ÿ‡น๐Ÿ‡ญ' }, { code: 'id', label: 'Indonesian', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ' }, { code: 'ms', label: 'Malay', flag: '๐Ÿ‡ฒ๐Ÿ‡พ' }, { code: 'tl', label: 'Filipino', flag: '๐Ÿ‡ต๐Ÿ‡ญ' }, { code: 'hi', label: 'Hindi', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'bn', label: 'Bengali', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, { code: 'ta', label: 'Tamil', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'te', label: 'Telugu', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'mr', label: 'Marathi', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'gu', label: 'Gujarati', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'kn', label: 'Kannada', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'ml', label: 'Malayalam', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'pa', label: 'Punjabi', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, { code: 'ne', label: 'Nepali', flag: '๐Ÿ‡ณ๐Ÿ‡ต' }, { code: 'si', label: 'Sinhala', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ' }, { code: 'my', label: 'Burmese', flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ' }, { code: 'km', label: 'Khmer', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ' }, { code: 'lo', label: 'Lao', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ' }, { code: 'mn', label: 'Mongolian', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ' }, // Middle Eastern { code: 'ar', label: 'Arabic', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, { code: 'he', label: 'Hebrew', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ' }, { code: 'fa', label: 'Persian', flag: '๐Ÿ‡ฎ๐Ÿ‡ท' }, { code: 'ur', label: 'Urdu', flag: '๐Ÿ‡ต๐Ÿ‡ฐ' }, // African { code: 'sw', label: 'Swahili', flag: '๐Ÿ‡ฐ๐Ÿ‡ช' }, { code: 'am', label: 'Amharic', flag: '๐Ÿ‡ช๐Ÿ‡น' }, { code: 'zu', label: 'Zulu', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, { code: 'xh', label: 'Xhosa', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, { code: 'af', label: 'Afrikaans', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, { code: 'ha', label: 'Hausa', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, { code: 'yo', label: 'Yoruba', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, { code: 'ig', label: 'Igbo', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, // Americas { code: 'pt-BR', label: 'Brazilian Portuguese', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, { code: 'es-MX', label: 'Mexican Spanish', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, { code: 'es-AR', label: 'Argentine Spanish', flag: '๐Ÿ‡ฆ๐Ÿ‡ท' }, { code: 'qu', label: 'Quechua', flag: '๐Ÿ‡ต๐Ÿ‡ช' }, { code: 'gn', label: 'Guarani', flag: '๐Ÿ‡ต๐Ÿ‡พ' }, // Sign Languages { code: 'asl', label: 'ASL (American Sign)', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, { code: 'bsl', label: 'BSL (British Sign)', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, { code: 'lsf', label: 'LSF (French Sign)', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, { code: 'dgs', label: 'DGS (German Sign)', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, { code: 'jsl', label: 'JSL (Japanese Sign)', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, { code: 'auslan', label: 'Auslan (Australian Sign)', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, { code: 'lse', label: 'LSE (Spanish Sign)', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, { code: 'libras', label: 'Libras (Brazilian Sign)', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, // Oceanian { code: 'mi', label: 'Mฤori', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ' }, { code: 'haw', label: 'Hawaiian', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, { code: 'sm', label: 'Samoan', flag: '๐Ÿ‡ผ๐Ÿ‡ธ' }, { code: 'to', label: 'Tongan', flag: '๐Ÿ‡น๐Ÿ‡ด' }, { code: 'fj', label: 'Fijian', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ' }, // Additional European { code: 'ga', label: 'Irish (Gaeilge)', flag: '๐Ÿ‡ฎ๐Ÿ‡ช' }, { code: 'cy', label: 'Welsh', flag: '๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ' }, { code: 'gd', label: 'Scottish Gaelic', flag: '๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ' }, { code: 'mt', label: 'Maltese', flag: '๐Ÿ‡ฒ๐Ÿ‡น' }, { code: 'sq', label: 'Albanian', flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ' }, { code: 'mk', label: 'Macedonian', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ' }, { code: 'bs', label: 'Bosnian', flag: '๐Ÿ‡ง๐Ÿ‡ฆ' }, { code: 'be', label: 'Belarusian', flag: '๐Ÿ‡ง๐Ÿ‡พ' }, { code: 'ka', label: 'Georgian', flag: '๐Ÿ‡ฌ๐Ÿ‡ช' }, { code: 'hy', label: 'Armenian', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ' }, { code: 'az', label: 'Azerbaijani', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ' }, // Central Asian { code: 'kk', label: 'Kazakh', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ' }, { code: 'uz', label: 'Uzbek', flag: '๐Ÿ‡บ๐Ÿ‡ฟ' }, { code: 'tg', label: 'Tajik', flag: '๐Ÿ‡น๐Ÿ‡ฏ' }, { code: 'ky', label: 'Kyrgyz', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ' }, { code: 'tk', label: 'Turkmen', flag: '๐Ÿ‡น๐Ÿ‡ฒ' }, // Additional African { code: 'so', label: 'Somali', flag: '๐Ÿ‡ธ๐Ÿ‡ด' }, { code: 'rw', label: 'Kinyarwanda', flag: '๐Ÿ‡ท๐Ÿ‡ผ' }, { code: 'sn', label: 'Shona', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ' }, { code: 'mg', label: 'Malagasy', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ' }, { code: 'wo', label: 'Wolof', flag: '๐Ÿ‡ธ๐Ÿ‡ณ' }, // Caribbean & Creoles { code: 'ht', label: 'Haitian Creole', flag: '๐Ÿ‡ญ๐Ÿ‡น' }, { code: 'pap', label: 'Papiamento', flag: '๐Ÿ‡จ๐Ÿ‡ผ' }, { code: 'jm', label: 'Jamaican Patois', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ' }, ]; export default function CreateTourPage() { useSeoMeta({ title: 'Create a Tour - SkateSpots', description: 'Organize skate tours - local hidden gem tours, national road trips, international adventures, or KOTR-style destruction competitions.', }); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { user, metadata, isLoadingProfile } = useCurrentUser(); const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); // Tour category state const [tourCategory, setTourCategory] = useState('local'); // Form state const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [location, setLocation] = useState(''); // City/region const [meetingPoint, setMeetingPoint] = useState(''); const [meetingLat, setMeetingLat] = useState(''); const [meetingLng, setMeetingLng] = useState(''); const [duration, setDuration] = useState([3]); // Hours/Days depending on category const [durationUnit, setDurationUnit] = useState<'hours' | 'days' | 'weeks'>('hours'); const [skillLevel, setSkillLevel] = useState('all'); const [spotTypes, setSpotTypes] = useState(['street']); const [maxGroupSize, setMaxGroupSize] = useState([4]); const [price, setPrice] = useState(''); const [availability, setAvailability] = useState(''); const [languages, setLanguages] = useState(['en']); const [languagesExpanded, setLanguagesExpanded] = useState(false); const [image, setImage] = useState(null); const [isGettingLocation, setIsGettingLocation] = useState(false); // Partner shop state const [partnerShopId, setPartnerShopId] = useState(''); const [shopDiscount, setShopDiscount] = useState([10]); // Default 10% const [shopExpanded, setShopExpanded] = useState(false); // National/International specific const [cities, setCities] = useState([]); const [newCity, setNewCity] = useState(''); const [countries, setCountries] = useState([]); const [newCountry, setNewCountry] = useState(''); const [transportInfo, setTransportInfo] = useState(''); const [accommodationInfo, setAccommodationInfo] = useState(''); // World Destruction Tour specific const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [entryFee, setEntryFee] = useState(''); const [prizePool, setPrizePool] = useState(''); const [prizeDistribution, setPrizeDistribution] = useState([ { place: 1, percentage: 50 }, { place: 2, percentage: 30 }, { place: 3, percentage: 20 }, ]); const [maxTeams, setMaxTeams] = useState([4]); const [minTeamSize, setMinTeamSize] = useState([4]); const [maxTeamSize, setMaxTeamSize] = useState([6]); const [challenges, setChallenges] = useState([]); const [acceptedClipSources, setAcceptedClipSources] = useState(['blossom', 'youtube', 'instagram']); const [organizerContact, setOrganizerContact] = useState(''); // Organizer contact details // Film release options for World Destruction Tours const [enableFilmRelease, setEnableFilmRelease] = useState(false); const [filmReleaseAtShops, setFilmReleaseAtShops] = useState(true); // Release at partner skate shops const [filmReleaseOnline, setFilmReleaseOnline] = useState(true); // Also release online const [filmReleaseDate, setFilmReleaseDate] = useState(''); // Optional specific date for film release const [showAvailableShops, setShowAvailableShops] = useState(false); // Toggle to show available shops for premieres // Fetch shops for partner selection const { data: shops, isLoading: shopsLoading } = useShops(100); // Update duration unit based on category useEffect(() => { if (tourCategory === 'local') { setDurationUnit('hours'); setDuration([3]); } else if (tourCategory === 'national') { setDurationUnit('days'); setDuration([5]); } else if (tourCategory === 'international') { setDurationUnit('weeks'); setDuration([4]); // Default 4 weeks / 1 month } else if (tourCategory === 'destruction') { // World Destruction uses months (stored as 'months' but we'll use weeks * 4) setDurationUnit('weeks'); // We'll display as months setDuration([4]); // 1 month = 4 weeks } }, [tourCategory]); // Challenge management - all challenges are secret until competition start date const addChallenge = () => { const newChallenge: DestructionChallenge = { id: `ch-${Date.now()}`, title: '', description: '', points: 100, isSecret: true, // Always secret until start date }; setChallenges([...challenges, newChallenge]); }; const updateChallenge = (index: number, updates: Partial) => { const updated = [...challenges]; updated[index] = { ...updated[index], ...updates }; setChallenges(updated); }; const removeChallenge = (index: number) => { setChallenges(challenges.filter((_, i) => i !== index)); }; // City/Country management for multi-city tours const addCity = () => { if (newCity.trim() && !cities.includes(newCity.trim())) { setCities([...cities, newCity.trim()]); setNewCity(''); } }; const removeCity = (city: string) => { setCities(cities.filter(c => c !== city)); }; const addCountry = () => { if (newCountry.trim() && !countries.includes(newCountry.trim())) { setCountries([...countries, newCountry.trim()]); setNewCountry(''); } }; const removeCountry = (country: string) => { setCountries(countries.filter(c => c !== country)); }; // Pre-fill coordinates from URL params (from map click) useEffect(() => { const lat = searchParams.get('lat'); const lng = searchParams.get('lng'); if (lat && lng) { setMeetingLat(lat); setMeetingLng(lng); } }, [searchParams]); const getCurrentLocation = () => { if (!navigator.geolocation) { toast({ title: 'Geolocation not supported', description: 'Your browser does not support geolocation.', variant: 'destructive', }); return; } setIsGettingLocation(true); navigator.geolocation.getCurrentPosition( (position) => { setMeetingLat(position.coords.latitude.toFixed(6)); setMeetingLng(position.coords.longitude.toFixed(6)); setIsGettingLocation(false); toast({ title: 'Location found', description: 'Your current location has been set as meeting point.', }); }, (error) => { setIsGettingLocation(false); toast({ title: 'Location error', description: error.message, variant: 'destructive', }); } ); }; const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; try { const file = files[0]; const tags = await uploadFile(file); const url = tags[0]?.[1]; if (url) { setImage(url); toast({ title: 'Image uploaded', description: 'Your tour image has been uploaded.', }); } } catch (error) { toast({ title: 'Upload failed', description: error instanceof Error ? error.message : 'Failed to upload image', variant: 'destructive', }); } e.target.value = ''; }; const toggleSpotType = (type: string) => { setSpotTypes((prev) => prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type] ); }; const toggleLanguage = (code: string) => { setLanguages((prev) => prev.includes(code) ? prev.filter((l) => l !== code) : [...prev, code] ); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) { toast({ title: 'Tour title required', description: 'Please enter a name for your tour.', variant: 'destructive', }); return; } // Location required only for local/national tours if ((tourCategory === 'local' || tourCategory === 'national') && !location.trim()) { toast({ title: 'City/region required', description: 'Please enter your city or region.', variant: 'destructive', }); return; } // Meeting point required only for local/national tours if ((tourCategory === 'local' || tourCategory === 'national') && !meetingPoint.trim()) { toast({ title: 'Meeting point required', description: 'Please enter where you will meet tour participants.', variant: 'destructive', }); return; } // Coordinates required only for local/national tours if ((tourCategory === 'local' || tourCategory === 'national') && (!meetingLat || !meetingLng)) { toast({ title: 'Meeting point coordinates required', description: 'Please enter coordinates or use your current location.', variant: 'destructive', }); return; } let lat = 0; let lng = 0; if (tourCategory === 'local' || tourCategory === 'national') { lat = parseFloat(meetingLat); lng = parseFloat(meetingLng); if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { toast({ title: 'Invalid coordinates', description: 'Please enter valid latitude (-90 to 90) and longitude (-180 to 180).', variant: 'destructive', }); return; } } // Spot types required only for non-destruction tours if (tourCategory !== 'destruction' && spotTypes.length === 0) { toast({ title: 'Spot types required', description: 'Please select at least one type of spot you will show.', variant: 'destructive', }); return; } // International tour requires at least one country if (tourCategory === 'international' && countries.length === 0) { toast({ title: 'Countries required', description: 'Please add at least one country for your international tour.', variant: 'destructive', }); return; } // Destruction tour validations if (tourCategory === 'destruction') { if (!startDate) { toast({ title: 'Start date required', description: 'Please set when the competition starts (challenges will be revealed on this date).', variant: 'destructive', }); return; } if (challenges.length === 0) { toast({ title: 'Challenges required', description: 'Please add at least one challenge for teams to complete.', variant: 'destructive', }); return; } } try { // Generate low-precision geohash for city-level location (4 chars = ~39km) const geohash = ngeohash.encode(lat, lng, 4); // Create unique identifier const identifier = title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 50) + '-' + Date.now().toString(36); // Build tags const tags: string[][] = [ ['d', identifier], ['title', title.trim()], ['t', 'skate-tour'], // Primary identifier ['tour-category', tourCategory], // Tour category ]; // Location/geohash only for local/national tours if (tourCategory === 'local' || tourCategory === 'national') { tags.push(['g', geohash]); tags.push(['location', location.trim()]); tags.push(['meeting-point', meetingPoint.trim()]); tags.push(['meeting-point-coords', lat.toString(), lng.toString()]); } // Duration - store in appropriate unit const durationVal = tourCategory === 'destruction' ? duration[0].toString() // weeks (displayed as months) : duration[0].toString(); const durationUnitVal = tourCategory === 'destruction' ? 'weeks' : durationUnit; tags.push(['duration', durationVal, durationUnitVal]); // Skill level - destruction tours are always advanced if (tourCategory === 'destruction') { tags.push(['skill-level', 'advanced']); } else { tags.push(['skill-level', skillLevel]); } // Max group size (not for destruction tours) if (tourCategory !== 'destruction') { tags.push(['max-group', maxGroupSize[0].toString()]); } // Add spot type tags - destruction tours include ALL spot types automatically if (tourCategory === 'destruction') { Object.keys(TOUR_SPOT_TYPES).forEach((type) => { tags.push(['t', type]); }); } else { spotTypes.forEach((type) => { tags.push(['t', type]); }); } // Add optional tags if (price && tourCategory !== 'destruction') { tags.push(['price', price]); } // Availability only for non-destruction tours if (availability.trim() && tourCategory !== 'destruction') { tags.push(['available', availability.trim()]); } if (languages.length > 0) { tags.push(['languages', ...languages]); } if (image) { tags.push(['image', image]); } // Partner shop discount if (partnerShopId) { const selectedShop = shops?.find(s => `${s.pubkey}:${s.identifier}` === partnerShopId); if (selectedShop) { // Store reference as naddr-like format tags.push(['shop', `30017:${selectedShop.pubkey}:${selectedShop.identifier}`]); tags.push(['shop-name', selectedShop.name]); tags.push(['shop-discount', shopDiscount[0].toString()]); } } // ============ NATIONAL/INTERNATIONAL SPECIFIC TAGS ============ if (tourCategory === 'national' || tourCategory === 'international') { if (cities.length > 0) { tags.push(['cities', ...cities]); } if (tourCategory === 'international' && countries.length > 0) { tags.push(['countries', ...countries]); } if (transportInfo.trim()) { tags.push(['transport', transportInfo.trim()]); } if (accommodationInfo.trim()) { tags.push(['accommodation', accommodationInfo.trim()]); } } // ============ WORLD DESTRUCTION TOUR SPECIFIC TAGS ============ if (tourCategory === 'destruction') { // Cities and countries for the competition if (cities.length > 0) { tags.push(['cities', ...cities]); } if (countries.length > 0) { tags.push(['countries', ...countries]); } // Organizer contact if (organizerContact.trim()) { tags.push(['organizer-contact', organizerContact.trim()]); } // Transport info if (transportInfo.trim()) { tags.push(['transport', transportInfo.trim()]); } // Competition dates - challenges are revealed on start date const startTimestamp = startDate ? (new Date(startDate).getTime() / 1000).toString() : ''; if (startTimestamp) { tags.push(['start-date', startTimestamp]); } if (endDate) { tags.push(['end-date', (new Date(endDate).getTime() / 1000).toString()]); } // Entry fee if (entryFee) { tags.push(['entry-fee', entryFee]); } // Prize pool and distribution if (prizePool) { tags.push(['prize-pool', prizePool]); // Format: ["prize-dist", "1", "50", "2", "30", "3", "20"] const distParts = prizeDistribution.flatMap(p => [p.place.toString(), p.percentage.toString()]); tags.push(['prize-dist', ...distParts]); } // Team settings tags.push(['max-teams', maxTeams[0].toString()]); tags.push(['min-team-size', minTeamSize[0].toString()]); tags.push(['max-team-size', maxTeamSize[0].toString()]); // Challenges - format: ["challenge", "id", "title", "description", "points", "revealDate"] // All challenges are secret and revealed on start date challenges.forEach((ch) => { tags.push(['challenge', ch.id, ch.title, ch.description || '', ch.points.toString(), startTimestamp]); }); // Accepted clip sources if (acceptedClipSources.length > 0) { tags.push(['clip-sources', ...acceptedClipSources]); } // Film release options if (enableFilmRelease) { const releaseOptions: string[] = []; if (filmReleaseAtShops) releaseOptions.push('shops'); if (filmReleaseOnline) releaseOptions.push('online'); if (releaseOptions.length > 0) { tags.push(['film-release', ...releaseOptions]); } if (filmReleaseDate) { tags.push(['film-release-date', (new Date(filmReleaseDate).getTime() / 1000).toString()]); } } } // Platform fee - varies by tour category // Local: 10%, National: 7%, International: 5%, World Destruction: 5% const platformFees: Record = { local: '10', national: '7', international: '5', destruction: '5', }; tags.push(['zap-split', 'skatespots@minibits.cash', platformFees[tourCategory]]); // NIP-31 alt tag const locationText = tourCategory === 'destruction' || tourCategory === 'international' ? (countries.length > 0 ? countries.join(', ') : cities.join(', ')) : location.trim(); const altText = tourCategory === 'destruction' ? `World Destruction Tour: ${title.trim()} - ${locationText}` : `Skate Tour: ${title.trim()} in ${locationText}`; tags.push(['alt', altText]); console.log('Publishing tour event:', { kind: SKATE_TOUR_KIND, tags, content: description.trim(), }); await createEvent({ kind: SKATE_TOUR_KIND, content: description.trim(), tags, }); const toastMessages = { local: { title: '๐Ÿ  Local Tour Created!', description: 'Your tour is now visible. Skaters can book via DM!' }, national: { title: '๐Ÿš National Tour Created!', description: 'Your road trip is live! Start coordinating with interested skaters.' }, international: { title: 'โœˆ๏ธ International Tour Created!', description: 'Your global adventure is listed! Connect with skaters worldwide.' }, destruction: { title: '๐Ÿ’€ WORLD DESTRUCTION TOUR CREATED!', description: 'Teams can register. Challenges revealed on start date!' }, }; toast(toastMessages[tourCategory]); navigate(`/tour/${user?.pubkey}/${identifier}`); } catch (error) { console.error('Failed to create tour:', error); toast({ title: 'Failed to publish', description: error instanceof Error ? error.message : 'Something went wrong.', variant: 'destructive', }); } }; if (!user) { return (

Login Required

You need to log in to create a tour.

); } // NIP-05 verification required for tour creation (spam prevention) // Note: metadata comes from useAuthor which loads asynchronously const hasNip05 = !!metadata?.nip05; // Show loading state while profile is being fetched if (isLoadingProfile) { return (

Loading Profile...

Checking your NIP-05 verification status.

); } // Require NIP-05 for tour creation if (!hasNip05) { return (

NIP-05 Verification Required

To prevent spam and build trust, tour organizers must have NIP-05 verification.

NIP-05 proves your identity by linking your Nostr profile to a domain you control.

); } return (
Back to Map

Create a Tour

{TOUR_CATEGORIES[tourCategory].description}

{/* Tour Category Selection */}
{Object.entries(TOUR_CATEGORIES).map(([key, cat]) => ( ))}
{/* Info banner - changes based on category */}
{tourCategory === 'local' && ( <>

How Local Tours Work

  • โ€ข You set your price - platform takes 10%
  • โ€ข Hidden spots stay hidden - you show them in person
  • โ€ข Visitors book via DM and pay in Lightning
  • โ€ข Build your reputation with reviews and zaps
)} {tourCategory === 'national' && ( <>

How National Tours Work

  • โ€ข Multi-city road trips across your country
  • โ€ข You set the price - platform takes only 7%
  • โ€ข Include transport/accommodation info for participants
  • โ€ข Build legendary skate adventures!
)} {tourCategory === 'international' && ( <>

How International Tours Work

  • โ€ข Cross-border adventures across multiple countries
  • โ€ข You set the price - platform takes only 5%
  • โ€ข Connect skaters from around the world
  • โ€ข Create unforgettable global experiences!
)} {tourCategory === 'destruction' && ( <>

๐Ÿ’€ WORLD DESTRUCTION TOUR - KOTR Style

  • โ€ข Global team competition - up to 12 months long
  • โ€ข YOU define challenges, points, prizes - we just provide the platform
  • โ€ข All challenges are SECRET until competition start date
  • โ€ข Clips via Blossom upload OR external links (YouTube, Instagram, any URL)
  • โ€ข Advanced level only - all spot types included automatically
  • โ€ข Platform takes only 5% of entry fees (NOT prize pool)
)}
{tourCategory === 'destruction' ? : } {tourCategory === 'destruction' ? 'World Destruction Tour' : 'Tour Details'} {tourCategory === 'destruction' ? 'Set up your global KOTR-style competition - you define EVERYTHING!' : 'Tell skaters what to expect on your tour'} {/* Tour Title */}
setTitle(e.target.value)} className="bg-black border-cyan-500/30" required />
{/* Description */}