diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 0cce5f5d..21410a52 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -348,12 +348,89 @@ export const ModerationQueue = forwardRef((props, ref) => { // Set up realtime subscriptions const { connectionState: submissionsConnectionState, reconnect: reconnectSubmissions } = useRealtimeSubmissions({ - onInsert: (payload) => { - toast({ - title: 'New Submission', - description: 'A new content submission has been added', - }); - fetchItems(activeEntityFilter, activeStatusFilter); + 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 diff --git a/src/hooks/useRealtimeModerationStats.ts b/src/hooks/useRealtimeModerationStats.ts index 4a0b8f29..07abe766 100644 --- a/src/hooks/useRealtimeModerationStats.ts +++ b/src/hooks/useRealtimeModerationStats.ts @@ -108,36 +108,189 @@ export const useRealtimeModerationStats = (options: UseRealtimeModerationStatsOp .on( 'postgres_changes', { - event: '*', + event: 'INSERT', schema: 'public', table: 'content_submissions', }, - () => { - console.log('Content submissions changed'); + (payload) => { + console.log('Content submission inserted'); + // Optimistic update: increment pending submissions + if (payload.new.status === 'pending') { + setStats(prev => ({ + ...prev, + pendingSubmissions: prev.pendingSubmissions + 1 + })); + } + // Debounced sync as backup debouncedFetchStats(); } ) .on( 'postgres_changes', { - event: '*', + event: 'UPDATE', + schema: 'public', + table: 'content_submissions', + }, + (payload) => { + console.log('Content submission updated'); + // Optimistic update: adjust counter based on status change + const oldStatus = payload.old.status; + const newStatus = payload.new.status; + + if (oldStatus === 'pending' && newStatus !== 'pending') { + setStats(prev => ({ + ...prev, + pendingSubmissions: Math.max(0, prev.pendingSubmissions - 1) + })); + } else if (oldStatus !== 'pending' && newStatus === 'pending') { + setStats(prev => ({ + ...prev, + pendingSubmissions: prev.pendingSubmissions + 1 + })); + } + + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'DELETE', + schema: 'public', + table: 'content_submissions', + }, + (payload) => { + console.log('Content submission deleted'); + // Optimistic update: decrement if was pending + if (payload.old.status === 'pending') { + setStats(prev => ({ + ...prev, + pendingSubmissions: Math.max(0, prev.pendingSubmissions - 1) + })); + } + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'INSERT', schema: 'public', table: 'reports', }, () => { - console.log('Reports changed'); + console.log('Report inserted'); + setStats(prev => ({ + ...prev, + openReports: prev.openReports + 1 + })); debouncedFetchStats(); } ) .on( 'postgres_changes', { - event: '*', + event: 'UPDATE', + schema: 'public', + table: 'reports', + }, + (payload) => { + console.log('Report updated'); + const oldStatus = payload.old.status; + const newStatus = payload.new.status; + + if (oldStatus === 'pending' && newStatus !== 'pending') { + setStats(prev => ({ + ...prev, + openReports: Math.max(0, prev.openReports - 1) + })); + } else if (oldStatus !== 'pending' && newStatus === 'pending') { + setStats(prev => ({ + ...prev, + openReports: prev.openReports + 1 + })); + } + + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'DELETE', + schema: 'public', + table: 'reports', + }, + (payload) => { + console.log('Report deleted'); + if (payload.old.status === 'pending') { + setStats(prev => ({ + ...prev, + openReports: Math.max(0, prev.openReports - 1) + })); + } + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'INSERT', schema: 'public', table: 'reviews', }, () => { - console.log('Reviews changed'); + console.log('Review inserted'); + setStats(prev => ({ + ...prev, + flaggedContent: prev.flaggedContent + 1 + })); + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'reviews', + }, + (payload) => { + console.log('Review updated'); + const oldStatus = payload.old.moderation_status; + const newStatus = payload.new.moderation_status; + + if (oldStatus === 'flagged' && newStatus !== 'flagged') { + setStats(prev => ({ + ...prev, + flaggedContent: Math.max(0, prev.flaggedContent - 1) + })); + } else if (oldStatus !== 'flagged' && newStatus === 'flagged') { + setStats(prev => ({ + ...prev, + flaggedContent: prev.flaggedContent + 1 + })); + } + + debouncedFetchStats(); + } + ) + .on( + 'postgres_changes', + { + event: 'DELETE', + schema: 'public', + table: 'reviews', + }, + (payload) => { + console.log('Review deleted'); + if (payload.old.moderation_status === 'flagged') { + setStats(prev => ({ + ...prev, + flaggedContent: Math.max(0, prev.flaggedContent - 1) + })); + } debouncedFetchStats(); } );