From 0e2ecd766d4d13e2ac1ab5a19d46643931fe36a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:54:57 +0000 Subject: [PATCH] feat: Implement optimistic stats updates --- src/components/moderation/ModerationQueue.tsx | 8 ++++- .../moderation/useModerationQueueManager.ts | 21 +++++++++++- src/hooks/useModerationStats.ts | 33 ++++++++++++++++++- src/pages/AdminDashboard.tsx | 4 +-- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 12ddfcf1..1bde5d7b 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -25,7 +25,12 @@ import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submiss import type { ModerationQueueRef } from '@/types/moderation'; import type { PhotoItem } from '@/types/photos'; -export const ModerationQueue = forwardRef((props, ref) => { +interface ModerationQueueProps { + optimisticallyUpdateStats?: (delta: Partial<{ pendingSubmissions: number; openReports: number; flaggedContent: number }>) => void; +} + +export const ModerationQueue = forwardRef((props, ref) => { + const { optimisticallyUpdateStats } = props; const isMobile = useIsMobile(); const { user } = useAuth(); const { toast } = useToast(); @@ -54,6 +59,7 @@ export const ModerationQueue = forwardRef((props, ref) => { isAdmin: isAdmin(), isSuperuser: isSuperuser(), toast, + optimisticallyUpdateStats, settings, }); diff --git a/src/hooks/moderation/useModerationQueueManager.ts b/src/hooks/moderation/useModerationQueueManager.ts index c2ed6a41..fb67fb38 100644 --- a/src/hooks/moderation/useModerationQueueManager.ts +++ b/src/hooks/moderation/useModerationQueueManager.ts @@ -18,6 +18,12 @@ import { useModerationQueue } from "@/hooks/useModerationQueue"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation"; +interface ModerationStats { + pendingSubmissions: number; + openReports: number; + flaggedContent: number; +} + /** * Configuration for useModerationQueueManager */ @@ -26,6 +32,7 @@ export interface ModerationQueueManagerConfig { isAdmin: boolean; isSuperuser: boolean; toast: ReturnType["toast"]; + optimisticallyUpdateStats?: (delta: Partial) => void; settings: { refreshMode: "auto" | "manual"; pollInterval: number; @@ -81,7 +88,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): timestamp: new Date().toISOString() }); - const { user, isAdmin, isSuperuser, toast, settings } = config; + const { user, isAdmin, isSuperuser, toast, optimisticallyUpdateStats, settings } = config; const queryClient = useQueryClient(); // Initialize sub-hooks @@ -279,6 +286,18 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): return; } + // Calculate stat delta for optimistic update + const statDelta: Partial = {}; + + if (action === 'approved' || action === 'rejected') { + statDelta.pendingSubmissions = -1; + } + + // Optimistically update stats IMMEDIATELY + if (optimisticallyUpdateStats) { + optimisticallyUpdateStats(statDelta); + } + // Optimistic update const shouldRemove = (filters.statusFilter === "pending" || filters.statusFilter === "flagged") && diff --git a/src/hooks/useModerationStats.ts b/src/hooks/useModerationStats.ts index 457ef390..5e28e068 100644 --- a/src/hooks/useModerationStats.ts +++ b/src/hooks/useModerationStats.ts @@ -38,6 +38,13 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => { flaggedContent: 0, }); + // Optimistic deltas for immediate UI updates + const [optimisticDeltas, setOptimisticDeltas] = useState({ + pendingSubmissions: 0, + openReports: 0, + flaggedContent: 0, + }); + const [isLoading, setIsLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); @@ -49,6 +56,15 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => { onStatsChangeRef.current = onStatsChange; }, [onStatsChange]); + // Optimistic update function + const optimisticallyUpdateStats = useCallback((delta: Partial) => { + 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; @@ -82,6 +98,13 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => { setStats(newStats); setLastUpdated(new Date()); onStatsChangeRef.current?.(newStats); + + // Clear optimistic deltas when real data arrives + setOptimisticDeltas({ + pendingSubmissions: 0, + openReports: 0, + flaggedContent: 0, + }); } catch (error) { console.error('Error fetching moderation stats:', error); } finally { @@ -180,9 +203,17 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => { }; }, [enabled, pollingEnabled, realtimeEnabled, 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, + stats: displayStats, refresh: fetchStats, + optimisticallyUpdateStats, isLoading, lastUpdated }; diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 3ab702b3..f9d927cd 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -40,7 +40,7 @@ export default function AdminDashboard() { const refreshMode = getAdminPanelRefreshMode(); const pollInterval = getAdminPanelPollInterval(); - const { stats, refresh: refreshStats, lastUpdated } = useModerationStats({ + const { stats, refresh: refreshStats, optimisticallyUpdateStats, lastUpdated } = useModerationStats({ enabled: !!user && !authLoading && !roleLoading && isModerator(), pollingEnabled: refreshMode === 'auto', pollingInterval: pollInterval, @@ -293,7 +293,7 @@ export default function AdminDashboard() {