mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:11:13 -05:00
Fix admin page flashing
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
setLoading(true);
|
// Only show skeleton on initial load, show refreshing indicator on refresh
|
||||||
|
if (loading) {
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user