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:
parent
a9f9931430
commit
9d069cb2f0
5 changed files with 233 additions and 77 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.2.32",
|
||||
"version": "1.2.33",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.com",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const CreateTourPage = lazy(() => import("./pages/CreateTourPage"));
|
|||
const EditSpotPage = lazy(() => import("./pages/EditSpotPage"));
|
||||
const EditShopPage = lazy(() => import("./pages/EditShopPage"));
|
||||
const EditEventPage = lazy(() => import("./pages/EditEventPage"));
|
||||
const EditTourPage = lazy(() => import("./pages/EditTourPage"));
|
||||
const GearSwapPage = lazy(() => import("./pages/GearSwapPage"));
|
||||
const SpotDetailPage = lazy(() => import("./pages/SpotDetailPage"));
|
||||
const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage"));
|
||||
|
|
@ -129,6 +130,7 @@ export function AppRouter() {
|
|||
<Route path="/edit-spot/:pubkey/:identifier" element={<EditSpotPage />} />
|
||||
<Route path="/edit-shop/:pubkey/:identifier" element={<EditShopPage />} />
|
||||
<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="/shop/:pubkey/:identifier" element={<ShopDetailPage />} />
|
||||
<Route path="/event/:pubkey/:identifier" element={<EventDetailPage />} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import ngeohash from 'ngeohash';
|
||||
|
|
@ -19,7 +19,7 @@ import { useToast } from '@/hooks/useToast';
|
|||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade';
|
||||
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 {
|
||||
Route,
|
||||
Upload,
|
||||
|
|
@ -185,9 +185,11 @@ const ALL_LANGUAGES = [
|
|||
{ code: 'jm', label: 'Jamaican Patois', flag: '🇯🇲' },
|
||||
];
|
||||
|
||||
export default function CreateTourPage() {
|
||||
export default function CreateTourPage({ existing }: { existing?: SkateTour } = {}) {
|
||||
const isEditing = !!existing;
|
||||
|
||||
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.',
|
||||
});
|
||||
|
||||
|
|
@ -202,69 +204,96 @@ export default function CreateTourPage() {
|
|||
const { toast } = useToast();
|
||||
|
||||
// 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 [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 [tourCategory, setTourCategory] = useState<TourCategory>(existing?.category ?? 'local');
|
||||
|
||||
// Form state — prefilled from `existing` when editing.
|
||||
const [title, setTitle] = useState(existing?.title ?? '');
|
||||
const [description, setDescription] = useState(existing?.description ?? '');
|
||||
const [location, setLocation] = useState(existing?.location ?? ''); // City/region
|
||||
const [meetingPoint, setMeetingPoint] = useState(existing?.meetingPoint ?? '');
|
||||
const [meetingLat, setMeetingLat] = useState(existing?.meetingPointCoords ? String(existing.meetingPointCoords.lat) : '');
|
||||
const [meetingLng, setMeetingLng] = useState(existing?.meetingPointCoords ? String(existing.meetingPointCoords.lon) : '');
|
||||
const [duration, setDuration] = useState([existing?.duration ?? 3]); // Hours/Days depending on category
|
||||
const [durationUnit, setDurationUnit] = useState<'hours' | 'days' | 'weeks'>(existing?.durationUnit ?? 'hours');
|
||||
const [skillLevel, setSkillLevel] = useState<string>(existing?.skillLevel ?? 'all');
|
||||
const [spotTypes, setSpotTypes] = useState<string[]>(existing?.spotTypes?.length ? existing.spotTypes : ['street']);
|
||||
const [maxGroupSize, setMaxGroupSize] = useState([existing?.maxGroupSize ?? 4]);
|
||||
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 [image, setImage] = useState<string | null>(null);
|
||||
const [image, setImage] = useState<string | null>(existing?.image ?? 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);
|
||||
const [partnerShopId, setPartnerShopId] = useState<string>(() => {
|
||||
// shopRef is stored as "30017:pubkey:identifier"; the picker keys on "pubkey:identifier".
|
||||
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
|
||||
const [cities, setCities] = useState<string[]>([]);
|
||||
const [cities, setCities] = useState<string[]>(existing?.cities ?? []);
|
||||
const [newCity, setNewCity] = useState('');
|
||||
const [countries, setCountries] = useState<string[]>([]);
|
||||
const [countries, setCountries] = useState<string[]>(existing?.countries ?? []);
|
||||
const [newCountry, setNewCountry] = useState('');
|
||||
const [transportInfo, setTransportInfo] = useState('');
|
||||
const [accommodationInfo, setAccommodationInfo] = useState('');
|
||||
const [transportInfo, setTransportInfo] = useState(existing?.transportInfo ?? '');
|
||||
const [accommodationInfo, setAccommodationInfo] = useState(existing?.accommodationInfo ?? '');
|
||||
|
||||
// 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
|
||||
const [startDate, setStartDate] = useState(toLocalInput(existing?.startDate));
|
||||
const [endDate, setEndDate] = useState(toLocalInput(existing?.endDate));
|
||||
const [entryFee, setEntryFee] = useState(existing?.entryFee != null ? String(existing.entryFee) : '');
|
||||
const [prizePool, setPrizePool] = useState(existing?.prizePool != null ? String(existing.prizePool) : '');
|
||||
const [prizeDistribution, setPrizeDistribution] = useState(
|
||||
existing?.prizeDistribution?.length
|
||||
? existing.prizeDistribution
|
||||
: [
|
||||
{ place: 1, percentage: 50 },
|
||||
{ place: 2, percentage: 30 },
|
||||
{ place: 3, percentage: 20 },
|
||||
]
|
||||
);
|
||||
const [maxTeams, setMaxTeams] = useState([existing?.maxTeams ?? 4]);
|
||||
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
|
||||
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 [enableFilmRelease, setEnableFilmRelease] = useState(!!existing?.filmRelease);
|
||||
const [filmReleaseAtShops, setFilmReleaseAtShops] = useState(existing?.filmRelease?.atShops ?? true); // Release at partner skate shops
|
||||
const [filmReleaseOnline, setFilmReleaseOnline] = useState(existing?.filmRelease?.online ?? true); // Also release online
|
||||
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
|
||||
|
||||
// Fetch shops for partner selection
|
||||
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(() => {
|
||||
if (skipDurationReset.current) {
|
||||
skipDurationReset.current = false;
|
||||
return;
|
||||
}
|
||||
if (tourCategory === 'local') {
|
||||
setDurationUnit('hours');
|
||||
setDuration([3]);
|
||||
|
|
@ -515,12 +544,13 @@ export default function CreateTourPage() {
|
|||
// Generate low-precision geohash for city-level location (4 chars = ~39km)
|
||||
const geohash = hasCoords ? ngeohash.encode(lat, lng, 4) : '';
|
||||
|
||||
// Create unique identifier
|
||||
const identifier = title
|
||||
// Reuse the identifier when editing (keeps the same addressable event &
|
||||
// URL); otherwise mint a fresh one.
|
||||
const identifier = existing?.identifier ?? (title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 50) + '-' + Date.now().toString(36);
|
||||
.slice(0, 50) + '-' + Date.now().toString(36));
|
||||
|
||||
// Build tags
|
||||
const tags: string[][] = [
|
||||
|
|
@ -707,9 +737,32 @@ export default function CreateTourPage() {
|
|||
: `Skate Tour: ${title.trim()} in ${locationText}`;
|
||||
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) {
|
||||
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({
|
||||
|
|
@ -718,13 +771,17 @@ export default function CreateTourPage() {
|
|||
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]);
|
||||
if (isEditing) {
|
||||
toast({ title: 'Tour updated!', description: 'Your changes have been published.' });
|
||||
} else {
|
||||
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) {
|
||||
|
|
@ -845,7 +902,7 @@ export default function CreateTourPage() {
|
|||
fontFamily: 'Impact, "Arial Black", sans-serif',
|
||||
color: TOUR_CATEGORIES[tourCategory].color,
|
||||
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">
|
||||
{TOUR_CATEGORIES[tourCategory].description}
|
||||
</p>
|
||||
|
|
@ -2193,14 +2250,18 @@ export default function CreateTourPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional featured upgrade — tour posts for free regardless */}
|
||||
<FeaturedUpgrade
|
||||
type="featuredTour"
|
||||
noun="tour"
|
||||
placement="the tours list"
|
||||
enabled={goFeatured}
|
||||
onEnabledChange={setGoFeatured}
|
||||
/>
|
||||
{/* Optional featured upgrade — tour posts for free regardless.
|
||||
Hidden while editing: an existing featured window is preserved
|
||||
automatically, and the paid upsell doesn't belong in an edit. */}
|
||||
{!isEditing && (
|
||||
<FeaturedUpgrade
|
||||
type="featuredTour"
|
||||
noun="tour"
|
||||
placement="the tours list"
|
||||
enabled={goFeatured}
|
||||
onEnabledChange={setGoFeatured}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<Button
|
||||
|
|
@ -2215,7 +2276,7 @@ export default function CreateTourPage() {
|
|||
{isPublishing ? (
|
||||
<>
|
||||
<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 === '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'}
|
||||
{isEditing
|
||||
? 'Save Changes'
|
||||
: tourCategory === 'destruction'
|
||||
? 'Create World Destruction Tour'
|
||||
: 'Create Tour'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
74
src/pages/EditTourPage.tsx
Normal file
74
src/pages/EditTourPage.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
Flag,
|
||||
Store,
|
||||
Percent,
|
||||
Pencil,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SponsorSlot } from '@/components/SponsorSlot';
|
||||
|
|
@ -132,12 +133,26 @@ export default function TourDetailPage() {
|
|||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Map
|
||||
</Link>
|
||||
<DeleteEntityDialog
|
||||
event={tour.event}
|
||||
entityLabel="tour"
|
||||
entityName={tour.title}
|
||||
redirectTo="/map"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{user?.pubkey === tour.pubkey && (
|
||||
<Button
|
||||
asChild
|
||||
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>
|
||||
|
||||
{/* Main card */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue