From 13223fd833429fff2e6f7821065d8b1b72dc8ba0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:58:22 +0000 Subject: [PATCH] feat: Enhance ride detail page with new sections --- src/components/rides/FormerNames.tsx | 63 ++++++++++++ src/components/rides/RatingDistribution.tsx | 83 +++++++++++++++ src/components/rides/RecentPhotosPreview.tsx | 92 +++++++++++++++++ src/components/rides/RideHighlights.tsx | 90 ++++++++++++++++ src/components/rides/SimilarRides.tsx | 102 +++++++++++++++++++ src/pages/RideDetail.tsx | 88 ++++++++++++++-- 6 files changed, 510 insertions(+), 8 deletions(-) create mode 100644 src/components/rides/FormerNames.tsx create mode 100644 src/components/rides/RatingDistribution.tsx create mode 100644 src/components/rides/RecentPhotosPreview.tsx create mode 100644 src/components/rides/RideHighlights.tsx create mode 100644 src/components/rides/SimilarRides.tsx diff --git a/src/components/rides/FormerNames.tsx b/src/components/rides/FormerNames.tsx new file mode 100644 index 00000000..062bc1e5 --- /dev/null +++ b/src/components/rides/FormerNames.tsx @@ -0,0 +1,63 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { History } from 'lucide-react'; + +interface FormerName { + name: string; + from_year?: number; + to_year?: number; +} + +interface FormerNamesProps { + formerNames: FormerName[]; + currentName: string; +} + +export function FormerNames({ formerNames, currentName }: FormerNamesProps) { + if (!formerNames || formerNames.length === 0) { + return null; + } + + return ( + + + + + Ride History + + + + + + + + {currentName} + Current name + + Current + + + {formerNames.map((former, index) => ( + + + + {former.name} + {(former.from_year || former.to_year) && ( + + {former.from_year && former.to_year + ? `${former.from_year} - ${former.to_year}` + : former.from_year + ? `Since ${former.from_year}` + : `Until ${former.to_year}` + } + + )} + + Former + + ))} + + + + ); +} diff --git a/src/components/rides/RatingDistribution.tsx b/src/components/rides/RatingDistribution.tsx new file mode 100644 index 00000000..749e0e4b --- /dev/null +++ b/src/components/rides/RatingDistribution.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { Star } from 'lucide-react'; + +interface RatingDistributionProps { + rideId: string; + totalReviews: number; + averageRating: number; +} + +interface RatingCount { + rating: number; + count: number; +} + +export function RatingDistribution({ rideId, totalReviews, averageRating }: RatingDistributionProps) { + const [distribution, setDistribution] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchDistribution() { + const { data, error } = await supabase + .from('reviews') + .select('rating') + .eq('ride_id', rideId) + .eq('moderation_status', 'approved'); + + if (!error && data) { + const counts = [5, 4, 3, 2, 1].map(rating => ({ + rating, + count: data.filter(r => r.rating === rating).length + })); + setDistribution(counts); + } + setLoading(false); + } + + fetchDistribution(); + }, [rideId]); + + if (loading) { + return null; + } + + return ( + + + Rating Breakdown + + + + + + {averageRating.toFixed(1)} + + + Based on {totalReviews} {totalReviews === 1 ? 'review' : 'reviews'} + + + + + {distribution.map(({ rating, count }) => { + const percentage = totalReviews > 0 ? (count / totalReviews) * 100 : 0; + return ( + + + {rating} + + + + + {count} + + + ); + })} + + + + ); +} diff --git a/src/components/rides/RecentPhotosPreview.tsx b/src/components/rides/RecentPhotosPreview.tsx new file mode 100644 index 00000000..95729c9b --- /dev/null +++ b/src/components/rides/RecentPhotosPreview.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Camera } from 'lucide-react'; + +interface RecentPhotosPreviewProps { + rideId: string; + onViewAll: () => void; +} + +interface Photo { + id: string; + image_url: string; + caption: string | null; +} + +export function RecentPhotosPreview({ rideId, onViewAll }: RecentPhotosPreviewProps) { + const [photos, setPhotos] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchPhotos() { + const { data, error } = await supabase + .from('reviews') + .select('photos') + .eq('ride_id', rideId) + .eq('moderation_status', 'approved') + .not('photos', 'is', null) + .order('created_at', { ascending: false }) + .limit(10); + + if (!error && data) { + const allPhotos: Photo[] = []; + data.forEach((review: any) => { + if (review.photos && Array.isArray(review.photos)) { + review.photos.forEach((photo: any) => { + if (allPhotos.length < 4) { + allPhotos.push({ + id: photo.id || Math.random().toString(), + image_url: photo.image_url || photo.url, + caption: photo.caption || null + }); + } + }); + } + }); + setPhotos(allPhotos); + } + setLoading(false); + } + + fetchPhotos(); + }, [rideId]); + + if (loading || photos.length === 0) { + return null; + } + + return ( + + + + + Recent Photos + + + + + {photos.map((photo) => ( + + + + ))} + + + + View All Photos + + + + + ); +} diff --git a/src/components/rides/RideHighlights.tsx b/src/components/rides/RideHighlights.tsx new file mode 100644 index 00000000..6e8b1fd4 --- /dev/null +++ b/src/components/rides/RideHighlights.tsx @@ -0,0 +1,90 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Zap, TrendingUp, Award, Sparkles } from 'lucide-react'; + +interface RideHighlight { + icon: React.ReactNode; + label: string; + value: string; +} + +interface RideHighlightsProps { + ride: any; +} + +export function RideHighlights({ ride }: RideHighlightsProps) { + const highlights: RideHighlight[] = []; + + // Add speed highlight if notable + if (ride.max_speed_kmh && ride.max_speed_kmh > 60) { + highlights.push({ + icon: , + label: 'High Speed', + value: `${ride.max_speed_kmh} km/h` + }); + } + + // Add height highlight if notable + if (ride.max_height_meters && ride.max_height_meters > 30) { + highlights.push({ + icon: , + label: 'Tall Structure', + value: `${ride.max_height_meters}m high` + }); + } + + // Add inversions highlight + if (ride.inversions && ride.inversions > 0) { + highlights.push({ + icon: , + label: 'Inversions', + value: `${ride.inversions} ${ride.inversions === 1 ? 'inversion' : 'inversions'}` + }); + } + + // Add rating highlight if high + if (ride.average_rating >= 4.0) { + highlights.push({ + icon: , + label: 'Highly Rated', + value: `${ride.average_rating.toFixed(1)} stars` + }); + } + + if (highlights.length === 0) { + return null; + } + + return ( + + + Ride Highlights + + + + {highlights.map((highlight, index) => ( + + {highlight.icon} + {highlight.label} + {highlight.value} + + ))} + + + {ride.intensity_level && ( + + + Intensity Level: + + {ride.intensity_level.charAt(0).toUpperCase() + ride.intensity_level.slice(1)} + + + + )} + + + ); +} diff --git a/src/components/rides/SimilarRides.tsx b/src/components/rides/SimilarRides.tsx new file mode 100644 index 00000000..102efbf0 --- /dev/null +++ b/src/components/rides/SimilarRides.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { supabase } from '@/integrations/supabase/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Star } from 'lucide-react'; + +interface SimilarRidesProps { + currentRideId: string; + parkId: string; + parkSlug: string; + category: string; +} + +interface SimilarRide { + id: string; + name: string; + slug: string; + image_url: string | null; + average_rating: number; + status: string; +} + +export function SimilarRides({ currentRideId, parkId, parkSlug, category }: SimilarRidesProps) { + const [rides, setRides] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchSimilarRides() { + const { data, error } = await supabase + .from('rides') + .select('id, name, slug, image_url, average_rating, status') + .eq('park_id', parkId) + .eq('category', category) + .neq('id', currentRideId) + .order('average_rating', { ascending: false }) + .limit(4); + + if (!error && data) { + setRides(data); + } + setLoading(false); + } + + fetchSimilarRides(); + }, [currentRideId, parkId, category]); + + if (loading || rides.length === 0) { + return null; + } + + return ( + + + Similar Rides You Might Enjoy + + + + {rides.map((ride) => ( + + + {ride.image_url ? ( + + + + ) : ( + + No image + + )} + + + {ride.name} + + + + {ride.average_rating.toFixed(1)} + • + {ride.status} + + + + + ))} + + + + View All Rides + + + + + ); +} diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index 8c1892b6..94568235 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -29,6 +29,12 @@ import { import { ReviewsSection } from '@/components/reviews/ReviewsSection'; import { MeasurementDisplay } from '@/components/ui/measurement-display'; import { RidePhotoGallery } from '@/components/rides/RidePhotoGallery'; +import { RatingDistribution } from '@/components/rides/RatingDistribution'; +import { RideHighlights } from '@/components/rides/RideHighlights'; +import { SimilarRides } from '@/components/rides/SimilarRides'; +import { FormerNames } from '@/components/rides/FormerNames'; +import { RecentPhotosPreview } from '@/components/rides/RecentPhotosPreview'; +import { ParkLocationMap } from '@/components/maps/ParkLocationMap'; import { Ride } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; @@ -37,6 +43,7 @@ export default function RideDetail() { const navigate = useNavigate(); const [ride, setRide] = useState(null); const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("overview"); useEffect(() => { if (parkSlug && rideSlug) { @@ -54,7 +61,7 @@ export default function RideDetail() { .maybeSingle(); if (parkData) { - // Then get ride details + // Then get ride details with park_id stored separately const { data: rideData } = await supabase .from('rides') .select(` @@ -67,6 +74,11 @@ export default function RideDetail() { .eq('slug', rideSlug) .maybeSingle(); + if (rideData) { + // Store park_id for easier access + (rideData as any).currentParkId = parkData.id; + } + setRide(rideData); } } catch (error) { @@ -193,14 +205,35 @@ export default function RideDetail() { {ride.average_rating > 0 && ( - - - - {ride.average_rating.toFixed(1)} + + + + + {ride.average_rating.toFixed(1)} + - - {ride.review_count} reviews + + {[1, 2, 3, 4, 5].map((star) => ( + + ))} + + {ride.review_count} {ride.review_count === 1 ? "review" : "reviews"} + + setActiveTab("reviews")} + > + Write Review + )} @@ -323,7 +356,7 @@ export default function RideDetail() { )} {/* Main Content */} - + Overview Specifications @@ -347,9 +380,32 @@ export default function RideDetail() { )} + + + + {ride.former_names && ride.former_names.length > 0 && ( + + )} + + + + setActiveTab("photos")} + /> + {/* Ride Information */} @@ -457,6 +513,22 @@ export default function RideDetail() { + + {ride.park?.location?.latitude && ride.park?.location?.longitude && ( + + + Location + + + + + + )}