|
|
|
|
@@ -61,6 +61,7 @@ interface ModerationItem {
|
|
|
|
|
escalated?: boolean;
|
|
|
|
|
assigned_to?: string;
|
|
|
|
|
locked_until?: string;
|
|
|
|
|
_removing?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos';
|
|
|
|
|
@@ -81,8 +82,7 @@ export interface ModerationQueueRef {
|
|
|
|
|
export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
const [items, setItems] = useState<ModerationItem[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
|
|
|
const [loadingState, setLoadingState] = useState<'initial' | 'loading' | 'refreshing' | 'ready'>('initial');
|
|
|
|
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
|
|
|
const [notes, setNotes] = useState<Record<string, string>>({});
|
|
|
|
|
const [activeTab, setActiveTab] = useState<QueueTab>('mainQueue');
|
|
|
|
|
@@ -100,7 +100,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
|
|
|
|
|
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
|
|
|
|
|
const [newItemsCount, setNewItemsCount] = useState(0);
|
|
|
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
|
const [profileCache, setProfileCache] = useState<Map<string, any>>(new Map());
|
|
|
|
|
const [entityCache, setEntityCache] = useState<{
|
|
|
|
|
rides: Map<string, any>,
|
|
|
|
|
@@ -192,7 +191,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
|
|
|
|
|
// Enable transitions after initial render
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!loading && items.length > 0 && !hasRenderedOnce) {
|
|
|
|
|
if (loadingState === 'ready' && items.length > 0 && !hasRenderedOnce) {
|
|
|
|
|
// Use requestAnimationFrame to enable transitions AFTER first paint
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
@@ -200,7 +199,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [loading, items.length, hasRenderedOnce]);
|
|
|
|
|
}, [loadingState, items.length, hasRenderedOnce]);
|
|
|
|
|
|
|
|
|
|
const fetchItems = useCallback(async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false, tab: QueueTab = 'mainQueue') => {
|
|
|
|
|
if (!userRef.current) {
|
|
|
|
|
@@ -236,9 +235,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
try {
|
|
|
|
|
// Set loading states
|
|
|
|
|
if (!silent) {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
} else {
|
|
|
|
|
setIsRefreshing(true);
|
|
|
|
|
setLoadingState('refreshing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build base query for content submissions
|
|
|
|
|
@@ -681,15 +680,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
fetchInProgressRef.current = false;
|
|
|
|
|
setLoading(false);
|
|
|
|
|
setIsRefreshing(false);
|
|
|
|
|
setIsInitialLoad(false);
|
|
|
|
|
setLoadingState('ready');
|
|
|
|
|
}
|
|
|
|
|
}, []); // Empty deps - use refs instead
|
|
|
|
|
|
|
|
|
|
// Debounced filters to prevent rapid-fire calls
|
|
|
|
|
const debouncedEntityFilter = useDebounce(activeEntityFilter, 1000);
|
|
|
|
|
const debouncedStatusFilter = useDebounce(activeStatusFilter, 1000);
|
|
|
|
|
const debouncedEntityFilter = useDebounce(activeEntityFilter, 300);
|
|
|
|
|
const debouncedStatusFilter = useDebounce(activeStatusFilter, 300);
|
|
|
|
|
|
|
|
|
|
// Store latest filter values in ref to avoid dependency issues
|
|
|
|
|
const filtersRef = useRef({ entityFilter: debouncedEntityFilter, statusFilter: debouncedStatusFilter });
|
|
|
|
|
@@ -760,7 +757,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
// Polling for auto-refresh (only if realtime is disabled)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// STRICT CHECK: Only enable polling if explicitly disabled
|
|
|
|
|
if (!user || refreshMode !== 'auto' || isInitialLoad || useRealtimeQueue) {
|
|
|
|
|
if (!user || refreshMode !== 'auto' || loadingState === 'initial' || useRealtimeQueue) {
|
|
|
|
|
if (useRealtimeQueue && refreshMode === 'auto') {
|
|
|
|
|
console.log('✅ Polling DISABLED - using realtime subscriptions');
|
|
|
|
|
}
|
|
|
|
|
@@ -778,7 +775,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
console.log('🛑 Polling stopped');
|
|
|
|
|
};
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [user, refreshMode, pollInterval, isInitialLoad, useRealtimeQueue]);
|
|
|
|
|
}, [user, refreshMode, pollInterval, loadingState, useRealtimeQueue]);
|
|
|
|
|
|
|
|
|
|
// Real-time subscription for NEW submissions (replaces polling)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
@@ -1199,22 +1196,28 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
const shouldRemove = (activeStatusFilter === 'pending' || activeStatusFilter === 'flagged') &&
|
|
|
|
|
(action === 'approved' || action === 'rejected');
|
|
|
|
|
|
|
|
|
|
// Optimistic UI update - batch with requestAnimationFrame for smoother rendering
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
if (shouldRemove) {
|
|
|
|
|
// Optimistic UI update with smooth exit animation
|
|
|
|
|
if (shouldRemove) {
|
|
|
|
|
// Step 1: Mark item as "removing" for exit animation
|
|
|
|
|
setItems(prev => prev.map(i =>
|
|
|
|
|
i.id === item.id ? { ...i, _removing: true } : i
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// Step 2: Wait for exit animation (300ms), then remove
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setItems(prev => prev.filter(i => i.id !== item.id));
|
|
|
|
|
|
|
|
|
|
// Mark as recently removed - ignore realtime updates for 10 seconds
|
|
|
|
|
recentlyRemovedRef.current.add(item.id);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
recentlyRemovedRef.current.delete(item.id);
|
|
|
|
|
}, 10000); // Increased from 3000
|
|
|
|
|
} else {
|
|
|
|
|
setItems(prev => prev.map(i =>
|
|
|
|
|
i.id === item.id ? { ...i, status: action } : i
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 10000);
|
|
|
|
|
}, 300);
|
|
|
|
|
} else {
|
|
|
|
|
setItems(prev => prev.map(i =>
|
|
|
|
|
i.id === item.id ? { ...i, status: action } : i
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Release lock if this submission is claimed by current user
|
|
|
|
|
if (queue.currentLock?.submissionId === item.id) {
|
|
|
|
|
@@ -1836,9 +1839,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const QueueContent = () => {
|
|
|
|
|
// Show skeleton during initial load OR during mounting phase
|
|
|
|
|
if ((isInitialLoad && loading) || (isMountingRef.current && !initialFetchCompleteRef.current)) {
|
|
|
|
|
return <QueueSkeleton count={5} />;
|
|
|
|
|
// Show skeleton during ANY loading state (except refreshing)
|
|
|
|
|
if (loadingState === 'initial' || loadingState === 'loading') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="animate-in fade-in-50 duration-200">
|
|
|
|
|
<QueueSkeleton count={Math.min(pageSize, 10)} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
@@ -1859,35 +1866,47 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
}, [items, sortConfig]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="flex flex-col gap-6"
|
|
|
|
|
data-initial-load={!hasRenderedOnce ? "true" : "false"}
|
|
|
|
|
style={{ willChange: 'transform' }}
|
|
|
|
|
>
|
|
|
|
|
{sortedItems.map((item) => (
|
|
|
|
|
<QueueItem
|
|
|
|
|
<div
|
|
|
|
|
className="flex flex-col gap-6 transition-opacity duration-300 ease-in-out queue-fade-enter"
|
|
|
|
|
data-initial-load={!hasRenderedOnce ? "true" : "false"}
|
|
|
|
|
style={{
|
|
|
|
|
willChange: 'transform, opacity'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{sortedItems.map((item, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={item.id}
|
|
|
|
|
item={item}
|
|
|
|
|
isMobile={isMobile}
|
|
|
|
|
actionLoading={actionLoading}
|
|
|
|
|
lockedSubmissions={lockedSubmissions}
|
|
|
|
|
currentLockSubmissionId={queue.currentLock?.submissionId}
|
|
|
|
|
notes={notes}
|
|
|
|
|
isAdmin={isAdmin()}
|
|
|
|
|
isSuperuser={isSuperuser()}
|
|
|
|
|
queueIsLoading={queue.isLoading}
|
|
|
|
|
isInitialRender={!hasRenderedOnce}
|
|
|
|
|
onNoteChange={handleNoteChange}
|
|
|
|
|
onApprove={handleModerationAction}
|
|
|
|
|
onResetToPending={handleResetToPending}
|
|
|
|
|
onRetryFailed={handleRetryFailedItems}
|
|
|
|
|
onOpenPhotos={handleOpenPhotos}
|
|
|
|
|
onOpenReviewManager={handleOpenReviewManager}
|
|
|
|
|
onClaimSubmission={(id) => queue.claimSubmission(id)}
|
|
|
|
|
onDeleteSubmission={handleDeleteSubmission}
|
|
|
|
|
onInteractionFocus={handleInteractionFocus}
|
|
|
|
|
onInteractionBlur={handleInteractionBlur}
|
|
|
|
|
/>
|
|
|
|
|
className="animate-in fade-in-0 slide-in-from-bottom-2"
|
|
|
|
|
style={{
|
|
|
|
|
animationDelay: hasRenderedOnce ? `${index * 30}ms` : '0ms',
|
|
|
|
|
animationDuration: '250ms',
|
|
|
|
|
animationFillMode: 'backwards'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<QueueItem
|
|
|
|
|
key={item.id}
|
|
|
|
|
item={item}
|
|
|
|
|
isMobile={isMobile}
|
|
|
|
|
actionLoading={actionLoading}
|
|
|
|
|
lockedSubmissions={lockedSubmissions}
|
|
|
|
|
currentLockSubmissionId={queue.currentLock?.submissionId}
|
|
|
|
|
notes={notes}
|
|
|
|
|
isAdmin={isAdmin()}
|
|
|
|
|
isSuperuser={isSuperuser()}
|
|
|
|
|
queueIsLoading={queue.isLoading}
|
|
|
|
|
isInitialRender={!hasRenderedOnce}
|
|
|
|
|
onNoteChange={handleNoteChange}
|
|
|
|
|
onApprove={handleModerationAction}
|
|
|
|
|
onResetToPending={handleResetToPending}
|
|
|
|
|
onRetryFailed={handleRetryFailedItems}
|
|
|
|
|
onOpenPhotos={handleOpenPhotos}
|
|
|
|
|
onOpenReviewManager={handleOpenReviewManager}
|
|
|
|
|
onClaimSubmission={(id) => queue.claimSubmission(id)}
|
|
|
|
|
onDeleteSubmission={handleDeleteSubmission}
|
|
|
|
|
onInteractionFocus={handleInteractionFocus}
|
|
|
|
|
onInteractionBlur={handleInteractionBlur}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
@@ -2010,7 +2029,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<div className={`flex gap-4 flex-1 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}>
|
|
|
|
|
<div className={`space-y-2 ${isMobile ? 'w-full' : 'min-w-[140px]'}`}>
|
|
|
|
|
<Label className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>Entity Type</Label>
|
|
|
|
|
<Select value={activeEntityFilter} onValueChange={(value) => setActiveEntityFilter(value as EntityFilter)}>
|
|
|
|
|
<Select
|
|
|
|
|
value={activeEntityFilter}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setActiveEntityFilter(value as EntityFilter);
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className={isMobile ? "h-10" : ""}>
|
|
|
|
|
<SelectValue>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
@@ -2050,7 +2075,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
|
|
|
|
|
<div className={`space-y-2 ${isMobile ? 'w-full' : 'min-w-[120px]'}`}>
|
|
|
|
|
<Label className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>Status</Label>
|
|
|
|
|
<Select value={activeStatusFilter} onValueChange={(value) => setActiveStatusFilter(value as StatusFilter)}>
|
|
|
|
|
<Select
|
|
|
|
|
value={activeStatusFilter}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setActiveStatusFilter(value as StatusFilter);
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className={isMobile ? "h-10" : ""}>
|
|
|
|
|
<SelectValue>
|
|
|
|
|
<span className="capitalize">{activeStatusFilter === 'all' ? 'All Status' : activeStatusFilter}</span>
|
|
|
|
|
@@ -2174,12 +2205,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
variant="default"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// Merge pending new items into the main queue at the top
|
|
|
|
|
// Smooth merge with loading state
|
|
|
|
|
if (pendingNewItems.length > 0) {
|
|
|
|
|
setItems(prev => [...pendingNewItems, ...prev]);
|
|
|
|
|
setPendingNewItems([]);
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
|
|
|
|
|
// After 150ms, merge items
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setItems(prev => [...pendingNewItems, ...prev]);
|
|
|
|
|
setPendingNewItems([]);
|
|
|
|
|
setNewItemsCount(0);
|
|
|
|
|
|
|
|
|
|
// Show content again after brief pause
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setLoadingState('ready');
|
|
|
|
|
}, 100);
|
|
|
|
|
}, 150);
|
|
|
|
|
}
|
|
|
|
|
setNewItemsCount(0);
|
|
|
|
|
console.log('✅ New items merged into queue');
|
|
|
|
|
}}
|
|
|
|
|
className="ml-4"
|
|
|
|
|
@@ -2196,7 +2237,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<QueueContent />
|
|
|
|
|
|
|
|
|
|
{/* Pagination Controls */}
|
|
|
|
|
{totalPages > 1 && !loading && (
|
|
|
|
|
{totalPages > 1 && loadingState === 'ready' && (
|
|
|
|
|
<div className="flex items-center justify-between border-t pt-4 mt-6">
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
<span>
|
|
|
|
|
@@ -2208,8 +2249,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<Select
|
|
|
|
|
value={pageSize.toString()}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setPageSize(parseInt(value));
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-[120px] h-8">
|
|
|
|
|
@@ -2231,7 +2274,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(p => Math.max(1, p - 1));
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
>
|
|
|
|
|
Previous
|
|
|
|
|
@@ -2242,7 +2289,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(p => Math.min(totalPages, p + 1));
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
@@ -2253,7 +2304,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<PaginationContent>
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
<PaginationPrevious
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(p => Math.max(1, p - 1));
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
|
|
|
|
|
/>
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
@@ -2261,7 +2316,14 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
{currentPage > 3 && (
|
|
|
|
|
<>
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
<PaginationLink onClick={() => setCurrentPage(1)} isActive={currentPage === 1}>
|
|
|
|
|
<PaginationLink
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
isActive={currentPage === 1}
|
|
|
|
|
>
|
|
|
|
|
1
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
@@ -2274,7 +2336,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
.map(page => (
|
|
|
|
|
<PaginationItem key={page}>
|
|
|
|
|
<PaginationLink
|
|
|
|
|
onClick={() => setCurrentPage(page)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(page);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
isActive={currentPage === page}
|
|
|
|
|
>
|
|
|
|
|
{page}
|
|
|
|
|
@@ -2287,7 +2353,14 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
<>
|
|
|
|
|
{currentPage < totalPages - 3 && <PaginationEllipsis />}
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
<PaginationLink onClick={() => setCurrentPage(totalPages)} isActive={currentPage === totalPages}>
|
|
|
|
|
<PaginationLink
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(totalPages);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
isActive={currentPage === totalPages}
|
|
|
|
|
>
|
|
|
|
|
{totalPages}
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
@@ -2296,7 +2369,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
|
|
|
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
<PaginationNext
|
|
|
|
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLoadingState('loading');
|
|
|
|
|
setCurrentPage(p => Math.min(totalPages, p + 1));
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
|
|
|
|
|
/>
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|