Optimize recent changes query

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 22:40:23 +00:00
parent 8623291c62
commit 46ca1c29bc
6 changed files with 253 additions and 60 deletions

View File

@@ -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 (
<div className="flex items-center justify-center gap-2 mt-8">
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1 || isLoading}
>
<ChevronLeft className="w-4 h-4" />
Previous
</Button>
<div className="flex items-center gap-1">
{getPageNumbers().map((page, idx) => (
typeof page === 'number' ? (
<Button
key={idx}
variant={currentPage === page ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(page)}
disabled={isLoading}
className="min-w-[40px]"
>
{page}
</Button>
) : (
<span key={idx} className="px-2 text-muted-foreground">
{page}
</span>
)
))}
</div>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages || isLoading}
>
Next
<ChevronRight className="w-4 h-4" />
</Button>
</div>
);
}

View File

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

View File

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

View File

@@ -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'] });
},
}; };
} }

View File

@@ -37,6 +37,8 @@ import { useAuth } from '@/hooks/useAuth';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { useAuthModal } from '@/hooks/useAuthModal'; import { useAuthModal } from '@/hooks/useAuthModal';
import { useOpenGraph } from '@/hooks/useOpenGraph'; import { useOpenGraph } from '@/hooks/useOpenGraph';
import { useParks } from '@/hooks/parks/useParks';
import { Pagination } from '@/components/common/Pagination';
export interface FilterState { export interface FilterState {
search: string; search: string;
@@ -91,13 +93,12 @@ const initialSort: SortState = {
}; };
export default function Parks() { export default function Parks() {
const [parks, setParks] = useState<Park[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<FilterState>(initialFilters); const [filters, setFilters] = useState<FilterState>(initialFilters);
const [sort, setSort] = useState<SortState>(initialSort); const [sort, setSort] = useState<SortState>(initialSort);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [isAddParkModalOpen, setIsAddParkModalOpen] = useState(false); const [isAddParkModalOpen, setIsAddParkModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
const saved = localStorage.getItem('parks-sidebar-collapsed'); const saved = localStorage.getItem('parks-sidebar-collapsed');
return saved ? JSON.parse(saved) : false; return saved ? JSON.parse(saved) : false;
@@ -108,40 +109,23 @@ export default function Parks() {
const { isModerator } = useUserRole(); const { isModerator } = useUserRole();
const { requireAuth } = useAuthModal(); const { requireAuth } = useAuthModal();
useEffect(() => { // Use TanStack Query hook for data fetching with caching
fetchParks(); const { data: parks = [], isLoading: loading, error } = useParks();
}, []);
useEffect(() => { useEffect(() => {
localStorage.setItem('parks-sidebar-collapsed', JSON.stringify(sidebarCollapsed)); localStorage.setItem('parks-sidebar-collapsed', JSON.stringify(sidebarCollapsed));
}, [sidebarCollapsed]); }, [sidebarCollapsed]);
const fetchParks = async () => { // Show error toast if query fails
try { useEffect(() => {
setLoading(true); if (error) {
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);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error loading parks", title: "Error loading parks",
description: error instanceof Error ? error.message : 'Failed to load parks', description: error instanceof Error ? error.message : 'Failed to load parks',
}); });
} finally {
setLoading(false);
} }
}; }, [error, toast]);
const filteredAndSortedParks = useMemo(() => { const filteredAndSortedParks = useMemo(() => {
let filtered = parks.filter(park => { let filtered = parks.filter(park => {
@@ -333,6 +317,21 @@ export default function Parks() {
navigate(`/parks/${park.slug}`); 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) => { const handleParkSubmit = async (parkData: any) => {
try { try {
const { submitParkCreation } = await import('@/lib/entitySubmissionHelpers'); const { submitParkCreation } = await import('@/lib/entitySubmissionHelpers');
@@ -548,14 +547,21 @@ export default function Parks() {
<div> <div>
{viewMode === 'grid' ? ( {viewMode === 'grid' ? (
<ParkGridView <ParkGridView
parks={filteredAndSortedParks} parks={paginatedParks}
/> />
) : ( ) : (
<ParkListView <ParkListView
parks={filteredAndSortedParks} parks={paginatedParks}
onParkClick={handleParkClick} onParkClick={handleParkClick}
/> />
)} )}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
isLoading={loading}
/>
</div> </div>
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">

View File

@@ -24,6 +24,8 @@ import { getErrorMessage } from '@/lib/errorHandler';
import { useAuthModal } from '@/hooks/useAuthModal'; import { useAuthModal } from '@/hooks/useAuthModal';
import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { useDocumentTitle } from '@/hooks/useDocumentTitle';
import { useOpenGraph } from '@/hooks/useOpenGraph'; import { useOpenGraph } from '@/hooks/useOpenGraph';
import { useRides } from '@/hooks/rides/useRides';
import { Pagination } from '@/components/common/Pagination';
export default function Rides() { export default function Rides() {
useDocumentTitle('Rides & Attractions'); useDocumentTitle('Rides & Attractions');
@@ -31,45 +33,35 @@ export default function Rides() {
const { user } = useAuth(); const { user } = useAuth();
const { isModerator } = useUserRole(); const { isModerator } = useUserRole();
const { requireAuth } = useAuthModal(); const { requireAuth } = useAuthModal();
const [rides, setRides] = useState<Ride[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('name'); const [sortBy, setSortBy] = useState('name');
const [filters, setFilters] = useState<RideFilterState>(defaultRideFilters); const [filters, setFilters] = useState<RideFilterState>(defaultRideFilters);
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
const saved = localStorage.getItem('rides-sidebar-collapsed'); const saved = localStorage.getItem('rides-sidebar-collapsed');
return saved ? JSON.parse(saved) : false; return saved ? JSON.parse(saved) : false;
}); });
useEffect(() => { // Use TanStack Query hook for data fetching with caching
fetchRides(); const { data: rides = [], isLoading: loading, error } = useRides();
}, []);
useEffect(() => { useEffect(() => {
localStorage.setItem('rides-sidebar-collapsed', JSON.stringify(sidebarCollapsed)); localStorage.setItem('rides-sidebar-collapsed', JSON.stringify(sidebarCollapsed));
}, [sidebarCollapsed]); }, [sidebarCollapsed]);
const fetchRides = async () => { // Show error toast if query fails
try { useEffect(() => {
const { data } = await supabase if (error) {
.from('rides') toast({
.select(` title: "Error loading rides",
*, description: error instanceof Error ? error.message : 'Failed to load rides',
park:parks!inner(name, slug, location:locations(*)), variant: "destructive"
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);
} }
}; }, [error]);
const handleCreateSubmit = async (data: any) => { const handleCreateSubmit = async (data: any) => {
try { try {
@@ -138,8 +130,8 @@ export default function Rides() {
// Parks filter // Parks filter
if (filters.parks.length > 0) { if (filters.parks.length > 0) {
const parkId = (ride.park as Park)?.id; // Use park_id from the ride object, not the nested park
if (!parkId || !filters.parks.includes(parkId)) { if (!ride.park_id || !filters.parks.includes(ride.park_id)) {
return false; return false;
} }
} }
@@ -284,6 +276,21 @@ export default function Rides() {
return filtered; return filtered;
}, [rides, searchQuery, sortBy, filters]); }, [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 = () => { const generateDescription = () => {
if (!filteredAndSortedRides.length) return 'Discover thrilling rides and roller coasters worldwide'; if (!filteredAndSortedRides.length) return 'Discover thrilling rides and roller coasters worldwide';
@@ -488,15 +495,24 @@ export default function Rides() {
{/* Results Area */} {/* Results Area */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{filteredAndSortedRides.length > 0 ? ( {filteredAndSortedRides.length > 0 ? (
viewMode === 'grid' ? ( <div>
<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 3xl:grid-cols-7 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5"> {viewMode === 'grid' ? (
{filteredAndSortedRides.map((ride) => ( <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 3xl:grid-cols-7 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
<RideCard key={ride.id} ride={ride} showParkName={true} /> {paginatedRides.map((ride) => (
))} <RideCard key={ride.id} ride={ride} showParkName={true} />
</div> ))}
) : ( </div>
<RideListView rides={filteredAndSortedRides} onRideClick={(ride) => navigate(`/parks/${ride.park?.slug}/rides/${ride.slug}`)} /> ) : (
) <RideListView rides={paginatedRides} onRideClick={(ride) => navigate(`/parks/${ride.park?.slug}/rides/${ride.slug}`)} />
)}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
isLoading={loading}
/>
</div>
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<FerrisWheel className="w-16 h-16 mb-4 mx-auto text-muted-foreground" /> <FerrisWheel className="w-16 h-16 mb-4 mx-auto text-muted-foreground" />