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 { 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 { user } = useAuth();
const { toast } = useToast();
@@ -54,6 +59,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
isAdmin: isAdmin(),
isSuperuser: isSuperuser(),
toast,
optimisticallyUpdateStats,
settings,
});

View File

@@ -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<typeof useToast>["toast"];
optimisticallyUpdateStats?: (delta: Partial<ModerationStats>) => 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<ModerationStats> = {};
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") &&

View File

@@ -38,6 +38,13 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
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 [isInitialLoad, setIsInitialLoad] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -49,6 +56,15 @@ export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
onStatsChangeRef.current = 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) => {
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
};

View File

@@ -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() {
</TabsList>
<TabsContent value="moderation" className="mt-6" forceMount={true} hidden={activeTab !== 'moderation'}>
<ModerationQueue ref={moderationQueueRef} />
<ModerationQueue ref={moderationQueueRef} optimisticallyUpdateStats={optimisticallyUpdateStats} />
</TabsContent>
<TabsContent value="reports" className="mt-6" forceMount={true} hidden={activeTab !== 'reports'}>