mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 22:51:14 -05:00
Implement cache management
This commit is contained in:
49
src/components/dev/CacheMonitor.tsx
Normal file
49
src/components/dev/CacheMonitor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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) ? (
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user