From 46ca1c29bc9f1e0434ad7d588e3ab69a797700a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:40:23 +0000 Subject: [PATCH] Optimize recent changes query --- src/components/common/Pagination.tsx | 87 ++++++++++++++++++++++++++++ src/hooks/parks/useParks.ts | 34 +++++++++++ src/hooks/rides/useRides.ts | 34 +++++++++++ src/lib/queryInvalidation.ts | 16 +++++ src/pages/Parks.tsx | 60 ++++++++++--------- src/pages/Rides.tsx | 82 +++++++++++++++----------- 6 files changed, 253 insertions(+), 60 deletions(-) create mode 100644 src/components/common/Pagination.tsx create mode 100644 src/hooks/parks/useParks.ts create mode 100644 src/hooks/rides/useRides.ts diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx new file mode 100644 index 00000000..173b1377 --- /dev/null +++ b/src/components/common/Pagination.tsx @@ -0,0 +1,87 @@ +import { Button } from '@/components/ui/button'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + isLoading?: boolean; +} + +export function Pagination({ currentPage, totalPages, onPageChange, isLoading }: PaginationProps) { + const getPageNumbers = () => { + const pages: (number | string)[] = []; + const showPages = 5; + + if (totalPages <= showPages) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + if (currentPage <= 3) { + for (let i = 1; i <= 4; i++) pages.push(i); + pages.push('...'); + pages.push(totalPages); + } else if (currentPage >= totalPages - 2) { + pages.push(1); + pages.push('...'); + for (let i = totalPages - 3; i <= totalPages; i++) pages.push(i); + } else { + pages.push(1); + pages.push('...'); + for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i); + pages.push('...'); + pages.push(totalPages); + } + } + + return pages; + }; + + if (totalPages <= 1) return null; + + return ( +
+ + +
+ {getPageNumbers().map((page, idx) => ( + typeof page === 'number' ? ( + + ) : ( + + {page} + + ) + ))} +
+ + +
+ ); +} diff --git a/src/hooks/parks/useParks.ts b/src/hooks/parks/useParks.ts new file mode 100644 index 00000000..697f30ab --- /dev/null +++ b/src/hooks/parks/useParks.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; + +interface UseParksOptions { + enabled?: boolean; +} + +/** + * Hook to fetch all parks with caching + * Loads all parks for client-side filtering + */ +export function useParks({ enabled = true }: UseParksOptions = {}) { + return useQuery({ + queryKey: ['parks', 'all'], + queryFn: async () => { + const { data, error } = await supabase + .from('parks') + .select(` + *, + location:locations(*), + operator:companies!parks_operator_id_fkey(*), + property_owner:companies!parks_property_owner_id_fkey(*) + `) + .order('name'); + + if (error) throw error; + return data || []; + }, + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 15 * 60 * 1000, // 15 minutes + refetchOnWindowFocus: false, + }); +} diff --git a/src/hooks/rides/useRides.ts b/src/hooks/rides/useRides.ts new file mode 100644 index 00000000..709ccdd5 --- /dev/null +++ b/src/hooks/rides/useRides.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; + +interface UseRidesOptions { + enabled?: boolean; +} + +/** + * Hook to fetch all rides with caching + * Loads all rides for client-side filtering + */ +export function useRides({ enabled = true }: UseRidesOptions = {}) { + return useQuery({ + queryKey: ['rides', 'all'], + queryFn: async () => { + const { data, error } = await supabase + .from('rides') + .select(` + *, + park:parks!inner(name, slug, location:locations(*)), + manufacturer:companies!rides_manufacturer_id_fkey(*), + designer:companies!rides_designer_id_fkey(*) + `) + .order('name'); + + if (error) throw error; + return data || []; + }, + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 15 * 60 * 1000, // 15 minutes + refetchOnWindowFocus: false, + }); +} diff --git a/src/lib/queryInvalidation.ts b/src/lib/queryInvalidation.ts index f2655e86..e315e28a 100644 --- a/src/lib/queryInvalidation.ts +++ b/src/lib/queryInvalidation.ts @@ -94,5 +94,21 @@ export function useQueryInvalidation() { }); } }, + + /** + * Invalidate parks listing cache + * Call this after creating/updating/deleting parks + */ + invalidateParks: () => { + queryClient.invalidateQueries({ queryKey: ['parks'] }); + }, + + /** + * Invalidate rides listing cache + * Call this after creating/updating/deleting rides + */ + invalidateRides: () => { + queryClient.invalidateQueries({ queryKey: ['rides'] }); + }, }; } diff --git a/src/pages/Parks.tsx b/src/pages/Parks.tsx index 590f23e1..fda9b3fe 100644 --- a/src/pages/Parks.tsx +++ b/src/pages/Parks.tsx @@ -37,6 +37,8 @@ import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useOpenGraph } from '@/hooks/useOpenGraph'; +import { useParks } from '@/hooks/parks/useParks'; +import { Pagination } from '@/components/common/Pagination'; export interface FilterState { search: string; @@ -91,13 +93,12 @@ const initialSort: SortState = { }; export default function Parks() { - const [parks, setParks] = useState([]); - const [loading, setLoading] = useState(true); const [filters, setFilters] = useState(initialFilters); const [sort, setSort] = useState(initialSort); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [showFilters, setShowFilters] = useState(false); const [isAddParkModalOpen, setIsAddParkModalOpen] = useState(false); + const [currentPage, setCurrentPage] = useState(1); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const saved = localStorage.getItem('parks-sidebar-collapsed'); return saved ? JSON.parse(saved) : false; @@ -108,40 +109,23 @@ export default function Parks() { const { isModerator } = useUserRole(); const { requireAuth } = useAuthModal(); - useEffect(() => { - fetchParks(); - }, []); + // Use TanStack Query hook for data fetching with caching + const { data: parks = [], isLoading: loading, error } = useParks(); useEffect(() => { localStorage.setItem('parks-sidebar-collapsed', JSON.stringify(sidebarCollapsed)); }, [sidebarCollapsed]); - const fetchParks = async () => { - try { - setLoading(true); - const { data, error } = await supabase - .from('parks') - .select(` - *, - location:locations(*), - operator:companies!parks_operator_id_fkey(*), - property_owner:companies!parks_property_owner_id_fkey(*) - `) - .order('name'); - - if (error) throw error; - setParks(data || []); - } catch (error) { - console.error('Error fetching parks:', error); + // Show error toast if query fails + useEffect(() => { + if (error) { toast({ variant: "destructive", title: "Error loading parks", description: error instanceof Error ? error.message : 'Failed to load parks', }); - } finally { - setLoading(false); } - }; + }, [error, toast]); const filteredAndSortedParks = useMemo(() => { let filtered = parks.filter(park => { @@ -333,6 +317,21 @@ export default function Parks() { navigate(`/parks/${park.slug}`); }; + // Pagination for display + const ITEMS_PER_PAGE = 24; + const paginatedParks = useMemo(() => { + const start = (currentPage - 1) * ITEMS_PER_PAGE; + const end = start + ITEMS_PER_PAGE; + return filteredAndSortedParks.slice(start, end); + }, [filteredAndSortedParks, currentPage]); + + const totalPages = Math.ceil(filteredAndSortedParks.length / ITEMS_PER_PAGE); + + // Reset to page 1 when filters change + useEffect(() => { + setCurrentPage(1); + }, [filters, sort]); + const handleParkSubmit = async (parkData: any) => { try { const { submitParkCreation } = await import('@/lib/entitySubmissionHelpers'); @@ -548,14 +547,21 @@ export default function Parks() {
{viewMode === 'grid' ? ( ) : ( )} + +
) : (
diff --git a/src/pages/Rides.tsx b/src/pages/Rides.tsx index 27297fcd..f77c8928 100644 --- a/src/pages/Rides.tsx +++ b/src/pages/Rides.tsx @@ -24,6 +24,8 @@ import { getErrorMessage } from '@/lib/errorHandler'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { useOpenGraph } from '@/hooks/useOpenGraph'; +import { useRides } from '@/hooks/rides/useRides'; +import { Pagination } from '@/components/common/Pagination'; export default function Rides() { useDocumentTitle('Rides & Attractions'); @@ -31,45 +33,35 @@ export default function Rides() { const { user } = useAuth(); const { isModerator } = useUserRole(); const { requireAuth } = useAuthModal(); - const [rides, setRides] = useState([]); - const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('name'); const [filters, setFilters] = useState(defaultRideFilters); const [showFilters, setShowFilters] = useState(false); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [currentPage, setCurrentPage] = useState(1); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const saved = localStorage.getItem('rides-sidebar-collapsed'); return saved ? JSON.parse(saved) : false; }); - useEffect(() => { - fetchRides(); - }, []); + // Use TanStack Query hook for data fetching with caching + const { data: rides = [], isLoading: loading, error } = useRides(); useEffect(() => { localStorage.setItem('rides-sidebar-collapsed', JSON.stringify(sidebarCollapsed)); }, [sidebarCollapsed]); - const fetchRides = async () => { - try { - const { data } = await supabase - .from('rides') - .select(` - *, - park:parks!inner(name, slug, location:locations(*)), - manufacturer:companies!rides_manufacturer_id_fkey(*), - designer:companies!rides_designer_id_fkey(*) - `) - .order('name'); - setRides(data || []); - } catch (error) { - console.error('Error fetching rides:', error); - } finally { - setLoading(false); + // Show error toast if query fails + useEffect(() => { + if (error) { + toast({ + title: "Error loading rides", + description: error instanceof Error ? error.message : 'Failed to load rides', + variant: "destructive" + }); } - }; + }, [error]); const handleCreateSubmit = async (data: any) => { try { @@ -138,8 +130,8 @@ export default function Rides() { // Parks filter if (filters.parks.length > 0) { - const parkId = (ride.park as Park)?.id; - if (!parkId || !filters.parks.includes(parkId)) { + // Use park_id from the ride object, not the nested park + if (!ride.park_id || !filters.parks.includes(ride.park_id)) { return false; } } @@ -284,6 +276,21 @@ export default function Rides() { return filtered; }, [rides, searchQuery, sortBy, filters]); + // Pagination for display + const ITEMS_PER_PAGE = 24; + const paginatedRides = React.useMemo(() => { + const start = (currentPage - 1) * ITEMS_PER_PAGE; + const end = start + ITEMS_PER_PAGE; + return filteredAndSortedRides.slice(start, end); + }, [filteredAndSortedRides, currentPage]); + + const totalPages = Math.ceil(filteredAndSortedRides.length / ITEMS_PER_PAGE); + + // Reset to page 1 when filters change + useEffect(() => { + setCurrentPage(1); + }, [filters, sortBy, searchQuery]); + const generateDescription = () => { if (!filteredAndSortedRides.length) return 'Discover thrilling rides and roller coasters worldwide'; @@ -488,15 +495,24 @@ export default function Rides() { {/* Results Area */}
{filteredAndSortedRides.length > 0 ? ( - viewMode === 'grid' ? ( -
- {filteredAndSortedRides.map((ride) => ( - - ))} -
- ) : ( - navigate(`/parks/${ride.park?.slug}/rides/${ride.slug}`)} /> - ) +
+ {viewMode === 'grid' ? ( +
+ {paginatedRides.map((ride) => ( + + ))} +
+ ) : ( + navigate(`/parks/${ride.park?.slug}/rides/${ride.slug}`)} /> + )} + + +
) : (