diff --git a/src/components/homepage/ContentTabs.tsx b/src/components/homepage/ContentTabs.tsx index 150dae87..badab0b4 100644 --- a/src/components/homepage/ContentTabs.tsx +++ b/src/components/homepage/ContentTabs.tsx @@ -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([]); - const [trendingRides, setTrendingRides] = useState([]); - const [recentParks, setRecentParks] = useState([]); - const [recentRides, setRecentRides] = useState([]); - const [recentChanges, setRecentChanges] = useState([]); - const [recentlyOpened, setRecentlyOpened] = useState>([]); - const [highestRatedParks, setHighestRatedParks] = useState([]); - const [highestRatedRides, setHighestRatedRides] = useState([]); - const [openingSoon, setOpeningSoon] = useState>([]); - const [closingSoon, setClosingSoon] = useState>([]); - const [recentlyClosed, setRecentlyClosed] = useState>([]); - 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(); - 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 (
@@ -374,7 +88,7 @@ export function ContentTabs() { return (
- +
@@ -413,265 +127,341 @@ export function ContentTabs() {
- -
-

Trending Parks

-

Most viewed parks in the last 30 days

-
-
-
- {trendingParks.map((park) => ( - - ))} -
-
- - -
-

Trending Rides

-

Most viewed rides in the last 30 days

-
-
-
- {trendingRides.map((ride) => ( - - ))} -
-
- - -
-

Recently Added Parks

-

Latest parks added to our database

-
-
-
- {recentParks.map((park) => ( - - ))} -
-
- - -
-

Recently Added Rides

-

Latest attractions added to our database

-
-
-
- {recentRides.map((ride) => ( - - ))} -
-
- - -
-

Recent Changes

-

Latest updates across all entities

-
-
- {recentChanges.length > 0 ? ( -
- {recentChanges.map((change) => ( - - ))} -
- ) : ( -
-
-
-
- -
+ +
+

Trending Parks

+

Most viewed parks in the last 30 days

+
-

No Recent Changes

-

- There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies! -

-
-
- )} -
- - -
-

Recently Opened

-

Parks and rides that opened in the last year

-
-
-
- {recentlyOpened.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- - - {new Date(entity.opening_date).getFullYear()} - + {trendingParks.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))}
) : ( -
- - - {new Date(entity.opening_date).getFullYear()} - +
+ {trendingParks.data?.map((park) => ( + + ))}
- ) - ))} -
- + )} + - -
-

Highest Rated Parks

-

Top-rated theme parks based on visitor reviews

-
-
- {highestRatedParks.length > 0 ? ( -
- {highestRatedParks.map((park) => ( - - ))} -
- ) : ( -
- No rated parks available yet. Be the first to leave a review! -
- )} -
+ +
+

Trending Rides

+

Most viewed rides in the last 30 days

+
+
+ {trendingRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {trendingRides.data?.map((ride) => ( + + ))} +
+ )} + - -
-

Highest Rated Rides

-

Top-rated attractions based on rider reviews

-
-
- {highestRatedRides.length > 0 ? ( -
- {highestRatedRides.map((ride) => ( - - ))} -
- ) : ( -
- No rated rides available yet. Be the first to leave a review! -
- )} -
+ +
+

Recently Added Parks

+

Latest parks added to our database

+
+
+ {recentParks.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {recentParks.data?.map((park) => ( + + ))} +
+ )} + - -
-

Opening Soon

-

Parks and rides opening in the next 6 months

-
-
- {openingSoon.length > 0 ? ( -
- {openingSoon.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- - - {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - -
- ) : ( -
- - - {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - -
- ) - ))} -
- ) : ( -
- No parks or rides scheduled to open in the next 6 months. -
- )} -
+ +
+

Recently Added Rides

+

Latest attractions added to our database

+
+
+ {recentRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {recentRides.data?.map((ride) => ( + + ))} +
+ )} + - -
-

Closing Soon

-

Last chance: Parks and rides closing in the next 6 months

-
-
- {closingSoon.length > 0 ? ( -
- {closingSoon.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- - - Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - + +
+

Recent Changes

+

Latest updates across all entities

+
+
+ {recentChanges.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : recentChanges.data && recentChanges.data.length > 0 ? ( +
+ {recentChanges.data.map((change) => ( + + ))} +
+ ) : ( +
+
+
+
+ +
- ) : ( -
- - - Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - -
- ) - ))} -
- ) : ( -
- No parks or rides scheduled to close in the next 6 months. -
- )} - +

No Recent Changes

+

+ There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies! +

+
+
+ )} + - -
-

Recently Closed

-

Parks and rides that closed in the last year

-
-
- {recentlyClosed.length > 0 ? ( -
- {recentlyClosed.map((entity: any) => ( - entity.entityType === 'park' ? ( -
- - - Closed {new Date(entity.closing_date).getFullYear()} - -
- ) : ( -
- - - Closed {new Date(entity.closing_date).getFullYear()} - -
- ) - ))} -
- ) : ( -
- No parks or rides closed in the last year. -
- )} -
+ +
+

Recently Opened

+

Parks and rides that opened in the last year

