mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
Refactor homepage content fetching
This commit is contained in:
@@ -1,349 +1,63 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ParkCard } from '@/components/parks/ParkCard';
|
||||
import { RideCard } from '@/components/rides/RideCard';
|
||||
import { RecentChangeCard } from './RecentChangeCard';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Park, Ride } from '@/types/database';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { Clock } from 'lucide-react';
|
||||
|
||||
interface RecentChange {
|
||||
entityType: 'park' | 'ride' | 'company';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
entitySlug: string;
|
||||
parkSlug?: string;
|
||||
imageUrl: string | null;
|
||||
changeType: string;
|
||||
changedAt: string;
|
||||
changedByUsername?: string | null;
|
||||
changedByAvatar?: string | null;
|
||||
changeReason: string | null;
|
||||
}
|
||||
import { useHomepageTrendingParks, useHomepageTrendingRides } from '@/hooks/homepage/useHomepageTrending';
|
||||
import { useHomepageRecentParks, useHomepageRecentRides } from '@/hooks/homepage/useHomepageRecent';
|
||||
import { useHomepageRecentChanges } from '@/hooks/homepage/useHomepageRecentChanges';
|
||||
import { useHomepageRecentlyOpenedParks, useHomepageRecentlyOpenedRides } from '@/hooks/homepage/useHomepageOpened';
|
||||
import { useHomepageHighestRatedParks, useHomepageHighestRatedRides } from '@/hooks/homepage/useHomepageRated';
|
||||
import { useHomepageOpeningSoonParks, useHomepageOpeningSoonRides } from '@/hooks/homepage/useHomepageOpeningSoon';
|
||||
import { useHomepageClosingSoonParks, useHomepageClosingSoonRides } from '@/hooks/homepage/useHomepageClosing';
|
||||
import { useHomepageRecentlyClosedParks, useHomepageRecentlyClosedRides } from '@/hooks/homepage/useHomepageClosed';
|
||||
|
||||
export function ContentTabs() {
|
||||
const [trendingParks, setTrendingParks] = useState<Park[]>([]);
|
||||
const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
|
||||
const [recentParks, setRecentParks] = useState<Park[]>([]);
|
||||
const [recentRides, setRecentRides] = useState<Ride[]>([]);
|
||||
const [recentChanges, setRecentChanges] = useState<RecentChange[]>([]);
|
||||
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 [recentlyClosed, setRecentlyClosed] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('trending-parks');
|
||||
|
||||
useEffect(() => {
|
||||
fetchContent();
|
||||
}, []);
|
||||
// Lazy load data - only fetch when tab is active
|
||||
const trendingParks = useHomepageTrendingParks(activeTab === 'trending-parks');
|
||||
const trendingRides = useHomepageTrendingRides(activeTab === 'trending-rides');
|
||||
const recentParks = useHomepageRecentParks(activeTab === 'recent-parks');
|
||||
const recentRides = useHomepageRecentRides(activeTab === 'recent-rides');
|
||||
const recentChanges = useHomepageRecentChanges(activeTab === 'recent-changes');
|
||||
const recentlyOpenedParks = useHomepageRecentlyOpenedParks(activeTab === 'recently-opened');
|
||||
const recentlyOpenedRides = useHomepageRecentlyOpenedRides(activeTab === 'recently-opened');
|
||||
const highestRatedParks = useHomepageHighestRatedParks(activeTab === 'highest-rated-parks');
|
||||
const highestRatedRides = useHomepageHighestRatedRides(activeTab === 'highest-rated-rides');
|
||||
const openingSoonParks = useHomepageOpeningSoonParks(activeTab === 'opening-soon');
|
||||
const openingSoonRides = useHomepageOpeningSoonRides(activeTab === 'opening-soon');
|
||||
const closingSoonParks = useHomepageClosingSoonParks(activeTab === 'closing-soon');
|
||||
const closingSoonRides = useHomepageClosingSoonRides(activeTab === 'closing-soon');
|
||||
const recentlyClosedParks = useHomepageRecentlyClosedParks(activeTab === 'recently-closed');
|
||||
const recentlyClosedRides = useHomepageRecentlyClosedRides(activeTab === 'recently-closed');
|
||||
|
||||
const fetchContent = async () => {
|
||||
try {
|
||||
// Trending Parks (by 30-day view count)
|
||||
const { data: trending } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.order('view_count_30d', { ascending: false })
|
||||
.limit(12);
|
||||
// Combine parks and rides for mixed tabs
|
||||
const recentlyOpened = [
|
||||
...(recentlyOpenedParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(recentlyOpenedRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
].sort((a, b) => new Date(b.opening_date || 0).getTime() - new Date(a.opening_date || 0).getTime()).slice(0, 24);
|
||||
|
||||
// Recently Added Parks
|
||||
const { data: recent } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
const openingSoon = [
|
||||
...(openingSoonParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(openingSoonRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
].sort((a, b) => new Date(a.opening_date || 0).getTime() - new Date(b.opening_date || 0).getTime()).slice(0, 24);
|
||||
|
||||
// Trending Rides (by 30-day view count)
|
||||
const { data: trendingRidesData } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
|
||||
.order('view_count_30d', { ascending: false })
|
||||
.limit(12);
|
||||
const closingSoon = [
|
||||
...(closingSoonParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(closingSoonRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
].sort((a, b) => new Date(a.closing_date || 0).getTime() - new Date(b.closing_date || 0).getTime()).slice(0, 24);
|
||||
|
||||
// Recently Added Rides
|
||||
const { data: recentRidesData } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
const recentlyClosed = [
|
||||
...(recentlyClosedParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(recentlyClosedRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
].sort((a, b) => new Date(b.closing_date || 0).getTime() - new Date(a.closing_date || 0).getTime()).slice(0, 24);
|
||||
|
||||
// Fetch recent park versions
|
||||
const { data: parkVersions } = await supabase
|
||||
.from('park_versions')
|
||||
.select(`
|
||||
version_id,
|
||||
park_id,
|
||||
name,
|
||||
slug,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
card_image_url,
|
||||
profiles:profiles!park_versions_created_by_fkey(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
const isLoadingInitial = activeTab === 'trending-parks' && trendingParks.isLoading;
|
||||
|
||||
// Fetch recent ride versions with park slug for proper routing
|
||||
const { data: rideVersions } = await supabase
|
||||
.from('ride_versions')
|
||||
.select(`
|
||||
version_id,
|
||||
ride_id,
|
||||
name,
|
||||
slug,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
card_image_url,
|
||||
park_id,
|
||||
profiles:profiles!ride_versions_created_by_fkey(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
// Fetch park slugs for rides that have a park_id
|
||||
const rideParksMap = new Map<string, string>();
|
||||
if (rideVersions) {
|
||||
const parkIds = [...new Set(rideVersions.map(v => v.park_id).filter(Boolean))];
|
||||
if (parkIds.length > 0) {
|
||||
const { data: parks } = await supabase
|
||||
.from('parks')
|
||||
.select('id, slug')
|
||||
.in('id', parkIds);
|
||||
|
||||
parks?.forEach(park => {
|
||||
rideParksMap.set(park.id, park.slug);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch recent company versions
|
||||
const { data: companyVersions } = await supabase
|
||||
.from('company_versions')
|
||||
.select(`
|
||||
version_id,
|
||||
company_id,
|
||||
name,
|
||||
slug,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
card_image_url,
|
||||
profiles:profiles!company_versions_created_by_fkey(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
// Combine all changes into a unified structure
|
||||
const allChanges: RecentChange[] = [
|
||||
...(parkVersions || []).map(v => ({
|
||||
entityType: 'park' as const,
|
||||
entityId: v.park_id,
|
||||
entityName: v.name,
|
||||
entitySlug: v.slug,
|
||||
imageUrl: v.card_image_url,
|
||||
changeType: v.change_type,
|
||||
changedAt: v.created_at,
|
||||
changedByUsername: v.profiles?.username,
|
||||
changedByAvatar: v.profiles?.avatar_url,
|
||||
changeReason: v.change_reason,
|
||||
})),
|
||||
...(rideVersions || []).map(v => ({
|
||||
entityType: 'ride' as const,
|
||||
entityId: v.ride_id,
|
||||
entityName: v.name,
|
||||
entitySlug: v.slug,
|
||||
parkSlug: v.park_id ? rideParksMap.get(v.park_id) : undefined,
|
||||
imageUrl: v.card_image_url,
|
||||
changeType: v.change_type,
|
||||
changedAt: v.created_at,
|
||||
changedByUsername: v.profiles?.username,
|
||||
changedByAvatar: v.profiles?.avatar_url,
|
||||
changeReason: v.change_reason,
|
||||
})),
|
||||
...(companyVersions || []).map(v => ({
|
||||
entityType: 'company' as const,
|
||||
entityId: v.company_id,
|
||||
entityName: v.name,
|
||||
entitySlug: v.slug,
|
||||
imageUrl: v.card_image_url,
|
||||
changeType: v.change_type,
|
||||
changedAt: v.created_at,
|
||||
changedByUsername: v.profiles?.username,
|
||||
changedByAvatar: v.profiles?.avatar_url,
|
||||
changeReason: v.change_reason,
|
||||
}))
|
||||
]
|
||||
.sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime())
|
||||
.slice(0, 24);
|
||||
|
||||
// Fetch recently opened parks and rides
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
const dateThreshold = oneYearAgo.toISOString().split('T')[0];
|
||||
|
||||
const { data: openedParks } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.not('opening_date', 'is', null)
|
||||
.gte('opening_date', dateThreshold)
|
||||
.order('opening_date', { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
const { data: openedRides } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
|
||||
.not('opening_date', 'is', null)
|
||||
.gte('opening_date', dateThreshold)
|
||||
.order('opening_date', { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
// Combine and sort by opening date
|
||||
const combinedOpened = [
|
||||
...(openedParks || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(openedRides || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
]
|
||||
.sort((a, b) => new Date(b.opening_date).getTime() - new Date(a.opening_date).getTime())
|
||||
.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);
|
||||
|
||||
// Recently Closed (closed in last year)
|
||||
const { data: parksClosed } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.not('closing_date', 'is', null)
|
||||
.lt('closing_date', new Date().toISOString().split('T')[0])
|
||||
.gte('closing_date', dateThreshold)
|
||||
.in('status', ['closed', 'permanently_closed'])
|
||||
.order('closing_date', { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
const { data: ridesClosed } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
|
||||
.not('closing_date', 'is', null)
|
||||
.lt('closing_date', new Date().toISOString().split('T')[0])
|
||||
.gte('closing_date', dateThreshold)
|
||||
.in('status', ['closed', 'removed', 'permanently_closed'])
|
||||
.order('closing_date', { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
const combinedClosed = [
|
||||
...(parksClosed || []).map(p => ({ ...p, entityType: 'park' as const })),
|
||||
...(ridesClosed || []).map(r => ({ ...r, entityType: 'ride' as const }))
|
||||
]
|
||||
.sort((a, b) => new Date(b.closing_date).getTime() - new Date(a.closing_date).getTime())
|
||||
.slice(0, 24);
|
||||
|
||||
setTrendingParks(trending || []);
|
||||
setRecentParks(recent || []);
|
||||
setTrendingRides(trendingRidesData || []);
|
||||
setRecentRides(recentRidesData || []);
|
||||
setRecentChanges(allChanges);
|
||||
setRecentlyOpened(combinedOpened);
|
||||
setHighestRatedParks(topRatedParks || []);
|
||||
setHighestRatedRides(topRatedRides || []);
|
||||
setOpeningSoon(combinedOpening);
|
||||
setClosingSoon(combinedClosing);
|
||||
setRecentlyClosed(combinedClosed);
|
||||
} catch (error: unknown) {
|
||||
logger.error('Failed to fetch content', { error: getErrorMessage(error) });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
if (isLoadingInitial) {
|
||||
return (
|
||||
<section className="py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
@@ -374,7 +88,7 @@ export function ContentTabs() {
|
||||
return (
|
||||
<section className="py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<Tabs defaultValue="trending-parks" className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="text-center mb-8">
|
||||
<TabsList className="flex flex-wrap justify-center gap-2 p-3 bg-muted/30 rounded-lg max-w-5xl mx-auto">
|
||||
<TabsTrigger value="trending-parks" className="px-4 py-2.5 text-sm font-medium rounded-full data-[state=active]:bg-primary data-[state=active]:text-primary-foreground hover:bg-muted/50 transition-colors text-center md:w-[calc(25%-0.375rem)]">
|
||||
@@ -413,263 +127,339 @@ export function ContentTabs() {
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="trending-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)]">Trending Parks</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed parks in the last 30 days</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>
|
||||
<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">
|
||||
{trendingParks.map((park) => (
|
||||
<ParkCard key={park.id} park={park} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="trending-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)]">Trending Rides</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed rides in the last 30 days</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>
|
||||
<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">
|
||||
{trendingRides.map((ride) => (
|
||||
<RideCard key={ride.id} ride={ride} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recent-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)]">Recently Added Parks</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest parks added to our database</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>
|
||||
<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">
|
||||
{recentParks.map((park) => (
|
||||
<ParkCard key={park.id} park={park} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recent-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)]">Recently Added Rides</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest attractions added to our database</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>
|
||||
<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">
|
||||
{recentRides.map((ride) => (
|
||||
<RideCard key={ride.id} ride={ride} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recent-changes" 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)]">Recent Changes</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest updates across all entities</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>
|
||||
{recentChanges.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{recentChanges.map((change) => (
|
||||
<RecentChangeCard
|
||||
key={`${change.entityType}-${change.entityId}-${change.changedAt}`}
|
||||
entityType={change.entityType}
|
||||
entityId={change.entityId}
|
||||
entityName={change.entityName}
|
||||
entitySlug={change.entitySlug}
|
||||
parkSlug={change.parkSlug}
|
||||
imageUrl={change.imageUrl}
|
||||
changeType={change.changeType}
|
||||
changedAt={change.changedAt}
|
||||
changedByUsername={change.changedByUsername}
|
||||
changedByAvatar={change.changedByAvatar}
|
||||
changeReason={change.changeReason}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 rounded-full bg-primary/20 blur-2xl animate-pulse" />
|
||||
<div className="relative w-20 h-20 rounded-full bg-gradient-to-br from-card via-card to-card/80 flex items-center justify-center border-2 border-primary/20 shadow-xl">
|
||||
<Clock className="w-10 h-10 text-muted-foreground" />
|
||||
</div>
|
||||
<TabsContent value="trending-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)]">Trending Parks</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed parks in the last 30 days</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>
|
||||
<h3 className="text-xl font-semibold mb-2 text-foreground">No Recent Changes</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md text-sm">
|
||||
There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies!
|
||||
</p>
|
||||
<div className="mt-6 w-32 h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recently-opened" 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)]">Recently Opened</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that opened in the last year</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>
|
||||
<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">
|
||||
{recentlyOpened.map((entity: any) => (
|
||||
entity.entityType === 'park' ? (
|
||||
<div key={entity.id} className="relative">
|
||||
<ParkCard park={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||
{new Date(entity.opening_date).getFullYear()}
|
||||
</Badge>
|
||||
{trendingParks.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div key={entity.id} className="relative">
|
||||
<RideCard ride={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||
{new Date(entity.opening_date).getFullYear()}
|
||||
</Badge>
|
||||
<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">
|
||||
{trendingParks.data?.map((park) => (
|
||||
<ParkCard key={park.id} park={park} />
|
||||
))}
|
||||
</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="trending-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)]">Trending Rides</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed rides in the last 30 days</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>
|
||||
{trendingRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{trendingRides.data?.map((ride) => (
|
||||
<RideCard key={ride.id} ride={ride} />
|
||||
))}
|
||||
</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="recent-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)]">Recently Added Parks</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest parks added to our database</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>
|
||||
{recentParks.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{recentParks.data?.map((park) => (
|
||||
<ParkCard key={park.id} park={park} />
|
||||
))}
|
||||
</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="recent-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)]">Recently Added Rides</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest attractions added to our database</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>
|
||||
{recentRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{recentRides.data?.map((ride) => (
|
||||
<RideCard key={ride.id} ride={ride} />
|
||||
))}
|
||||
</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>
|
||||
<TabsContent value="recent-changes" 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)]">Recent Changes</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest updates across all entities</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>
|
||||
{recentChanges.isLoading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-64 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : recentChanges.data && recentChanges.data.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{recentChanges.data.map((change) => (
|
||||
<RecentChangeCard
|
||||
key={`${change.type}-${change.id}-${change.changedAt}`}
|
||||
entityType={change.type}
|
||||
entityId={change.id}
|
||||
entityName={change.name}
|
||||
entitySlug={change.slug}
|
||||
parkSlug={change.parkSlug}
|
||||
imageUrl={change.imageUrl}
|
||||
changeType={change.changeType}
|
||||
changedAt={change.changedAt}
|
||||
changedByUsername={change.changedBy?.username}
|
||||
changedByAvatar={change.changedBy?.avatarUrl}
|
||||
changeReason={change.changeReason}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 rounded-full bg-primary/20 blur-2xl animate-pulse" />
|
||||
<div className="relative w-20 h-20 rounded-full bg-gradient-to-br from-card via-card to-card/80 flex items-center justify-center border-2 border-primary/20 shadow-xl">
|
||||
<Clock className="w-10 h-10 text-muted-foreground" />
|
||||
</div>
|
||||
</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>
|
||||
<h3 className="text-xl font-semibold mb-2 text-foreground">No Recent Changes</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md text-sm">
|
||||
There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies!
|
||||
</p>
|
||||
<div className="mt-6 w-32 h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recently-closed" 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)]">Recently Closed</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that closed in the last year</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>
|
||||
{recentlyClosed.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">
|
||||
{recentlyClosed.map((entity: any) => (
|
||||
entity.entityType === 'park' ? (
|
||||
<div key={entity.id} className="relative">
|
||||
<ParkCard park={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
||||
Closed {new Date(entity.closing_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<div key={entity.id} className="relative">
|
||||
<RideCard ride={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
||||
Closed {new Date(entity.closing_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No parks or rides closed in the last year.
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="recently-opened" 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)]">Recently Opened</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that opened in the last year</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>
|
||||
{recentlyOpenedParks.isLoading || recentlyOpenedRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{recentlyOpened.map((entity: any) => (
|
||||
entity.entityType === 'park' ? (
|
||||
<div key={entity.id} className="relative">
|
||||
<ParkCard park={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||
{new Date(entity.opening_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<div key={entity.id} className="relative">
|
||||
<RideCard ride={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||
{new Date(entity.opening_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</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.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : highestRatedParks.data && highestRatedParks.data.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.data.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.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : highestRatedRides.data && highestRatedRides.data.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.data.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>
|
||||
{openingSoonParks.isLoading || openingSoonRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</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>
|
||||
{closingSoonParks.isLoading || closingSoonRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</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>
|
||||
|
||||
<TabsContent value="recently-closed" 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)]">Recently Closed</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that closed in the last year</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>
|
||||
{recentlyClosedParks.isLoading || recentlyClosedRides.isLoading ? (
|
||||
<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">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : recentlyClosed.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">
|
||||
{recentlyClosed.map((entity: any) => (
|
||||
entity.entityType === 'park' ? (
|
||||
<div key={entity.id} className="relative">
|
||||
<ParkCard park={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
||||
Closed {new Date(entity.closing_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<div key={entity.id} className="relative">
|
||||
<RideCard ride={entity} />
|
||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
||||
Closed {new Date(entity.closing_date).getFullYear()}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No parks or rides closed in the last year.
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
55
src/hooks/homepage/useHomepageClosed.ts
Normal file
55
src/hooks/homepage/useHomepageClosed.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageRecentlyClosedParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentlyClosedParks(),
|
||||
queryFn: async () => {
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
const today = new Date();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.gte('closed_date', oneYearAgo.toISOString())
|
||||
.lte('closed_date', today.toISOString())
|
||||
.order('closed_date', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageRecentlyClosedRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentlyClosedRides(),
|
||||
queryFn: async () => {
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
const today = new Date();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.gte('closed_date', oneYearAgo.toISOString())
|
||||
.lte('closed_date', today.toISOString())
|
||||
.order('closed_date', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
55
src/hooks/homepage/useHomepageClosing.ts
Normal file
55
src/hooks/homepage/useHomepageClosing.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageClosingSoonParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.closingSoonParks(),
|
||||
queryFn: async () => {
|
||||
const today = new Date();
|
||||
const sixMonthsFromNow = new Date();
|
||||
sixMonthsFromNow.setMonth(sixMonthsFromNow.getMonth() + 6);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.gte('closed_date', today.toISOString())
|
||||
.lte('closed_date', sixMonthsFromNow.toISOString())
|
||||
.order('closed_date', { ascending: true })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageClosingSoonRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.closingSoonRides(),
|
||||
queryFn: async () => {
|
||||
const today = new Date();
|
||||
const sixMonthsFromNow = new Date();
|
||||
sixMonthsFromNow.setMonth(sixMonthsFromNow.getMonth() + 6);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.gte('closed_date', today.toISOString())
|
||||
.lte('closed_date', sixMonthsFromNow.toISOString())
|
||||
.order('closed_date', { ascending: true })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
51
src/hooks/homepage/useHomepageOpened.ts
Normal file
51
src/hooks/homepage/useHomepageOpened.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageRecentlyOpenedParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentlyOpenedParks(),
|
||||
queryFn: async () => {
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.gte('opened_date', oneYearAgo.toISOString())
|
||||
.order('opened_date', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageRecentlyOpenedRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentlyOpenedRides(),
|
||||
queryFn: async () => {
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.gte('opened_date', oneYearAgo.toISOString())
|
||||
.order('opened_date', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
55
src/hooks/homepage/useHomepageOpeningSoon.ts
Normal file
55
src/hooks/homepage/useHomepageOpeningSoon.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageOpeningSoonParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.openingSoonParks(),
|
||||
queryFn: async () => {
|
||||
const today = new Date();
|
||||
const sixMonthsFromNow = new Date();
|
||||
sixMonthsFromNow.setMonth(sixMonthsFromNow.getMonth() + 6);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.gte('opened_date', today.toISOString())
|
||||
.lte('opened_date', sixMonthsFromNow.toISOString())
|
||||
.order('opened_date', { ascending: true })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageOpeningSoonRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.openingSoonRides(),
|
||||
queryFn: async () => {
|
||||
const today = new Date();
|
||||
const sixMonthsFromNow = new Date();
|
||||
sixMonthsFromNow.setMonth(sixMonthsFromNow.getMonth() + 6);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.gte('opened_date', today.toISOString())
|
||||
.lte('opened_date', sixMonthsFromNow.toISOString())
|
||||
.order('opened_date', { ascending: true })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
45
src/hooks/homepage/useHomepageRated.ts
Normal file
45
src/hooks/homepage/useHomepageRated.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageHighestRatedParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.highestRatedParks(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.not('average_rating', 'is', null)
|
||||
.order('average_rating', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageHighestRatedRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.highestRatedRides(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.not('average_rating', 'is', null)
|
||||
.order('average_rating', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
43
src/hooks/homepage/useHomepageRecent.ts
Normal file
43
src/hooks/homepage/useHomepageRecent.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageRecentParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentParks(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageRecentRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentRides(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
149
src/hooks/homepage/useHomepageRecentChanges.ts
Normal file
149
src/hooks/homepage/useHomepageRecentChanges.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
interface RecentChange {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'park' | 'ride' | 'company';
|
||||
slug: string;
|
||||
parkSlug?: string;
|
||||
imageUrl?: string;
|
||||
changeType: string;
|
||||
changedAt: string;
|
||||
changedBy?: {
|
||||
username: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
changeReason?: string;
|
||||
}
|
||||
|
||||
export function useHomepageRecentChanges(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.recentChanges(),
|
||||
queryFn: async () => {
|
||||
// Fetch recent park versions
|
||||
const { data: parkVersions } = await supabase
|
||||
.from('park_versions')
|
||||
.select(`
|
||||
park_id,
|
||||
name,
|
||||
slug,
|
||||
card_image_url,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
profiles:created_by(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(8);
|
||||
|
||||
// Fetch recent ride versions
|
||||
const { data: rideVersions } = await supabase
|
||||
.from('ride_versions')
|
||||
.select(`
|
||||
ride_id,
|
||||
name,
|
||||
slug,
|
||||
card_image_url,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
park_id,
|
||||
profiles:created_by(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(8);
|
||||
|
||||
// Fetch park slugs for rides
|
||||
const parkIds = rideVersions?.map(rv => rv.park_id).filter(Boolean) || [];
|
||||
const { data: parks } = parkIds.length > 0
|
||||
? await supabase
|
||||
.from('parks')
|
||||
.select('id, slug')
|
||||
.in('id', parkIds)
|
||||
: { data: [] };
|
||||
|
||||
const parkSlugMap = new Map<string, string>(
|
||||
(parks || []).map(p => [p.id, p.slug])
|
||||
);
|
||||
|
||||
// Fetch recent company versions
|
||||
const { data: companyVersions } = await supabase
|
||||
.from('company_versions')
|
||||
.select(`
|
||||
company_id,
|
||||
name,
|
||||
slug,
|
||||
card_image_url,
|
||||
change_type,
|
||||
created_at,
|
||||
created_by,
|
||||
change_reason,
|
||||
profiles:created_by(username, avatar_url)
|
||||
`)
|
||||
.eq('is_current', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(8);
|
||||
|
||||
// Combine and sort all changes
|
||||
const changes: RecentChange[] = [
|
||||
...(parkVersions || []).map(pv => ({
|
||||
id: pv.park_id,
|
||||
name: pv.name,
|
||||
type: 'park' as const,
|
||||
slug: pv.slug,
|
||||
imageUrl: pv.card_image_url,
|
||||
changeType: pv.change_type,
|
||||
changedAt: pv.created_at,
|
||||
changedBy: pv.profiles ? {
|
||||
username: pv.profiles.username,
|
||||
avatarUrl: pv.profiles.avatar_url
|
||||
} : undefined,
|
||||
changeReason: pv.change_reason
|
||||
})),
|
||||
...(rideVersions || []).map(rv => ({
|
||||
id: rv.ride_id,
|
||||
name: rv.name,
|
||||
type: 'ride' as const,
|
||||
slug: rv.slug,
|
||||
parkSlug: rv.park_id ? (parkSlugMap.get(rv.park_id) || undefined) : undefined,
|
||||
imageUrl: rv.card_image_url,
|
||||
changeType: rv.change_type,
|
||||
changedAt: rv.created_at,
|
||||
changedBy: rv.profiles ? {
|
||||
username: rv.profiles.username,
|
||||
avatarUrl: rv.profiles.avatar_url
|
||||
} : undefined,
|
||||
changeReason: rv.change_reason
|
||||
})),
|
||||
...(companyVersions || []).map(cv => ({
|
||||
id: cv.company_id,
|
||||
name: cv.name,
|
||||
type: 'company' as const,
|
||||
slug: cv.slug,
|
||||
imageUrl: cv.card_image_url,
|
||||
changeType: cv.change_type,
|
||||
changedAt: cv.created_at,
|
||||
changedBy: cv.profiles ? {
|
||||
username: cv.profiles.username,
|
||||
avatarUrl: cv.profiles.avatar_url
|
||||
} : undefined,
|
||||
changeReason: cv.change_reason
|
||||
}))
|
||||
];
|
||||
|
||||
return changes
|
||||
.sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime())
|
||||
.slice(0, 24);
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
43
src/hooks/homepage/useHomepageTrending.ts
Normal file
43
src/hooks/homepage/useHomepageTrending.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export function useHomepageTrendingParks(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.trendingParks(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('parks')
|
||||
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
|
||||
.order('view_count_30d', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useHomepageTrendingRides(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.homepage.trendingRides(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('rides')
|
||||
.select(`*, park:parks(*), location:locations(*)`)
|
||||
.order('view_count_30d', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -68,5 +68,31 @@ export function useQueryInvalidation() {
|
||||
invalidateModerationStats: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.moderationStats() });
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate homepage data
|
||||
* Call this after creating/updating parks or rides
|
||||
*/
|
||||
invalidateHomepageData: (entityType?: 'parks' | 'rides' | 'all') => {
|
||||
if (!entityType || entityType === 'all') {
|
||||
queryClient.invalidateQueries({ queryKey: ['homepage'] });
|
||||
} else if (entityType === 'parks') {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['homepage'],
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey[1] as string;
|
||||
return typeof key === 'string' && key.includes('parks');
|
||||
}
|
||||
});
|
||||
} else if (entityType === 'rides') {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['homepage'],
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey[1] as string;
|
||||
return typeof key === 'string' && key.includes('rides');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,5 +14,24 @@ export const queryKeys = {
|
||||
moderationQueue: (config: Record<string, any>) => ['moderation-queue', config] as const,
|
||||
moderationStats: () => ['moderation-stats'] as const,
|
||||
|
||||
// Homepage queries
|
||||
homepage: {
|
||||
trendingParks: () => ['homepage', 'trending-parks'] as const,
|
||||
trendingRides: () => ['homepage', 'trending-rides'] as const,
|
||||
recentParks: () => ['homepage', 'recent-parks'] as const,
|
||||
recentRides: () => ['homepage', 'recent-rides'] as const,
|
||||
recentChanges: () => ['homepage', 'recent-changes'] as const,
|
||||
recentlyOpenedParks: () => ['homepage', 'recently-opened-parks'] as const,
|
||||
recentlyOpenedRides: () => ['homepage', 'recently-opened-rides'] as const,
|
||||
highestRatedParks: () => ['homepage', 'highest-rated-parks'] as const,
|
||||
highestRatedRides: () => ['homepage', 'highest-rated-rides'] as const,
|
||||
openingSoonParks: () => ['homepage', 'opening-soon-parks'] as const,
|
||||
openingSoonRides: () => ['homepage', 'opening-soon-rides'] as const,
|
||||
closingSoonParks: () => ['homepage', 'closing-soon-parks'] as const,
|
||||
closingSoonRides: () => ['homepage', 'closing-soon-rides'] as const,
|
||||
recentlyClosedParks: () => ['homepage', 'recently-closed-parks'] as const,
|
||||
recentlyClosedRides: () => ['homepage', 'recently-closed-rides'] as const,
|
||||
},
|
||||
|
||||
// Add more query keys as needed
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user