diff --git a/src/App.tsx b/src/App.tsx index 2549212d..ad094032 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import Manufacturers from "./pages/Manufacturers"; import ManufacturerDetail from "./pages/ManufacturerDetail"; import ManufacturerRides from "./pages/ManufacturerRides"; import ManufacturerModels from "./pages/ManufacturerModels"; +import RideModelDetail from "./pages/RideModelDetail"; import Designers from "./pages/Designers"; import DesignerDetail from "./pages/DesignerDetail"; import DesignerRides from "./pages/DesignerRides"; @@ -85,6 +86,7 @@ function AppContent() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/rides/RideModelCard.tsx b/src/components/rides/RideModelCard.tsx index d392a348..efccbe9f 100644 --- a/src/components/rides/RideModelCard.tsx +++ b/src/components/rides/RideModelCard.tsx @@ -84,9 +84,9 @@ export function RideModelCard({ model, manufacturerSlug }: RideModelCardProps) { diff --git a/src/pages/DesignerRides.tsx b/src/pages/DesignerRides.tsx index e73405d2..ba07d61d 100644 --- a/src/pages/DesignerRides.tsx +++ b/src/pages/DesignerRides.tsx @@ -4,15 +4,21 @@ import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel } from 'lucide-react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel, Plus } from 'lucide-react'; import { Ride, Company } from '@/types/database'; +import { RideSubmissionData } from '@/types/submission-data'; import { supabase } from '@/integrations/supabase/client'; import { RideCard } from '@/components/rides/RideCard'; +import { RideForm } from '@/components/admin/RideForm'; import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; +import { useAuth } from '@/hooks/useAuth'; +import { toast } from '@/hooks/use-toast'; export default function DesignerRides() { const { designerSlug } = useParams<{ designerSlug: string }>(); const navigate = useNavigate(); + const { user } = useAuth(); const [designer, setDesigner] = useState(null); const [rides, setRides] = useState([]); const [loading, setLoading] = useState(true); @@ -20,6 +26,7 @@ export default function DesignerRides() { const [sortBy, setSortBy] = useState('name'); const [filterCategory, setFilterCategory] = useState('all'); const [filterStatus, setFilterStatus] = useState('all'); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); useEffect(() => { if (designerSlug) { @@ -91,6 +98,37 @@ export default function DesignerRides() { ride.park?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ); + const handleCreateSubmit = async (data: any) => { + try { + if (!user) { + navigate('/auth'); + return; + } + + const submissionData = { + ...data, + designer_id: designer.id, + }; + + const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers'); + await submitRideCreation(submissionData, user.id); + + toast({ + title: "Ride Submitted", + description: "Your ride submission has been sent for review." + }); + + setIsCreateModalOpen(false); + fetchData(); + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Failed to submit ride.", + variant: "destructive" + }); + } + }; + const categories = [ { value: 'all', label: 'All Categories' }, { value: 'roller_coaster', label: 'Roller Coasters' }, @@ -157,9 +195,15 @@ export default function DesignerRides() {
-
- -

Rides by {designer.name}

+
+
+ +

Rides by {designer.name}

+
+

Explore all rides designed by {designer.name} @@ -232,7 +276,7 @@ export default function DesignerRides() {

{filteredRides.length > 0 ? ( -
+
{filteredRides.map((ride) => ( ))} @@ -247,6 +291,15 @@ export default function DesignerRides() {
)} + + + + setIsCreateModalOpen(false)} + /> + +
); } diff --git a/src/pages/ManufacturerModels.tsx b/src/pages/ManufacturerModels.tsx index 9960f45a..88612b69 100644 --- a/src/pages/ManufacturerModels.tsx +++ b/src/pages/ManufacturerModels.tsx @@ -4,21 +4,28 @@ import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel } from 'lucide-react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel, Plus } from 'lucide-react'; import { RideModel, Company } from '@/types/database'; +import { RideModelSubmissionData } from '@/types/submission-data'; import { supabase } from '@/integrations/supabase/client'; import { RideModelCard } from '@/components/rides/RideModelCard'; +import { RideModelForm } from '@/components/admin/RideModelForm'; import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; +import { useAuth } from '@/hooks/useAuth'; +import { toast } from '@/hooks/use-toast'; export default function ManufacturerModels() { const { manufacturerSlug } = useParams<{ manufacturerSlug: string }>(); const navigate = useNavigate(); + const { user } = useAuth(); const [manufacturer, setManufacturer] = useState(null); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('name'); const [filterCategory, setFilterCategory] = useState('all'); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const fetchData = useCallback(async () => { try { @@ -84,6 +91,36 @@ export default function ManufacturerModels() { model.description?.toLowerCase().includes(searchQuery.toLowerCase()) ); + const handleCreateSubmit = async (data: any) => { + try { + if (!user) { + navigate('/auth'); + return; + } + + // For now, just show a toast since ride model submission isn't implemented yet + toast({ + title: "Coming Soon", + description: "Ride model submission is not yet available.", + }); + return; + + toast({ + title: "Ride Model Submitted", + description: "Your ride model submission has been sent for review." + }); + + setIsCreateModalOpen(false); + fetchData(); + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Failed to submit ride model.", + variant: "destructive" + }); + } + }; + const categories = [ { value: 'all', label: 'All Categories' }, { value: 'roller_coaster', label: 'Roller Coasters' }, @@ -142,9 +179,15 @@ export default function ManufacturerModels() {
-
- -

