Add tour editing — the gap flagged in the addressable-events audit (1.2.33)

Tours (kind 30823) were the only major addressable entity with no edit path: an
organizer had to delete and recreate (losing the URL/identifier) just to fix a
typo or price. Rather than duplicate the enormous multi-category tour form (and
risk schema drift / the exact tag-clobber bugs fixed earlier this session), I
made the existing CreateTourPage do double duty:

- CreateTourPage now accepts an optional `existing?: SkateTour`. When present it
  prefills every field (all four categories: local/national/international/
  destruction — including challenges, prize distribution, film-release, partner
  shop, cities/countries, coords, dates), reuses the same `d` identifier, and
  adapts copy (title, submit button, toast).
- On save it PRESERVES unmanaged tags (sponsors, tags from other clients) and
  keeps any existing paid `featured` window — same "preserve unmanaged tags"
  discipline as the event/shop edit fixes, since 30823 is addressable and
  re-publishing replaces the whole tag set.
- Guarded the category→duration reset effect so it no longer clobbers the
  prefilled duration on the first render in edit mode.
- The paid FeaturedUpgrade upsell is hidden when editing (existing status is
  carried through automatically).

New EditTourPage wrapper loads the tour, gates to the owner (login / access-
denied states), and renders <CreateTourPage existing={tour} />. Added the
/edit-tour/:pubkey/:identifier route and an owner-only Edit button on
TourDetailPage beside the existing Delete control.

This closes the one concrete gap the addressable-events audit surfaced: every
entity now has both a working edit path (preserving unmanaged tags) and owner
management.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-07 22:42:34 +07:00
parent a9f9931430
commit 9d069cb2f0
5 changed files with 233 additions and 77 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.2.32", "version": "1.2.33",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "scripts": {

View file

