SkateSpots/src/pages/CreateTourPage.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

2215 lines
103 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TourCategory>('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<string>('all');
const [spotTypes, setSpotTypes] = useState<string[]>(['street']);
const [maxGroupSize, setMaxGroupSize] = useState([4]);
const [price, setPrice] = useState('');
const [availability, setAvailability] = useState('');
const [languages, setLanguages] = useState<string[]>(['en']);
const [languagesExpanded, setLanguagesExpanded] = useState(false);
const [image, setImage] = useState<string | null>(null);
const [isGettingLocation, setIsGettingLocation] = useState(false);
// Partner shop state
const [partnerShopId, setPartnerShopId] = useState<string>('');
const [shopDiscount, setShopDiscount] = useState([10]); // Default 10%
const [shopExpanded, setShopExpanded] = useState(false);
// National/International specific
const [cities, setCities] = useState<string[]>([]);
const [newCity, setNewCity] = useState('');
const [countries, setCountries] = useState<string[]>([]);
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<DestructionChallenge[]>([]);
const [acceptedClipSources, setAcceptedClipSources] = useState<string[]>(['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<DestructionChallenge>) => {
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<HTMLInputElement>) => {
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<TourCategory, string> = {
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 (
<Layout>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto border-dashed">
<CardContent className="py-12 text-center">
<Route className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h2 className="text-xl font-bold mb-2">Login Required</h2>
<p className="text-muted-foreground mb-6">
You need to log in to create a tour.
</p>
<LoginArea className="justify-center" />
</CardContent>
</Card>
</div>
</Layout>
);
}
// 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 (
<Layout>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto border-dashed">
<CardContent className="py-12 text-center">
<Loader2 className="h-12 w-12 mx-auto mb-4 animate-spin text-cyan-400" />
<h2 className="text-xl font-bold mb-2">Loading Profile...</h2>
<p className="text-muted-foreground">
Checking your NIP-05 verification status.
</p>
</CardContent>
</Card>
</div>
</Layout>
);
}
// Require NIP-05 for tour creation
if (!hasNip05) {
return (
<Layout>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto border-2 border-red-500/50 bg-red-950/20">
<CardContent className="py-12 text-center">
<div className="w-16 h-16 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-4">
<Route className="h-8 w-8 text-red-400" />
</div>
<h2 className="text-xl font-bold mb-2 text-red-400">NIP-05 Verification Required</h2>
<p className="text-gray-300 mb-2">
To prevent spam and build trust, tour organizers must have NIP-05 verification.
</p>
<p className="text-gray-500 text-sm mb-6">
NIP-05 proves your identity by linking your Nostr profile to a domain you control.
</p>
<div className="space-y-3 max-w-sm mx-auto mb-6">
<p className="text-xs text-gray-400 font-bold uppercase">Get Free NIP-05:</p>
<div className="grid grid-cols-2 gap-2">
<a
href="https://nip05.social"
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-black/50 border border-green-500/30 hover:border-green-500/60 transition-all text-left"
>
<div className="font-bold text-white text-sm">nip05.social</div>
<div className="text-xs text-gray-400">Free NIP-05</div>
</a>
<a
href="https://nostr.directory"
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-black/50 border border-purple-500/30 hover:border-purple-500/60 transition-all text-left"
>
<div className="font-bold text-white text-sm">nostr.directory</div>
<div className="text-xs text-gray-400">Free NIP-05</div>
</a>
</div>
</div>
<Link to="/settings">
<Button className="bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold">
Update Your Profile
</Button>
</Link>
</CardContent>
</Card>
</div>
</Layout>
);
}
return (
<Layout>
<div className="container px-4 sm:px-6 py-6 sm:py-8 max-w-2xl">
<div className="mb-6">
<Link to="/map" className="inline-flex items-center text-gray-400 hover:text-white transition-colors mb-4">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Map
</Link>
<h1 className="text-3xl font-black uppercase" style={{
fontFamily: 'Impact, "Arial Black", sans-serif',
color: TOUR_CATEGORIES[tourCategory].color,
textShadow: `0 0 10px ${TOUR_CATEGORIES[tourCategory].color}50`,
}}>Create a Tour</h1>
<p className="text-gray-400 mt-1">
{TOUR_CATEGORIES[tourCategory].description}
</p>
</div>
{/* Tour Category Selection */}
<div className="mb-6">
<Label className="text-base font-bold mb-3 block">Tour Type</Label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{Object.entries(TOUR_CATEGORIES).map(([key, cat]) => (
<Button
key={key}
type="button"
variant={tourCategory === key ? 'default' : 'outline'}
onClick={() => setTourCategory(key as TourCategory)}
className={cn(
'flex-col h-auto py-3 gap-1',
tourCategory === key
? 'border-2'
: 'border-gray-700 hover:border-gray-500'
)}
style={{
backgroundColor: tourCategory === key ? cat.color : undefined,
borderColor: tourCategory === key ? cat.color : undefined,
color: tourCategory === key ? (key === 'local' || key === 'international' ? 'white' : 'black') : undefined,
}}
>
<span className="text-xl">{cat.emoji}</span>
<span className="text-xs font-bold">{cat.label}</span>
<span className="text-[10px] opacity-70">{cat.durationLabel}</span>
</Button>
))}
</div>
</div>
{/* Info banner - changes based on category */}
<Card className="mb-6" style={{
backgroundColor: `${TOUR_CATEGORIES[tourCategory].color}10`,
borderColor: `${TOUR_CATEGORIES[tourCategory].color}30`
}}>
<CardContent className="py-4">
<div className="flex gap-3">
<Info className="h-5 w-5 flex-shrink-0 mt-0.5" style={{ color: TOUR_CATEGORIES[tourCategory].color }} />
<div className="text-sm">
{tourCategory === 'local' && (
<>
<p className="font-bold mb-1" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>How Local Tours Work</p>
<ul className="text-gray-300 space-y-1 text-xs">
<li>• You set your price - <strong>platform takes 10%</strong></li>
<li>• Hidden spots stay hidden - you show them in person</li>
<li>• Visitors book via DM and pay in Lightning</li>
<li>• Build your reputation with reviews and zaps</li>
</ul>
</>
)}
{tourCategory === 'national' && (
<>
<p className="font-bold mb-1" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>How National Tours Work</p>
<ul className="text-gray-300 space-y-1 text-xs">
<li>• Multi-city road trips across your country</li>
<li>• You set the price - <strong>platform takes only 7%</strong></li>
<li>• Include transport/accommodation info for participants</li>
<li>• Build legendary skate adventures!</li>
</ul>
</>
)}
{tourCategory === 'international' && (
<>
<p className="font-bold mb-1" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>How International Tours Work</p>
<ul className="text-gray-300 space-y-1 text-xs">
<li>• Cross-border adventures across multiple countries</li>
<li>• You set the price - <strong>platform takes only 5%</strong></li>
<li>• Connect skaters from around the world</li>
<li>• Create unforgettable global experiences!</li>
</ul>
</>
)}
{tourCategory === 'destruction' && (
<>
<p className="font-bold mb-1" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>💀 WORLD DESTRUCTION TOUR - KOTR Style</p>
<ul className="text-gray-300 space-y-1 text-xs">
<li>• Global team competition - up to 12 months long</li>
<li>• YOU define challenges, points, prizes - we just provide the platform</li>
<li>• <strong>All challenges are SECRET</strong> until competition start date</li>
<li>• Clips via Blossom upload OR external links (YouTube, Instagram, any URL)</li>
<li>• Advanced level only - all spot types included automatically</li>
<li>• <strong>Platform takes only 5%</strong> of entry fees (NOT prize pool)</li>
</ul>
</>
)}
</div>
</div>
</CardContent>
</Card>
<form onSubmit={handleSubmit}>
<Card className="bg-black/90 border-2 border-cyan-500/30">
<CardHeader>
<CardTitle className="uppercase font-black flex items-center gap-2" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>
{tourCategory === 'destruction' ? <Skull className="h-5 w-5" /> : <Route className="h-5 w-5" />}
{tourCategory === 'destruction' ? 'World Destruction Tour' : 'Tour Details'}
</CardTitle>
<CardDescription className="text-gray-400">
{tourCategory === 'destruction'
? 'Set up your global KOTR-style competition - you define EVERYTHING!'
: 'Tell skaters what to expect on your tour'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Tour Title */}
<div className="space-y-2">
<Label htmlFor="title">Tour Title *</Label>
<Input
id="title"
placeholder="e.g., Berlin Kreuzberg - Underground Spots Tour"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="bg-black border-cyan-500/30"
required
/>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe what skaters will experience: spots you'll visit, vibe, what makes your tour unique..."
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={4}
className="bg-black border-cyan-500/30"
/>
</div>
{/* City/Region - only for local and national tours */}
{(tourCategory === 'local' || tourCategory === 'national') && (
<div className="space-y-2">
<Label htmlFor="location">{tourCategory === 'local' ? 'City / Region *' : 'Starting City *'}</Label>
<Input
id="location"
placeholder={tourCategory === 'local' ? 'e.g., Berlin, Germany' : 'e.g., Los Angeles, USA'}
value={location}
onChange={(e) => setLocation(e.target.value)}
className="bg-black"
style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30` }}
required
/>
</div>
)}
{/* Meeting Point - only for local and national tours */}
{(tourCategory === 'local' || tourCategory === 'national') && (
<div className="space-y-3 p-4 rounded-lg border-2 bg-opacity-5" style={{
borderColor: `${TOUR_CATEGORIES[tourCategory].color}30`,
backgroundColor: `${TOUR_CATEGORIES[tourCategory].color}05`
}}>
<div className="flex items-center gap-2 mb-2">
<MapPin className="h-5 w-5" style={{ color: TOUR_CATEGORIES[tourCategory].color }} />
<Label className="text-base font-bold" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>Meeting Point *</Label>
</div>
<p className="text-sm text-gray-400 mb-3">
Where will you meet tour participants? Choose a public, well-known spot.
</p>
<Input
placeholder="e.g., Warschauer Straße Station"
value={meetingPoint}
onChange={(e) => setMeetingPoint(e.target.value)}
className="bg-black"
style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30` }}
required={tourCategory === 'local' || tourCategory === 'national'}
/>
<div className="grid grid-cols-2 gap-4 mt-3">
<div>
<Input
placeholder="Latitude"
value={meetingLat}
onChange={(e) => setMeetingLat(e.target.value)}
className="bg-black border-cyan-500/30"
required
/>
</div>
<div>
<Input
placeholder="Longitude"
value={meetingLng}
onChange={(e) => setMeetingLng(e.target.value)}
className="bg-black border-cyan-500/30"
required
/>
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={getCurrentLocation}
disabled={isGettingLocation}
className="mt-2 border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10"
>
{isGettingLocation ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Getting location...
</>
) : (
<>
<MapPin className="h-4 w-4 mr-2" />
Use my current location
</>
)}
</Button>
</div>
)}
{/* Duration - changes based on category */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Clock className="h-4 w-4" style={{ color: TOUR_CATEGORIES[tourCategory].color }} />
Duration
</Label>
<span className="font-bold" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>
{tourCategory === 'destruction'
? `${Math.ceil(duration[0] / 4)} month${Math.ceil(duration[0] / 4) > 1 ? 's' : ''}`
: tourCategory === 'international'
? `${duration[0]} week${duration[0] > 1 ? 's' : ''} (~${Math.ceil(duration[0] / 4)} month${Math.ceil(duration[0] / 4) > 1 ? 's' : ''})`
: `${duration[0]} ${durationUnit}`
}
</span>
</div>
<Slider
value={duration}
onValueChange={setDuration}
min={tourCategory === 'local' ? 1 : tourCategory === 'destruction' ? 4 : 1}
max={tourCategory === 'local' ? 8 : tourCategory === 'national' ? 14 : tourCategory === 'international' ? 8 : 48}
step={tourCategory === 'local' ? 0.5 : tourCategory === 'destruction' ? 4 : 1}
className="w-full"
/>
<p className="text-xs text-gray-500">
{tourCategory === 'local' && 'How long is your typical tour?'}
{tourCategory === 'national' && 'How many days is the road trip?'}
{tourCategory === 'international' && 'How many weeks is the adventure? (up to 2 months)'}
{tourCategory === 'destruction' && 'How long is the competition? (1-12 months)'}
</p>
</div>
{/* Skill Level - not shown for destruction tours (always advanced) */}
{tourCategory !== 'destruction' && (
<div className="space-y-3">
<Label>Skill Level</Label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{Object.entries(TOUR_SKILL_LEVELS).map(([key, level]) => (
<Button
key={key}
type="button"
variant={skillLevel === key ? 'default' : 'outline'}
size="sm"
onClick={() => setSkillLevel(key)}
className={cn(
'flex-col h-auto py-2',
skillLevel === key
? ''
: 'border-gray-700 hover:border-gray-500'
)}
style={skillLevel === key ? { backgroundColor: TOUR_CATEGORIES[tourCategory].color } : undefined}
>
<span className="text-lg mb-1">{level.emoji}</span>
<span className="text-xs">{level.label}</span>
</Button>
))}
</div>
</div>
)}
{/* Spot Types - not shown for destruction tours (all spots included) */}
{tourCategory !== 'destruction' && (
<div className="space-y-3">
<Label>Types of Spots *</Label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{Object.entries(TOUR_SPOT_TYPES).map(([key, type]) => (
<Button
key={key}
type="button"
variant={spotTypes.includes(key) ? 'default' : 'outline'}
size="sm"
onClick={() => toggleSpotType(key)}
className={cn(
'gap-2',
spotTypes.includes(key)
? ''
: 'border-gray-700 hover:border-gray-500'
)}
style={spotTypes.includes(key) ? { backgroundColor: TOUR_CATEGORIES[tourCategory].color } : undefined}
>
<span>{type.emoji}</span>
<span>{type.label}</span>
</Button>
))}
</div>
</div>
)}
{/* Max Group Size - only for non-destruction tours */}
{tourCategory !== 'destruction' && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Users className="h-4 w-4" style={{ color: TOUR_CATEGORIES[tourCategory].color }} />
Max Group Size
</Label>
<span className="font-bold" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>{maxGroupSize[0]} people</span>
</div>
<Slider
value={maxGroupSize}
onValueChange={setMaxGroupSize}
min={1}
max={tourCategory === 'local' ? 10 : 20}
step={1}
className="w-full"
/>
</div>
)}
{/* ============ NATIONAL/INTERNATIONAL SPECIFIC FIELDS ============ */}
{(tourCategory === 'national' || tourCategory === 'international') && (
<div className="space-y-4 p-4 rounded-lg border-2" style={{
borderColor: `${TOUR_CATEGORIES[tourCategory].color}30`,
backgroundColor: `${TOUR_CATEGORIES[tourCategory].color}05`
}}>
<div className="flex items-center gap-2 mb-2">
{tourCategory === 'national' ? <Car className="h-5 w-5" /> : <Plane className="h-5 w-5" />}
<Label className="text-base font-bold" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>
{tourCategory === 'national' ? 'Road Trip Details' : 'International Adventure Details'}
</Label>
</div>
{/* Cities */}
<div className="space-y-2">
<Label>Cities on the Tour</Label>
<div className="flex gap-2">
<Input
placeholder="Add a city..."
value={newCity}
onChange={(e) => setNewCity(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCity())}
className="bg-black border-gray-700"
/>
<Button type="button" onClick={addCity} variant="outline" size="icon">
<Plus className="h-4 w-4" />
</Button>
</div>
{cities.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{cities.map((city) => (
<span key={city} className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full" style={{
backgroundColor: `${TOUR_CATEGORIES[tourCategory].color}20`,
color: TOUR_CATEGORIES[tourCategory].color
}}>
{city}
<button type="button" onClick={() => removeCity(city)} className="hover:text-red-400">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
)}
</div>
{/* Countries (International only) */}
{tourCategory === 'international' && (
<div className="space-y-2">
<Label>Countries *</Label>
<div className="flex gap-2">
<Input
placeholder="Add a country..."
value={newCountry}
onChange={(e) => setNewCountry(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCountry())}
className="bg-black"
style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30` }}
/>
<Button type="button" onClick={addCountry} variant="outline" size="icon" style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}50` }}>
<Plus className="h-4 w-4" />
</Button>
</div>
{countries.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{countries.map((country) => (
<span key={country} className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full" style={{
backgroundColor: `${TOUR_CATEGORIES[tourCategory].color}20`,
color: TOUR_CATEGORIES[tourCategory].color
}}>
{country}
<button type="button" onClick={() => removeCountry(country)} className="hover:text-red-400">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
)}
</div>
)}
{/* Transport Info */}
<div className="space-y-2">
<Label>Transport Info</Label>
<Textarea
placeholder="How will participants travel? e.g., 'Van provided', 'Fly into Barcelona, we rent a van', 'Train between cities'"
value={transportInfo}
onChange={(e) => setTransportInfo(e.target.value)}
rows={2}
className="bg-black border-gray-700"
/>
</div>
{/* Accommodation Info */}
<div className="space-y-2">
<Label>Accommodation Info</Label>
<Textarea
placeholder="Accommodation recommendations or arrangements, e.g., 'Budget hostels along the route', 'Camping gear required', 'Airbnbs included'"
value={accommodationInfo}
onChange={(e) => setAccommodationInfo(e.target.value)}
rows={2}
className="bg-black border-gray-700"
/>
</div>
</div>
)}
{/* ============ WORLD DESTRUCTION TOUR SPECIFIC FIELDS ============ */}
{tourCategory === 'destruction' && (
<div className="space-y-6">
{/* Cities & Countries */}
<div className="space-y-4 p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">
<div className="flex items-center gap-2">
<Globe className="h-5 w-5 text-red-500" />
<Label className="text-base font-bold text-red-500">Competition Locations</Label>
</div>
<p className="text-xs text-gray-400">Where will teams be competing? Add all cities and countries involved.</p>
{/* Cities */}
<div className="space-y-2">
<Label className="text-sm">Cities</Label>
<div className="flex gap-2">
<Input
placeholder="Add a city..."
value={newCity}
onChange={(e) => setNewCity(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCity())}
className="bg-black border-red-500/30"
/>
<Button type="button" onClick={addCity} variant="outline" size="icon" className="border-red-500/50">
<Plus className="h-4 w-4" />
</Button>
</div>
{cities.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{cities.map((city) => (
<span key={city} className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-red-500/20 text-red-400">
{city}
<button type="button" onClick={() => removeCity(city)} className="hover:text-white">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
)}
</div>
{/* Countries */}
<div className="space-y-2">
<Label className="text-sm">Countries</Label>
<div className="flex gap-2">
<Input
placeholder="Add a country..."
value={newCountry}
onChange={(e) => setNewCountry(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCountry())}
className="bg-black border-red-500/30"
/>
<Button type="button" onClick={addCountry} variant="outline" size="icon" className="border-red-500/50">
<Plus className="h-4 w-4" />
</Button>
</div>
{countries.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{countries.map((country) => (
<span key={country} className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-red-500/20 text-red-400">
{country}
<button type="button" onClick={() => removeCountry(country)} className="hover:text-white">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
)}
</div>
</div>
{/* Organizer Contact */}
<div className="space-y-2">
<Label className="flex items-center gap-2 text-red-500">
<Users className="h-4 w-4" />
Organizer Contact Details *
</Label>
<Textarea
placeholder="How can teams contact you? e.g., Nostr npub, email, Telegram, etc."
value={organizerContact}
onChange={(e) => setOrganizerContact(e.target.value)}
rows={2}
className="bg-black border-red-500/30"
/>
<p className="text-xs text-gray-500">This will be visible to registered teams</p>
</div>
{/* Transport Info */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Car className="h-4 w-4 text-red-500" />
Transport Info
</Label>
<Textarea
placeholder="How will teams travel between locations? Any transport provided or recommended?"
value={transportInfo}
onChange={(e) => setTransportInfo(e.target.value)}
rows={2}
className="bg-black border-red-500/30"
/>
</div>
{/* Competition Dates */}
<div className="space-y-4 p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">
<div className="flex items-center gap-2">
<Calendar className="h-5 w-5 text-red-500" />
<Label className="text-base font-bold text-red-500">Competition Dates</Label>
</div>
<p className="text-xs text-gray-400">
⚠️ <strong>All challenges are SECRET until the start date!</strong> Teams won't see challenges until competition begins.
</p>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs">Start Date</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-black border-red-500/30"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">End Date</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-black border-red-500/30"
/>
</div>
</div>
</div>
{/* Team Settings */}
<div className="space-y-4 p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-red-500" />
<Label className="text-base font-bold text-red-500">Team Settings</Label>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm">Max Teams</Label>
<span className="text-red-400 font-bold">{maxTeams[0]} teams</span>
</div>
<Slider
value={maxTeams}
onValueChange={setMaxTeams}
min={2}
max={16}
step={1}
className="w-full"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm">Min Team Size</Label>
<span className="text-red-400 font-bold">{minTeamSize[0]}</span>
</div>
<Slider
value={minTeamSize}
onValueChange={setMinTeamSize}
min={1}
max={8}
step={1}
className="w-full"
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm">Max Team Size</Label>
<span className="text-red-400 font-bold">{maxTeamSize[0]}</span>
</div>
<Slider
value={maxTeamSize}
onValueChange={setMaxTeamSize}
min={2}
max={12}
step={1}
className="w-full"
/>
</div>
</div>
</div>
{/* Entry Fee & Prize Pool */}
<div className="space-y-4 p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">
<div className="flex items-center gap-2">
<Trophy className="h-5 w-5 text-red-500" />
<Label className="text-base font-bold text-red-500">Entry & Prizes</Label>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm flex items-center gap-2">
<Zap className="h-3 w-3 text-orange-400" />
Entry Fee per Team (sats)
</Label>
<Input
type="number"
placeholder="e.g., 100000"
value={entryFee}
onChange={(e) => setEntryFee(e.target.value)}
className="bg-black border-red-500/30"
/>
<p className="text-[10px] text-gray-500">You keep 95% • Platform takes 5% • Leave empty for free entry</p>
</div>
<div className="space-y-2">
<Label className="text-sm flex items-center gap-2">
<Trophy className="h-3 w-3 text-yellow-400" />
Total Prize Pool (sats)
</Label>
<Input
type="number"
placeholder="e.g., 2000000"
value={prizePool}
onChange={(e) => setPrizePool(e.target.value)}
className="bg-black border-red-500/30"
/>
<p className="text-[10px] text-gray-500">Optional - total prizes available</p>
</div>
</div>
{/* Prize Distribution */}
{prizePool && parseInt(prizePool) > 0 && (
<div className="space-y-3 mt-4 p-3 rounded bg-black/50">
<Label className="text-sm">Prize Distribution</Label>
<div className="space-y-2">
{prizeDistribution.map((prize, idx) => (
<div key={prize.place} className="flex items-center gap-3">
<span className="text-sm w-16">
{prize.place === 1 && '🥇 1st'}
{prize.place === 2 && '🥈 2nd'}
{prize.place === 3 && '🥉 3rd'}
</span>
<Input
type="number"
value={prize.percentage}
onChange={(e) => {
const updated = [...prizeDistribution];
updated[idx].percentage = parseInt(e.target.value) || 0;
setPrizeDistribution(updated);
}}
className="w-20 bg-black border-gray-700 text-center"
/>
<span className="text-sm text-gray-500">%</span>
<span className="text-sm text-yellow-400 ml-auto">
{Math.floor(parseInt(prizePool) * prize.percentage / 100).toLocaleString()} sats
</span>
</div>
))}
</div>
<p className="text-[10px] text-gray-500">
Total: {prizeDistribution.reduce((sum, p) => sum + p.percentage, 0)}%
</p>
</div>
)}
</div>
{/* Challenges */}
<div className="space-y-4 p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-red-500" />
<Label className="text-base font-bold text-red-500">Challenges</Label>
</div>
<Button type="button" onClick={addChallenge} variant="outline" size="sm" className="border-red-500/50 text-red-400 hover:bg-red-500/10">
<Plus className="h-4 w-4 mr-1" />
Add Challenge
</Button>
</div>
<div className="flex items-center gap-2 p-2 rounded bg-yellow-500/10 border border-yellow-500/30">
<EyeOff className="h-4 w-4 text-yellow-500 flex-shrink-0" />
<p className="text-xs text-yellow-400">
<strong>All challenges are SECRET</strong> until competition start date. Teams won't see them until then!
</p>
</div>
{challenges.length === 0 ? (
<div className="text-center py-6 border-2 border-dashed border-gray-700 rounded-lg">
<Target className="h-8 w-8 mx-auto mb-2 text-gray-600" />
<p className="text-sm text-gray-500">No challenges yet</p>
<p className="text-xs text-gray-600">Add challenges for teams to complete</p>
</div>
) : (
<div className="space-y-3">
{challenges.map((challenge, idx) => (
<div key={challenge.id} className="p-3 rounded-lg bg-black/50 border border-gray-700 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 space-y-2">
<Input
placeholder="Challenge title, e.g., 'Kickflip a 10 stair'"
value={challenge.title}
onChange={(e) => updateChallenge(idx, { title: e.target.value })}
className="bg-black border-gray-700"
/>
<Textarea
placeholder="Optional description..."
value={challenge.description || ''}
onChange={(e) => updateChallenge(idx, { description: e.target.value })}
rows={2}
className="bg-black border-gray-700 text-sm"
/>
</div>
<Button type="button" variant="ghost" size="icon" onClick={() => removeChallenge(idx)} className="text-red-400 hover:bg-red-500/20">
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Label className="text-xs">Points:</Label>
<Input
type="number"
value={challenge.points}
onChange={(e) => updateChallenge(idx, { points: parseInt(e.target.value) || 0 })}
className="w-20 bg-black border-gray-700 text-center"
/>
</div>
<span className="text-xs text-gray-500 flex items-center gap-1">
<EyeOff className="h-3 w-3" /> Secret until start
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Accepted Clip Sources */}
<div className="space-y-3">
<Label>Accepted Clip Sources</Label>
<p className="text-xs text-gray-400">What formats will you accept for clip submissions?</p>
<div className="flex flex-wrap gap-2">
{[
{ id: 'blossom', label: '📦 Blossom Upload', desc: '< 50MB' },
{ id: 'youtube', label: '▶️ YouTube' },
{ id: 'instagram', label: '📸 Instagram' },
{ id: 'any', label: '🔗 Any URL' },
].map((source) => (
<Button
key={source.id}
type="button"
variant={acceptedClipSources.includes(source.id) ? 'default' : 'outline'}
size="sm"
onClick={() => {
setAcceptedClipSources((prev) =>
prev.includes(source.id)
? prev.filter((s) => s !== source.id)
: [...prev, source.id]
);
}}
className={cn(
acceptedClipSources.includes(source.id)
? 'bg-red-500 hover:bg-red-600'
: 'border-gray-700 hover:border-red-500/50'
)}
>
{source.label}
</Button>
))}
</div>
</div>
{/* Film Release Options */}
<div className="space-y-4 p-4 rounded-lg border-2 border-purple-500/30 bg-purple-500/5">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center flex-shrink-0">
<span className="text-xl">🎬</span>
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<Label className="text-base font-bold text-purple-400">Team Film Premieres</Label>
<Button
type="button"
variant={enableFilmRelease ? 'default' : 'outline'}
size="sm"
onClick={() => setEnableFilmRelease(!enableFilmRelease)}
className={cn(
enableFilmRelease
? 'bg-purple-500 hover:bg-purple-600'
: 'border-purple-500/50 text-purple-400 hover:bg-purple-500/10'
)}
>
{enableFilmRelease ? 'Enabled' : 'Enable'}
</Button>
</div>
<p className="text-xs text-gray-400 mt-1">
After the competition, each team's clips become a full film that can premiere at local skate shops worldwide.
</p>
</div>
</div>
{enableFilmRelease && (
<div className="space-y-4 pt-3 border-t border-purple-500/20">
<p className="text-xs text-purple-300">
🎥 Teams compile their challenge clips into a final film. Shops can host premiere events, connecting global competition to local communities.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* Premiere at Shops */}
<Button
type="button"
variant={filmReleaseAtShops ? 'default' : 'outline'}
onClick={() => setFilmReleaseAtShops(!filmReleaseAtShops)}
className={cn(
'h-auto py-3 flex-col items-start gap-1',
filmReleaseAtShops
? 'bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
: 'border-[#ccff00]/50 hover:bg-[#ccff00]/10'
)}
>
<div className="flex items-center gap-2 w-full">
<Store className="h-4 w-4" />
<span className="font-bold">Local Shop Premieres</span>
</div>
<span className={cn(
'text-xs',
filmReleaseAtShops ? 'text-black/70' : 'text-gray-400'
)}>
Skate shops worldwide can host film nights
</span>
</Button>
{/* Online Release */}
<Button
type="button"
variant={filmReleaseOnline ? 'default' : 'outline'}
onClick={() => setFilmReleaseOnline(!filmReleaseOnline)}
className={cn(
'h-auto py-3 flex-col items-start gap-1',
filmReleaseOnline
? 'bg-cyan-500 text-white hover:bg-cyan-600'
: 'border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10'
)}
>
<div className="flex items-center gap-2 w-full">
<Globe className="h-4 w-4" />
<span className="font-bold">Online Release</span>
</div>
<span className={cn(
'text-xs',
filmReleaseOnline ? 'text-white/70' : 'text-gray-400'
)}>
Also release films on YouTube/Nostr
</span>
</Button>
</div>
{/* Film Release Date */}
<div className="space-y-2">
<Label className="text-purple-400 text-sm">Film Release Date (Optional)</Label>
<Input
type="date"
value={filmReleaseDate}
onChange={(e) => setFilmReleaseDate(e.target.value)}
min={endDate || startDate}
className="bg-black border-purple-500/30"
/>
<p className="text-xs text-gray-500">
When films will be available. Leave blank to release immediately after competition ends.
</p>
</div>
{filmReleaseAtShops && (
<div className="space-y-3">
<div className="p-3 rounded-lg bg-[#ccff00]/10 border border-[#ccff00]/30">
<p className="text-xs text-[#ccff00]">
<strong>💡 How Shop Premieres Work:</strong> Contact shops directly to arrange premiere nights.
All arrangements are private between you and the shops.
</p>
</div>
{/* Toggle to show available shops */}
<Button
type="button"
variant="outline"
onClick={() => setShowAvailableShops(!showAvailableShops)}
className="w-full border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10"
>
<Store className="h-4 w-4 mr-2" />
{showAvailableShops ? 'Hide' : 'Show'} Available Skate Shops ({shops?.length || 0})
<ChevronDown className={cn(
'h-4 w-4 ml-2 transition-transform',
showAvailableShops && 'rotate-180'
)} />
</Button>
{/* Available shops list */}
{showAvailableShops && (
<div className="space-y-2 max-h-64 overflow-y-auto rounded-lg border border-[#ccff00]/20 p-2">
{shopsLoading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-[#ccff00]" />
<span className="ml-2 text-sm text-gray-400">Loading shops...</span>
</div>
) : shops && shops.length > 0 ? (
<>
<p className="text-xs text-gray-400 px-2 pb-2 border-b border-gray-800">
Contact these shops directly to arrange film premiere nights. All arrangements are private.
</p>
{shops.map((shop) => (
<div
key={shop.id}
className="p-3 rounded-lg bg-black/50 border border-gray-800 hover:border-[#ccff00]/30 transition-colors"
>
<div className="flex items-start gap-3">
{shop.images && shop.images[0] ? (
<img
src={shop.images[0]}
alt={shop.name}
className="w-12 h-12 rounded-lg object-cover flex-shrink-0"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-[#ccff00]/20 flex items-center justify-center flex-shrink-0">
<Store className="h-5 w-5 text-[#ccff00]" />
</div>
)}
<div className="flex-1 min-w-0">
<h4 className="font-bold text-white text-sm truncate">{shop.name}</h4>
{shop.location && (
<p className="text-xs text-gray-400 flex items-center gap-1">
<MapPin className="h-3 w-3 flex-shrink-0" />
<span className="truncate">{shop.location}</span>
</p>
)}
<div className="flex items-center gap-2 mt-1 flex-wrap">
{shop.website && (
<a
href={shop.website}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-cyan-400 hover:text-cyan-300"
onClick={(e) => e.stopPropagation()}
>
🌐 Website
</a>
)}
{shop.instagram && (
<a
href={`https://instagram.com/${shop.instagram.replace('@', '')}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-pink-400 hover:text-pink-300"
onClick={(e) => e.stopPropagation()}
>
📸 Instagram
</a>
)}
{shop.email && (
<a
href={`mailto:${shop.email}`}
className="text-xs text-orange-400 hover:text-orange-300"
onClick={(e) => e.stopPropagation()}
>
✉️ Email
</a>
)}
{shop.phone && (
<a
href={`tel:${shop.phone}`}
className="text-xs text-green-400 hover:text-green-300"
onClick={(e) => e.stopPropagation()}
>
📞 Call
</a>
)}
</div>
</div>
<Link
to={`/shop/${shop.pubkey}/${shop.identifier}`}
className="text-xs text-[#ccff00] hover:underline flex-shrink-0"
onClick={(e) => e.stopPropagation()}
>
View →
</Link>
</div>
</div>
))}
</>
) : (
<div className="text-center py-4">
<Store className="h-8 w-8 mx-auto mb-2 text-gray-600" />
<p className="text-sm text-gray-400">No skate shops registered yet</p>
<p className="text-xs text-gray-500 mt-1">
Shops can register at /create-shop
</p>
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Price - only for non-destruction tours (destruction uses entry fee) */}
{tourCategory !== 'destruction' && (
<div className="space-y-2">
<Label htmlFor="price" className="flex items-center gap-2">
<Zap className="h-4 w-4 text-orange-400" />
Price per Person (sats)
</Label>
<Input
id="price"
type="number"
placeholder="e.g., 25000"
value={price}
onChange={(e) => setPrice(e.target.value)}
className="bg-black"
style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30` }}
/>
<p className="text-xs text-gray-500">
You set the price • Platform takes {tourCategory === 'local' ? '10%' : tourCategory === 'national' ? '7%' : '5%'}
</p>
{price && parseInt(price) > 0 && (
<div className="text-xs mt-1" style={{ color: TOUR_CATEGORIES[tourCategory].color }}>
You receive: {Math.floor(parseInt(price) * (tourCategory === 'local' ? 0.9 : tourCategory === 'national' ? 0.93 : 0.95)).toLocaleString()} sats per person
</div>
)}
</div>
)}
{/* Availability */}
<div className="space-y-2">
<Label htmlFor="availability">Availability</Label>
<Input
id="availability"
placeholder="e.g., Weekends, By appointment, Mornings only"
value={availability}
onChange={(e) => setAvailability(e.target.value)}
className="bg-black border-cyan-500/30"
/>
</div>
{/* Partner Shop Discount */}
<Collapsible open={shopExpanded} onOpenChange={setShopExpanded}>
<div className="space-y-3 p-4 rounded-lg border-2 border-[#ccff00]/30 bg-[#ccff00]/5">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center justify-between w-full text-left"
>
<div className="flex items-center gap-2">
<Store className="h-5 w-5 text-[#ccff00]" />
<div>
<Label className="text-base font-bold text-[#ccff00] cursor-pointer">Partner Shop Discount</Label>
<p className="text-xs text-gray-400">
Offer tour participants a discount at a local skate shop
</p>
</div>
</div>
<ChevronDown className={cn(
'h-5 w-5 text-[#ccff00] transition-transform',
shopExpanded && 'rotate-180'
)} />
</button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-3">
<div className="text-xs text-gray-400 p-3 rounded bg-gray-900/50 border border-gray-800">
<p className="font-medium text-white mb-1">How it works:</p>
<ul className="space-y-1">
<li>1. Partner with a local skate shop you trust</li>
<li>2. Set a discount percentage for tour participants</li>
<li>3. After the tour, give participants a confirmation to show at the shop</li>
<li>4. Shop honors the discount - builds community & drives foot traffic!</li>
</ul>
</div>
{/* Shop Selection */}
<div className="space-y-2">
<Label>Select Partner Shop</Label>
{shopsLoading ? (
<div className="flex items-center gap-2 text-gray-400 py-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Loading shops...</span>
</div>
) : shops && shops.length > 0 ? (
<div className="grid grid-cols-1 gap-2 max-h-48 overflow-y-auto">
<Button
type="button"
variant={!partnerShopId ? 'default' : 'outline'}
size="sm"
onClick={() => setPartnerShopId('')}
className={cn(
'justify-start gap-2',
!partnerShopId
? 'bg-gray-700'
: 'border-gray-700'
)}
>
<X className="h-4 w-4" />
No partner shop
</Button>
{shops.map((shop) => {
const shopKey = `${shop.pubkey}:${shop.identifier}`;
return (
<Button
key={shopKey}
type="button"
variant={partnerShopId === shopKey ? 'default' : 'outline'}
size="sm"
onClick={() => setPartnerShopId(shopKey)}
className={cn(
'justify-start gap-2 h-auto py-2',
partnerShopId === shopKey
? 'bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
: 'border-gray-700 hover:border-[#ccff00]/50'
)}
>
<Store className="h-4 w-4 flex-shrink-0" />
<div className="text-left">
<div className="font-medium">{shop.name}</div>
{shop.location && (
<div className={cn(
'text-xs',
partnerShopId === shopKey ? 'text-black/70' : 'text-gray-500'
)}>
{shop.location}
</div>
)}
</div>
</Button>
);
})}
</div>
) : (
<div className="text-sm text-gray-500 space-y-2">
<p>No skate shops found on the network yet.</p>
<Link to="/create-shop" className="text-[#ccff00] hover:underline inline-flex items-center gap-1">
<Store className="h-3 w-3" />
Add a shop first
</Link>
</div>
)}
</div>
{/* Pro tip about coordinating with shop */}
<div className="text-xs text-amber-400/80 p-3 rounded bg-amber-500/10 border border-amber-500/20">
<p className="font-medium text-amber-400 mb-1">💡 Pro Tip: Coordinate with the shop first!</p>
<p>Before listing a partner discount, contact the shop owner via DM to agree on the discount percentage. This ensures they're on board and will honor the discount for your tour participants.</p>
</div>
{/* Discount Slider */}
{partnerShopId && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Percent className="h-4 w-4 text-[#ccff00]" />
Discount Percentage
</Label>
<span className="text-[#ccff00] font-bold text-lg">{shopDiscount[0]}% OFF</span>
</div>
<Slider
value={shopDiscount}
onValueChange={setShopDiscount}
min={5}
max={50}
step={5}
className="w-full"
/>
<p className="text-xs text-gray-500">
Tour participants can show their booking confirmation at the shop to receive this discount
</p>
</div>
)}
</CollapsibleContent>
</div>
</Collapsible>
{/* Languages */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Globe className="h-4 w-4 text-cyan-400" />
Languages Spoken
</Label>
{languages.length > 0 && (
<span className="text-xs text-cyan-400">
{languages.length} selected
</span>
)}
</div>
{/* Common languages - always visible */}
<div className="flex flex-wrap gap-2">
{COMMON_LANGUAGES.map((lang) => (
<Button
key={lang.code}
type="button"
variant={languages.includes(lang.code) ? 'default' : 'outline'}
size="sm"
onClick={() => toggleLanguage(lang.code)}
className={cn(
'gap-1.5 h-8',
languages.includes(lang.code)
? 'bg-cyan-500 hover:bg-cyan-600'
: 'border-gray-700 hover:border-cyan-500/50'
)}
>
<span>{lang.flag}</span>
<span className="text-xs">{lang.label}</span>
</Button>
))}
</div>
{/* Collapsible section for all languages */}
<Collapsible open={languagesExpanded} onOpenChange={setLanguagesExpanded}>
<CollapsibleTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="w-full justify-between text-gray-400 hover:text-cyan-400 hover:bg-cyan-500/10"
>
<span>{languagesExpanded ? 'Show less' : `Show all ${ALL_LANGUAGES.length} languages`}</span>
<ChevronDown className={cn(
'h-4 w-4 transition-transform',
languagesExpanded && 'rotate-180'
)} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-3">
<div className="flex flex-wrap gap-1.5 max-h-64 overflow-y-auto p-2 rounded-lg bg-gray-900/50 border border-gray-800">
{ALL_LANGUAGES.filter(l => !COMMON_LANGUAGES.find(c => c.code === l.code)).map((lang) => (
<Button
key={lang.code}
type="button"
variant={languages.includes(lang.code) ? 'default' : 'outline'}
size="sm"
onClick={() => toggleLanguage(lang.code)}
className={cn(
'gap-1 h-7 text-xs px-2',
languages.includes(lang.code)
? 'bg-cyan-500 hover:bg-cyan-600'
: 'border-gray-700 hover:border-cyan-500/50'
)}
>
<span>{lang.flag}</span>
<span>{lang.label}</span>
</Button>
))}
</div>
</CollapsibleContent>
</Collapsible>
{/* Show selected languages not in common list */}
{languages.filter(l => !COMMON_LANGUAGES.find(c => c.code === l)).length > 0 && !languagesExpanded && (
<div className="flex flex-wrap gap-1.5">
<span className="text-xs text-gray-500 mr-1">Also selected:</span>
{languages.filter(l => !COMMON_LANGUAGES.find(c => c.code === l)).map((code) => {
const lang = ALL_LANGUAGES.find(l => l.code === code);
if (!lang) return null;
return (
<span key={code} className="inline-flex items-center gap-1 text-xs bg-cyan-500/20 text-cyan-300 px-2 py-0.5 rounded-full">
{lang.flag} {lang.label}
<button
type="button"
onClick={() => toggleLanguage(code)}
className="hover:text-red-400"
>
<X className="h-3 w-3" />
</button>
</span>
);
})}
</div>
)}
</div>
{/* Tour Image */}
<div className="space-y-2">
<Label>Tour Photo</Label>
<div className="flex gap-4">
{image ? (
<div className="relative w-32 h-24 rounded-lg overflow-hidden bg-muted">
<img src={image} alt="Tour" className="w-full h-full object-cover" />
<button
type="button"
onClick={() => setImage(null)}
className="absolute top-1 right-1 p-1 rounded-full bg-background/80 hover:bg-destructive hover:text-destructive-foreground transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<label className="w-32 h-24 rounded-lg border-2 border-dashed border-cyan-500/30 flex flex-col items-center justify-center cursor-pointer hover:bg-cyan-500/5 transition-colors">
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
disabled={isUploading}
/>
{isUploading ? (
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
) : (
<>
<Upload className="h-6 w-6 text-cyan-400 mb-1" />
<span className="text-xs text-gray-400">Upload</span>
</>
)}
</label>
)}
<div className="text-xs text-gray-500 flex-1">
<p>Add a photo of yourself skating or a cool spot from your tour.</p>
</div>
</div>
</div>
{/* Submit */}
<Button
type="submit"
className="w-full font-black uppercase"
style={{
backgroundColor: TOUR_CATEGORIES[tourCategory].color,
color: tourCategory === 'destruction' || tourCategory === 'national' ? 'black' : 'white',
}}
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Publishing...
</>
) : (
<>
{tourCategory === 'destruction' ? <Skull className="h-4 w-4 mr-2" /> :
tourCategory === 'international' ? <Plane className="h-4 w-4 mr-2" /> :
tourCategory === 'national' ? <Car className="h-4 w-4 mr-2" /> :
<Route className="h-4 w-4 mr-2" />}
{tourCategory === 'destruction' ? 'Create World Destruction Tour' : 'Create Tour'}
</>
)}
</Button>
</CardContent>
</Card>
</form>
</div>
</Layout>
);
}