From 356cf2b54b7bdb7802d00d25927430fd810ba021 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:14:45 +0000 Subject: [PATCH] Fix admin page flashing --- src/components/moderation/ModerationQueue.tsx | 51 ++++++++++--------- .../moderation/SubmissionItemsList.tsx | 24 +++++++-- src/hooks/useModerationStats.ts | 25 +++++++-- src/pages/AdminModeration.tsx | 5 +- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index cc93786d..fcbca083 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -541,8 +541,8 @@ export const ModerationQueue = forwardRef((props, ref) => { }, []); // Empty deps - use refs instead // Debounced filters to prevent rapid-fire calls - const debouncedEntityFilter = useDebounce(activeEntityFilter, 500); - const debouncedStatusFilter = useDebounce(activeStatusFilter, 500); + const debouncedEntityFilter = useDebounce(activeEntityFilter, 1000); + const debouncedStatusFilter = useDebounce(activeStatusFilter, 1000); // Store latest filter values in ref to avoid dependency issues const filtersRef = useRef({ entityFilter: debouncedEntityFilter, statusFilter: debouncedStatusFilter }); @@ -568,9 +568,18 @@ export const ModerationQueue = forwardRef((props, ref) => { } fetchDebounceRef.current = setTimeout(() => { fetchItems(entityFilter, statusFilter, silent, tab); - }, 100); + }, 1000); // 1 second debounce }, [fetchItems]); + // Clean up debounce on unmount + useEffect(() => { + return () => { + if (fetchDebounceRef.current) { + clearTimeout(fetchDebounceRef.current); + } + }; + }, []); + // Initial fetch on mount and filter changes useEffect(() => { if (!user) return; @@ -797,10 +806,8 @@ export const ModerationQueue = forwardRef((props, ref) => { description: "Submission and all items have been reset to pending status", }); - // Silent cleanup after delay - setTimeout(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); - }, 2000); + // Optimistic update - item will reappear via realtime + setItems(prev => prev.filter(i => i.id !== item.id)); } catch (error: any) { console.error('Error resetting submission:', error); toast({ @@ -1008,10 +1015,9 @@ export const ModerationQueue = forwardRef((props, ref) => { description: "All entities created successfully", }); - // Silent cleanup after delay - setTimeout(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); - }, 2000); + // Optimistic update - remove from queue + setItems(prev => prev.filter(i => i.id !== item.id)); + recentlyRemovedRef.current.add(item.id); return; } @@ -1107,10 +1113,9 @@ export const ModerationQueue = forwardRef((props, ref) => { description: `Successfully approved and published ${photoSubmission.items.length} photo(s)`, }); - // Silent cleanup after delay - setTimeout(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); - }, 2000); + // Optimistic update - remove from queue + setItems(prev => prev.filter(i => i.id !== item.id)); + recentlyRemovedRef.current.add(item.id); return; } catch (error: any) { @@ -1149,10 +1154,9 @@ export const ModerationQueue = forwardRef((props, ref) => { description: `Successfully processed ${submissionItems.length} item(s)`, }); - // Silent cleanup after delay - setTimeout(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); - }, 2000); + // Optimistic update - remove from queue + setItems(prev => prev.filter(i => i.id !== item.id)); + recentlyRemovedRef.current.add(item.id); return; } else if (action === 'rejected') { // Cascade rejection to all pending items @@ -1225,10 +1229,11 @@ export const ModerationQueue = forwardRef((props, ref) => { return newNotes; }); - // Silent cleanup after delay - setTimeout(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); - }, 2000); + // Optimistic update - remove from queue if approved or rejected + if (action === 'approved' || action === 'rejected') { + setItems(prev => prev.filter(i => i.id !== item.id)); + recentlyRemovedRef.current.add(item.id); + } } catch (error: any) { console.error('Error moderating content:', error); diff --git a/src/components/moderation/SubmissionItemsList.tsx b/src/components/moderation/SubmissionItemsList.tsx index a9e52a7e..7371aee2 100644 --- a/src/components/moderation/SubmissionItemsList.tsx +++ b/src/components/moderation/SubmissionItemsList.tsx @@ -1,10 +1,10 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, memo } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { SubmissionChangesDisplay } from './SubmissionChangesDisplay'; import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { AlertCircle } from 'lucide-react'; +import { AlertCircle, Loader2 } from 'lucide-react'; import type { SubmissionItemData } from '@/types/submissions'; interface SubmissionItemsListProps { @@ -13,7 +13,7 @@ interface SubmissionItemsListProps { showImages?: boolean; } -export function SubmissionItemsList({ +export const SubmissionItemsList = memo(function SubmissionItemsList({ submissionId, view = 'summary', showImages = true @@ -21,6 +21,7 @@ export function SubmissionItemsList({ const [items, setItems] = useState([]); const [hasPhotos, setHasPhotos] = useState(false); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); useEffect(() => { @@ -29,7 +30,12 @@ export function SubmissionItemsList({ const fetchSubmissionItems = async () => { try { - setLoading(true); + // Only show skeleton on initial load, show refreshing indicator on refresh + if (loading) { + setLoading(true); + } else { + setRefreshing(true); + } setError(null); // Fetch submission items @@ -58,6 +64,7 @@ export function SubmissionItemsList({ setError('Failed to load submission details'); } finally { setLoading(false); + setRefreshing(false); } }; @@ -89,6 +96,13 @@ export function SubmissionItemsList({ return (
+ {refreshing && ( +
+ + Refreshing... +
+ )} + {/* Show regular submission items */} {items.map((item) => (
@@ -109,4 +123,4 @@ export function SubmissionItemsList({ )}
); -} +}); diff --git a/src/hooks/useModerationStats.ts b/src/hooks/useModerationStats.ts index 874d8c31..261697e5 100644 --- a/src/hooks/useModerationStats.ts +++ b/src/hooks/useModerationStats.ts @@ -102,18 +102,33 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => { statsDebounceRef.current = setTimeout(() => { fetchStats(true); // Silent refresh - }, 500); // 500ms debounce + }, 2000); // 2 second debounce to reduce flashing }, [fetchStats]); - // Realtime subscription for instant stat updates + // Realtime subscription - only trigger on INSERT of new pending items useEffect(() => { if (!enabled || !realtimeEnabled) return; const channel = supabase .channel('moderation-stats-realtime') - .on('postgres_changes', { event: '*', schema: 'public', table: 'content_submissions' }, debouncedFetchStats) - .on('postgres_changes', { event: '*', schema: 'public', table: 'reports' }, debouncedFetchStats) - .on('postgres_changes', { event: '*', schema: 'public', table: 'reviews' }, debouncedFetchStats) + .on('postgres_changes', { + event: 'INSERT', + schema: 'public', + table: 'content_submissions', + filter: 'status=eq.pending' + }, debouncedFetchStats) + .on('postgres_changes', { + event: 'INSERT', + schema: 'public', + table: 'reports', + filter: 'status=eq.pending' + }, debouncedFetchStats) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'reviews', + filter: 'moderation_status=eq.flagged' + }, debouncedFetchStats) .subscribe(); return () => { diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx index 11ee2e52..217f825a 100644 --- a/src/pages/AdminModeration.tsx +++ b/src/pages/AdminModeration.tsx @@ -21,7 +21,7 @@ export default function AdminModeration() { const refreshMode = getAdminPanelRefreshMode(); const pollInterval = getAdminPanelPollInterval(); - const { refresh: refreshStats, lastUpdated } = useModerationStats({ + const { lastUpdated } = useModerationStats({ enabled: !!user && !authLoading && !roleLoading && isModerator(), pollingEnabled: refreshMode === 'auto', pollingInterval: pollInterval, @@ -29,8 +29,7 @@ export default function AdminModeration() { const handleRefresh = useCallback(() => { moderationQueueRef.current?.refresh(); - refreshStats(); - }, [refreshStats]); + }, []); useEffect(() => { if (!authLoading && !roleLoading) {