mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 02:31:13 -05:00
Implement the plan to refactor the address display in the park detail page. This includes updating the sidebar address to show the street address on its own line, followed by city, state, and postal code on the next line, and the country on a separate line. This change aims to create a more compact and natural address format.
693 lines
31 KiB
TypeScript
693 lines
31 KiB
TypeScript
import { useState, lazy, Suspense, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { Header } from '@/components/layout/Header';
|
|
import { getBannerUrls } from '@/lib/cloudflareImageUtils';
|
|
import { trackPageView } from '@/lib/viewTracking';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { MapPin, Star, Clock, Phone, Globe, Calendar, ArrowLeft, Users, Zap, Camera, Castle, FerrisWheel, Waves, Tent, Plus } from 'lucide-react';
|
|
import { formatLocationShort } from '@/lib/locationFormatter';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { ReviewsSection } from '@/components/reviews/ReviewsSection';
|
|
import { RideCard } from '@/components/rides/RideCard';
|
|
import { Park, Ride } from '@/types/database';
|
|
import { ParkLocationMap } from '@/components/maps/ParkLocationMap';
|
|
import { EntityPhotoGallery } from '@/components/upload/EntityPhotoGallery';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { AdminFormSkeleton } from '@/components/loading/PageSkeletons';
|
|
import { toast } from '@/hooks/use-toast';
|
|
import { useParkDetail } from '@/hooks/parks/useParkDetail';
|
|
import { useParkRides } from '@/hooks/parks/useParkRides';
|
|
import { usePhotoCount } from '@/hooks/photos/usePhotoCount';
|
|
|
|
// Lazy load admin forms
|
|
const RideForm = lazy(() => import('@/components/admin/RideForm').then(m => ({ default: m.RideForm })));
|
|
const ParkForm = lazy(() => import('@/components/admin/ParkForm').then(m => ({ default: m.ParkForm })));
|
|
import { getErrorMessage } from '@/lib/errorHandler';
|
|
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
|
|
import { useUserRole } from '@/hooks/useUserRole';
|
|
import { Edit } from 'lucide-react';
|
|
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
|
|
|
export default function ParkDetail() {
|
|
const { slug } = useParams<{ slug: string }>();
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const { requireAuth } = useAuthModal();
|
|
const [isAddRideModalOpen, setIsAddRideModalOpen] = useState(false);
|
|
const [isEditParkModalOpen, setIsEditParkModalOpen] = useState(false);
|
|
const { isModerator } = useUserRole();
|
|
|
|
// Fetch park data with caching
|
|
const { data: park, isLoading: loading, error } = useParkDetail(slug);
|
|
|
|
// Fetch rides with caching
|
|
const { data: rides = [] } = useParkRides(park?.id, !!park?.id);
|
|
|
|
// Fetch photo count with caching
|
|
const { data: photoCount = 0, isLoading: statsLoading } = usePhotoCount('park', park?.id, !!park?.id);
|
|
|
|
// Update document title when park changes
|
|
useDocumentTitle(park?.name || 'Park Details');
|
|
|
|
// Update Open Graph meta tags
|
|
useOpenGraph({
|
|
title: park?.name || '',
|
|
description: park?.description ?? (park ? `${park.name} - A theme park${park.location ? ` in ${park.location.city}, ${park.location.country}` : ''}` : undefined),
|
|
imageUrl: park?.banner_image_url ?? undefined,
|
|
imageId: park?.banner_image_id ?? undefined,
|
|
type: 'website',
|
|
enabled: !!park
|
|
});
|
|
|
|
// Track page view when park is loaded
|
|
useEffect(() => {
|
|
if (park?.id) {
|
|
trackPageView('park', park.id);
|
|
}
|
|
}, [park?.id]);
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'operating':
|
|
return 'bg-green-500/20 text-green-400 border-green-500/30';
|
|
case 'seasonal':
|
|
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
|
|
case 'under_construction':
|
|
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
|
default:
|
|
return 'bg-red-500/20 text-red-400 border-red-500/30';
|
|
}
|
|
};
|
|
const getParkTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'theme_park':
|
|
return <Castle className="w-20 h-20" />;
|
|
case 'amusement_park':
|
|
return <FerrisWheel className="w-20 h-20" />;
|
|
case 'water_park':
|
|
return <Waves className="w-20 h-20" />;
|
|
case 'family_entertainment':
|
|
return <Tent className="w-20 h-20" />;
|
|
default:
|
|
return <FerrisWheel className="w-20 h-20" />;
|
|
}
|
|
};
|
|
const formatParkType = (type: string) => {
|
|
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
};
|
|
|
|
const handleRideSubmit = async (rideData: any) => {
|
|
|
|
try {
|
|
const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers');
|
|
await submitRideCreation(
|
|
{
|
|
...rideData,
|
|
park_id: park?.id
|
|
},
|
|
user!.id
|
|
);
|
|
|
|
toast({
|
|
title: "Submission Sent",
|
|
description: "Your ride submission has been sent for moderation review.",
|
|
});
|
|
|
|
setIsAddRideModalOpen(false);
|
|
} catch (error) {
|
|
const errorMsg = getErrorMessage(error);
|
|
toast({
|
|
title: "Submission Failed",
|
|
description: errorMsg,
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
const handleEditParkSubmit = async (parkData: any) => {
|
|
if (!user || !park) return;
|
|
|
|
try {
|
|
// Everyone goes through submission queue
|
|
const { submitParkUpdate } = await import('@/lib/entitySubmissionHelpers');
|
|
await submitParkUpdate(park.id, parkData, user.id);
|
|
|
|
toast({
|
|
title: "Edit Submitted",
|
|
description: isModerator()
|
|
? "Your edit has been submitted. You can approve it in the moderation queue."
|
|
: "Your park edit has been submitted for review.",
|
|
});
|
|
|
|
setIsEditParkModalOpen(false);
|
|
} catch (error) {
|
|
const errorMsg = getErrorMessage(error);
|
|
toast({
|
|
title: "Error",
|
|
description: errorMsg,
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="min-h-screen bg-background">
|
|
<Header />
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="animate-pulse space-y-6">
|
|
<div className="h-64 bg-muted rounded-lg"></div>
|
|
<div className="h-8 bg-muted rounded w-1/2"></div>
|
|
<div className="h-4 bg-muted rounded w-1/3"></div>
|
|
</div>
|
|
</div>
|
|
</div>;
|
|
}
|
|
if (!park) {
|
|
return <div className="min-h-screen bg-background">
|
|
<Header />
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="text-center py-12">
|
|
<h1 className="text-2xl font-bold mb-4">Park Not Found</h1>
|
|
<p className="text-muted-foreground mb-6">
|
|
The park you're looking for doesn't exist or has been removed.
|
|
</p>
|
|
<Button onClick={() => navigate('/parks')}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back to Parks
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>;
|
|
}
|
|
return <div className="min-h-screen bg-background">
|
|
<Header />
|
|
|
|
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
|
{/* Back Button and Edit Button */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<Button variant="ghost" onClick={() => navigate('/parks')}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back to Parks
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => requireAuth(() => setIsEditParkModalOpen(true), "Sign in to edit this park")}
|
|
>
|
|
<Edit className="w-4 h-4 mr-2" />
|
|
Edit Park
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Hero Section */}
|
|
<div className="relative mb-8">
|
|
<div className="aspect-[21/9] bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 rounded-lg overflow-hidden relative">
|
|
{(park.banner_image_url || park.banner_image_id) ? (
|
|
<picture>
|
|
<source
|
|
media="(max-width: 768px)"
|
|
srcSet={getBannerUrls(park.banner_image_id ?? undefined).mobile ?? park.banner_image_url ?? undefined}
|
|
/>
|
|
<img
|
|
src={getBannerUrls(park.banner_image_id ?? undefined).desktop ?? park.banner_image_url ?? undefined}
|
|
alt={park.name}
|
|
className="w-full h-full object-cover"
|
|
loading="eager"
|
|
/>
|
|
</picture>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="opacity-50">
|
|
{getParkTypeIcon(park.park_type)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
|
|
|
{/* Park Title Overlay */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-8">
|
|
<div className="flex items-end justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<Badge className={`${getStatusColor(park.status)} border`}>
|
|
{park.status.replace('_', ' ').toUpperCase()}
|
|
</Badge>
|
|
<Badge variant="outline" className="bg-black/20 text-white border-white/20">
|
|
{formatParkType(park.park_type)}
|
|
</Badge>
|
|
</div>
|
|
<h1 className="text-4xl md:text-6xl font-bold text-white mb-2">
|
|
{park.name}
|
|
</h1>
|
|
{park.location && <div className="flex items-center text-white/90 text-lg">
|
|
<MapPin className="w-5 h-5 mr-2" />
|
|
{formatLocationShort(park.location)}
|
|
</div>}
|
|
<div className="mt-3">
|
|
<VersionIndicator
|
|
entityType="park"
|
|
entityId={park.id}
|
|
entityName={park.name}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{(park.average_rating ?? 0) > 0 && <div className="bg-black/20 backdrop-blur-sm rounded-lg p-4 text-center">
|
|
<div className="flex items-center gap-2 text-white mb-1">
|
|
<Star className="w-5 h-5 fill-yellow-400 text-yellow-400" />
|
|
<span className="text-2xl font-bold">{(park.average_rating ?? 0).toFixed(1)}</span>
|
|
</div>
|
|
<div className="text-white/70 text-sm">
|
|
{park.review_count} reviews
|
|
</div>
|
|
</div>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="relative mb-12 max-w-6xl mx-auto">
|
|
{/* Background decorative elements */}
|
|
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 rounded-3xl blur-xl"></div>
|
|
|
|
<div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 p-4 bg-gradient-to-br from-background/80 via-card/90 to-background/80 backdrop-blur-sm rounded-xl border border-border/50 shadow-md">
|
|
{/* Total Rides */}
|
|
<div className="group relative overflow-hidden">
|
|
<Card className="h-full border-0 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent hover:shadow-lg hover:shadow-primary/15 transition-all duration-300 hover:scale-[1.02]">
|
|
<CardContent className="p-4 text-center relative">
|
|
<div className="absolute top-1 right-1 opacity-20 group-hover:opacity-40 transition-opacity">
|
|
<FerrisWheel className="w-6 h-6 text-primary" />
|
|
</div>
|
|
<div className="text-2xl font-bold text-primary mb-1 group-hover:scale-105 transition-transform">
|
|
{park.ride_count}
|
|
</div>
|
|
<div className="text-xs font-medium text-muted-foreground">Total Rides</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Roller Coasters */}
|
|
<div className="group relative overflow-hidden">
|
|
<Card className="h-full border-0 bg-gradient-to-br from-accent/10 via-accent/5 to-transparent hover:shadow-lg hover:shadow-accent/15 transition-all duration-300 hover:scale-[1.02]">
|
|
<CardContent className="p-4 text-center relative">
|
|
<div className="absolute top-1 right-1 opacity-20 group-hover:opacity-40 transition-opacity">
|
|
<Zap className="w-6 h-6 text-accent" />
|
|
</div>
|
|
<div className="text-2xl font-bold text-accent mb-1 group-hover:scale-105 transition-transform">
|
|
{park.coaster_count}
|
|
</div>
|
|
<div className="text-xs font-medium text-muted-foreground">Roller Coasters</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Reviews */}
|
|
<div className="group relative overflow-hidden">
|
|
<Card className="h-full border-0 bg-gradient-to-br from-secondary/10 via-secondary/5 to-transparent hover:shadow-lg hover:shadow-secondary/15 transition-all duration-300 hover:scale-[1.02]">
|
|
<CardContent className="p-4 text-center relative">
|
|
<div className="absolute top-1 right-1 opacity-20 group-hover:opacity-40 transition-opacity">
|
|
<Star className="w-6 h-6 text-secondary" />
|
|
</div>
|
|
<div className="text-2xl font-bold text-secondary mb-1 group-hover:scale-105 transition-transform">
|
|
{park.review_count}
|
|
</div>
|
|
<div className="text-xs font-medium text-muted-foreground">Reviews</div>
|
|
{(park.average_rating ?? 0) > 0 && <div className="flex items-center justify-center gap-1 mt-1">
|
|
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
|
|
<span className="text-xs font-medium text-yellow-500">
|
|
{(park.average_rating ?? 0).toFixed(1)}
|
|
</span>
|
|
</div>}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Operating Status */}
|
|
<div className="group relative overflow-hidden">
|
|
<Card className="h-full border-0 bg-gradient-to-br from-muted/20 via-muted/10 to-transparent hover:shadow-lg hover:shadow-muted/15 transition-all duration-300 hover:scale-[1.02]">
|
|
<CardContent className="p-4 text-center relative">
|
|
<div className="flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
|
|
<div className="p-2 rounded-full bg-gradient-to-br from-primary/20 to-accent/20">
|
|
{park.opening_date ? <Calendar className="w-5 h-5" /> : <Clock className="w-5 h-5" />}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs font-medium text-foreground">
|
|
{park.opening_date ? `Opened ${park.opening_date.split('-')[0]}` : 'Opening Soon'}
|
|
</div>
|
|
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<Tabs defaultValue="overview" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2 md:grid-cols-5">
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="rides">
|
|
Rides {rides.length > 0 && `(${rides.length})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="reviews">
|
|
Reviews {(park.review_count ?? 0) > 0 && `(${park.review_count})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="photos">
|
|
Photos {!statsLoading && photoCount > 0 && `(${photoCount})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="history">History</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview" className="mt-6">
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Description */}
|
|
{park.description && <Card>
|
|
<CardHeader>
|
|
<CardTitle>About {park.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground leading-relaxed">
|
|
{park.description}
|
|
</p>
|
|
</CardContent>
|
|
</Card>}
|
|
|
|
{/* Featured Rides */}
|
|
{rides.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Featured Rides</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-5 xl:gap-4">
|
|
{rides.slice(0, 4).map(ride => (
|
|
<RideCard
|
|
key={ride.id}
|
|
ride={ride}
|
|
showParkName={false}
|
|
parkSlug={park.slug}
|
|
className="h-full"
|
|
/>
|
|
))}
|
|
</div>
|
|
{rides.length > 4 && (
|
|
<div className="mt-4 text-center">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate(`/parks/${park.slug}/rides/`)}
|
|
>
|
|
View All {park.ride_count} Rides
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{/* Park Information */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Park Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{park.opening_date && <div className="flex items-center gap-3">
|
|
<Calendar className="w-4 h-4 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">Opened</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{new Date(park.opening_date).getFullYear()}
|
|
</div>
|
|
</div>
|
|
</div>}
|
|
|
|
{park.operator && <div className="flex items-center gap-3">
|
|
<Users className="w-4 h-4 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">Operator</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{park.operator.name}
|
|
</div>
|
|
</div>
|
|
</div>}
|
|
|
|
{park.website_url && <div className="flex items-center gap-3">
|
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">Website</div>
|
|
<a href={park.website_url} target="_blank" rel="noopener noreferrer" className="text-sm text-primary hover:underline">
|
|
Visit Website
|
|
</a>
|
|
</div>
|
|
</div>}
|
|
|
|
{park.phone && <div className="flex items-center gap-3">
|
|
<Phone className="w-4 h-4 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">Phone</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{park.phone}
|
|
</div>
|
|
</div>
|
|
</div>}
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-4">
|
|
<div className="font-medium">Location</div>
|
|
{park.location && (
|
|
<div className="space-y-3">
|
|
{/* Full Address Display */}
|
|
<div className="text-sm text-muted-foreground">
|
|
<div className="font-medium text-foreground mb-1">Address:</div>
|
|
<div className="space-y-1">
|
|
{/* Street Address on its own line if it exists */}
|
|
{park.location.street_address && (
|
|
<div>{park.location.street_address}</div>
|
|
)}
|
|
|
|
{/* City, State Postal on same line */}
|
|
{(park.location.city || park.location.state_province || park.location.postal_code) && (
|
|
<div>
|
|
{park.location.city}
|
|
{park.location.city && park.location.state_province && ', '}
|
|
{park.location.state_province}
|
|
{park.location.postal_code && ` ${park.location.postal_code}`}
|
|
</div>
|
|
)}
|
|
|
|
{/* Country on its own line */}
|
|
{park.location.country && (
|
|
<div>{park.location.country}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map Links */}
|
|
{park.location?.latitude && park.location?.longitude && (
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
asChild
|
|
className="text-xs"
|
|
>
|
|
<a
|
|
href={`https://maps.apple.com/?q=${encodeURIComponent(park.name)}&ll=${park.location.latitude},${park.location.longitude}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1"
|
|
>
|
|
<MapPin className="w-3 h-3" />
|
|
Apple Maps
|
|
</a>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
asChild
|
|
className="text-xs"
|
|
>
|
|
<a
|
|
href={`https://maps.google.com/?q=${encodeURIComponent(park.name)}&ll=${park.location.latitude},${park.location.longitude}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Globe className="w-3 h-3" />
|
|
Google Maps
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{park.location?.latitude && park.location?.longitude && (
|
|
<div className="mt-4">
|
|
<ParkLocationMap
|
|
latitude={Number(park.location.latitude)}
|
|
longitude={Number(park.location.longitude)}
|
|
parkName={park.name}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="rides" className="mt-6">
|
|
{/* Header with Add Ride button */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-bold">Rides at {park.name}</h2>
|
|
<Button onClick={() => requireAuth(() => setIsAddRideModalOpen(true), "Sign in to add a ride")}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Ride
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Conditional rendering */}
|
|
{rides.length === 0 ? (
|
|
<Card className="border-dashed bg-muted/50">
|
|
<CardContent className="p-12 text-center">
|
|
<FerrisWheel className="w-16 h-16 mx-auto mb-4 text-muted-foreground/40" />
|
|
<h3 className="text-xl font-semibold mb-2">No rides yet</h3>
|
|
<p className="text-muted-foreground">
|
|
Be the first to add a ride to this park
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{rides.map(ride => (
|
|
<RideCard
|
|
key={ride.id}
|
|
ride={ride}
|
|
showParkName={false}
|
|
parkSlug={park.slug}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="mt-8 text-center">
|
|
<Button
|
|
variant="outline"
|
|
size="lg"
|
|
onClick={() => navigate(`/parks/${park.slug}/rides/`)}
|
|
>
|
|
View All {park.ride_count} Rides
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="reviews" className="mt-6">
|
|
<ReviewsSection entityType="park" entityId={park.id} entityName={park.name} averageRating={park.average_rating ?? 0} reviewCount={park.review_count ?? 0} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="photos" className="mt-6">
|
|
<EntityPhotoGallery
|
|
entityId={park.id}
|
|
entityType="park"
|
|
entityName={park.name}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="history" className="mt-6">
|
|
<EntityHistoryTabs
|
|
entityType="park"
|
|
entityId={park.id}
|
|
entityName={park.name}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Add Ride Modal */}
|
|
<Dialog open={isAddRideModalOpen} onOpenChange={setIsAddRideModalOpen}>
|
|
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Add New Ride to {park.name}</DialogTitle>
|
|
<DialogDescription>
|
|
Submit a new ride for moderation. All submissions are reviewed before being published.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Suspense fallback={<AdminFormSkeleton />}>
|
|
<SubmissionErrorBoundary>
|
|
<RideForm
|
|
onSubmit={handleRideSubmit}
|
|
onCancel={() => setIsAddRideModalOpen(false)}
|
|
initialData={{ park_id: park.id }}
|
|
/>
|
|
</SubmissionErrorBoundary>
|
|
</Suspense>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Park Modal */}
|
|
<Dialog open={isEditParkModalOpen} onOpenChange={setIsEditParkModalOpen}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Park</DialogTitle>
|
|
<DialogDescription>
|
|
Make changes to the park information. {isModerator() ? 'Changes will be applied immediately.' : 'Your changes will be submitted for review.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Suspense fallback={<AdminFormSkeleton />}>
|
|
<SubmissionErrorBoundary>
|
|
<ParkForm
|
|
onSubmit={handleEditParkSubmit}
|
|
onCancel={() => setIsEditParkModalOpen(false)}
|
|
initialData={{
|
|
id: park?.id,
|
|
name: park?.name,
|
|
slug: park?.slug,
|
|
description: park?.description ?? undefined,
|
|
park_type: park?.park_type,
|
|
status: park?.status,
|
|
opening_date: park?.opening_date ?? undefined,
|
|
opening_date_precision: (park?.opening_date_precision as 'day' | 'month' | 'year') ?? undefined,
|
|
closing_date: park?.closing_date ?? undefined,
|
|
closing_date_precision: (park?.closing_date_precision as 'day' | 'month' | 'year') ?? undefined,
|
|
location_id: park?.location?.id,
|
|
location: park?.location ? {
|
|
name: park.location.name || '',
|
|
city: park.location.city || '',
|
|
state_province: park.location.state_province || '',
|
|
country: park.location.country || '',
|
|
postal_code: park.location.postal_code || '',
|
|
latitude: park.location.latitude || 0,
|
|
longitude: park.location.longitude || 0,
|
|
timezone: park.location.timezone || '',
|
|
display_name: park.location.name || '',
|
|
} : undefined,
|
|
website_url: park?.website_url ?? undefined,
|
|
phone: park?.phone ?? undefined,
|
|
email: park?.email ?? undefined,
|
|
operator_id: park?.operator?.id,
|
|
property_owner_id: park?.property_owner?.id,
|
|
banner_image_url: park?.banner_image_url ?? undefined,
|
|
card_image_url: park?.card_image_url ?? undefined
|
|
}}
|
|
isEditing={true}
|
|
/>
|
|
</SubmissionErrorBoundary>
|
|
</Suspense>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</div>;
|
|
} |