diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 21410a52..8b74c0bd 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -14,12 +14,11 @@ import { useAuth } from '@/hooks/useAuth'; import { format } from 'date-fns'; import { PhotoModal } from './PhotoModal'; import { SubmissionReviewManager } from './SubmissionReviewManager'; -import { useRealtimeSubmissions } from '@/hooks/useRealtimeSubmissions'; import { useIsMobile } from '@/hooks/use-mobile'; import { SubmissionChangesDisplay } from './SubmissionChangesDisplay'; import { SubmissionItemsList } from './SubmissionItemsList'; -import { RealtimeConnectionStatus } from './RealtimeConnectionStatus'; import { MeasurementDisplay } from '@/components/ui/measurement-display'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; interface ModerationItem { id: string; @@ -70,6 +69,11 @@ export const ModerationQueue = forwardRef((props, ref) => { const { isAdmin, isSuperuser } = useUserRole(); const { user } = useAuth(); + // Get admin settings for polling configuration + const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings(); + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + // Expose refresh method via ref useImperativeHandle(ref, () => ({ refresh: () => { @@ -346,116 +350,26 @@ export const ModerationQueue = forwardRef((props, ref) => { } }; - // Set up realtime subscriptions - const { connectionState: submissionsConnectionState, reconnect: reconnectSubmissions } = useRealtimeSubmissions({ - onInsert: async (payload) => { - const newSubmission = payload.new; - - // Only add if it matches current filters - const matchesStatusFilter = - activeStatusFilter === 'all' || - (activeStatusFilter === 'pending' && (newSubmission.status === 'pending' || newSubmission.status === 'partially_approved')) || - activeStatusFilter === newSubmission.status; - - const matchesEntityFilter = - activeEntityFilter === 'all' || - (activeEntityFilter === 'submissions' && newSubmission.submission_type !== 'photo') || - (activeEntityFilter === 'photos' && newSubmission.submission_type === 'photo'); - - if (!matchesStatusFilter || !matchesEntityFilter) return; - - // Fetch minimal data for the new submission - try { - const { data: profile } = await supabase - .from('profiles') - .select('user_id, username, display_name, avatar_url') - .eq('user_id', newSubmission.user_id) - .single(); - - // Fetch entity name if photo submission - let entity_name, park_name; - if (newSubmission.submission_type === 'photo' && newSubmission.content) { - const contentObj = newSubmission.content as any; - const contextType = typeof contentObj.context === 'string' ? contentObj.context : null; - const entityId = contentObj.entity_id || contentObj.ride_id || contentObj.park_id || contentObj.company_id; - - if (contextType === 'ride' && entityId) { - const { data: rideData } = await supabase - .from('rides') - .select('name, parks:park_id(name)') - .eq('id', entityId) - .single(); - if (rideData) { - entity_name = rideData.name; - park_name = rideData.parks?.name; - } - } else if (contextType === 'park' && entityId) { - const { data: parkData } = await supabase - .from('parks') - .select('name') - .eq('id', entityId) - .single(); - if (parkData) entity_name = parkData.name; - } else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType) && entityId) { - const { data: companyData } = await supabase - .from('companies') - .select('name') - .eq('id', entityId) - .single(); - if (companyData) entity_name = companyData.name; - } - } - - // Create new item and prepend to list - const newItem: ModerationItem = { - id: newSubmission.id, - type: 'content_submission', - content: newSubmission.submission_type === 'photo' ? newSubmission.content : newSubmission, - created_at: newSubmission.created_at, - user_id: newSubmission.user_id, - status: newSubmission.status, - submission_type: newSubmission.submission_type, - user_profile: profile || undefined, - entity_name, - park_name, - }; - - setItems(prevItems => [newItem, ...prevItems]); - - toast({ - title: 'New Submission', - description: 'A new content submission has been added', - }); - } catch (error) { - console.error('Error adding new submission to queue:', error); - // Fallback to full refresh on error - fetchItems(activeEntityFilter, activeStatusFilter); - } - }, - onUpdate: (payload) => { - // Update items state directly for better UX - setItems(prevItems => - prevItems.map(item => - item.id === payload.new.id && item.type === 'content_submission' - ? { ...item, status: payload.new.status, content: { ...item.content, ...payload.new } } - : item - ) - ); - }, - onDelete: (payload) => { - setItems(prevItems => - prevItems.filter(item => !(item.id === payload.old.id && item.type === 'content_submission')) - ); - }, - enabled: !!user, - }); - + // Initial fetch on mount and filter changes useEffect(() => { if (user) { fetchItems(activeEntityFilter, activeStatusFilter); } }, [activeEntityFilter, activeStatusFilter, user]); + // Polling for auto-refresh + useEffect(() => { + if (!user || refreshMode !== 'auto') return; + + const interval = setInterval(() => { + fetchItems(activeEntityFilter, activeStatusFilter); + }, pollInterval); + + return () => { + clearInterval(interval); + }; + }, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter]); + const handleResetToPending = async (item: ModerationItem) => { setActionLoading(item.id); try { @@ -1765,10 +1679,6 @@ export const ModerationQueue = forwardRef((props, ref) => {

Moderation Queue

-
diff --git a/src/components/moderation/RealtimeConnectionStatus.tsx b/src/components/moderation/RealtimeConnectionStatus.tsx deleted file mode 100644 index 4152236b..00000000 --- a/src/components/moderation/RealtimeConnectionStatus.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { RefreshCw, Wifi, WifiOff, AlertCircle } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; - -type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'; - -interface RealtimeConnectionStatusProps { - connectionState: ConnectionState; - onReconnect: () => void; - className?: string; -} - -export function RealtimeConnectionStatus({ - connectionState, - onReconnect, - className = '', -}: RealtimeConnectionStatusProps) { - const getStatusConfig = () => { - switch (connectionState) { - case 'connected': - return { - icon: Wifi, - color: 'text-green-500', - label: 'Connected', - description: 'Live updates active', - showReconnect: false, - }; - case 'connecting': - return { - icon: RefreshCw, - color: 'text-yellow-500', - label: 'Connecting', - description: 'Establishing connection...', - showReconnect: false, - animate: 'animate-spin', - }; - case 'error': - return { - icon: AlertCircle, - color: 'text-red-500', - label: 'Error', - description: 'Connection failed. Retrying...', - showReconnect: true, - }; - case 'disconnected': - return { - icon: WifiOff, - color: 'text-muted-foreground', - label: 'Disconnected', - description: 'Live updates unavailable', - showReconnect: true, - }; - default: - return { - icon: WifiOff, - color: 'text-muted-foreground', - label: 'Unknown', - description: 'Connection status unknown', - showReconnect: true, - }; - } - }; - - const config = getStatusConfig(); - const Icon = config.icon; - - return ( -
- - - -
- - - {config.label} - -
-
- -

{config.description}

-
-
-
- - {config.showReconnect && ( - - )} -
- ); -} diff --git a/src/components/moderation/ReportsQueue.tsx b/src/components/moderation/ReportsQueue.tsx index a42ba1be..2dcb9e7d 100644 --- a/src/components/moderation/ReportsQueue.tsx +++ b/src/components/moderation/ReportsQueue.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { CheckCircle, XCircle, ExternalLink, Calendar, User, Flag } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -8,6 +8,8 @@ import { Label } from '@/components/ui/label'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { format } from 'date-fns'; +import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { useAuth } from '@/hooks/useAuth'; interface Report { id: string; @@ -38,11 +40,26 @@ const STATUS_COLORS = { dismissed: 'outline', } as const; -export function ReportsQueue() { +export interface ReportsQueueRef { + refresh: () => void; +} + +export const ReportsQueue = forwardRef((props, ref) => { const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const { toast } = useToast(); + const { user } = useAuth(); + + // Get admin settings for polling configuration + const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings(); + const refreshMode = getAdminPanelRefreshMode(); + const pollInterval = getAdminPanelPollInterval(); + + // Expose refresh method via ref + useImperativeHandle(ref, () => ({ + refresh: fetchReports + }), []); const fetchReports = async () => { try { @@ -110,9 +127,25 @@ export function ReportsQueue() { } }; + // Initial fetch on mount useEffect(() => { - fetchReports(); - }, []); + if (user) { + fetchReports(); + } + }, [user]); + + // Polling for auto-refresh + useEffect(() => { + if (!user || refreshMode !== 'auto') return; + + const interval = setInterval(() => { + fetchReports(); + }, pollInterval); + + return () => { + clearInterval(interval); + }; + }, [user, refreshMode, pollInterval]); const handleReportAction = async (reportId: string, action: 'reviewed' | 'dismissed') => { setActionLoading(reportId); @@ -258,4 +291,4 @@ export function ReportsQueue() { ))}
); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index fb49be7c..8815868d 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; -import { useRealtimeSubmissionItems } from '@/hooks/useRealtimeSubmissionItems'; import { fetchSubmissionItems, buildDependencyTree, @@ -60,20 +59,6 @@ export function SubmissionReviewManager({ const isMobile = useIsMobile(); const Container = isMobile ? Sheet : Dialog; - // Set up realtime subscription for submission items - useRealtimeSubmissionItems({ - submissionId, - onUpdate: (payload) => { - console.log('Submission item updated in real-time:', payload); - toast({ - title: 'Item Updated', - description: 'A submission item was updated by another moderator', - }); - loadSubmissionItems(); - }, - enabled: open && !!submissionId, - }); - useEffect(() => { if (open && submissionId) { loadSubmissionItems(); diff --git a/src/hooks/useRealtimeModerationStats.ts b/src/hooks/useRealtimeModerationStats.ts deleted file mode 100644 index c6da4e8b..00000000 --- a/src/hooks/useRealtimeModerationStats.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { useEffect, useState, useRef, useCallback } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { RealtimeChannel } from '@supabase/supabase-js'; -import { useUserRole } from './useUserRole'; - -type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'; - -interface ModerationStats { - pendingSubmissions: number; - openReports: number; - flaggedContent: number; -} - -interface UseRealtimeModerationStatsOptions { - onStatsChange?: (stats: ModerationStats) => void; - enabled?: boolean; - debounceMs?: number; -} - -export const useRealtimeModerationStats = (options: UseRealtimeModerationStatsOptions = {}) => { - const { onStatsChange, enabled = true, debounceMs = 1000 } = options; - const { isModerator, loading: roleLoading } = useUserRole(); - const [stats, setStats] = useState({ - pendingSubmissions: 0, - openReports: 0, - flaggedContent: 0, - }); - const [channel, setChannel] = useState(null); - const [connectionState, setConnectionState] = useState('disconnected'); - const updateTimerRef = useRef(null); - const onStatsChangeRef = useRef(onStatsChange); - - // Only enable realtime when user is confirmed as moderator - const realtimeEnabled = enabled && !roleLoading && isModerator(); - - // Update ref when callback changes - useEffect(() => { - onStatsChangeRef.current = onStatsChange; - }, [onStatsChange]); - - const fetchStats = useCallback(async () => { - try { - const [submissionsResult, reportsResult, reviewsResult] = await Promise.all([ - supabase - .from('content_submissions') - .select('id', { count: 'exact', head: true }) - .eq('status', 'pending'), - supabase - .from('reports') - .select('id', { count: 'exact', head: true }) - .eq('status', 'pending'), - supabase - .from('reviews') - .select('id', { count: 'exact', head: true }) - .eq('moderation_status', 'flagged'), - ]); - - const newStats = { - pendingSubmissions: submissionsResult.count || 0, - openReports: reportsResult.count || 0, - flaggedContent: reviewsResult.count || 0, - }; - - setStats(newStats); - onStatsChangeRef.current?.(newStats); - } catch (error) { - console.error('Error fetching moderation stats:', error); - } - }, []); - - const debouncedFetchStats = useCallback(() => { - if (updateTimerRef.current) { - clearTimeout(updateTimerRef.current); - } - updateTimerRef.current = setTimeout(fetchStats, debounceMs); - }, [fetchStats, debounceMs]); - - const reconnect = useCallback(() => { - if (channel) { - supabase.removeChannel(channel); - } - setConnectionState('connecting'); - fetchStats(); - }, [channel, fetchStats]); - - // Initial fetch and polling fallback - useEffect(() => { - if (!enabled) return; - - fetchStats(); - - let pollInterval: NodeJS.Timeout | null = null; - if (connectionState !== 'connected') { - pollInterval = setInterval(fetchStats, 30000); - } - - return () => { - if (pollInterval) { - clearInterval(pollInterval); - } - if (updateTimerRef.current) { - clearTimeout(updateTimerRef.current); - } - }; - }, [enabled, fetchStats, connectionState]); - - // Set up broadcast channels for real-time updates - useEffect(() => { - if (!realtimeEnabled) { - console.log('[Realtime:moderation-stats] Realtime disabled'); - return; - } - - console.log('[Realtime:moderation-stats] Creating broadcast channels'); - setConnectionState('connecting'); - - const setupChannels = async () => { - // Set auth token for private channels - await supabase.realtime.setAuth(); - - const submissionsChannel = supabase - .channel('moderation:content_submissions', { - config: { private: true }, - }) - .on('broadcast', { event: 'INSERT' }, () => { - console.log('[Realtime:moderation-stats] Content submission inserted'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'UPDATE' }, () => { - console.log('[Realtime:moderation-stats] Content submission updated'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'DELETE' }, () => { - console.log('[Realtime:moderation-stats] Content submission deleted'); - debouncedFetchStats(); - }) - .subscribe((status) => { - console.log('[Realtime:moderation-stats] Submissions channel status:', status); - - if (status === 'SUBSCRIBED') { - setConnectionState('connected'); - } else if (status === 'CHANNEL_ERROR') { - setConnectionState('error'); - } else if (status === 'TIMED_OUT') { - setConnectionState('disconnected'); - console.log('[Realtime:moderation-stats] Falling back to polling'); - } else if (status === 'CLOSED') { - setConnectionState('disconnected'); - } - }); - - const reportsChannel = supabase - .channel('moderation:reports', { - config: { private: true }, - }) - .on('broadcast', { event: 'INSERT' }, () => { - console.log('[Realtime:moderation-stats] Report inserted'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'UPDATE' }, () => { - console.log('[Realtime:moderation-stats] Report updated'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'DELETE' }, () => { - console.log('[Realtime:moderation-stats] Report deleted'); - debouncedFetchStats(); - }) - .subscribe(); - - const reviewsChannel = supabase - .channel('moderation:reviews', { - config: { private: true }, - }) - .on('broadcast', { event: 'INSERT' }, () => { - console.log('[Realtime:moderation-stats] Review inserted'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'UPDATE' }, () => { - console.log('[Realtime:moderation-stats] Review updated'); - debouncedFetchStats(); - }) - .on('broadcast', { event: 'DELETE' }, () => { - console.log('[Realtime:moderation-stats] Review deleted'); - debouncedFetchStats(); - }) - .subscribe(); - - setChannel(submissionsChannel); - - return { submissionsChannel, reportsChannel, reviewsChannel }; - }; - - let channels: Awaited>; - setupChannels().then((c) => { - channels = c; - }); - - return () => { - console.log('[Realtime:moderation-stats] Cleaning up channels'); - if (channels) { - supabase.removeChannel(channels.submissionsChannel); - supabase.removeChannel(channels.reportsChannel); - supabase.removeChannel(channels.reviewsChannel); - } - }; - }, [realtimeEnabled, debouncedFetchStats]); - - return { stats, refresh: fetchStats, connectionState, reconnect }; -}; diff --git a/src/hooks/useRealtimeSubmissionItems.ts b/src/hooks/useRealtimeSubmissionItems.ts deleted file mode 100644 index a3ac1fc2..00000000 --- a/src/hooks/useRealtimeSubmissionItems.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { RealtimeChannel } from '@supabase/supabase-js'; - -type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'; - -interface UseRealtimeSubmissionItemsOptions { - submissionId?: string; - onUpdate?: (payload: any) => void; - enabled?: boolean; -} - -export const useRealtimeSubmissionItems = (options: UseRealtimeSubmissionItemsOptions = {}) => { - const { submissionId, onUpdate, enabled = true } = options; - - const [channel, setChannel] = useState(null); - const [connectionState, setConnectionState] = useState('disconnected'); - - // Use ref to store latest callback without triggering re-subscriptions - const onUpdateRef = useRef(onUpdate); - - // Update ref when callback changes - useEffect(() => { - onUpdateRef.current = onUpdate; - }, [onUpdate]); - - const reconnect = useCallback(() => { - if (channel) { - supabase.removeChannel(channel); - } - setConnectionState('connecting'); - }, [channel]); - - useEffect(() => { - if (!enabled || !submissionId) { - console.log('[Realtime:submission-items] Disabled or no submission ID'); - return; - } - - console.log('[Realtime:submission-items] Creating new broadcast channel for submission:', submissionId); - setConnectionState('connecting'); - - const setupChannel = async () => { - // Set auth token for private channel - await supabase.realtime.setAuth(); - - const newChannel = supabase - .channel('moderation:submission_items', { - config: { private: true }, - }) - .on('broadcast', { event: 'UPDATE' }, (payload) => { - // Client-side filtering for specific submission - const itemData = payload.payload; - if (itemData?.new?.submission_id === submissionId) { - console.log('Submission item updated:', payload); - onUpdateRef.current?.(payload); - } - }) - .subscribe((status) => { - console.log(`[Realtime:submission-items] Subscription status:`, status); - - if (status === 'SUBSCRIBED') { - setConnectionState('connected'); - } else if (status === 'CHANNEL_ERROR') { - setConnectionState('error'); - } else if (status === 'TIMED_OUT') { - setConnectionState('disconnected'); - } else if (status === 'CLOSED') { - setConnectionState('disconnected'); - } - }); - - setChannel(newChannel); - }; - - setupChannel(); - - return () => { - if (channel) { - console.log('[Realtime:submission-items] Cleaning up channel'); - supabase.removeChannel(channel); - } - }; - }, [enabled, submissionId]); - - return { channel, connectionState, reconnect }; -}; diff --git a/src/hooks/useRealtimeSubmissions.ts b/src/hooks/useRealtimeSubmissions.ts deleted file mode 100644 index 14cd1cde..00000000 --- a/src/hooks/useRealtimeSubmissions.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { RealtimeChannel } from '@supabase/supabase-js'; -import { useUserRole } from './useUserRole'; - -type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'; - -interface UseRealtimeSubmissionsOptions { - onInsert?: (payload: any) => void; - onUpdate?: (payload: any) => void; - onDelete?: (payload: any) => void; - enabled?: boolean; -} - -export const useRealtimeSubmissions = (options: UseRealtimeSubmissionsOptions = {}) => { - const { onInsert, onUpdate, onDelete, enabled = true } = options; - const { isModerator, loading: roleLoading } = useUserRole(); - - const [channel, setChannel] = useState(null); - const [connectionState, setConnectionState] = useState('disconnected'); - - // Only enable realtime when user is confirmed as moderator - const realtimeEnabled = enabled && !roleLoading && isModerator(); - - // Use refs to store latest callbacks without triggering re-subscriptions - const onInsertRef = useRef(onInsert); - const onUpdateRef = useRef(onUpdate); - const onDeleteRef = useRef(onDelete); - - // Update refs when callbacks change - useEffect(() => { - onInsertRef.current = onInsert; - onUpdateRef.current = onUpdate; - onDeleteRef.current = onDelete; - }, [onInsert, onUpdate, onDelete]); - - const reconnect = useCallback(() => { - if (channel) { - supabase.removeChannel(channel); - } - setConnectionState('connecting'); - }, [channel]); - - useEffect(() => { - if (!realtimeEnabled) { - console.log('[Realtime:content-submissions] Realtime disabled'); - return; - } - - console.log('[Realtime:content-submissions] Creating new broadcast channel'); - setConnectionState('connecting'); - - const setupChannel = async () => { - // Set auth token for private channel - await supabase.realtime.setAuth(); - - const newChannel = supabase - .channel('moderation:content_submissions', { - config: { private: true }, - }) - .on('broadcast', { event: 'INSERT' }, (payload) => { - console.log('Submission inserted:', payload); - onInsertRef.current?.(payload); - }) - .on('broadcast', { event: 'UPDATE' }, (payload) => { - console.log('Submission updated:', payload); - onUpdateRef.current?.(payload); - }) - .on('broadcast', { event: 'DELETE' }, (payload) => { - console.log('Submission deleted:', payload); - onDeleteRef.current?.(payload); - }) - .subscribe((status) => { - console.log('[Realtime:content-submissions] Subscription status:', status); - - if (status === 'SUBSCRIBED') { - setConnectionState('connected'); - } else if (status === 'CHANNEL_ERROR') { - setConnectionState('error'); - } else if (status === 'TIMED_OUT') { - setConnectionState('disconnected'); - } else if (status === 'CLOSED') { - setConnectionState('disconnected'); - } - }); - - setChannel(newChannel); - }; - - setupChannel(); - - return () => { - if (channel) { - console.log('[Realtime:content-submissions] Cleaning up channel'); - supabase.removeChannel(channel); - } - }; - }, [realtimeEnabled]); - - return { channel, connectionState, reconnect }; -}; diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 895fecde..8e4f8139 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -8,7 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue'; -import { ReportsQueue } from '@/components/moderation/ReportsQueue'; +import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue'; import { UserManagement } from '@/components/admin/UserManagement'; import { AdminHeader } from '@/components/layout/AdminHeader'; import { useModerationStats } from '@/hooks/useModerationStats'; @@ -20,6 +20,7 @@ export default function Admin() { const { isModerator, loading: roleLoading } = useUserRole(); const navigate = useNavigate(); const moderationQueueRef = useRef(null); + const reportsQueueRef = useRef(null); // Get admin settings for polling configuration const { @@ -40,6 +41,7 @@ export default function Admin() { const handleRefresh = useCallback(() => { moderationQueueRef.current?.refresh(); + reportsQueueRef.current?.refresh(); refreshStats(); }, [refreshStats]); @@ -162,7 +164,7 @@ export default function Admin() { - +