Refactor homepage content fetching

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 22:26:02 +00:00
parent d7ef581220
commit 662c2fbed4
11 changed files with 911 additions and 580 deletions

View File

@@ -1,349 +1,63 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ParkCard } from '@/components/parks/ParkCard'; import { ParkCard } from '@/components/parks/ParkCard';
import { RideCard } from '@/components/rides/RideCard'; import { RideCard } from '@/components/rides/RideCard';
import { RecentChangeCard } from './RecentChangeCard'; import { RecentChangeCard } from './RecentChangeCard';
import { Badge } from '@/components/ui/badge'; 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'; import { Clock } from 'lucide-react';
import { useHomepageTrendingParks, useHomepageTrendingRides } from '@/hooks/homepage/useHomepageTrending';
interface RecentChange { import { useHomepageRecentParks, useHomepageRecentRides } from '@/hooks/homepage/useHomepageRecent';
entityType: 'park' | 'ride' | 'company'; import { useHomepageRecentChanges } from '@/hooks/homepage/useHomepageRecentChanges';
entityId: string; import { useHomepageRecentlyOpenedParks, useHomepageRecentlyOpenedRides } from '@/hooks/homepage/useHomepageOpened';
entityName: string; import { useHomepageHighestRatedParks, useHomepageHighestRatedRides } from '@/hooks/homepage/useHomepageRated';
entitySlug: string; import { useHomepageOpeningSoonParks, useHomepageOpeningSoonRides } from '@/hooks/homepage/useHomepageOpeningSoon';
parkSlug?: string; import { useHomepageClosingSoonParks, useHomepageClosingSoonRides } from '@/hooks/homepage/useHomepageClosing';
imageUrl: string | null; import { useHomepageRecentlyClosedParks, useHomepageRecentlyClosedRides } from '@/hooks/homepage/useHomepageClosed';
changeType: string;
changedAt: string;
changedByUsername?: string | null;
changedByAvatar?: string | null;
changeReason: string | null;
}
export function ContentTabs() { export function ContentTabs() {
const [trendingParks, setTrendingParks] = useState<Park[]>([]); const [activeTab, setActiveTab] = useState('trending-parks');
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);
useEffect(() => { // Lazy load data - only fetch when tab is active
fetchContent(); 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 () => { // Combine parks and rides for mixed tabs
try { const recentlyOpened = [
// Trending Parks (by 30-day view count) ...(recentlyOpenedParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
const { data: trending } = await supabase ...(recentlyOpenedRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
.from('parks') ].sort((a, b) => new Date(b.opening_date || 0).getTime() - new Date(a.opening_date || 0).getTime()).slice(0, 24);
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
.order('view_count_30d', { ascending: false })
.limit(12);
// Recently Added Parks const openingSoon = [
const { data: recent } = await supabase ...(openingSoonParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
.from('parks') ...(openingSoonRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`) ].sort((a, b) => new Date(a.opening_date || 0).getTime() - new Date(b.opening_date || 0).getTime()).slice(0, 24);
.order('created_at', { ascending: false })
.limit(12);
// Trending Rides (by 30-day view count) const closingSoon = [
const { data: trendingRidesData } = await supabase ...(closingSoonParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
.from('rides') ...(closingSoonRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
.select(`*, park:parks!inner(name, slug, location:locations(*))`) ].sort((a, b) => new Date(a.closing_date || 0).getTime() - new Date(b.closing_date || 0).getTime()).slice(0, 24);
.order('view_count_30d', { ascending: false })
.limit(12);
// Recently Added Rides const recentlyClosed = [
const { data: recentRidesData } = await supabase ...(recentlyClosedParks.data || []).map(p => ({ ...p, entityType: 'park' as const })),
.from('rides') ...(recentlyClosedRides.data || []).map(r => ({ ...r, entityType: 'ride' as const }))
.select(`*, park:parks!inner(name, slug, location:locations(*))`) ].sort((a, b) => new Date(b.closing_date || 0).getTime() - new Date(a.closing_date || 0).getTime()).slice(0, 24);
.order('created_at', { ascending: false })
.limit(12);
// Fetch recent park versions const isLoadingInitial = activeTab === 'trending-parks' && trendingParks.isLoading;
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);
// Fetch recent ride versions with park slug for proper routing if (isLoadingInitial) {
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) {
return ( return (
<section className="py-12"> <section className="py-12">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
@@ -374,7 +88,7 @@ export function ContentTabs() {
return ( return (
<section className="py-8"> <section className="py-8">
<div className="container mx-auto px-4"> <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"> <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"> <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)]"> <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)]">
@@ -419,11 +133,19 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed parks in the last 30 days</p> <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 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>
{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 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"> <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) => ( {trendingParks.data?.map((park) => (
<ParkCard key={park.id} park={park} /> <ParkCard key={park.id} park={park} />
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
<TabsContent value="trending-rides" className="mt-8"> <TabsContent value="trending-rides" className="mt-8">
@@ -432,11 +154,19 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Most viewed rides in the last 30 days</p> <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 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>
{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"> <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) => ( {trendingRides.data?.map((ride) => (
<RideCard key={ride.id} ride={ride} /> <RideCard key={ride.id} ride={ride} />
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
<TabsContent value="recent-parks" className="mt-8"> <TabsContent value="recent-parks" className="mt-8">
@@ -445,11 +175,19 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest parks added to our database</p> <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 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>
{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"> <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) => ( {recentParks.data?.map((park) => (
<ParkCard key={park.id} park={park} /> <ParkCard key={park.id} park={park} />
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
<TabsContent value="recent-rides" className="mt-8"> <TabsContent value="recent-rides" className="mt-8">
@@ -458,11 +196,19 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest attractions added to our database</p> <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 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>
{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"> <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) => ( {recentRides.data?.map((ride) => (
<RideCard key={ride.id} ride={ride} /> <RideCard key={ride.id} ride={ride} />
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
<TabsContent value="recent-changes" className="mt-8"> <TabsContent value="recent-changes" className="mt-8">
@@ -471,21 +217,27 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Latest updates across all entities</p> <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 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>
{recentChanges.length > 0 ? ( {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"> <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) => ( {[...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 <RecentChangeCard
key={`${change.entityType}-${change.entityId}-${change.changedAt}`} key={`${change.type}-${change.id}-${change.changedAt}`}
entityType={change.entityType} entityType={change.type}
entityId={change.entityId} entityId={change.id}
entityName={change.entityName} entityName={change.name}
entitySlug={change.entitySlug} entitySlug={change.slug}
parkSlug={change.parkSlug} parkSlug={change.parkSlug}
imageUrl={change.imageUrl} imageUrl={change.imageUrl}
changeType={change.changeType} changeType={change.changeType}
changedAt={change.changedAt} changedAt={change.changedAt}
changedByUsername={change.changedByUsername} changedByUsername={change.changedBy?.username}
changedByAvatar={change.changedByAvatar} changedByAvatar={change.changedBy?.avatarUrl}
changeReason={change.changeReason} changeReason={change.changeReason}
/> />
))} ))}
@@ -513,6 +265,13 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that opened in the last year</p> <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 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>
{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"> <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) => ( {recentlyOpened.map((entity: any) => (
entity.entityType === 'park' ? ( entity.entityType === 'park' ? (
@@ -532,6 +291,7 @@ export function ContentTabs() {
) )
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
<TabsContent value="highest-rated-parks" className="mt-8"> <TabsContent value="highest-rated-parks" className="mt-8">
@@ -540,9 +300,15 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Top-rated theme parks based on visitor reviews</p> <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 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>
{highestRatedParks.length > 0 ? ( {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"> <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) => ( {highestRatedParks.data.map((park) => (
<ParkCard key={park.id} park={park} /> <ParkCard key={park.id} park={park} />
))} ))}
</div> </div>
@@ -559,9 +325,15 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Top-rated attractions based on rider reviews</p> <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 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>
{highestRatedRides.length > 0 ? ( {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"> <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) => ( {highestRatedRides.data.map((ride) => (
<RideCard key={ride.id} ride={ride} /> <RideCard key={ride.id} ride={ride} />
))} ))}
</div> </div>
@@ -578,7 +350,13 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides opening in the next 6 months</p> <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 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>
{openingSoon.length > 0 ? ( {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"> <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) => ( {openingSoon.map((entity: any) => (
entity.entityType === 'park' ? ( entity.entityType === 'park' ? (
@@ -611,7 +389,13 @@ export function ContentTabs() {
<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> <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 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>
{closingSoon.length > 0 ? ( {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"> <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) => ( {closingSoon.map((entity: any) => (
entity.entityType === 'park' ? ( entity.entityType === 'park' ? (
@@ -644,7 +428,13 @@ export function ContentTabs() {
<p className="text-muted-foreground text-sm md:text-base animate-fade-in">Parks and rides that closed in the last year</p> <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 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>
{recentlyClosed.length > 0 ? ( {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"> <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) => ( {recentlyClosed.map((entity: any) => (
entity.entityType === 'park' ? ( entity.entityType === 'park' ? (

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View File

@@ -68,5 +68,31 @@ export function useQueryInvalidation() {
invalidateModerationStats: () => { invalidateModerationStats: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.moderationStats() }); 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');
}
});
}
},
}; };
} }

View File

@@ -14,5 +14,24 @@ export const queryKeys = {
moderationQueue: (config: Record<string, any>) => ['moderation-queue', config] as const, moderationQueue: (config: Record<string, any>) => ['moderation-queue', config] as const,
moderationStats: () => ['moderation-stats'] 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 // Add more query keys as needed
} as const; } as const;