/** * TanStack Query hook for moderation queue data fetching * * Wraps the existing fetchSubmissions query builder with React Query * to provide automatic caching, deduplication, and background refetching. */ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries'; import { supabase } from '@/lib/supabaseClient'; import { logger } from '@/lib/logger'; import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import { validateModerationItems } from '@/lib/moderation/validation'; import type { ModerationItem, EntityFilter, StatusFilter, QueueTab, SortField, SortDirection } from '@/types/moderation'; /** * Get specific, actionable error message based on error type */ function getSpecificErrorMessage(error: unknown): string { // Offline detection if (!navigator.onLine) { return 'You appear to be offline. Check your internet connection and try again.'; } // Timeout if (error instanceof Error && error.name === 'AbortError') { return 'Request timed out. The server is taking too long to respond. Please try again.'; } // Check for Supabase-specific errors if (typeof error === 'object' && error !== null) { const err = error as any; // 500 errors if (err.status === 500 || err.code === '500') { return 'Server error occurred. Our team has been notified. Please try again in a few minutes.'; } // 429 Rate limiting if (err.status === 429 || err.message?.includes('rate limit')) { return 'Too many requests. Please wait a moment before trying again.'; } // Authentication errors if (err.status === 401 || err.message?.includes('JWT')) { return 'Your session has expired. Please refresh the page and sign in again.'; } // Permission errors if (err.status === 403 || err.message?.includes('permission')) { return 'You do not have permission to access the moderation queue.'; } } // Fallback return getErrorMessage(error) || 'Failed to load moderation queue. Please try again.'; } /** * Configuration for queue query */ export interface UseQueueQueryConfig { /** User making the query */ userId: string | undefined; /** Whether user is admin */ isAdmin: boolean; /** Whether user is superuser */ isSuperuser: boolean; /** Entity filter */ entityFilter: EntityFilter; /** Status filter */ statusFilter: StatusFilter; /** Active tab */ tab: QueueTab; /** Current page */ currentPage: number; /** Page size */ pageSize: number; /** Sort configuration */ sortConfig: { field: SortField; direction: SortDirection; }; /** Whether query is enabled (defaults to true) */ enabled?: boolean; } /** * Return type for useQueueQuery */ export interface UseQueueQueryReturn { /** Queue items */ items: ModerationItem[]; /** Total count of items matching filters */ totalCount: number; /** Initial loading state (no data yet) */ isLoading: boolean; /** Background refresh in progress (has data already) */ isRefreshing: boolean; /** Any error that occurred */ error: Error | null; /** Manually trigger a refetch */ refetch: () => Promise; /** Invalidate this query (triggers background refetch) */ invalidate: () => Promise; } /** * Hook to fetch moderation queue data using TanStack Query */ export function useQueueQuery(config: UseQueueQueryConfig): UseQueueQueryReturn { const queryClient = useQueryClient(); // Build query config for fetchSubmissions const queryConfig: QueryConfig = { userId: config.userId || '', isAdmin: config.isAdmin, isSuperuser: config.isSuperuser, entityFilter: config.entityFilter, statusFilter: config.statusFilter, tab: config.tab, currentPage: config.currentPage, pageSize: config.pageSize, sortConfig: config.sortConfig, }; // Create stable query key (TanStack Query uses this for caching/deduplication) // Include user context to ensure proper cache isolation per user/role const queryKey = [ 'moderation-queue', config.userId, config.isAdmin, config.isSuperuser, config.entityFilter, config.statusFilter, config.tab, config.currentPage, config.pageSize, config.sortConfig.field, config.sortConfig.direction, ]; // Execute query const query = useQuery({ queryKey, queryFn: async () => { logger.log('🔍 [TanStack Query] Fetching queue data:', queryKey); // Create timeout controller (30s timeout) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); try { const result = await fetchSubmissions(supabase, queryConfig); clearTimeout(timeoutId); if (result.error) { const specificMessage = getSpecificErrorMessage(result.error); // Error already captured in context throw new Error(specificMessage); } // Validate data shape before returning const validation = validateModerationItems(result.submissions); if (!validation.success) { // Invalid data shape throw new Error(validation.error || 'Invalid data format'); } logger.log('✅ [TanStack Query] Fetched', validation.data!.length, 'items'); return { ...result, submissions: validation.data! }; } catch (error) { clearTimeout(timeoutId); throw error; } }, enabled: config.enabled !== false && !!config.userId, staleTime: MODERATION_CONSTANTS.QUERY_STALE_TIME, gcTime: MODERATION_CONSTANTS.QUERY_GC_TIME, retry: MODERATION_CONSTANTS.QUERY_RETRY_COUNT, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), networkMode: 'offlineFirst', // Handle offline gracefully meta: { errorMessage: 'Failed to load moderation queue', }, }); // Invalidate helper const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: ['moderation-queue'] }); }; return { items: query.data?.submissions || [], totalCount: query.data?.totalCount || 0, isLoading: query.isLoading, isRefreshing: query.isFetching && !query.isLoading, error: query.error as Error | null, refetch: query.refetch, invalidate, }; }