feat: Add new tabs for content filtering

This commit is contained in:
gpt-engineer-app[bot]
2025-10-28 13:32:42 +00:00
parent 1f44e7afcb
commit 385cc33961

View File

@@ -31,6 +31,10 @@ export function ContentTabs() {
const [recentRides, setRecentRides] = useState<Ride[]>([]); const [recentRides, setRecentRides] = useState<Ride[]>([]);
const [recentChanges, setRecentChanges] = useState<RecentChange[]>([]); const [recentChanges, setRecentChanges] = useState<RecentChange[]>([]);
const [recentlyOpened, setRecentlyOpened] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]); const [recentlyOpened, setRecentlyOpened] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
const [highestRatedParks, setHighestRatedParks] = useState<Park[]>([]);
const [highestRatedRides, setHighestRatedRides] = useState<Ride[]>([]);
const [openingSoon, setOpeningSoon] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
const [closingSoon, setClosingSoon] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -213,12 +217,94 @@ export function ContentTabs() {
.sort((a, b) => new Date(b.opening_date).getTime() - new Date(a.opening_date).getTime()) .sort((a, b) => new Date(b.opening_date).getTime() - new Date(a.opening_date).getTime())
.slice(0, 24); .slice(0, 24);
// Highest Rated Parks
const { data: topRatedParks } = await supabase
.from('parks')
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
.gt('review_count', 0)
.order('average_rating', { ascending: false })
.order('review_count', { ascending: false })
.limit(12);
// Highest Rated Rides
const { data: topRatedRides } = await supabase
.from('rides')
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
.gt('review_count', 0)
.order('average_rating', { ascending: false })
.order('review_count', { ascending: false })
.limit(12);
// Opening Soon (next 6 months)
const sixMonthsFromNow = new Date();
sixMonthsFromNow.setMonth(sixMonthsFromNow.getMonth() + 6);
const futureThreshold = sixMonthsFromNow.toISOString().split('T')[0];
const { data: parksSoon } = await supabase
.from('parks')
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
.not('opening_date', 'is', null)
.gt('opening_date', new Date().toISOString().split('T')[0])
.lte('opening_date', futureThreshold)
.in('status', ['under_construction', 'planned'])
.order('opening_date', { ascending: true })
.limit(20);
const { data: ridesSoon } = await supabase
.from('rides')
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
.not('opening_date', 'is', null)
.gt('opening_date', new Date().toISOString().split('T')[0])
.lte('opening_date', futureThreshold)
.in('status', ['under_construction', 'planned'])
.order('opening_date', { ascending: true })
.limit(20);
const combinedOpening = [
...(parksSoon || []).map(p => ({ ...p, entityType: 'park' as const })),
...(ridesSoon || []).map(r => ({ ...r, entityType: 'ride' as const }))
]
.sort((a, b) => new Date(a.opening_date).getTime() - new Date(b.opening_date).getTime())
.slice(0, 24);
// Closing Soon (next 6 months)
const { data: parksClosing } = await supabase
.from('parks')
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
.not('closing_date', 'is', null)
.gt('closing_date', new Date().toISOString().split('T')[0])
.lte('closing_date', futureThreshold)
.eq('status', 'operating')
.order('closing_date', { ascending: true })
.limit(20);
const { data: ridesClosing } = await supabase
.from('rides')
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
.not('closing_date', 'is', null)
.gt('closing_date', new Date().toISOString().split('T')[0])
.lte('closing_date', futureThreshold)
.eq('status', 'operating')
.order('closing_date', { ascending: true })
.limit(20);
const combinedClosing = [
...(parksClosing || []).map(p => ({ ...p, entityType: 'park' as const })),
...(ridesClosing || []).map(r => ({ ...r, entityType: 'ride' as const }))
]
.sort((a, b) => new Date(a.closing_date).getTime() - new Date(b.closing_date).getTime())
.slice(0, 24);
setTrendingParks(trending || []); setTrendingParks(trending || []);
setRecentParks(recent || []); setRecentParks(recent || []);
setTrendingRides(trendingRidesData || []); setTrendingRides(trendingRidesData || []);
setRecentRides(recentRidesData || []); setRecentRides(recentRidesData || []);
setRecentChanges(allChanges); setRecentChanges(allChanges);
setRecentlyOpened(combinedOpened); setRecentlyOpened(combinedOpened);
setHighestRatedParks(topRatedParks || []);
setHighestRatedRides(topRatedRides || []);
setOpeningSoon(combinedOpening);
setClosingSoon(combinedClosing);
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Failed to fetch content', { error: getErrorMessage(error) }); logger.error('Failed to fetch content', { error: getErrorMessage(error) });
} finally { } finally {
@@ -260,7 +346,7 @@ export function ContentTabs() {
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<Tabs defaultValue="trending-parks" className="w-full"> <Tabs defaultValue="trending-parks" className="w-full">
<div className="text-center mb-8"> <div className="text-center mb-8">
<TabsList className="grid w-full max-w-5xl mx-auto grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 md:gap-2 p-3 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 dark:from-primary/10 dark:via-secondary/10 dark:to-accent/10 border border-primary/10 dark:border-primary/20 rounded-xl backdrop-blur-sm"> <TabsList className="flex flex-wrap justify-center w-full max-w-7xl mx-auto gap-2 p-3 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5 dark:from-primary/10 dark:via-secondary/10 dark:to-accent/10 border border-primary/10 dark:border-primary/20 rounded-xl backdrop-blur-sm">
<TabsTrigger value="trending-parks" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium"> <TabsTrigger value="trending-parks" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Trending Parks Trending Parks
</TabsTrigger> </TabsTrigger>
@@ -279,6 +365,18 @@ export function ContentTabs() {
<TabsTrigger value="recently-opened" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium"> <TabsTrigger value="recently-opened" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Recently Opened Recently Opened
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="highest-rated-parks" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Top Parks
</TabsTrigger>
<TabsTrigger value="highest-rated-rides" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Top Rides
</TabsTrigger>
<TabsTrigger value="opening-soon" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Opening Soon
</TabsTrigger>
<TabsTrigger value="closing-soon" className="text-sm md:text-xs lg:text-sm px-3 py-4 md:px-2 md:py-3 rounded-lg border border-border/30 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md hover:border-primary/40 hover:-translate-y-0.5 data-[state=active]:border-primary/60 data-[state=active]:bg-gradient-to-br data-[state=active]:from-primary/15 data-[state=active]:to-accent/10 data-[state=active]:shadow-[0_0_20px_rgba(139,92,246,0.4)] data-[state=active]:translate-y-0 transition-all duration-300 font-medium">
Closing Soon
</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -402,6 +500,110 @@ export function ContentTabs() {
))} ))}
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="highest-rated-parks" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl md:text-3xl font-bold mb-2 bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(139,92,246,0.3)]">Highest Rated Parks</h2>
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Top-rated theme parks based on visitor reviews</p>
<div className="mt-4 mx-auto w-24 h-1 bg-gradient-to-r from-transparent via-primary to-transparent rounded-full opacity-60"></div>
</div>
{highestRatedParks.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 3xl:grid-cols-8 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
{highestRatedParks.map((park) => (
<ParkCard key={park.id} park={park} />
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
No rated parks available yet. Be the first to leave a review!
</div>
)}
</TabsContent>
<TabsContent value="highest-rated-rides" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl md:text-3xl font-bold mb-2 bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(139,92,246,0.3)]">Highest Rated Rides</h2>
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Top-rated attractions based on rider reviews</p>
<div className="mt-4 mx-auto w-24 h-1 bg-gradient-to-r from-transparent via-primary to-transparent rounded-full opacity-60"></div>
</div>
{highestRatedRides.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 3xl:grid-cols-8 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
{highestRatedRides.map((ride) => (
<RideCard key={ride.id} ride={ride} />
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
No rated rides available yet. Be the first to leave a review!
</div>
)}
</TabsContent>
<TabsContent value="opening-soon" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl md:text-3xl font-bold mb-2 bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(139,92,246,0.3)]">Opening Soon</h2>
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides opening in the next 6 months</p>
<div className="mt-4 mx-auto w-24 h-1 bg-gradient-to-r from-transparent via-primary to-transparent rounded-full opacity-60"></div>
</div>
{openingSoon.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 3xl:grid-cols-8 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
{openingSoon.map((entity: any) => (
entity.entityType === 'park' ? (
<div key={entity.id} className="relative">
<ParkCard park={entity} />
<Badge className="absolute top-2 right-2 bg-blue-500/90 text-white backdrop-blur-sm">
{new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Badge>
</div>
) : (
<div key={entity.id} className="relative">
<RideCard ride={entity} />
<Badge className="absolute top-2 right-2 bg-blue-500/90 text-white backdrop-blur-sm">
{new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Badge>
</div>
)
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
No parks or rides scheduled to open in the next 6 months.
</div>
)}
</TabsContent>
<TabsContent value="closing-soon" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl md:text-3xl font-bold mb-2 bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(139,92,246,0.3)]">Closing Soon</h2>
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Last chance: Parks and rides closing in the next 6 months</p>
<div className="mt-4 mx-auto w-24 h-1 bg-gradient-to-r from-transparent via-primary to-transparent rounded-full opacity-60"></div>
</div>
{closingSoon.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 3xl:grid-cols-8 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
{closingSoon.map((entity: any) => (
entity.entityType === 'park' ? (
<div key={entity.id} className="relative">
<ParkCard park={entity} />
<Badge className="absolute top-2 right-2 bg-red-500/90 text-white backdrop-blur-sm">
Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Badge>
</div>
) : (
<div key={entity.id} className="relative">
<RideCard ride={entity} />
<Badge className="absolute top-2 right-2 bg-red-500/90 text-white backdrop-blur-sm">
Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Badge>
</div>
)
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
No parks or rides scheduled to close in the next 6 months.
</div>
)}
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</section> </section>