mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08: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 { 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)]">
|
||||||
@@ -413,265 +127,341 @@ export function ContentTabs() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="trending-parks" className="mt-8">
|
<TabsContent value="trending-parks" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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>
|
<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 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>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold mb-2 text-foreground">No Recent Changes</h3>
|
{trendingParks.isLoading ? (
|
||||||
<p className="text-muted-foreground text-center max-w-md text-sm">
|
<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">
|
||||||
There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies!
|
{[...Array(12)].map((_, i) => (
|
||||||
</p>
|
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div key={entity.id} className="relative">
|
<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">
|
||||||
<RideCard ride={entity} />
|
{trendingParks.data?.map((park) => (
|
||||||
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
<ParkCard key={park.id} park={park} />
|
||||||
{new Date(entity.opening_date).getFullYear()}
|
))}
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
))}
|
</TabsContent>
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="highest-rated-parks" className="mt-8">
|
<TabsContent value="trending-rides" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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">Top-rated theme parks based on visitor reviews</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>
|
||||||
{highestRatedParks.length > 0 ? (
|
{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 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">
|
||||||
{highestRatedParks.map((park) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
<ParkCard key={park.id} park={park} />
|
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<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">
|
||||||
No rated parks available yet. Be the first to leave a review!
|
{trendingRides.data?.map((ride) => (
|
||||||
</div>
|
<RideCard key={ride.id} ride={ride} />
|
||||||
)}
|
))}
|
||||||
</TabsContent>
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="highest-rated-rides" className="mt-8">
|
<TabsContent value="recent-parks" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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">Top-rated attractions based on rider reviews</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>
|
||||||
{highestRatedRides.length > 0 ? (
|
{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 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">
|
||||||
{highestRatedRides.map((ride) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
<RideCard key={ride.id} ride={ride} />
|
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<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">
|
||||||
No rated rides available yet. Be the first to leave a review!
|
{recentParks.data?.map((park) => (
|
||||||
</div>
|
<ParkCard key={park.id} park={park} />
|
||||||
)}
|
))}
|
||||||
</TabsContent>
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="opening-soon" className="mt-8">
|
<TabsContent value="recent-rides" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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">Parks and rides opening in the next 6 months</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>
|
||||||
{openingSoon.length > 0 ? (
|
{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 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">
|
||||||
{openingSoon.map((entity: any) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
entity.entityType === 'park' ? (
|
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
<div key={entity.id} className="relative">
|
))}
|
||||||
<ParkCard park={entity} />
|
</div>
|
||||||
<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' })}
|
<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">
|
||||||
</Badge>
|
{recentRides.data?.map((ride) => (
|
||||||
</div>
|
<RideCard key={ride.id} ride={ride} />
|
||||||
) : (
|
))}
|
||||||
<div key={entity.id} className="relative">
|
</div>
|
||||||
<RideCard ride={entity} />
|
)}
|
||||||
<Badge className="absolute top-2 right-2 bg-blue-500/90 text-white backdrop-blur-sm">
|
</TabsContent>
|
||||||
{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">
|
<TabsContent value="recent-changes" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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">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">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>
|
||||||
{closingSoon.length > 0 ? (
|
{recentChanges.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 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-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||||
{closingSoon.map((entity: any) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
entity.entityType === 'park' ? (
|
<div key={i} className="h-64 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
<div key={entity.id} className="relative">
|
))}
|
||||||
<ParkCard park={entity} />
|
</div>
|
||||||
<Badge className="absolute top-2 right-2 bg-red-500/90 text-white backdrop-blur-sm">
|
) : recentChanges.data && recentChanges.data.length > 0 ? (
|
||||||
Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
<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">
|
||||||
</Badge>
|
{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>
|
||||||
) : (
|
<h3 className="text-xl font-semibold mb-2 text-foreground">No Recent Changes</h3>
|
||||||
<div key={entity.id} className="relative">
|
<p className="text-muted-foreground text-center max-w-md text-sm">
|
||||||
<RideCard ride={entity} />
|
There are no recent entity changes to display yet. Check back soon for the latest updates to parks, rides, and companies!
|
||||||
<Badge className="absolute top-2 right-2 bg-red-500/90 text-white backdrop-blur-sm">
|
</p>
|
||||||
Closes {new Date(entity.closing_date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
<div className="mt-6 w-32 h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent rounded-full" />
|
||||||
</Badge>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)
|
</TabsContent>
|
||||||
))}
|
|
||||||
</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">
|
<TabsContent value="recently-opened" className="mt-8">
|
||||||
<div className="text-center mb-6">
|
<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>
|
<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 closed 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>
|
||||||
{recentlyClosed.length > 0 ? (
|
{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 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">
|
||||||
{recentlyClosed.map((entity: any) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
entity.entityType === 'park' ? (
|
<div key={i} className="h-72 bg-card rounded-lg border border-border animate-pulse" />
|
||||||
<div key={entity.id} className="relative">
|
))}
|
||||||
<ParkCard park={entity} />
|
</div>
|
||||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
) : (
|
||||||
Closed {new Date(entity.closing_date).getFullYear()}
|
<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">
|
||||||
</Badge>
|
{recentlyOpened.map((entity: any) => (
|
||||||
</div>
|
entity.entityType === 'park' ? (
|
||||||
) : (
|
<div key={entity.id} className="relative">
|
||||||
<div key={entity.id} className="relative">
|
<ParkCard park={entity} />
|
||||||
<RideCard ride={entity} />
|
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||||
<Badge className="absolute top-2 right-2 bg-gray-500/90 text-white backdrop-blur-sm">
|
{new Date(entity.opening_date).getFullYear()}
|
||||||
Closed {new Date(entity.closing_date).getFullYear()}
|
</Badge>
|
||||||
</Badge>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)
|
<div key={entity.id} className="relative">
|
||||||
))}
|
<RideCard ride={entity} />
|
||||||
</div>
|
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
|
||||||
) : (
|
{new Date(entity.opening_date).getFullYear()}
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
</Badge>
|
||||||
No parks or rides closed in the last year.
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
))}
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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: () => {
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user