mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 16:11:12 -05:00
Implement cache management
This commit is contained in:
45
src/hooks/blog/useBlogPost.ts
Normal file
45
src/hooks/blog/useBlogPost.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
82
src/hooks/entities/useEntityName.ts
Normal file
82
src/hooks/entities/useEntityName.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
47
src/hooks/pagination/usePrefetchNextPage.ts
Normal file
47
src/hooks/pagination/usePrefetchNextPage.ts
Normal 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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user