Files
thrilltrack-explorer/src-old/hooks/useModerationStats.ts

218 lines
6.9 KiB
TypeScript

import { useEffect, useState, useRef, useCallback } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { reportsService } from '@/services/reports';
// Type for submission realtime payload
interface SubmissionPayload {
status?: string;
assigned_to?: string | null;
locked_until?: string | null;
escalated?: boolean;
}
interface ModerationStats {
pendingSubmissions: number;
openReports: number;
flaggedContent: number;
}
interface UseModerationStatsOptions {
onStatsChange?: (stats: ModerationStats) => void;
enabled?: boolean;
pollingEnabled?: boolean;
pollingInterval?: number;
realtimeEnabled?: boolean;
}
export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
const {
onStatsChange,
enabled = true,
pollingEnabled = true,
pollingInterval = 60000, // Reduced to 60 seconds
realtimeEnabled = true
} = options;
const [stats, setStats] = useState<ModerationStats>({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
// Optimistic deltas for immediate UI updates
const [optimisticDeltas, setOptimisticDeltas] = useState<ModerationStats>({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
const [isLoading, setIsLoading] = useState(true);
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const onStatsChangeRef = useRef(onStatsChange);
const statsDebounceRef = useRef<NodeJS.Timeout | null>(null);
// Update ref when callback changes
useEffect(() => {
onStatsChangeRef.current = onStatsChange;
}, [onStatsChange]);
// Optimistic update function
const optimisticallyUpdateStats = useCallback((delta: Partial<ModerationStats>) => {
setOptimisticDeltas(prev => ({
pendingSubmissions: (prev.pendingSubmissions || 0) + (delta.pendingSubmissions || 0),
openReports: (prev.openReports || 0) + (delta.openReports || 0),
flaggedContent: (prev.flaggedContent || 0) + (delta.flaggedContent || 0),
}));
}, []);
const fetchStats = useCallback(async (silent = false) => {
if (!enabled) return;
try {
// Only show loading on initial load
if (!silent) {
setIsLoading(true);
}
// Fetch stats - use Django API for reports, Supabase for submissions and reviews
const [submissionsResult, reportsStatsResult, reviewsResult] = await Promise.all([
supabase
.from('content_submissions')
.select('id', { count: 'exact', head: true })
.eq('status', 'pending'),
reportsService.getStatistics(),
supabase
.from('reviews')
.select('id', { count: 'exact', head: true })
.eq('moderation_status', 'flagged'),
]);
const newStats = {
pendingSubmissions: submissionsResult.count || 0,
openReports: reportsStatsResult.success && reportsStatsResult.data
? reportsStatsResult.data.pending_reports
: 0,
flaggedContent: reviewsResult.count || 0,
};
setStats(newStats);
setLastUpdated(new Date());
onStatsChangeRef.current?.(newStats);
// Clear optimistic deltas when real data arrives
setOptimisticDeltas({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
} catch (error: unknown) {
// Silent failure - stats refresh periodically in background
// Error already captured for potential monitoring
} finally {
// Only clear loading if it was set
if (!silent) {
setIsLoading(false);
}
if (isInitialLoad) {
setIsInitialLoad(false);
}
}
}, [enabled, isInitialLoad]);
// Initial fetch
useEffect(() => {
if (enabled) {
fetchStats(false); // Show loading
}
}, [enabled, fetchStats]);
// Debounced stats fetch to prevent rapid-fire updates
const debouncedFetchStats = useCallback(() => {
if (statsDebounceRef.current) {
clearTimeout(statsDebounceRef.current);
}
statsDebounceRef.current = setTimeout(() => {
fetchStats(true); // Silent refresh
}, 2000); // 2 second debounce to reduce flashing
}, [fetchStats]);
// Realtime subscription - only for content_submissions and reviews
// Reports use polling since Django API doesn't support realtime
useEffect(() => {
if (!enabled || !realtimeEnabled) return;
const channel = supabase
.channel('moderation-stats-realtime')
// Listen to ALL events on content_submissions without filter
// Manual filtering catches submissions leaving pending state
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'content_submissions'
}, (payload) => {
const oldData = payload.old as SubmissionPayload;
const newData = payload.new as SubmissionPayload;
const oldStatus = oldData?.status;
const newStatus = newData?.status;
const oldAssignedTo = oldData?.assigned_to;
const newAssignedTo = newData?.assigned_to;
const oldLockedUntil = oldData?.locked_until;
const newLockedUntil = newData?.locked_until;
// Only refresh if change affects pending count or assignments
if (
payload.eventType === 'INSERT' && newStatus === 'pending' ||
payload.eventType === 'UPDATE' && (oldStatus === 'pending' || newStatus === 'pending') ||
payload.eventType === 'DELETE' && oldStatus === 'pending' ||
payload.eventType === 'UPDATE' && (oldAssignedTo !== newAssignedTo || oldLockedUntil !== newLockedUntil)
) {
debouncedFetchStats();
}
})
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'reviews',
filter: 'moderation_status=eq.flagged'
}, debouncedFetchStats)
.subscribe();
return () => {
supabase.removeChannel(channel);
if (statsDebounceRef.current) {
clearTimeout(statsDebounceRef.current);
}
};
}, [enabled, realtimeEnabled, debouncedFetchStats]);
// Polling (fallback when realtime is disabled OR always for reports since Django has no realtime)
useEffect(() => {
if (!enabled || !pollingEnabled || isInitialLoad) return;
const interval = setInterval(() => {
fetchStats(true); // Silent refresh
}, pollingInterval);
return () => {
clearInterval(interval);
};
}, [enabled, pollingEnabled, pollingInterval, fetchStats, isInitialLoad]);
// Combine real stats with optimistic deltas for display
const displayStats = {
pendingSubmissions: Math.max(0, stats.pendingSubmissions + optimisticDeltas.pendingSubmissions),
openReports: Math.max(0, stats.openReports + optimisticDeltas.openReports),
flaggedContent: Math.max(0, stats.flaggedContent + optimisticDeltas.flaggedContent),
};
return {
stats: displayStats,
refresh: fetchStats,
optimisticallyUpdateStats,
isLoading,
lastUpdated
};
};