mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 03:51:12 -05:00
feat: Implement optimistic stats updates
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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") &&
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
Reference in New Issue
Block a user