Models by {manufacturer.name}

+
+
+ +

Models by {manufacturer.name}

+
+

Explore all ride models manufactured by {manufacturer.name} @@ -214,6 +257,16 @@ export default function ManufacturerModels() {

)} + + + + setIsCreateModalOpen(false)} + /> + +
); } diff --git a/src/pages/ManufacturerRides.tsx b/src/pages/ManufacturerRides.tsx index 90e12096..328503c7 100644 --- a/src/pages/ManufacturerRides.tsx +++ b/src/pages/ManufacturerRides.tsx @@ -4,15 +4,21 @@ import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel } from 'lucide-react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { ArrowLeft, Filter, SlidersHorizontal, FerrisWheel, Plus } from 'lucide-react'; import { Ride, Company } from '@/types/database'; +import { RideSubmissionData } from '@/types/submission-data'; import { supabase } from '@/integrations/supabase/client'; import { RideCard } from '@/components/rides/RideCard'; +import { RideForm } from '@/components/admin/RideForm'; import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; +import { useAuth } from '@/hooks/useAuth'; +import { toast } from '@/hooks/use-toast'; export default function ManufacturerRides() { const { manufacturerSlug } = useParams<{ manufacturerSlug: string }>(); const navigate = useNavigate(); + const { user } = useAuth(); const [manufacturer, setManufacturer] = useState(null); const [rides, setRides] = useState([]); const [loading, setLoading] = useState(true); @@ -20,6 +26,7 @@ export default function ManufacturerRides() { const [sortBy, setSortBy] = useState('name'); const [filterCategory, setFilterCategory] = useState('all'); const [filterStatus, setFilterStatus] = useState('all'); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const fetchData = useCallback(async () => { try { @@ -91,6 +98,37 @@ export default function ManufacturerRides() { ride.park?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ); + const handleCreateSubmit = async (data: any) => { + try { + if (!user) { + navigate('/auth'); + return; + } + + const submissionData = { + ...data, + manufacturer_id: manufacturer.id, + }; + + const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers'); + await submitRideCreation(submissionData, user.id); + + toast({ + title: "Ride Submitted", + description: "Your ride submission has been sent for review." + }); + + setIsCreateModalOpen(false); + fetchData(); + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Failed to submit ride.", + variant: "destructive" + }); + } + }; + const categories = [ { value: 'all', label: 'All Categories' }, { value: 'roller_coaster', label: 'Roller Coasters' }, @@ -157,9 +195,15 @@ export default function ManufacturerRides() {
-
- -

Rides by {manufacturer.name}

+
+
+ +

Rides by {manufacturer.name}

+
+

