diff --git a/src/hooks/moderation/index.ts b/src/hooks/moderation/index.ts new file mode 100644 index 00000000..0af62212 --- /dev/null +++ b/src/hooks/moderation/index.ts @@ -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'; diff --git a/src/hooks/moderation/useEntityCache.ts b/src/hooks/moderation/useEntityCache.ts new file mode 100644 index 00000000..05637170 --- /dev/null +++ b/src/hooks/moderation/useEntityCache.ts @@ -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; + parks: Map; + companies: Map; +} + +/** + * 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({ + 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 => { + 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 => { + const rideIds = new Set(); + const parkIds = new Set(); + const companyIds = new Set(); + + // 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[] = []; + + 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, + }; +} diff --git a/src/hooks/moderation/useProfileCache.ts b/src/hooks/moderation/useProfileCache.ts new file mode 100644 index 00000000..1a1ca761 --- /dev/null +++ b/src/hooks/moderation/useProfileCache.ts @@ -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>(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 => { + 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> => { + 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> => { + 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, + }; +}