feat: Implement optimistic stats updates

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 18:54:57 +00:00
parent 6623074679
commit 0e2ecd766d
4 changed files with 61 additions and 5 deletions

View File

@@ -25,7 +25,12 @@ import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submiss
import type { ModerationQueueRef } from '@/types/moderation'; import type { ModerationQueueRef } from '@/types/moderation';
import type { PhotoItem } from '@/types/photos'; import type { PhotoItem } from '@/types/photos';
export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => { interface ModerationQueueProps {
optimisticallyUpdateStats?: (delta: Partial<{ pendingSubmissions: number; openReports: number; flaggedContent: number }>) => void;
}
export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueueProps>((props, ref) => {
const { optimisticallyUpdateStats } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { user } = useAuth(); const { user } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
@@ -54,6 +59,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
isAdmin: isAdmin(), isAdmin: isAdmin(),
isSuperuser: isSuperuser(), isSuperuser: isSuperuser(),
toast, toast,
optimisticallyUpdateStats,
settings, settings,
}); });

View File

@@ -18,6 +18,12 @@ import { useModerationQueue } from "@/hooks/useModerationQueue";
import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation";
interface ModerationStats {
pendingSubmissions: number;
openReports: number;
flaggedContent: number;
}
/** /**
* Configuration for useModerationQueueManager * Configuration for useModerationQueueManager
*/ */
@@ -26,6 +32,7 @@ export interface ModerationQueueManagerConfig {
isAdmin: boolean; isAdmin: boolean;
isSuperuser: boolean; isSuperuser: boolean;
toast: ReturnType<typeof useToast>["toast"]; toast: ReturnType<typeof useToast>["toast"];
optimisticallyUpdateStats?: (delta: Partial<ModerationStats>) => void;
settings: { settings: {
refreshMode: "auto" | "manual"; refreshMode: "auto" | "manual";
pollInterval: number; pollInterval: number;
@@ -81,7 +88,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
const { user, isAdmin, isSuperuser, toast, settings } = config; const { user, isAdmin, isSuperuser, toast, optimisticallyUpdateStats, settings } = config;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Initialize sub-hooks // Initialize sub-hooks
@@ -279,6 +286,18 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
return; return;
} }
// Calculate stat delta for optimistic update
const statDelta: Partial<ModerationStats> = {};
if (action === 'approved' || action === 'rejected') {
statDelta.pendingSubmissions = -1;
}
// Optimistically update stats IMMEDIATELY
if (optimisticallyUpdateStats) {
optimisticallyUpdateStats(statDelta);
}
// Optimistic update // Optimistic update
const shouldRemove = const shouldRemove =
(filters.statusFilter === "pending" || filters.statusFilter === "flagged") && (filters.statusFilter === "pending" || filters.statusFilter === "flagged") &&

View File

@@ -38,6 +38,13 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
flaggedContent: 0, flaggedContent: 0,
}); });
// Optimistic deltas for immediate UI updates
const [optimisticDeltas, setOptimisticDeltas] = useState<ModerationStats>({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null); const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -49,6 +56,15 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
onStatsChangeRef.current = onStatsChange; onStatsChangeRef.current = onStatsChange;
}, [onStatsChange]); }, [onStatsChange]);
// Optimistic update function
const optimisticallyUpdateStats = useCallback((delta: Partial<ModerationStats>) => {
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) => { const fetchStats = useCallback(async (silent = false) => {
if (!enabled) return; if (!enabled) return;
@@ -82,6 +98,13 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
setStats(newStats); setStats(newStats);
setLastUpdated(new Date()); setLastUpdated(new Date());
onStatsChangeRef.current?.(newStats); onStatsChangeRef.current?.(newStats);
// Clear optimistic deltas when real data arrives
setOptimisticDeltas({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
} catch (error) { } catch (error) {
console.error('Error fetching moderation stats:', error); console.error('Error fetching moderation stats:', error);
} finally { } finally {
@@ -180,9 +203,17 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
}; };
}, [enabled, pollingEnabled, realtimeEnabled, pollingInterval, fetchStats, isInitialLoad]); }, [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 { return {
stats, stats: displayStats,
refresh: fetchStats, refresh: fetchStats,
optimisticallyUpdateStats,
isLoading, isLoading,
lastUpdated lastUpdated
}; };

View File

@@ -40,7 +40,7 @@ export default function AdminDashboard() {
const refreshMode = getAdminPanelRefreshMode(); const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval(); const pollInterval = getAdminPanelPollInterval();
const { stats, refresh: refreshStats, lastUpdated } = useModerationStats({ const { stats, refresh: refreshStats, optimisticallyUpdateStats, lastUpdated } = useModerationStats({
enabled: !!user && !authLoading && !roleLoading && isModerator(), enabled: !!user && !authLoading && !roleLoading && isModerator(),
pollingEnabled: refreshMode === 'auto', pollingEnabled: refreshMode === 'auto',
pollingInterval: pollInterval, pollingInterval: pollInterval,
@@ -293,7 +293,7 @@ export default function AdminDashboard() {
</TabsList> </TabsList>
<TabsContent value="moderation" className="mt-6" forceMount={true} hidden={activeTab !== 'moderation'}> <TabsContent value="moderation" className="mt-6" forceMount={true} hidden={activeTab !== 'moderation'}>
<ModerationQueue ref={moderationQueueRef} /> <ModerationQueue ref={moderationQueueRef} optimisticallyUpdateStats={optimisticallyUpdateStats} />
</TabsContent> </TabsContent>
<TabsContent value="reports" className="mt-6" forceMount={true} hidden={activeTab !== 'reports'}> <TabsContent value="reports" className="mt-6" forceMount={true} hidden={activeTab !== 'reports'}>