Explore all rides manufactured by {manufacturer.name} @@ -232,7 +276,7 @@ export default function ManufacturerRides() {

{filteredRides.length > 0 ? ( -
+
{filteredRides.map((ride) => ( ))} @@ -247,6 +291,15 @@ export default function ManufacturerRides() {
)} + + + + setIsCreateModalOpen(false)} + /> + +
); } diff --git a/src/pages/RideModelDetail.tsx b/src/pages/RideModelDetail.tsx new file mode 100644 index 00000000..417fe55d --- /dev/null +++ b/src/pages/RideModelDetail.tsx @@ -0,0 +1,324 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Header } from '@/components/layout/Header'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { ArrowLeft, FerrisWheel, Building2, Filter, SlidersHorizontal } from 'lucide-react'; +import { RideModel, Ride, Company } from '@/types/database'; +import { supabase } from '@/integrations/supabase/client'; +import { RideCard } from '@/components/rides/RideCard'; +import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; + +export default function RideModelDetail() { + const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>(); + const navigate = useNavigate(); + const [model, setModel] = useState(null); + const [manufacturer, setManufacturer] = useState(null); + const [rides, setRides] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('name'); + const [filterStatus, setFilterStatus] = useState('all'); + + const fetchData = useCallback(async () => { + try { + // Fetch manufacturer + const { data: manufacturerData, error: manufacturerError } = await supabase + .from('companies') + .select('*') + .eq('slug', manufacturerSlug) + .eq('company_type', 'manufacturer') + .maybeSingle(); + + if (manufacturerError) throw manufacturerError; + setManufacturer(manufacturerData); + + if (manufacturerData) { + // Fetch ride model + const { data: modelData, error: modelError } = await supabase + .from('ride_models') + .select('*') + .eq('slug', modelSlug) + .eq('manufacturer_id', manufacturerData.id) + .maybeSingle(); + + if (modelError) throw modelError; + setModel(modelData as RideModel); + + if (modelData) { + // Fetch rides using this model + let query = supabase + .from('rides') + .select(` + *, + park:parks!inner(name, slug, location:locations(*)), + manufacturer:companies!rides_manufacturer_id_fkey(*) + `) + .eq('ride_model_id', modelData.id); + + if (filterStatus !== 'all') { + query = query.eq('status', filterStatus); + } + + switch (sortBy) { + case 'rating': + query = query.order('average_rating', { ascending: false }); + break; + case 'speed': + query = query.order('max_speed_kmh', { ascending: false, nullsFirst: false }); + break; + case 'height': + query = query.order('max_height_meters', { ascending: false, nullsFirst: false }); + break; + case 'reviews': + query = query.order('review_count', { ascending: false }); + break; + default: + query = query.order('name'); + } + + const { data: ridesData, error: ridesError } = await query; + if (ridesError) throw ridesError; + setRides(ridesData || []); + } + } + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }, [manufacturerSlug, modelSlug, sortBy, filterStatus]); + + useEffect(() => { + if (manufacturerSlug && modelSlug) { + fetchData(); + } + }, [manufacturerSlug, modelSlug, fetchData]); + + const filteredRides = rides.filter(ride => + ride.name.toLowerCase().includes(searchQuery.toLowerCase()) || + ride.park?.name?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const formatCategory = (category: string | null | undefined) => { + if (!category) return 'Unknown'; + return category.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + }; + + const formatRideType = (type: string | null | undefined) => { + if (!type) return 'Unknown'; + return type.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + }; + + const statusOptions = [ + { value: 'all', label: 'All Status' }, + { value: 'operating', label: 'Operating' }, + { value: 'seasonal', label: 'Seasonal' }, + { value: 'under_construction', label: 'Under Construction' }, + { value: 'closed', label: 'Closed' } + ]; + + if (loading) { + return ( +
+
+
+
+
+
+
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+
+
+
+ ); + } + + if (!model || !manufacturer) { + return ( +
+
+
+
+

Ride Model Not Found

+ +
+
+
+ ); + } + + const extendedModel = model as RideModel & { + card_image_url?: string; + card_image_id?: string; + banner_image_url?: string; + banner_image_id?: string; + }; + + return ( +
+
+ +
+
+ +
+ + {/* Hero Section */} +
+ {(extendedModel.banner_image_url || extendedModel.banner_image_id) && ( +
+ {model.name} +
+ )} + +
+
+
+ +

{model.name}

+
+ +
+ + +
+
+
+ +
+ + {formatCategory(model.category)} + + + {formatRideType(model.ride_type)} + + + {rides.length} {rides.length === 1 ? 'ride' : 'rides'} + +
+
+ + + + Overview + Rides ({rides.length}) + + + + {model.description && ( + + +

About

+

{model.description}

+
+
+ )} + + {(model as any).technical_specs && Object.keys((model as any).technical_specs).length > 0 && ( + + +

Technical Specifications

+
+ {Object.entries((model as any).technical_specs as Record).map(([key, value]) => ( +
+ {key.replace(/_/g, ' ')} + {String(value)} +
+ ))} +
+
+
+ )} +
+ + +
+
+ setSearchQuery(query)} + showRecentSearches={false} + /> +
+
+ + + +
+
+ + {filteredRides.length > 0 ? ( +
+ {filteredRides.map((ride) => ( + + ))} +
+ ) : ( +
+ +

No rides found

+

+ No rides match your search criteria +

+
+ )} +
+
+
+
+ ); +}