Fix admin page flashing

This commit is contained in:
gpt-engineer-app[bot]
2025-10-09 17:14:45 +00:00
parent ff7c90e62d
commit 356cf2b54b
4 changed files with 69 additions and 36 deletions

View File

@@ -541,8 +541,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
}, []); // Empty deps - use refs instead }, []); // Empty deps - use refs instead
// Debounced filters to prevent rapid-fire calls // Debounced filters to prevent rapid-fire calls
const debouncedEntityFilter = useDebounce(activeEntityFilter, 500); const debouncedEntityFilter = useDebounce(activeEntityFilter, 1000);
const debouncedStatusFilter = useDebounce(activeStatusFilter, 500); const debouncedStatusFilter = useDebounce(activeStatusFilter, 1000);
// Store latest filter values in ref to avoid dependency issues // Store latest filter values in ref to avoid dependency issues
const filtersRef = useRef({ entityFilter: debouncedEntityFilter, statusFilter: debouncedStatusFilter }); const filtersRef = useRef({ entityFilter: debouncedEntityFilter, statusFilter: debouncedStatusFilter });
@@ -568,9 +568,18 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
} }
fetchDebounceRef.current = setTimeout(() => { fetchDebounceRef.current = setTimeout(() => {
fetchItems(entityFilter, statusFilter, silent, tab); fetchItems(entityFilter, statusFilter, silent, tab);
}, 100); }, 1000); // 1 second debounce
}, [fetchItems]); }, [fetchItems]);
// Clean up debounce on unmount
useEffect(() => {
return () => {
if (fetchDebounceRef.current) {
clearTimeout(fetchDebounceRef.current);
}
};
}, []);
// Initial fetch on mount and filter changes // Initial fetch on mount and filter changes
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
@@ -797,10 +806,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
description: "Submission and all items have been reset to pending status", description: "Submission and all items have been reset to pending status",
}); });
// Silent cleanup after delay // Optimistic update - item will reappear via realtime
setTimeout(() => { setItems(prev => prev.filter(i => i.id !== item.id));
fetchItems(activeEntityFilter, activeStatusFilter, true);
}, 2000);
} catch (error: any) { } catch (error: any) {
console.error('Error resetting submission:', error); console.error('Error resetting submission:', error);
toast({ toast({
@@ -1008,10 +1015,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
description: "All entities created successfully", description: "All entities created successfully",
}); });
// Silent cleanup after delay // Optimistic update - remove from queue
setTimeout(() => { setItems(prev => prev.filter(i => i.id !== item.id));
fetchItems(activeEntityFilter, activeStatusFilter, true); recentlyRemovedRef.current.add(item.id);
}, 2000);
return; return;
} }
@@ -1107,10 +1113,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
description: `Successfully approved and published ${photoSubmission.items.length} photo(s)`, description: `Successfully approved and published ${photoSubmission.items.length} photo(s)`,
}); });
// Silent cleanup after delay // Optimistic update - remove from queue
setTimeout(() => { setItems(prev => prev.filter(i => i.id !== item.id));
fetchItems(activeEntityFilter, activeStatusFilter, true); recentlyRemovedRef.current.add(item.id);
}, 2000);
return; return;
} catch (error: any) { } catch (error: any) {
@@ -1149,10 +1154,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
description: `Successfully processed ${submissionItems.length} item(s)`, description: `Successfully processed ${submissionItems.length} item(s)`,
}); });
// Silent cleanup after delay // Optimistic update - remove from queue
setTimeout(() => { setItems(prev => prev.filter(i => i.id !== item.id));
fetchItems(activeEntityFilter, activeStatusFilter, true); recentlyRemovedRef.current.add(item.id);
}, 2000);
return; return;
} else if (action === 'rejected') { } else if (action === 'rejected') {
// Cascade rejection to all pending items // Cascade rejection to all pending items
@@ -1225,10 +1229,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
return newNotes; return newNotes;
}); });
// Silent cleanup after delay // Optimistic update - remove from queue if approved or rejected
setTimeout(() => { if (action === 'approved' || action === 'rejected') {
fetchItems(activeEntityFilter, activeStatusFilter, true); setItems(prev => prev.filter(i => i.id !== item.id));
}, 2000); recentlyRemovedRef.current.add(item.id);
}
} catch (error: any) { } catch (error: any) {
console.error('Error moderating content:', error); console.error('Error moderating content:', error);

View File

@@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, memo } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay'; import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay'; import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react'; import { AlertCircle, Loader2 } from 'lucide-react';
import type { SubmissionItemData } from '@/types/submissions'; import type { SubmissionItemData } from '@/types/submissions';
interface SubmissionItemsListProps { interface SubmissionItemsListProps {
@@ -13,7 +13,7 @@ interface SubmissionItemsListProps {
showImages?: boolean; showImages?: boolean;
} }
export function SubmissionItemsList({ export const SubmissionItemsList = memo(function SubmissionItemsList({
submissionId, submissionId,
view = 'summary', view = 'summary',
showImages = true showImages = true
@@ -21,6 +21,7 @@ export function SubmissionItemsList({
const [items, setItems] = useState<SubmissionItemData[]>([]); const [items, setItems] = useState<SubmissionItemData[]>([]);
const [hasPhotos, setHasPhotos] = useState(false); const [hasPhotos, setHasPhotos] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -29,7 +30,12 @@ export function SubmissionItemsList({
const fetchSubmissionItems = async () => { const fetchSubmissionItems = async () => {
try { try {
// Only show skeleton on initial load, show refreshing indicator on refresh
if (loading) {
setLoading(true); setLoading(true);
} else {
setRefreshing(true);
}
setError(null); setError(null);
// Fetch submission items // Fetch submission items
@@ -58,6 +64,7 @@ export function SubmissionItemsList({
setError('Failed to load submission details'); setError('Failed to load submission details');
} finally { } finally {
setLoading(false); setLoading(false);
setRefreshing(false);
} }
}; };
@@ -89,6 +96,13 @@ export function SubmissionItemsList({
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{refreshing && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
<span>Refreshing...</span>
</div>
)}
{/* Show regular submission items */} {/* Show regular submission items */}
{items.map((item) => ( {items.map((item) => (
<div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}> <div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
@@ -109,4 +123,4 @@ export function SubmissionItemsList({
)} )}
</div> </div>
); );
} });

View File

@@ -102,18 +102,33 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
statsDebounceRef.current = setTimeout(() => { statsDebounceRef.current = setTimeout(() => {
fetchStats(true); // Silent refresh fetchStats(true); // Silent refresh
}, 500); // 500ms debounce }, 2000); // 2 second debounce to reduce flashing
}, [fetchStats]); }, [fetchStats]);
// Realtime subscription for instant stat updates // Realtime subscription - only trigger on INSERT of new pending items
useEffect(() => { useEffect(() => {
if (!enabled || !realtimeEnabled) return; if (!enabled || !realtimeEnabled) return;
const channel = supabase const channel = supabase
.channel('moderation-stats-realtime') .channel('moderation-stats-realtime')
.on('postgres_changes', { event: '*', schema: 'public', table: 'content_submissions' }, debouncedFetchStats) .on('postgres_changes', {
.on('postgres_changes', { event: '*', schema: 'public', table: 'reports' }, debouncedFetchStats) event: 'INSERT',
.on('postgres_changes', { event: '*', schema: 'public', table: 'reviews' }, debouncedFetchStats) 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(); .subscribe();
return () => { return () => {

View File

@@ -21,7 +21,7 @@ export default function AdminModeration() {
const refreshMode = getAdminPanelRefreshMode(); const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval(); const pollInterval = getAdminPanelPollInterval();
const { refresh: refreshStats, lastUpdated } = useModerationStats({ const { lastUpdated } = useModerationStats({
enabled: !!user && !authLoading && !roleLoading && isModerator(), enabled: !!user && !authLoading && !roleLoading && isModerator(),
pollingEnabled: refreshMode === 'auto', pollingEnabled: refreshMode === 'auto',
pollingInterval: pollInterval, pollingInterval: pollInterval,
@@ -29,8 +29,7 @@ export default function AdminModeration() {
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
moderationQueueRef.current?.refresh(); moderationQueueRef.current?.refresh();
refreshStats(); }, []);
}, [refreshStats]);
useEffect(() => { useEffect(() => {
if (!authLoading && !roleLoading) { if (!authLoading && !roleLoading) {