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}`)} />
+ )}
+
+
+
) : (