mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:51:12 -05:00
226 lines
6.4 KiB
TypeScript
226 lines
6.4 KiB
TypeScript
/**
|
|
* 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<any>;
|
|
|
|
/** Invalidate this query (triggers background refetch) */
|
|
invalidate: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
}
|