mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
228 lines
9.1 KiB
TypeScript
228 lines
9.1 KiB
TypeScript
import { useState, useImperativeHandle, forwardRef, useMemo } from 'react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { useUserRole } from '@/hooks/useUserRole';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { PhotoModal } from './PhotoModal';
|
|
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
|
import { useIsMobile } from '@/hooks/use-mobile';
|
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
|
import { useModerationQueueManager } from '@/hooks/moderation';
|
|
import { QueueItem } from './QueueItem';
|
|
import { QueueSkeleton } from './QueueSkeleton';
|
|
import { LockStatusDisplay } from './LockStatusDisplay';
|
|
import { getLockStatus } from '@/lib/moderation/lockHelpers';
|
|
import { QueueStats } from './QueueStats';
|
|
import { QueueFilters } from './QueueFilters';
|
|
import { ActiveFiltersDisplay } from './ActiveFiltersDisplay';
|
|
import { AutoRefreshIndicator } from './AutoRefreshIndicator';
|
|
import { NewItemsAlert } from './NewItemsAlert';
|
|
import { EmptyQueueState } from './EmptyQueueState';
|
|
import { QueuePagination } from './QueuePagination';
|
|
import type { ModerationQueueRef } from '@/types/moderation';
|
|
import type { PhotoItem } from '@/types/photos';
|
|
|
|
export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|
const isMobile = useIsMobile();
|
|
const { user } = useAuth();
|
|
const { toast } = useToast();
|
|
const { isAdmin, isSuperuser } = useUserRole();
|
|
const adminSettings = useAdminSettings();
|
|
|
|
// Extract settings values to stable primitives for memoization
|
|
const refreshMode = adminSettings.getAdminPanelRefreshMode();
|
|
const pollInterval = adminSettings.getAdminPanelPollInterval();
|
|
const refreshStrategy = adminSettings.getAutoRefreshStrategy();
|
|
const preserveInteraction = adminSettings.getPreserveInteractionState();
|
|
const useRealtimeQueue = adminSettings.getUseRealtimeQueue();
|
|
|
|
// Memoize settings object using stable primitive dependencies
|
|
const settings = useMemo(() => ({
|
|
refreshMode,
|
|
pollInterval,
|
|
refreshStrategy,
|
|
preserveInteraction,
|
|
useRealtimeQueue,
|
|
}), [refreshMode, pollInterval, refreshStrategy, preserveInteraction, useRealtimeQueue]);
|
|
|
|
// Initialize queue manager (replaces all state management, fetchItems, effects)
|
|
const queueManager = useModerationQueueManager({
|
|
user,
|
|
isAdmin: isAdmin(),
|
|
isSuperuser: isSuperuser(),
|
|
toast,
|
|
settings,
|
|
});
|
|
|
|
// UI-only state
|
|
const [notes, setNotes] = useState<Record<string, string>>({});
|
|
const [photoModalOpen, setPhotoModalOpen] = useState(false);
|
|
const [selectedPhotos, setSelectedPhotos] = useState<PhotoItem[]>([]);
|
|
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0);
|
|
const [reviewManagerOpen, setReviewManagerOpen] = useState(false);
|
|
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
|
|
|
// UI-specific handlers
|
|
const handleNoteChange = (id: string, value: string) => {
|
|
setNotes(prev => ({ ...prev, [id]: value }));
|
|
};
|
|
|
|
const handleOpenPhotos = (photos: any[], index: number) => {
|
|
setSelectedPhotos(photos);
|
|
setSelectedPhotoIndex(index);
|
|
setPhotoModalOpen(true);
|
|
};
|
|
|
|
const handleOpenReviewManager = (submissionId: string) => {
|
|
setSelectedSubmissionId(submissionId);
|
|
setReviewManagerOpen(true);
|
|
};
|
|
|
|
// Expose imperative API
|
|
useImperativeHandle(ref, () => ({
|
|
refresh: async () => {
|
|
await queueManager.refresh();
|
|
}
|
|
}));
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Queue Statistics & Lock Status */}
|
|
{queueManager.queue.queueStats && (
|
|
<Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
|
|
<CardContent className="p-4">
|
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
<QueueStats stats={queueManager.queue.queueStats} isMobile={isMobile} />
|
|
<LockStatusDisplay
|
|
currentLock={queueManager.queue.currentLock}
|
|
queueStats={queueManager.queue.queueStats}
|
|
isLoading={queueManager.queue.isLoading}
|
|
onClaimNext={async () => { await queueManager.queue.claimNext(); }}
|
|
onExtendLock={queueManager.queue.extendLock}
|
|
onReleaseLock={queueManager.queue.releaseLock}
|
|
getTimeRemaining={queueManager.queue.getTimeRemaining}
|
|
getLockProgress={queueManager.queue.getLockProgress}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Filter Bar */}
|
|
<QueueFilters
|
|
activeEntityFilter={queueManager.filters.entityFilter}
|
|
activeStatusFilter={queueManager.filters.statusFilter}
|
|
sortConfig={queueManager.filters.sortConfig}
|
|
isMobile={isMobile}
|
|
isLoading={queueManager.loadingState === 'loading'}
|
|
onEntityFilterChange={queueManager.filters.setEntityFilter}
|
|
onStatusFilterChange={queueManager.filters.setStatusFilter}
|
|
onSortChange={queueManager.filters.setSortConfig}
|
|
onClearFilters={queueManager.filters.clearFilters}
|
|
showClearButton={queueManager.filters.hasActiveFilters}
|
|
/>
|
|
|
|
{/* Active Filters Display */}
|
|
{queueManager.filters.hasActiveFilters && (
|
|
<ActiveFiltersDisplay
|
|
entityFilter={queueManager.filters.entityFilter}
|
|
statusFilter={queueManager.filters.statusFilter}
|
|
/>
|
|
)}
|
|
|
|
{/* Auto-refresh Indicator */}
|
|
{adminSettings.getAdminPanelRefreshMode() === 'auto' && (
|
|
<AutoRefreshIndicator
|
|
enabled={true}
|
|
intervalSeconds={Math.round(adminSettings.getAdminPanelPollInterval() / 1000)}
|
|
mode={adminSettings.getUseRealtimeQueue() ? 'realtime' : 'polling'}
|
|
/>
|
|
)}
|
|
|
|
{/* New Items Alert */}
|
|
{queueManager.newItemsCount > 0 && (
|
|
<NewItemsAlert
|
|
count={queueManager.newItemsCount}
|
|
onShowNewItems={queueManager.showNewItems}
|
|
/>
|
|
)}
|
|
|
|
{/* Queue Content */}
|
|
{queueManager.loadingState === 'loading' || queueManager.loadingState === 'initial' ? (
|
|
<QueueSkeleton count={queueManager.pagination.pageSize} />
|
|
) : queueManager.items.length === 0 ? (
|
|
<EmptyQueueState
|
|
entityFilter={queueManager.filters.entityFilter}
|
|
statusFilter={queueManager.filters.statusFilter}
|
|
/>
|
|
) : (
|
|
<TooltipProvider>
|
|
<div className="space-y-6">
|
|
{queueManager.items.map((item, index) => (
|
|
<QueueItem
|
|
key={item.id}
|
|
item={item}
|
|
isMobile={isMobile}
|
|
actionLoading={queueManager.actionLoading}
|
|
isLockedByMe={queueManager.queue.isLockedByMe(item.id)}
|
|
isLockedByOther={queueManager.queue.isLockedByOther(item.id, item.assigned_to, item.locked_until)}
|
|
lockStatus={getLockStatus({ assigned_to: item.assigned_to, locked_until: item.locked_until }, user?.id || '')}
|
|
currentLockSubmissionId={queueManager.queue.currentLock?.submissionId}
|
|
notes={notes}
|
|
isAdmin={isAdmin()}
|
|
isSuperuser={isSuperuser()}
|
|
queueIsLoading={queueManager.queue.isLoading}
|
|
onNoteChange={handleNoteChange}
|
|
onApprove={queueManager.performAction}
|
|
onResetToPending={queueManager.resetToPending}
|
|
onRetryFailed={queueManager.retryFailedItems}
|
|
onOpenPhotos={handleOpenPhotos}
|
|
onOpenReviewManager={handleOpenReviewManager}
|
|
onClaimSubmission={queueManager.queue.claimSubmission}
|
|
onDeleteSubmission={queueManager.deleteSubmission}
|
|
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
|
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</TooltipProvider>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{queueManager.loadingState === 'ready' && queueManager.pagination.totalPages > 1 && (
|
|
<QueuePagination
|
|
currentPage={queueManager.pagination.currentPage}
|
|
totalPages={queueManager.pagination.totalPages}
|
|
pageSize={queueManager.pagination.pageSize}
|
|
totalCount={queueManager.pagination.totalCount}
|
|
isMobile={isMobile}
|
|
onPageChange={queueManager.pagination.setCurrentPage}
|
|
onPageSizeChange={queueManager.pagination.setPageSize}
|
|
/>
|
|
)}
|
|
|
|
{/* Modals */}
|
|
<PhotoModal
|
|
photos={selectedPhotos}
|
|
initialIndex={selectedPhotoIndex}
|
|
isOpen={photoModalOpen}
|
|
onClose={() => setPhotoModalOpen(false)}
|
|
/>
|
|
|
|
{selectedSubmissionId && (
|
|
<SubmissionReviewManager
|
|
submissionId={selectedSubmissionId}
|
|
open={reviewManagerOpen}
|
|
onOpenChange={setReviewManagerOpen}
|
|
onComplete={() => setReviewManagerOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
ModerationQueue.displayName = 'ModerationQueue';
|
|
|
|
export type { ModerationQueueRef } from '@/types/moderation';
|