mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Optimize recent changes query
This commit is contained in:
87
src/components/common/Pagination.tsx
Normal file
87
src/components/common/Pagination.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
src/hooks/parks/useParks.ts
Normal file
34
src/hooks/parks/useParks.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
34
src/hooks/rides/useRides.ts
Normal file
34
src/hooks/rides/useRides.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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'] });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
{viewMode === 'grid' ? (
|
||||||
<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">
|
<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">
|
||||||
{filteredAndSortedRides.map((ride) => (
|
{paginatedRides.map((ride) => (
|
||||||
<RideCard key={ride.id} ride={ride} showParkName={true} />
|
<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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user