mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 04:11:14 -05:00
feat: Extract caching hooks
This commit is contained in:
10
src/hooks/moderation/index.ts
Normal file
10
src/hooks/moderation/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Moderation Hooks
|
||||||
|
*
|
||||||
|
* Centralized exports for all moderation-related hooks.
|
||||||
|
* These hooks are designed to support the moderation queue system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useEntityCache } from './useEntityCache';
|
||||||
|
export { useProfileCache } from './useProfileCache';
|
||||||
|
export type { CachedProfile } from './useProfileCache';
|
||||||
237
src/hooks/moderation/useEntityCache.ts
Normal file
237
src/hooks/moderation/useEntityCache.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity types supported by the cache
|
||||||
|
*/
|
||||||
|
type EntityType = 'rides' | 'parks' | 'companies';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache structure for entities
|
||||||
|
*/
|
||||||
|
interface EntityCacheStructure {
|
||||||
|
rides: Map<string, any>;
|
||||||
|
parks: Map<string, any>;
|
||||||
|
companies: Map<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing entity name caching (rides, parks, companies)
|
||||||
|
*
|
||||||
|
* Uses ref-based storage to avoid triggering re-renders while providing
|
||||||
|
* efficient caching for entity lookups during moderation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const entityCache = useEntityCache();
|
||||||
|
*
|
||||||
|
* // Get cached entity
|
||||||
|
* const ride = entityCache.getCached('rides', rideId);
|
||||||
|
*
|
||||||
|
* // Bulk fetch and cache entities
|
||||||
|
* await entityCache.bulkFetch('rides', [id1, id2, id3]);
|
||||||
|
*
|
||||||
|
* // Clear specific cache
|
||||||
|
* entityCache.clear('rides');
|
||||||
|
*
|
||||||
|
* // Clear all caches
|
||||||
|
* entityCache.clearAll();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useEntityCache() {
|
||||||
|
// Use ref to prevent re-renders on cache updates
|
||||||
|
const cacheRef = useRef<EntityCacheStructure>({
|
||||||
|
rides: new Map(),
|
||||||
|
parks: new Map(),
|
||||||
|
companies: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached entity by ID
|
||||||
|
*/
|
||||||
|
const getCached = useCallback((type: EntityType, id: string): any | undefined => {
|
||||||
|
return cacheRef.current[type].get(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an entity is cached
|
||||||
|
*/
|
||||||
|
const has = useCallback((type: EntityType, id: string): boolean => {
|
||||||
|
return cacheRef.current[type].has(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a cached entity
|
||||||
|
*/
|
||||||
|
const setCached = useCallback((type: EntityType, id: string, data: any): void => {
|
||||||
|
cacheRef.current[type].set(id, data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get uncached IDs from a list
|
||||||
|
*/
|
||||||
|
const getUncachedIds = useCallback((type: EntityType, ids: string[]): string[] => {
|
||||||
|
return ids.filter(id => !cacheRef.current[type].has(id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk fetch entities from the database and cache them
|
||||||
|
* Only fetches entities that aren't already cached
|
||||||
|
*/
|
||||||
|
const bulkFetch = useCallback(async (
|
||||||
|
type: EntityType,
|
||||||
|
ids: string[]
|
||||||
|
): Promise<any[]> => {
|
||||||
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
|
// Filter to only uncached IDs
|
||||||
|
const uncachedIds = getUncachedIds(type, ids);
|
||||||
|
if (uncachedIds.length === 0) {
|
||||||
|
// All entities are cached, return them
|
||||||
|
return ids.map(id => getCached(type, id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine table name and select fields based on entity type
|
||||||
|
let tableName: string;
|
||||||
|
let selectFields: string;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'rides':
|
||||||
|
tableName = 'rides';
|
||||||
|
selectFields = 'id, name, park_id';
|
||||||
|
break;
|
||||||
|
case 'parks':
|
||||||
|
tableName = 'parks';
|
||||||
|
selectFields = 'id, name';
|
||||||
|
break;
|
||||||
|
case 'companies':
|
||||||
|
tableName = 'companies';
|
||||||
|
selectFields = 'id, name';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown entity type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from(tableName as any)
|
||||||
|
.select(selectFields)
|
||||||
|
.in('id', uncachedIds);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(`Error fetching ${type}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the fetched entities
|
||||||
|
if (data) {
|
||||||
|
data.forEach((entity: any) => {
|
||||||
|
setCached(type, entity.id, entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to bulk fetch ${type}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [getCached, setCached, getUncachedIds]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and cache related entities based on submission content
|
||||||
|
* Automatically determines which entities to fetch from submission data
|
||||||
|
*/
|
||||||
|
const fetchRelatedEntities = useCallback(async (submissions: any[]): Promise<void> => {
|
||||||
|
const rideIds = new Set<string>();
|
||||||
|
const parkIds = new Set<string>();
|
||||||
|
const companyIds = new Set<string>();
|
||||||
|
|
||||||
|
// Collect all entity IDs from submissions
|
||||||
|
submissions.forEach(submission => {
|
||||||
|
const content = submission.content as any;
|
||||||
|
if (content && typeof content === 'object') {
|
||||||
|
if (content.ride_id) rideIds.add(content.ride_id);
|
||||||
|
if (content.park_id) parkIds.add(content.park_id);
|
||||||
|
if (content.company_id) companyIds.add(content.company_id);
|
||||||
|
if (content.entity_id) {
|
||||||
|
if (submission.submission_type === 'ride') rideIds.add(content.entity_id);
|
||||||
|
if (submission.submission_type === 'park') parkIds.add(content.entity_id);
|
||||||
|
if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type)) {
|
||||||
|
companyIds.add(content.entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (content.manufacturer_id) companyIds.add(content.manufacturer_id);
|
||||||
|
if (content.designer_id) companyIds.add(content.designer_id);
|
||||||
|
if (content.operator_id) companyIds.add(content.operator_id);
|
||||||
|
if (content.property_owner_id) companyIds.add(content.property_owner_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all entities in parallel
|
||||||
|
const fetchPromises: Promise<any[]>[] = [];
|
||||||
|
|
||||||
|
if (rideIds.size > 0) {
|
||||||
|
fetchPromises.push(bulkFetch('rides', Array.from(rideIds)));
|
||||||
|
}
|
||||||
|
if (parkIds.size > 0) {
|
||||||
|
fetchPromises.push(bulkFetch('parks', Array.from(parkIds)));
|
||||||
|
}
|
||||||
|
if (companyIds.size > 0) {
|
||||||
|
fetchPromises.push(bulkFetch('companies', Array.from(companyIds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(fetchPromises);
|
||||||
|
}, [bulkFetch]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a specific entity type cache
|
||||||
|
*/
|
||||||
|
const clear = useCallback((type: EntityType): void => {
|
||||||
|
cacheRef.current[type].clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all entity caches
|
||||||
|
*/
|
||||||
|
const clearAll = useCallback((): void => {
|
||||||
|
cacheRef.current.rides.clear();
|
||||||
|
cacheRef.current.parks.clear();
|
||||||
|
cacheRef.current.companies.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache size for a specific type
|
||||||
|
*/
|
||||||
|
const getSize = useCallback((type: EntityType): number => {
|
||||||
|
return cacheRef.current[type].size;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total cache size across all entity types
|
||||||
|
*/
|
||||||
|
const getTotalSize = useCallback((): number => {
|
||||||
|
return cacheRef.current.rides.size +
|
||||||
|
cacheRef.current.parks.size +
|
||||||
|
cacheRef.current.companies.size;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get direct access to cache ref (for advanced use cases)
|
||||||
|
* Use with caution - prefer using the provided methods
|
||||||
|
*/
|
||||||
|
const getCacheRef = useCallback(() => cacheRef.current, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCached,
|
||||||
|
has,
|
||||||
|
setCached,
|
||||||
|
getUncachedIds,
|
||||||
|
bulkFetch,
|
||||||
|
fetchRelatedEntities,
|
||||||
|
clear,
|
||||||
|
clearAll,
|
||||||
|
getSize,
|
||||||
|
getTotalSize,
|
||||||
|
getCacheRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
199
src/hooks/moderation/useProfileCache.ts
Normal file
199
src/hooks/moderation/useProfileCache.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile data structure returned from the database
|
||||||
|
*/
|
||||||
|
export interface CachedProfile {
|
||||||
|
user_id: string;
|
||||||
|
username: string;
|
||||||
|
display_name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing user profile caching
|
||||||
|
*
|
||||||
|
* Uses ref-based storage to avoid triggering re-renders while providing
|
||||||
|
* efficient caching for user profile lookups during moderation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const profileCache = useProfileCache();
|
||||||
|
*
|
||||||
|
* // Get cached profile
|
||||||
|
* const profile = profileCache.getCached(userId);
|
||||||
|
*
|
||||||
|
* // Bulk fetch and cache profiles
|
||||||
|
* const profiles = await profileCache.bulkFetch([id1, id2, id3]);
|
||||||
|
*
|
||||||
|
* // Check if profile exists in cache
|
||||||
|
* if (profileCache.has(userId)) {
|
||||||
|
* const profile = profileCache.getCached(userId);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Clear cache
|
||||||
|
* profileCache.clear();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useProfileCache() {
|
||||||
|
// Use ref to prevent re-renders on cache updates
|
||||||
|
const cacheRef = useRef<Map<string, CachedProfile>>(new Map());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached profile by user ID
|
||||||
|
*/
|
||||||
|
const getCached = useCallback((userId: string): CachedProfile | undefined => {
|
||||||
|
return cacheRef.current.get(userId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a profile is cached
|
||||||
|
*/
|
||||||
|
const has = useCallback((userId: string): boolean => {
|
||||||
|
return cacheRef.current.has(userId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a cached profile
|
||||||
|
*/
|
||||||
|
const setCached = useCallback((userId: string, profile: CachedProfile): void => {
|
||||||
|
cacheRef.current.set(userId, profile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get uncached user IDs from a list
|
||||||
|
*/
|
||||||
|
const getUncachedIds = useCallback((userIds: string[]): string[] => {
|
||||||
|
return userIds.filter(id => !cacheRef.current.has(id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk fetch user profiles from the database and cache them
|
||||||
|
* Only fetches profiles that aren't already cached
|
||||||
|
*
|
||||||
|
* @param userIds - Array of user IDs to fetch
|
||||||
|
* @returns Array of fetched profiles
|
||||||
|
*/
|
||||||
|
const bulkFetch = useCallback(async (userIds: string[]): Promise<CachedProfile[]> => {
|
||||||
|
if (userIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Filter to only uncached IDs
|
||||||
|
const uncachedIds = getUncachedIds(userIds);
|
||||||
|
if (uncachedIds.length === 0) {
|
||||||
|
// All profiles are cached, return them
|
||||||
|
return userIds.map(id => getCached(id)).filter((p): p is CachedProfile => !!p);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('user_id, username, display_name, avatar_url')
|
||||||
|
.in('user_id', uncachedIds);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching profiles:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the fetched profiles
|
||||||
|
if (data) {
|
||||||
|
data.forEach((profile: CachedProfile) => {
|
||||||
|
setCached(profile.user_id, profile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to bulk fetch profiles:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [getCached, setCached, getUncachedIds]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and return profiles for a list of user IDs
|
||||||
|
* Returns a Map for easy lookup
|
||||||
|
*
|
||||||
|
* @param userIds - Array of user IDs to fetch
|
||||||
|
* @returns Map of userId -> profile
|
||||||
|
*/
|
||||||
|
const fetchAsMap = useCallback(async (userIds: string[]): Promise<Map<string, CachedProfile>> => {
|
||||||
|
const profiles = await bulkFetch(userIds);
|
||||||
|
return new Map(profiles.map(p => [p.user_id, p]));
|
||||||
|
}, [bulkFetch]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch profiles for submitters and reviewers from submissions
|
||||||
|
* Automatically extracts user IDs and reviewer IDs from submission data
|
||||||
|
*
|
||||||
|
* @param submissions - Array of submissions with user_id and reviewer_id
|
||||||
|
* @returns Map of userId -> profile for all users involved
|
||||||
|
*/
|
||||||
|
const fetchForSubmissions = useCallback(async (submissions: any[]): Promise<Map<string, CachedProfile>> => {
|
||||||
|
const userIds = submissions.map(s => s.user_id).filter(Boolean);
|
||||||
|
const reviewerIds = submissions.map(s => s.reviewer_id).filter((id): id is string => !!id);
|
||||||
|
const allUserIds = [...new Set([...userIds, ...reviewerIds])];
|
||||||
|
|
||||||
|
return await fetchAsMap(allUserIds);
|
||||||
|
}, [fetchAsMap]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a display name for a user (display_name or username)
|
||||||
|
* Returns 'Unknown User' if not found in cache
|
||||||
|
*/
|
||||||
|
const getDisplayName = useCallback((userId: string): string => {
|
||||||
|
const profile = getCached(userId);
|
||||||
|
if (!profile) return 'Unknown User';
|
||||||
|
return profile.display_name || profile.username || 'Unknown User';
|
||||||
|
}, [getCached]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate (remove) a specific profile from cache
|
||||||
|
*/
|
||||||
|
const invalidate = useCallback((userId: string): void => {
|
||||||
|
cacheRef.current.delete(userId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cached profiles
|
||||||
|
*/
|
||||||
|
const clear = useCallback((): void => {
|
||||||
|
cacheRef.current.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache size
|
||||||
|
*/
|
||||||
|
const getSize = useCallback((): number => {
|
||||||
|
return cacheRef.current.size;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cached profile user IDs
|
||||||
|
*/
|
||||||
|
const getAllCachedIds = useCallback((): string[] => {
|
||||||
|
return Array.from(cacheRef.current.keys());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get direct access to cache ref (for advanced use cases)
|
||||||
|
* Use with caution - prefer using the provided methods
|
||||||
|
*/
|
||||||
|
const getCacheRef = useCallback(() => cacheRef.current, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCached,
|
||||||
|
has,
|
||||||
|
setCached,
|
||||||
|
getUncachedIds,
|
||||||
|
bulkFetch,
|
||||||
|
fetchAsMap,
|
||||||
|
fetchForSubmissions,
|
||||||
|
getDisplayName,
|
||||||
|
invalidate,
|
||||||
|
clear,
|
||||||
|
getSize,
|
||||||
|
getAllCachedIds,
|
||||||
|
getCacheRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user