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,45 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
/**
* useBlogPost Hook
*
* Fetches a published blog post by slug with author profile information.
* Extracted from BlogPost.tsx for reusability and better caching.
*
* Features:
* - Caches blog posts for 5 minutes
* - Includes author profile data (username, display_name, avatar)
* - Only returns published posts
*
* @param slug - URL slug of the blog post
* @returns TanStack Query result with blog post data
*
* @example
* ```tsx
* const { data: post, isLoading } = useBlogPost(slug);
* ```
*/
export function useBlogPost(slug: string | undefined) {
return useQuery({
queryKey: queryKeys.blog.post(slug || ''),
queryFn: async () => {
if (!slug) return null;
const { data, error } = await supabase
.from('blog_posts')
.select('*, profiles!inner(username, display_name, avatar_url, avatar_image_id)')
.eq('slug', slug)
.eq('status', 'published')
.single();
if (error) throw error;
return data;
},
enabled: !!slug,
staleTime: 5 * 60 * 1000, // 5 minutes (blog content)
gcTime: 15 * 60 * 1000, // 15 minutes
refetchOnWindowFocus: false,
});
}

View File

@@ -0,0 +1,82 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
/**
* useEntityName Hook
*
* Fetches the name of an entity for display purposes.
* Replaces multiple sequential direct queries with a single cached hook.
*
* Features:
* - Caches entity names for 10 minutes (rarely change)
* - Supports parks, rides, ride_models, and all company types
* - Returns 'Unknown' for invalid types or missing data
*
* @param entityType - Type of entity ('park', 'ride', 'ride_model', 'manufacturer', etc.)
* @param entityId - UUID of the entity
* @returns TanStack Query result with entity name string
*
* @example
* ```tsx
* const { data: entityName = 'Unknown' } = useEntityName('park', parkId);
* ```
*/
export function useEntityName(entityType: string, entityId: string) {
return useQuery({
queryKey: queryKeys.entities.name(entityType, entityId),
queryFn: async () => {
// Type-safe approach: separate queries for each table type
try {
if (entityType === 'park') {
const { data, error } = await supabase
.from('parks')
.select('name')
.eq('id', entityId)
.single();
if (error) throw error;
return data?.name || 'Unknown';
}
if (entityType === 'ride') {
const { data, error } = await supabase
.from('rides')
.select('name')
.eq('id', entityId)
.single();
if (error) throw error;
return data?.name || 'Unknown';
}
if (entityType === 'ride_model') {
const { data, error } = await supabase
.from('ride_models')
.select('name')
.eq('id', entityId)
.single();
if (error) throw error;
return data?.name || 'Unknown';
}
if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(entityType)) {
const { data, error } = await supabase
.from('companies')
.select('name')
.eq('id', entityId)
.single();
if (error) throw error;
return data?.name || 'Unknown';
}
return 'Unknown';
} catch (error) {
console.error('Failed to fetch entity name:', error);
return 'Unknown';
}
},
enabled: !!entityType && !!entityId,
staleTime: 10 * 60 * 1000, // 10 minutes (entity names rarely change)
gcTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: false,
});
}

View File

@@ -0,0 +1,47 @@
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
/**
* usePrefetchNextPage Hook
*
* Prefetches the next page of paginated data for instant navigation.
*
* Features:
* - Automatically prefetches when user is on a page with more data
* - Short staleTime (2 minutes) prevents over-caching
* - Generic implementation works with any paginated data
*
* @param baseQueryKey - Base query key array for the paginated query
* @param currentPage - Current page number
* @param hasNextPage - Boolean indicating if there is a next page
* @param queryFn - Function to fetch the next page
*
* @example
* ```tsx
* usePrefetchNextPage(
* queryKeys.parks.all(),
* currentPage,
* hasNextPage,
* (page) => fetchParks(page)
* );
* ```
*/
export function usePrefetchNextPage<T>(
baseQueryKey: readonly unknown[],
currentPage: number,
hasNextPage: boolean,
queryFn: (page: number) => Promise<T>
) {
const queryClient = useQueryClient();
useEffect(() => {
if (!hasNextPage) return;
// Prefetch next page
queryClient.prefetchQuery({
queryKey: [...baseQueryKey, currentPage + 1],
queryFn: () => queryFn(currentPage + 1),
staleTime: 2 * 60 * 1000, // 2 minutes
});
}, [currentPage, hasNextPage, baseQueryKey, queryFn, queryClient]);
}