+
+
+ {recentlyOpenedParks.isLoading || recentlyOpenedRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {recentlyOpened.map((entity: any) => ( + entity.entityType === 'park' ? ( +
+ + + {new Date(entity.opening_date).getFullYear()} + +
+ ) : ( +
+ + + {new Date(entity.opening_date).getFullYear()} + +
+ ) + ))} +
+ )} + + + +
+

Highest Rated Parks

+

Top-rated theme parks based on visitor reviews

+
+
+ {highestRatedParks.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : highestRatedParks.data && highestRatedParks.data.length > 0 ? ( +
+ {highestRatedParks.data.map((park) => ( + + ))} +
+ ) : ( +
+ No rated parks available yet. Be the first to leave a review! +
+ )} + + + +
+

Highest Rated Rides

+

Top-rated attractions based on rider reviews

+
+
+ {highestRatedRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : highestRatedRides.data && highestRatedRides.data.length > 0 ? ( +
+ {highestRatedRides.data.map((ride) => ( + + ))} +
+ ) : ( +
+ No rated rides available yet. Be the first to leave a review! +
+ )} + + + +
+

Opening Soon

+

Parks and rides opening in the next 6 months

+
+
+ {openingSoonParks.isLoading || openingSoonRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : openingSoon.length > 0 ? ( +
+ {openingSoon.map((entity: any) => ( + entity.entityType === 'park' ? ( +
+ + + {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + +
+ ) : ( +
+ + + {new Date(entity.opening_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + +
+ ) + ))} +
+ ) : ( +
+ No parks or rides scheduled to open in the next 6 months. +
+ )} + + + +
+

Closing Soon

+

Last chance: Parks and rides closing in the next 6 months

+
+
+ {closingSoonParks.isLoading || closingSoonRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : closingSoon.length > 0 ? ( +
+ {closingSoon.map((entity: any) => ( + entity.entityType === 'park' ? ( +
+ + + Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + +
+ ) : ( +
+ + + Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + +
+ ) + ))} +
+ ) : ( +
+ No parks or rides scheduled to close in the next 6 months. +
+ )} + + + +
+

Recently Closed

+

Parks and rides that closed in the last year

+
+
+ {recentlyClosedParks.isLoading || recentlyClosedRides.isLoading ? ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ ) : recentlyClosed.length > 0 ? ( +
+ {recentlyClosed.map((entity: any) => ( + entity.entityType === 'park' ? ( +
+ + + Closed {new Date(entity.closing_date).getFullYear()} + +
+ ) : ( +
+ + + Closed {new Date(entity.closing_date).getFullYear()} + +
+ ) + ))} +
+ ) : ( +
+ No parks or rides closed in the last year. +
+ )} +
); -} \ No newline at end of file +} diff --git a/src/hooks/homepage/useHomepageClosed.ts b/src/hooks/homepage/useHomepageClosed.ts new file mode 100644 index 00000000..49beb807 --- /dev/null +++ b/src/hooks/homepage/useHomepageClosed.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageClosing.ts b/src/hooks/homepage/useHomepageClosing.ts new file mode 100644 index 00000000..0b0ebb91 --- /dev/null +++ b/src/hooks/homepage/useHomepageClosing.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageOpened.ts b/src/hooks/homepage/useHomepageOpened.ts new file mode 100644 index 00000000..bf8f5a61 --- /dev/null +++ b/src/hooks/homepage/useHomepageOpened.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageOpeningSoon.ts b/src/hooks/homepage/useHomepageOpeningSoon.ts new file mode 100644 index 00000000..34a38262 --- /dev/null +++ b/src/hooks/homepage/useHomepageOpeningSoon.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageRated.ts b/src/hooks/homepage/useHomepageRated.ts new file mode 100644 index 00000000..67eb5001 --- /dev/null +++ b/src/hooks/homepage/useHomepageRated.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageRecent.ts b/src/hooks/homepage/useHomepageRecent.ts new file mode 100644 index 00000000..8ae789b0 --- /dev/null +++ b/src/hooks/homepage/useHomepageRecent.ts @@ -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, + }); +} diff --git a/src/hooks/homepage/useHomepageRecentChanges.ts b/src/hooks/homepage/useHomepageRecentChanges.ts new file mode 100644 index 00000000..499c7300 --- /dev/null +++ b/src/hooks/homepage/useHomepageRecentChanges.ts @@ -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( + (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, + }); +} diff --git a/src/hooks/homepage/useHomepageTrending.ts b/src/hooks/homepage/useHomepageTrending.ts new file mode 100644 index 00000000..bd9dabe8 --- /dev/null +++ b/src/hooks/homepage/useHomepageTrending.ts @@ -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, + }); +} diff --git a/src/lib/queryInvalidation.ts b/src/lib/queryInvalidation.ts index 7384e5db..f2655e86 100644 --- a/src/lib/queryInvalidation.ts +++ b/src/lib/queryInvalidation.ts @@ -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'); + } + }); + } + }, }; } diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts index dd9618bd..117c2959 100644 --- a/src/lib/queryKeys.ts +++ b/src/lib/queryKeys.ts @@ -14,5 +14,24 @@ export const queryKeys = { moderationQueue: (config: Record) => ['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;