Implement cache management

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 00:46:42 +00:00
parent e2b064fa0b
commit 875d189881
16 changed files with 553 additions and 51 deletions

View File

@@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
/**
* CacheMonitor Component (Dev Only)
*
* Real-time cache performance monitoring for development.
* Displays total queries, stale queries, fetching queries, and cache size.
* Only renders in development mode.
*/
export function CacheMonitor() {
const queryClient = useQueryClient();
const [stats, setStats] = useState({
totalQueries: 0,
staleQueries: 0,
fetchingQueries: 0,
cacheSize: 0,
});
useEffect(() => {
const interval = setInterval(() => {
const cache = queryClient.getQueryCache();
const queries = cache.getAll();
setStats({
totalQueries: queries.length,
staleQueries: queries.filter(q => q.isStale()).length,
fetchingQueries: queries.filter(q => q.state.fetchStatus === 'fetching').length,
cacheSize: JSON.stringify(queries).length,
});
}, 1000);
return () => clearInterval(interval);
}, [queryClient]);
if (!import.meta.env.DEV) return null;
return (
<div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded-lg text-xs font-mono z-50 shadow-xl">
<h3 className="font-bold mb-2 text-primary">Cache Monitor</h3>
<div className="space-y-1">
<div>Total Queries: <span className="text-green-400">{stats.totalQueries}</span></div>
<div>Stale: <span className="text-yellow-400">{stats.staleQueries}</span></div>
<div>Fetching: <span className="text-blue-400">{stats.fetchingQueries}</span></div>
<div>Size: <span className="text-purple-400">{(stats.cacheSize / 1024).toFixed(1)} KB</span></div>
</div>
</div>
);
}

View File