View File

@@ -26,7 +26,8 @@
* ```
*/
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
@@ -42,9 +43,12 @@ interface EntityPhoto {
export function useEntityPhotos(
entityType: string,
entityId: string,
sortBy: 'newest' | 'oldest' = 'newest'
sortBy: 'newest' | 'oldest' = 'newest',
enableRealtime = false // New parameter for opt-in real-time updates
): UseQueryResult<EntityPhoto[]> {
return useQuery({
const queryClient = useQueryClient();
const query = useQuery({
queryKey: queryKeys.photos.entity(entityType, entityId, sortBy),
queryFn: async () => {
const startTime = performance.now();
@@ -82,4 +86,34 @@ export function useEntityPhotos(
gcTime: 15 * 60 * 1000,
refetchOnWindowFocus: false,
});
// Real-time subscription for photo uploads (opt-in)
useEffect(() => {
if (!enableRealtime || !entityType || !entityId) return;
const channel = supabase
.channel(`photos-${entityType}-${entityId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'photos',
filter: `entity_type=eq.${entityType},entity_id=eq.${entityId}`,
},
(payload) => {
console.log('📸 New photo uploaded:', payload.new);
queryClient.invalidateQueries({
queryKey: queryKeys.photos.entity(entityType, entityId)
});
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [enableRealtime, entityType, entityId, queryClient, sortBy]);
return query;
}

View File

@@ -1,12 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
/**
* Hook to fetch reviews for a specific entity (park or ride)
*/
export function useEntityReviews(entityType: 'park' | 'ride', entityId: string | undefined, enabled = true) {
return useQuery({
export function useEntityReviews(
entityType: 'park' | 'ride',
entityId: string | undefined,
enabled = true,
enableRealtime = false // New parameter for opt-in real-time updates
) {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: queryKeys.reviews.entity(entityType, entityId || ''),
queryFn: async () => {
if (!entityId) return [];
@@ -35,4 +43,34 @@ export function useEntityReviews(entityType: 'park' | 'ride', entityId: string |
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
});
// Real-time subscription for new reviews (opt-in)
useEffect(() => {
if (!enableRealtime || !entityId || !enabled) return;
const channel = supabase
.channel(`reviews-${entityType}-${entityId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'reviews',
filter: `${entityType}_id=eq.${entityId},moderation_status=eq.approved`,
},
(payload) => {
console.log('⭐ New review posted:', payload.new);
queryClient.invalidateQueries({
queryKey: queryKeys.reviews.entity(entityType, entityId)
});
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [enableRealtime, entityType, entityId, enabled, queryClient]);
return query;
}

View File

@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
export interface CoasterStat {
id: string;
@@ -15,7 +16,7 @@ export interface CoasterStat {
export function useCoasterStats(rideId: string | undefined) {
return useQuery({
queryKey: ['coaster-stats', rideId],
queryKey: queryKeys.stats.coaster(rideId || ''),
queryFn: async () => {
if (!rideId) return [];

View File

@@ -1,12 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
/**
* Hook to fetch public Novu settings accessible to all authenticated users
*/
export function usePublicNovuSettings() {
const { data: settings, isLoading, error } = useQuery({
queryKey: ['public-novu-settings'],
queryKey: queryKeys.settings.publicNovu(),
queryFn: async () => {
const { data, error } = await supabase
.from('admin_settings')