@ -20,6 +20,7 @@ const CreateTourPage = lazy(() => import("./pages/CreateTourPage"));
const EditSpotPage = lazy(() => import("./pages/EditSpotPage")); const EditSpotPage = lazy(() => import("./pages/EditSpotPage"));
const EditShopPage = lazy(() => import("./pages/EditShopPage")); const EditShopPage = lazy(() => import("./pages/EditShopPage"));
const EditEventPage = lazy(() => import("./pages/EditEventPage")); const EditEventPage = lazy(() => import("./pages/EditEventPage"));
const EditTourPage = lazy(() => import("./pages/EditTourPage"));
const GearSwapPage = lazy(() => import("./pages/GearSwapPage")); const GearSwapPage = lazy(() => import("./pages/GearSwapPage"));
const SpotDetailPage = lazy(() => import("./pages/SpotDetailPage")); const SpotDetailPage = lazy(() => import("./pages/SpotDetailPage"));
const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage")); const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage"));
@ -129,6 +130,7 @@ export function AppRouter() {
<Route path="/edit-spot/:pubkey/:identifier" element={<EditSpotPage />} /> <Route path="/edit-spot/:pubkey/:identifier" element={<EditSpotPage />} />
<Route path="/edit-shop/:pubkey/:identifier" element={<EditShopPage />} /> <Route path="/edit-shop/:pubkey/:identifier" element={<EditShopPage />} />
<Route path="/edit-event/:pubkey/:identifier" element={<EditEventPage />} /> <Route path="/edit-event/:pubkey/:identifier" element={<EditEventPage />} />
<Route path="/edit-tour/:pubkey/:identifier" element={<EditTourPage />} />
<Route path="/spot/:pubkey/:identifier" element={<SpotDetailPage />} /> <Route path="/spot/:pubkey/:identifier" element={<SpotDetailPage />} />
<Route path="/shop/:pubkey/:identifier" element={<ShopDetailPage />} /> <Route path="/shop/:pubkey/:identifier" element={<ShopDetailPage />} />
<Route path="/event/:pubkey/:identifier" element={<EventDetailPage />} /> <Route path="/event/:pubkey/:identifier" element={<EventDetailPage />} />

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import ngeohash from 'ngeohash'; import ngeohash from 'ngeohash';
@ -19,7 +19,7 @@ import { useToast } from '@/hooks/useToast';
import { LoginArea } from '@/components/auth/LoginArea'; import { LoginArea } from '@/components/auth/LoginArea';
import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade'; import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee'; import { usePlatformFee } from '@/hooks/usePlatformFee';
import { SKATE_TOUR_KIND, TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, type TourCategory, type DestructionChallenge } from '@/lib/types'; import { SKATE_TOUR_KIND, TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, type TourCategory, type DestructionChallenge, type SkateTour } from '@/lib/types';
import { import {
Route, Route,
Upload, Upload,
@ -185,9 +185,11 @@ const ALL_LANGUAGES = [
{ code: 'jm', label: 'Jamaican Patois', flag: '🇯🇲' }, { code: 'jm', label: 'Jamaican Patois', flag: '🇯🇲' },
]; ];
export default function CreateTourPage() { export default function CreateTourPage({ existing }: { existing?: SkateTour } = {}) {
const isEditing = !!existing;
useSeoMeta({ useSeoMeta({
title: 'Create a Tour - SkateSpots', title: isEditing ? 'Edit Tour - SkateSpots' : 'Create a Tour - SkateSpots',
description: 'Organize skate tours - local hidden gem tours, national road trips, international adventures, or KOTR-style destruction competitions.', description: 'Organize skate tours - local hidden gem tours, national road trips, international adventures, or KOTR-style destruction competitions.',
}); });
@ -202,69 +204,96 @@ export default function CreateTourPage() {
const { toast } = useToast(); const { toast } = useToast();
// Tour category state // Tour category state
const [tourCategory, setTourCategory] = useState<TourCategory>('local'); // Convert a unix-seconds timestamp to a value the datetime-local input accepts.
const toLocalInput = (secs?: number) => {
if (!secs) return '';
const d = new Date(secs * 1000);
// Adjust for the local timezone offset so the picker shows the same wall time.
const off = d.getTimezoneOffset() * 60000;
return new Date(d.getTime() - off).toISOString().slice(0, 16);
};
// Form state const [tourCategory, setTourCategory] = useState<TourCategory>(existing?.category ?? 'local');
const [title, setTitle] = useState('');
const [description, setDescription] = useState(''); // Form state — prefilled from `existing` when editing.
const [location, setLocation] = useState(''); // City/region const [title, setTitle] = useState(existing?.title ?? '');
const [meetingPoint, setMeetingPoint] = useState(''); const [description, setDescription] = useState(existing?.description ?? '');
const [meetingLat, setMeetingLat] = useState(''); const [location, setLocation] = useState(existing?.location ?? ''); // City/region
const [meetingLng, setMeetingLng] = useState(''); const [meetingPoint, setMeetingPoint] = useState(existing?.meetingPoint ?? '');
const [duration, setDuration] = useState([3]); // Hours/Days depending on category const [meetingLat, setMeetingLat] = useState(existing?.meetingPointCoords ? String(existing.meetingPointCoords.lat) : '');
const [durationUnit, setDurationUnit] = useState<'hours' | 'days' | 'weeks'>('hours'); const [meetingLng, setMeetingLng] = useState(existing?.meetingPointCoords ? String(existing.meetingPointCoords.lon) : '');
const [skillLevel, setSkillLevel] = useState<string>('all'); const [duration, setDuration] = useState([existing?.duration ?? 3]); // Hours/Days depending on category
const [spotTypes, setSpotTypes] = useState<string[]>(['street']); const [durationUnit, setDurationUnit] = useState<'hours' | 'days' | 'weeks'>(existing?.durationUnit ?? 'hours');
const [maxGroupSize, setMaxGroupSize] = useState([4]); const [skillLevel, setSkillLevel] = useState<string>(existing?.skillLevel ?? 'all');
const [price, setPrice] = useState(''); const [spotTypes, setSpotTypes] = useState<string[]>(existing?.spotTypes?.length ? existing.spotTypes : ['street']);
const [availability, setAvailability] = useState(''); const [maxGroupSize, setMaxGroupSize] = useState([existing?.maxGroupSize ?? 4]);
const [languages, setLanguages] = useState<string[]>(['en']); const [price, setPrice] = useState(existing?.price != null ? String(existing.price) : '');
const [availability, setAvailability] = useState(existing?.availability ?? '');
const [languages, setLanguages] = useState<string[]>(existing?.languages?.length ? existing.languages : ['en']);
const [languagesExpanded, setLanguagesExpanded] = useState(false); const [languagesExpanded, setLanguagesExpanded] = useState(false);
const [image, setImage] = useState<string | null>(null); const [image, setImage] = useState<string | null>(existing?.image ?? null);
const [isGettingLocation, setIsGettingLocation] = useState(false); const [isGettingLocation, setIsGettingLocation] = useState(false);
// Partner shop state // Partner shop state
const [partnerShopId, setPartnerShopId] = useState<string>(''); const [partnerShopId, setPartnerShopId] = useState<string>(() => {
const [shopDiscount, setShopDiscount] = useState([10]); // Default 10% // shopRef is stored as "30017:pubkey:identifier"; the picker keys on "pubkey:identifier".
const [shopExpanded, setShopExpanded] = useState(false); const parts = existing?.shopRef?.split(':');
return parts && parts.length >= 3 ? `${parts[1]}:${parts[2]}` : '';
});
const [shopDiscount, setShopDiscount] = useState([existing?.shopDiscount ?? 10]); // Default 10%
const [shopExpanded, setShopExpanded] = useState(!!existing?.shopRef);
// National/International specific // National/International specific
const [cities, setCities] = useState<string[]>([]); const [cities, setCities] = useState<string[]>(existing?.cities ?? []);
const [newCity, setNewCity] = useState(''); const [newCity, setNewCity] = useState('');
const [countries, setCountries] = useState<string[]>([]); const [countries, setCountries] = useState<string[]>(existing?.countries ?? []);
const [newCountry, setNewCountry] = useState(''); const [newCountry, setNewCountry] = useState('');
const [transportInfo, setTransportInfo] = useState(''); const [transportInfo, setTransportInfo] = useState(existing?.transportInfo ?? '');
const [accommodationInfo, setAccommodationInfo] = useState(''); const [accommodationInfo, setAccommodationInfo] = useState(existing?.accommodationInfo ?? '');
// World Destruction Tour specific // World Destruction Tour specific
const [startDate, setStartDate] = useState(''); const [startDate, setStartDate] = useState(toLocalInput(existing?.startDate));
const [endDate, setEndDate] = useState(''); const [endDate, setEndDate] = useState(toLocalInput(existing?.endDate));
const [entryFee, setEntryFee] = useState(''); const [entryFee, setEntryFee] = useState(existing?.entryFee != null ? String(existing.entryFee) : '');
const [prizePool, setPrizePool] = useState(''); const [prizePool, setPrizePool] = useState(existing?.prizePool != null ? String(existing.prizePool) : '');
const [prizeDistribution, setPrizeDistribution] = useState([ const [prizeDistribution, setPrizeDistribution] = useState(
{ place: 1, percentage: 50 }, existing?.prizeDistribution?.length
{ place: 2, percentage: 30 }, ? existing.prizeDistribution
{ place: 3, percentage: 20 }, : [
]); { place: 1, percentage: 50 },
const [maxTeams, setMaxTeams] = useState([4]); { place: 2, percentage: 30 },
const [minTeamSize, setMinTeamSize] = useState([4]); { place: 3, percentage: 20 },
const [maxTeamSize, setMaxTeamSize] = useState([6]); ]
const [challenges, setChallenges] = useState<DestructionChallenge[]>([]); );
const [acceptedClipSources, setAcceptedClipSources] = useState<string[]>(['blossom', 'youtube', 'instagram']); const [maxTeams, setMaxTeams] = useState([existing?.maxTeams ?? 4]);
const [organizerContact, setOrganizerContact] = useState(''); // Organizer contact details const [minTeamSize, setMinTeamSize] = useState([existing?.minTeamSize ?? 4]);
const [maxTeamSize, setMaxTeamSize] = useState([existing?.maxTeamSize ?? 6]);
const [challenges, setChallenges] = useState<DestructionChallenge[]>(existing?.challenges ?? []);
const [acceptedClipSources, setAcceptedClipSources] = useState<string[]>(
existing?.acceptedClipSources?.length ? existing.acceptedClipSources : ['blossom', 'youtube', 'instagram']
);
const [organizerContact, setOrganizerContact] = useState(existing?.organizerContact ?? ''); // Organizer contact details
// Film release options for World Destruction Tours // Film release options for World Destruction Tours
const [enableFilmRelease, setEnableFilmRelease] = useState(false); const [enableFilmRelease, setEnableFilmRelease] = useState(!!existing?.filmRelease);
const [filmReleaseAtShops, setFilmReleaseAtShops] = useState(true); // Release at partner skate shops const [filmReleaseAtShops, setFilmReleaseAtShops] = useState(existing?.filmRelease?.atShops ?? true); // Release at partner skate shops
const [filmReleaseOnline, setFilmReleaseOnline] = useState(true); // Also release online const [filmReleaseOnline, setFilmReleaseOnline] = useState(existing?.filmRelease?.online ?? true); // Also release online
const [filmReleaseDate, setFilmReleaseDate] = useState(''); // Optional specific date for film release const [filmReleaseDate, setFilmReleaseDate] = useState(toLocalInput(existing?.filmRelease?.releaseDate)); // Optional specific date for film release
const [showAvailableShops, setShowAvailableShops] = useState(false); // Toggle to show available shops for premieres const [showAvailableShops, setShowAvailableShops] = useState(false); // Toggle to show available shops for premieres
// Fetch shops for partner selection // Fetch shops for partner selection
const { data: shops, isLoading: shopsLoading } = useShops(100); const { data: shops, isLoading: shopsLoading } = useShops(100);
// Update duration unit based on category // When editing, keep the tour's saved duration on first render instead of
// resetting it to the category default (the effect below fires on mount).
const skipDurationReset = useRef(isEditing);
// Update duration unit based on category (skips the initial edit-mode render).
useEffect(() => { useEffect(() => {
if (skipDurationReset.current) {
skipDurationReset.current = false;
return;
}
if (tourCategory === 'local') { if (tourCategory === 'local') {
setDurationUnit('hours'); setDurationUnit('hours');
setDuration([3]); setDuration([3]);
@ -515,12 +544,13 @@ export default function CreateTourPage() {
// Generate low-precision geohash for city-level location (4 chars = ~39km) // Generate low-precision geohash for city-level location (4 chars = ~39km)
const geohash = hasCoords ? ngeohash.encode(lat, lng, 4) : ''; const geohash = hasCoords ? ngeohash.encode(lat, lng, 4) : '';
// Create unique identifier // Reuse the identifier when editing (keeps the same addressable event &
const identifier = title // URL); otherwise mint a fresh one.
const identifier = existing?.identifier ?? (title
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9]+/g, '-') .replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '') .replace(/^-|-$/g, '')
.slice(0, 50) + '-' + Date.now().toString(36); .slice(0, 50) + '-' + Date.now().toString(36));
// Build tags // Build tags
const tags: string[][] = [ const tags: string[][] = [
@ -707,9 +737,32 @@ export default function CreateTourPage() {
: `Skate Tour: ${title.trim()} in ${locationText}`; : `Skate Tour: ${title.trim()} in ${locationText}`;
tags.push(['alt', altText]); tags.push(['alt', altText]);
// Featured listing (optional, paid) // Featured listing (optional, paid). When editing, keep whatever featured
// window the tour already has so an edit never silently drops paid status.
const existingFeaturedTag = existing?.event.tags.find(([n]) => n === 'featured');
if (isFeatured && featuredUntilSeconds) { if (isFeatured && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]); tags.push(['featured', String(featuredUntilSeconds)]);
} else if (isEditing && existingFeaturedTag) {
tags.push(existingFeaturedTag);
}
// Preserve any tag this form doesn't manage (e.g. sponsors, or tags set by
// other clients) so an edit never strips them — kind 30823 is addressable,
// so re-publishing REPLACES the whole tag set.
if (isEditing && existing) {
const managed = new Set([
'd', 'title', 't', 'tour-category', 'location', 'meeting-point',
'meeting-point-coords', 'g', 'duration', 'skill-level', 'max-group',
'price', 'available', 'languages', 'image', 'shop', 'shop-name',
'shop-discount', 'cities', 'countries', 'transport', 'accommodation',
'organizer-contact', 'start-date', 'end-date', 'entry-fee',
'prize-pool', 'prize-dist', 'max-teams', 'min-team-size',
'max-team-size', 'challenge', 'clip-sources', 'film-release',
'film-release-date', 'zap-split', 'alt', 'featured',
]);
for (const tag of existing.event.tags) {
if (!managed.has(tag[0])) tags.push(tag);
}
} }
await createEvent({ await createEvent({
@ -718,13 +771,17 @@ export default function CreateTourPage() {
tags, tags,
}); });
const toastMessages = { if (isEditing) {
local: { title: '🏠 Local Tour Created!', description: 'Your tour is now visible. Skaters can book via DM!' }, toast({ title: 'Tour updated!', description: 'Your changes have been published.' });
national: { title: '🚐 National Tour Created!', description: 'Your road trip is live! Start coordinating with interested skaters.' }, } else {
international: { title: '✈️ International Tour Created!', description: 'Your global adventure is listed! Connect with skaters worldwide.' }, const toastMessages = {
destruction: { title: '💀 WORLD DESTRUCTION TOUR CREATED!', description: 'Teams can register. Challenges revealed on start date!' }, 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.' },
toast(toastMessages[tourCategory]); 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}`); navigate(`/tour/${user?.pubkey}/${identifier}`);
} catch (error) { } catch (error) {
@ -845,7 +902,7 @@ export default function CreateTourPage() {
fontFamily: 'Impact, "Arial Black", sans-serif', fontFamily: 'Impact, "Arial Black", sans-serif',
color: TOUR_CATEGORIES[tourCategory].color, color: TOUR_CATEGORIES[tourCategory].color,
textShadow: `0 0 10px ${TOUR_CATEGORIES[tourCategory].color}50`, textShadow: `0 0 10px ${TOUR_CATEGORIES[tourCategory].color}50`,
}}>Create a Tour</h1> }}>{isEditing ? 'Edit Tour' : 'Create a Tour'}</h1>
<p className="text-gray-400 mt-1"> <p className="text-gray-400 mt-1">
{TOUR_CATEGORIES[tourCategory].description} {TOUR_CATEGORIES[tourCategory].description}
</p> </p>
@ -2193,14 +2250,18 @@ export default function CreateTourPage() {
</div> </div>
</div> </div>
{/* Optional featured upgrade — tour posts for free regardless */} {/* Optional featured upgrade — tour posts for free regardless.
<FeaturedUpgrade Hidden while editing: an existing featured window is preserved
type="featuredTour" automatically, and the paid upsell doesn't belong in an edit. */}
noun="tour" {!isEditing && (
placement="the tours list" <FeaturedUpgrade
enabled={goFeatured} type="featuredTour"
onEnabledChange={setGoFeatured} noun="tour"
/> placement="the tours list"
enabled={goFeatured}
onEnabledChange={setGoFeatured}
/>
)}
{/* Submit */} {/* Submit */}
<Button <Button
@ -2215,7 +2276,7 @@ export default function CreateTourPage() {
{isPublishing ? ( {isPublishing ? (
<> <>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
Publishing... {isEditing ? 'Saving...' : 'Publishing...'}
</> </>
) : ( ) : (
<> <>
@ -2223,7 +2284,11 @@ export default function CreateTourPage() {
tourCategory === 'international' ? <Plane 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" /> : tourCategory === 'national' ? <Car className="h-4 w-4 mr-2" /> :
<Route className="h-4 w-4 mr-2" />} <Route className="h-4 w-4 mr-2" />}
{tourCategory === 'destruction' ? 'Create World Destruction Tour' : 'Create Tour'} {isEditing
? 'Save Changes'
: tourCategory === 'destruction'
? 'Create World Destruction Tour'
: 'Create Tour'}
</> </>
)} )}
</Button> </Button>

View file

@ -0,0 +1,74 @@
import { useParams } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { LoginArea } from '@/components/auth/LoginArea';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTour } from '@/hooks/useTours';
import { Route, AlertTriangle } from 'lucide-react';
import CreateTourPage from './CreateTourPage';
import NotFound from './NotFound';
/**
* Edit an existing tour. Loads the tour by pubkey + identifier and hands it to
* CreateTourPage as `existing`, which prefills every field, reuses the same
* addressable identifier, and preserves unmanaged tags on save. Only the tour's
* owner may edit.
*/
export default function EditTourPage() {
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
const { user } = useCurrentUser();
const { data: tour, isLoading, error } = useTour(identifier || '', pubkey);
if (isLoading) {
return (
<Layout>
<div className="container px-4 sm:px-6 py-6 sm:py-8 max-w-3xl space-y-4">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-10 w-64" />
<Skeleton className="h-40 w-full" />
<Skeleton className="h-64 w-full" />
</div>
</Layout>
);
}
if (error || !tour) {
return <NotFound />;
}
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 edit this tour.</p>
<LoginArea className="justify-center" />
</CardContent>
</Card>
</div>
</Layout>
);
}
if (user.pubkey !== tour.pubkey) {
return (
<Layout>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto border-destructive">
<CardContent className="py-12 text-center">
<AlertTriangle className="h-12 w-12 mx-auto mb-4 text-destructive" />
<h2 className="text-xl font-bold mb-2">Access Denied</h2>
<p className="text-muted-foreground">You can only edit tours you created.</p>
</CardContent>
</Card>
</div>
</Layout>
);
}
return <CreateTourPage existing={tour} />;
}

View file

@ -30,6 +30,7 @@ import {
Flag, Flag,
Store, Store,
Percent, Percent,
Pencil,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { SponsorSlot } from '@/components/SponsorSlot'; import { SponsorSlot } from '@/components/SponsorSlot';
@ -132,12 +133,26 @@ export default function TourDetailPage() {
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Map Back to Map
</Link> </Link>
<DeleteEntityDialog <div className="flex items-center gap-2">
event={tour.event} {user?.pubkey === tour.pubkey && (
entityLabel="tour" <Button
entityName={tour.title} asChild
redirectTo="/map" variant="outline"
/> size="sm"
className="gap-1.5 border-cyan-500/40 text-cyan-300 hover:bg-cyan-500/10 hover:text-cyan-200"
>
<Link to={`/edit-tour/${tour.pubkey}/${tour.identifier}`}>
<Pencil className="h-3.5 w-3.5" /> Edit
</Link>
</Button>
)}
<DeleteEntityDialog
event={tour.event}
entityLabel="tour"
entityName={tour.title}
redirectTo="/map"
/>
</div>
</div> </div>
{/* Main card */} {/* Main card */}