@@ -1,19 +1,55 @@
import { MapPin, Star, Users, Clock, Castle, FerrisWheel, Waves, Tent } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Park } from '@/types/database';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
import { queryKeys } from '@/lib/queryKeys';
import { supabase } from '@/integrations/supabase/client';
interface ParkCardProps {
park: Park;
}
export function ParkCard({ park }: ParkCardProps) {
const navigate = useNavigate();
const queryClient = useQueryClient();
const handleClick = () => {
navigate(`/parks/${park.slug}`);
};
// Prefetch park detail data on hover
const handleMouseEnter = () => {
// Prefetch park detail page data
queryClient.prefetchQuery({
queryKey: queryKeys.parks.detail(park.slug),
queryFn: async () => {
const { data } = await supabase
.from('parks')
.select('*')
.eq('slug', park.slug)
.single();
return data;
},
staleTime: 5 * 60 * 1000,
});
// Prefetch park photos (first 10)
queryClient.prefetchQuery({
queryKey: queryKeys.photos.entity('park', park.id),
queryFn: async () => {
const { data } = await supabase
.from('photos')
.select('*')
.eq('entity_type', 'park')
.eq('entity_id', park.id)
.limit(10);
return data;
},
staleTime: 5 * 60 * 1000,
});
};
const getStatusColor = (status: string) => {
switch (status) {
case 'operating':
@@ -56,7 +92,7 @@ export function ParkCard({ park }: ParkCardProps) {
const formatParkType = (type: string) => {
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
};
return <Card className="group overflow-hidden border-border/50 bg-gradient-to-br from-card via-card to-card/80 hover:shadow-2xl hover:shadow-primary/20 hover:border-primary/30 transition-all duration-300 cursor-pointer hover:scale-[1.02] relative before:absolute before:inset-0 before:rounded-lg before:p-[1px] before:bg-gradient-to-br before:from-primary/20 before:via-transparent before:to-accent/20 before:-z-10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300" onClick={handleClick}>
return <Card className="group overflow-hidden border-border/50 bg-gradient-to-br from-card via-card to-card/80 hover:shadow-2xl hover:shadow-primary/20 hover:border-primary/30 transition-all duration-300 cursor-pointer hover:scale-[1.02] relative before:absolute before:inset-0 before:rounded-lg before:p-[1px] before:bg-gradient-to-br before:from-primary/20 before:via-transparent before:to-accent/20 before:-z-10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300" onClick={handleClick} onMouseEnter={handleMouseEnter}>
<div className="relative overflow-hidden">
{/* Image Placeholder with Gradient */}
<div className="aspect-[3/2] bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 flex items-center justify-center relative">

View File

@@ -1,10 +1,13 @@
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Star, MapPin, Clock, Zap, FerrisWheel, Waves, Theater, Train, ArrowUp, CheckCircle, Calendar, Hammer, XCircle } from 'lucide-react';
import { MeasurementDisplay } from '@/components/ui/measurement-display';
import { Ride } from '@/types/database';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
import { queryKeys } from '@/lib/queryKeys';
import { supabase } from '@/integrations/supabase/client';
interface RideCardProps {
ride: Ride;
@@ -15,11 +18,47 @@ interface RideCardProps {
export function RideCard({ ride, showParkName = true, className, parkSlug }: RideCardProps) {
const navigate = useNavigate();
const queryClient = useQueryClient();
const handleRideClick = () => {
const slug = parkSlug || ride.park?.slug;
navigate(`/parks/${slug}/rides/${ride.slug}`);
};
// Prefetch ride detail data on hover
const handleMouseEnter = () => {
const slug = parkSlug || ride.park?.slug;
if (!slug) return;
// Prefetch ride detail page data
queryClient.prefetchQuery({
queryKey: queryKeys.rides.detail(slug, ride.slug),
queryFn: async () => {
const { data } = await supabase
.from('rides')
.select('*')
.eq('slug', ride.slug)
.single();
return data;
},
staleTime: 5 * 60 * 1000,
});
// Prefetch ride photos (first 10)
queryClient.prefetchQuery({
queryKey: queryKeys.photos.entity('ride', ride.id),
queryFn: async () => {
const { data } = await supabase
.from('photos')
.select('*')
.eq('entity_type', 'ride')
.eq('entity_id', ride.id)
.limit(10);
return data;
},
staleTime: 5 * 60 * 1000,
});
};
const getRideIcon = (category: string) => {
switch (category) {
@@ -61,6 +100,7 @@ export function RideCard({ ride, showParkName = true, className, parkSlug }: Rid
<Card
className={`group overflow-hidden border-border/50 bg-gradient-to-br from-card via-card to-card/80 hover:shadow-2xl hover:shadow-primary/20 hover:border-primary/30 transition-all duration-300 cursor-pointer hover:scale-[1.02] relative before:absolute before:inset-0 before:rounded-lg before:p-[1px] before:bg-gradient-to-br before:from-primary/20 before:via-transparent before:to-accent/20 before:-z-10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300 ${className}`}
onClick={handleRideClick}
onMouseEnter={handleMouseEnter}
>
<div className="relative overflow-hidden">
{/* Image/Icon Section */}

View File

@@ -3,7 +3,10 @@ import { Badge } from '@/components/ui/badge';
import { FerrisWheel } from 'lucide-react';
import { RideModel } from '@/types/database';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
import { queryKeys } from '@/lib/queryKeys';
import { supabase } from '@/integrations/supabase/client';
interface RideModelCardProps {
model: RideModel;
@@ -12,6 +15,23 @@ interface RideModelCardProps {
export function RideModelCard({ model, manufacturerSlug }: RideModelCardProps) {
const navigate = useNavigate();
const queryClient = useQueryClient();
// Prefetch ride model detail data on hover
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.rideModels.detail(manufacturerSlug, model.slug),
queryFn: async () => {
const { data } = await supabase
.from('ride_models')
.select('*')
.eq('slug', model.slug)
.single();
return data;
},
staleTime: 5 * 60 * 1000,
});
};
const formatCategory = (category: string | null | undefined) => {
if (!category) return 'Unknown';
@@ -42,6 +62,7 @@ export function RideModelCard({ model, manufacturerSlug }: RideModelCardProps) {
<Card
className="group overflow-hidden border-border/50 bg-gradient-to-br from-card via-card to-card/80 hover:shadow-2xl hover:shadow-primary/20 hover:border-primary/30 transition-all duration-300 cursor-pointer hover:scale-[1.02] relative before:absolute before:inset-0 before:rounded-lg before:p-[1px] before:bg-gradient-to-br before:from-primary/20 before:via-transparent before:to-accent/20 before:-z-10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300"
onClick={() => navigate(`/manufacturers/${manufacturerSlug}/models/${model.slug}`)}
onMouseEnter={handleMouseEnter}
>
<div className="aspect-[3/2] bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 relative overflow-hidden">
{(cardImageUrl || cardImageId) ? (

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useEntityName } from '@/hooks/entities/useEntityName';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -60,6 +61,9 @@ export function PhotoManagementDialog({
const [photoToDelete, setPhotoToDelete] = useState<Photo | null>(null);
const [deleteReason, setDeleteReason] = useState('');
const { toast } = useToast();
// Fetch entity name once using cached hook (replaces 4 sequential direct queries)
const { data: entityName = 'Unknown' } = useEntityName(entityType, entityId);
useEffect(() => {
if (open) {
@@ -106,27 +110,6 @@ export function PhotoManagementDialog({
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
// Fetch entity name from database based on entity type
let entityName = 'Unknown';
try {
if (entityType === 'park') {
const { data } = await supabase.from('parks').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (entityType === 'ride') {
const { data } = await supabase.from('rides').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (entityType === 'ride_model') {
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(entityType)) {
const { data } = await supabase.from('companies').select('name').eq('id', entityId).single();
if (data?.name) entityName = data.name;
}
} catch (err) {
logger.error('Failed to fetch entity name', { error: getErrorMessage(err), entityType, entityId });
}
// Create content submission
const { data: submission, error: submissionError } = await supabase
.from('content_submissions')