mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 11:51:14 -05:00
feat: Enhance ride detail page with new sections
This commit is contained in:
63
src/components/rides/FormerNames.tsx
Normal file
63
src/components/rides/FormerNames.tsx
Normal file
@@ -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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<History className="w-5 h-5" />
|
||||||
|
Ride History
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-primary" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{currentName}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Current name</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="default">Current</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formerNames.map((former, index) => (
|
||||||
|
<div key={index} className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{former.name}</div>
|
||||||
|
{(former.from_year || former.to_year) && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{former.from_year && former.to_year
|
||||||
|
? `${former.from_year} - ${former.to_year}`
|
||||||
|
: former.from_year
|
||||||
|
? `Since ${former.from_year}`
|
||||||
|
: `Until ${former.to_year}`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">Former</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
src/components/rides/RatingDistribution.tsx
Normal file
83
src/components/rides/RatingDistribution.tsx
Normal file
@@ -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<RatingCount[]>([]);
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Rating Breakdown</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="text-center pb-4 border-b border-border">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-2">
|
||||||
|
<Star className="w-8 h-8 fill-yellow-400 text-yellow-400" />
|
||||||
|
<span className="text-4xl font-bold">{averageRating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Based on {totalReviews} {totalReviews === 1 ? 'review' : 'reviews'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{distribution.map(({ rating, count }) => {
|
||||||
|
const percentage = totalReviews > 0 ? (count / totalReviews) * 100 : 0;
|
||||||
|
return (
|
||||||
|
<div key={rating} className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-1 w-16">
|
||||||
|
<span className="text-sm font-medium">{rating}</span>
|
||||||
|
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<Progress value={percentage} className="flex-1 h-2" />
|
||||||
|
<span className="text-sm text-muted-foreground w-12 text-right">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/components/rides/RecentPhotosPreview.tsx
Normal file
92
src/components/rides/RecentPhotosPreview.tsx
Normal file
@@ -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<Photo[]>([]);
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Camera className="w-5 h-5" />
|
||||||
|
Recent Photos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{photos.map((photo) => (
|
||||||
|
<div
|
||||||
|
key={photo.id}
|
||||||
|
className="aspect-square rounded-lg overflow-hidden bg-accent cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
onClick={onViewAll}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={photo.image_url}
|
||||||
|
alt={photo.caption || 'Ride photo'}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button variant="outline" onClick={onViewAll} className="w-full">
|
||||||
|
View All Photos
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/components/rides/RideHighlights.tsx
Normal file
90
src/components/rides/RideHighlights.tsx
Normal file
@@ -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: <Zap className="w-5 h-5 text-amber-500" />,
|
||||||
|
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: <TrendingUp className="w-5 h-5 text-blue-500" />,
|
||||||
|
label: 'Tall Structure',
|
||||||
|
value: `${ride.max_height_meters}m high`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inversions highlight
|
||||||
|
if (ride.inversions && ride.inversions > 0) {
|
||||||
|
highlights.push({
|
||||||
|
icon: <Sparkles className="w-5 h-5 text-purple-500" />,
|
||||||
|
label: 'Inversions',
|
||||||
|
value: `${ride.inversions} ${ride.inversions === 1 ? 'inversion' : 'inversions'}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rating highlight if high
|
||||||
|
if (ride.average_rating >= 4.0) {
|
||||||
|
highlights.push({
|
||||||
|
icon: <Award className="w-5 h-5 text-yellow-500" />,
|
||||||
|
label: 'Highly Rated',
|
||||||
|
value: `${ride.average_rating.toFixed(1)} stars`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlights.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ride Highlights</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{highlights.map((highlight, index) => (
|
||||||
|
<div key={index} className="flex flex-col items-center text-center p-4 rounded-lg bg-accent/50">
|
||||||
|
<div className="mb-2">{highlight.icon}</div>
|
||||||
|
<div className="font-medium text-sm mb-1">{highlight.label}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{highlight.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ride.intensity_level && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">Intensity Level:</span>
|
||||||
|
<Badge variant={
|
||||||
|
ride.intensity_level === 'extreme' ? 'destructive' :
|
||||||
|
ride.intensity_level === 'high' ? 'default' : 'secondary'
|
||||||
|
}>
|
||||||
|
{ride.intensity_level.charAt(0).toUpperCase() + ride.intensity_level.slice(1)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/components/rides/SimilarRides.tsx
Normal file
102
src/components/rides/SimilarRides.tsx
Normal file
@@ -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<SimilarRide[]>([]);
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Similar Rides You Might Enjoy</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{rides.map((ride) => (
|
||||||
|
<Link
|
||||||
|
key={ride.id}
|
||||||
|
to={`/parks/${parkSlug}/rides/${ride.slug}`}
|
||||||
|
className="group block"
|
||||||
|
>
|
||||||
|
<div className="border border-border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
|
||||||
|
{ride.image_url ? (
|
||||||
|
<div className="aspect-video overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={ride.image_url}
|
||||||
|
alt={ride.name}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="aspect-video bg-accent flex items-center justify-center">
|
||||||
|
<span className="text-muted-foreground">No image</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-3">
|
||||||
|
<h4 className="font-medium mb-1 group-hover:text-primary transition-colors">
|
||||||
|
{ride.name}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
|
||||||
|
<span>{ride.average_rating.toFixed(1)}</span>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<span className="text-muted-foreground capitalize">{ride.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link to={`/parks/${parkSlug}/rides`}>View All Rides</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,12 @@ import {
|
|||||||
import { ReviewsSection } from '@/components/reviews/ReviewsSection';
|
import { ReviewsSection } from '@/components/reviews/ReviewsSection';
|
||||||
import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
||||||
import { RidePhotoGallery } from '@/components/rides/RidePhotoGallery';
|
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 { Ride } from '@/types/database';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ export default function RideDetail() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [ride, setRide] = useState<Ride | null>(null);
|
const [ride, setRide] = useState<Ride | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parkSlug && rideSlug) {
|
if (parkSlug && rideSlug) {
|
||||||
@@ -54,7 +61,7 @@ export default function RideDetail() {
|
|||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (parkData) {
|
if (parkData) {
|
||||||
// Then get ride details
|
// Then get ride details with park_id stored separately
|
||||||
const { data: rideData } = await supabase
|
const { data: rideData } = await supabase
|
||||||
.from('rides')
|
.from('rides')
|
||||||
.select(`
|
.select(`
|
||||||
@@ -67,6 +74,11 @@ export default function RideDetail() {
|
|||||||
.eq('slug', rideSlug)
|
.eq('slug', rideSlug)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (rideData) {
|
||||||
|
// Store park_id for easier access
|
||||||
|
(rideData as any).currentParkId = parkData.id;
|
||||||
|
}
|
||||||
|
|
||||||
setRide(rideData);
|
setRide(rideData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -193,14 +205,35 @@ export default function RideDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ride.average_rating > 0 && (
|
{ride.average_rating > 0 && (
|
||||||
<div className="bg-black/20 backdrop-blur-sm rounded-lg p-4 text-center">
|
<div className="bg-black/30 backdrop-blur-md rounded-lg p-6 text-center min-w-[160px]">
|
||||||
<div className="flex items-center gap-2 text-white mb-1">
|
<div className="flex items-center justify-center gap-2 text-white mb-2">
|
||||||
<Star className="w-5 h-5 fill-yellow-400 text-yellow-400" />
|
<Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
|
||||||
<span className="text-2xl font-bold">{ride.average_rating.toFixed(1)}</span>
|
<span className="text-3xl font-bold">
|
||||||
|
{ride.average_rating.toFixed(1)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-white/70 text-sm">
|
<div className="flex items-center justify-center gap-1 mb-3">
|
||||||
{ride.review_count} reviews
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<Star
|
||||||
|
key={star}
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
star <= Math.round(ride.average_rating)
|
||||||
|
? 'fill-yellow-400 text-yellow-400'
|
||||||
|
: 'text-white/40'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-white/90 text-sm mb-3">
|
||||||
|
{ride.review_count} {ride.review_count === 1 ? "review" : "reviews"}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setActiveTab("reviews")}
|
||||||
|
>
|
||||||
|
Write Review
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +356,7 @@ export default function RideDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Tabs defaultValue="overview" className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-4">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<TabsTrigger value="specs">Specifications</TabsTrigger>
|
<TabsTrigger value="specs">Specifications</TabsTrigger>
|
||||||
@@ -347,9 +380,32 @@ export default function RideDetail() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<RideHighlights ride={ride} />
|
||||||
|
|
||||||
|
{ride.former_names && ride.former_names.length > 0 && (
|
||||||
|
<FormerNames formerNames={ride.former_names} currentName={ride.name} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SimilarRides
|
||||||
|
currentRideId={ride.id}
|
||||||
|
parkId={(ride as any).currentParkId}
|
||||||
|
parkSlug={parkSlug || ''}
|
||||||
|
category={ride.category}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RecentPhotosPreview
|
||||||
|
rideId={ride.id}
|
||||||
|
onViewAll={() => setActiveTab("photos")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<RatingDistribution
|
||||||
|
rideId={ride.id}
|
||||||
|
totalReviews={ride.review_count}
|
||||||
|
averageRating={ride.average_rating}
|
||||||
|
/>
|
||||||
{/* Ride Information */}
|
{/* Ride Information */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -457,6 +513,22 @@ export default function RideDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{ride.park?.location?.latitude && ride.park?.location?.longitude && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Location</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ParkLocationMap
|
||||||
|
latitude={ride.park.location.latitude}
|
||||||
|
longitude={ride.park.location.longitude}
|
||||||
|
parkName={ride.park.name}
|
||||||
|
className="h-48"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user