-
+ {/* Immersive header — layered, category-tinted, skate-punk */}
+
+
Back to Map
-
{isEditing ? 'Edit Tour' : 'Create a Tour'}
-
- {TOUR_CATEGORIES[tourCategory].description}
-
+
+ {/* Diagonal grunge stripes */}
+
+
+
+ {TOUR_CATEGORIES[tourCategory].emoji}
+
+
+
+ {isEditing ? 'Edit' : 'New'} · {TOUR_CATEGORIES[tourCategory].label}
+
+
+ {isEditing ? 'Edit Tour' : 'Drop a Tour'}
+
+
+ {TOUR_CATEGORIES[tourCategory].description}
+
+
+
+
{/* Tour Category Selection */}
-
Tour Type
-
+
+ Pick your ride
+
+
{Object.entries(TOUR_CATEGORIES).map(([key, cat]) => (
- setTourCategory(key as TourCategory)}
className={cn(
- 'flex-col h-auto py-3 gap-1',
+ 'group relative flex flex-col items-center gap-1 rounded-xl border-2 py-4 px-2 transition-all duration-200 overflow-hidden',
tourCategory === key
- ? 'border-2'
- : 'border-gray-700 hover:border-gray-500'
+ ? 'scale-[1.02]'
+ : 'border-gray-800 bg-black/40 hover:border-gray-600 hover:-translate-y-0.5'
)}
- style={{
- backgroundColor: tourCategory === key ? cat.color : undefined,
- borderColor: tourCategory === key ? cat.color : undefined,
- color: tourCategory === key ? (key === 'local' || key === 'international' ? 'white' : 'black') : undefined,
- }}
+ style={
+ tourCategory === key
+ ? {
+ borderColor: cat.color,
+ backgroundColor: `${cat.color}18`,
+ boxShadow: `0 8px 30px ${cat.color}30, inset 0 0 0 1px ${cat.color}30`,
+ }
+ : undefined
+ }
>
- {cat.emoji}
- {cat.label}
- {cat.durationLabel}
-
+
+ {cat.emoji}
+
+
+ {cat.label}
+
+
+ {cat.durationLabel}
+
+ {tourCategory === key && (
+
+ )}
+
))}
@@ -951,10 +1098,11 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
<>
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
+ • You set your price — platform takes 10%
+ • Hidden spots stay hidden — you show them in person
+ • Run a scheduled session or keep it flexible / by-appointment
+ • Skaters book via DM, pay in Lightning & can add it to their calendar
+ • Build your rep with reviews and zaps
>
)}
@@ -963,8 +1111,9 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
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
+ • You set the price — platform takes only 7%
+ • Set trip dates so skaters can plan time off & travel
+ • Include transport / accommodation info for the crew
• Build legendary skate adventures!
>
@@ -974,7 +1123,8 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
How International Tours Work
• Cross-border adventures across multiple countries
- • You set the price - platform takes only 5%
+ • You set the price — platform takes only 5%
+ • Set departure & return dates so the crew can book flights
• Connect skaters from around the world
• Create unforgettable global experiences!
@@ -1153,6 +1303,140 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
+ {/* ============ SCHEDULE (local / national / international) ============ */}
+ {/* Local: optional fixed session date+time OR flexible/by-appointment. */}
+ {tourCategory === 'local' && (
+
+
+
+ Run this as a one-off scheduled session, or keep it flexible and let skaters book a time by DM.
+
+
+ {/* Fixed vs flexible toggle */}
+
+ setHasFixedSession(true)}
+ className={cn(
+ 'h-auto py-2.5 flex-col items-start gap-0.5',
+ !hasFixedSession && 'border-gray-700 hover:border-gray-500'
+ )}
+ style={hasFixedSession ? { backgroundColor: TOUR_CATEGORIES[tourCategory].color, color: 'white' } : undefined}
+ >
+
+ Fixed session
+
+
+ A set date & time
+
+
+ setHasFixedSession(false)}
+ className={cn(
+ 'h-auto py-2.5 flex-col items-start gap-0.5',
+ hasFixedSession && 'border-gray-700 hover:border-gray-500'
+ )}
+ style={!hasFixedSession ? { backgroundColor: TOUR_CATEGORIES[tourCategory].color, color: 'white' } : undefined}
+ >
+
+ Flexible
+
+
+ Book anytime by DM
+
+
+
+
+ {hasFixedSession ? (
+
+
Session date & time *
+
setSessionDateTime(e.target.value)}
+ className="bg-black"
+ style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30`, colorScheme: 'dark' }}
+ />
+
+ Skaters will see this time and can add it to their calendar from the tour page.
+
+
+ ) : (
+
+ When are you usually available?
+ setAvailability(e.target.value)}
+ className="bg-black"
+ style={{ borderColor: `${TOUR_CATEGORIES[tourCategory].color}30` }}
+ />
+
+ )}
+
+ )}
+
+ {/* National & International: trip window (departure required, return optional). */}
+ {(tourCategory === 'national' || tourCategory === 'international') && (
+
+
+
+
+ Trip Dates
+
+
+
+ Lock in the dates so skaters can request time off, book flights, and plan the trip.
+
+
+
+ )}
+
{/* Skill Level - not shown for destruction tours (always advanced) */}
{tourCategory !== 'destruction' && (
@@ -1448,12 +1732,13 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
@@ -1792,8 +2079,9 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
type="date"
value={filmReleaseDate}
onChange={(e) => setFilmReleaseDate(e.target.value)}
- min={endDate || startDate}
+ min={endDate || startDate || undefined}
className="bg-black border-purple-500/30"
+ style={{ colorScheme: 'dark' }}
/>
When films will be available. Leave blank to release immediately after competition ends.
@@ -1962,17 +2250,8 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
)}
- {/* Availability */}
-
- Availability
- setAvailability(e.target.value)}
- className="bg-black border-cyan-500/30"
- />
-
+ {/* Availability for local tours is handled in the Schedule section above.
+ National/International/Destruction use concrete dates instead. */}
{/* Partner Shop Discount */}
diff --git a/src/pages/TourDetailPage.tsx b/src/pages/TourDetailPage.tsx
index 9c46453..87ffced 100644
--- a/src/pages/TourDetailPage.tsx
+++ b/src/pages/TourDetailPage.tsx
@@ -11,9 +11,10 @@ import { useTour } from '@/hooks/useTours';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { genUserName } from '@/lib/genUserName';
-import { TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, SKATE_TOUR_KIND } from '@/lib/types';
+import { TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, SKATE_TOUR_KIND } from '@/lib/types';
import { ZapButton } from '@/components/ZapButton';
import { ProBadge } from '@/components/ProBadge';
+import { AddToCalendar } from '@/components/AddToCalendar';
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
import {
ArrowLeft,
@@ -45,8 +46,10 @@ export default function TourDetailPage() {
const guideName = metadata?.name ?? genUserName(tour?.pubkey || '');
useSeoMeta({
- title: tour ? `${tour.title} - Local Tour - SkateSpots` : 'Local Tour - SkateSpots',
- description: tour?.description || 'Discover hidden skate spots with a local guide',
+ title: tour
+ ? `${tour.title} - ${TOUR_CATEGORIES[tour.category].label} - SkateSpots`
+ : 'Skate Tour - SkateSpots',
+ description: tour?.description || 'Discover skate spots and tours on SkateSpots',
});
const handleBookTour = () => {
@@ -123,6 +126,60 @@ export default function TourDetailPage() {
}
const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null;
+ const categoryInfo = TOUR_CATEGORIES[tour.category];
+ const accent = categoryInfo.color;
+
+ // A local tour with a fixed session uses a datetime (show time); everything else
+ // (national/international/destruction) is a multi-day, all-day date range.
+ const isLocalSession = tour.category === 'local';
+ const hasSchedule = !!tour.startDate;
+
+ const formatSchedule = (): string => {
+ if (!tour.startDate) return '';
+ const start = new Date(tour.startDate * 1000);
+ if (isLocalSession) {
+ return start.toLocaleString(undefined, {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+ }
+ const startStr = start.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
+ if (tour.endDate) {
+ const end = new Date(tour.endDate * 1000);
+ const endStr = end.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
+ return `${startStr} → ${endStr}`;
+ }
+ return startStr;
+ };
+
+ const durationLabel = (() => {
+ if (!tour.duration) return '';
+ const u = tour.durationUnit;
+ if (u === 'weeks') {
+ return tour.duration >= 4
+ ? `${Math.round(tour.duration / 4)} month${Math.round(tour.duration / 4) > 1 ? 's' : ''}`
+ : `${tour.duration} week${tour.duration > 1 ? 's' : ''}`;
+ }
+ if (u === 'days') return `${tour.duration} day${tour.duration > 1 ? 's' : ''}`;
+ return `${tour.duration} hour${tour.duration > 1 ? 's' : ''}`;
+ })();
+
+ const calendarEvent = tour.startDate
+ ? {
+ uid: `${SKATE_TOUR_KIND}:${tour.pubkey}:${tour.identifier}`,
+ title: tour.title,
+ description: tour.description || `${categoryInfo.label} on SkateSpots`,
+ location: tour.location || tour.meetingPoint || tour.cities?.join(', ') || tour.countries?.join(', '),
+ start: tour.startDate,
+ end: tour.endDate,
+ url: typeof window !== 'undefined' ? window.location.href : undefined,
+ allDay: !isLocalSession,
+ }
+ : null;
return (
@@ -176,8 +233,15 @@ export default function TourDetailPage() {
+ {/* Schedule + Add to Calendar */}
+ {hasSchedule && calendarEvent && (
+
+ )}
+
{/* Description */}
{tour.description && (
@@ -241,7 +332,7 @@ export default function TourDetailPage() {
Duration