diff --git a/src/components/moderation/ActiveFiltersDisplay.tsx b/src/components/moderation/ActiveFiltersDisplay.tsx
new file mode 100644
index 00000000..174a9b03
--- /dev/null
+++ b/src/components/moderation/ActiveFiltersDisplay.tsx
@@ -0,0 +1,71 @@
+import { Filter, MessageSquare, FileText, Image, ArrowUp, ArrowDown } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import type { EntityFilter, StatusFilter, SortConfig, SortField } from '@/types/moderation';
+
+interface ActiveFiltersDisplayProps {
+ entityFilter: EntityFilter;
+ statusFilter: StatusFilter;
+ sortConfig: SortConfig;
+ defaultEntityFilter?: EntityFilter;
+ defaultStatusFilter?: StatusFilter;
+ defaultSortField?: SortField;
+}
+
+const getEntityFilterIcon = (filter: EntityFilter) => {
+ switch (filter) {
+ case 'reviews': return ;
+ case 'submissions': return ;
+ case 'photos': return ;
+ default: return ;
+ }
+};
+
+const getSortFieldLabel = (field: SortField): string => {
+ switch (field) {
+ case 'username': return 'Submitter';
+ case 'submission_type': return 'Type';
+ case 'escalated': return 'Escalated';
+ case 'status': return 'Status';
+ case 'created_at': return 'Date';
+ default: return field;
+ }
+};
+
+export const ActiveFiltersDisplay = ({
+ entityFilter,
+ statusFilter,
+ sortConfig,
+ defaultEntityFilter = 'all',
+ defaultStatusFilter = 'pending',
+ defaultSortField = 'created_at'
+}: ActiveFiltersDisplayProps) => {
+ const hasActiveFilters =
+ entityFilter !== defaultEntityFilter ||
+ statusFilter !== defaultStatusFilter ||
+ sortConfig.field !== defaultSortField;
+
+ if (!hasActiveFilters) return null;
+
+ return (
+
+
Active filters:
+ {entityFilter !== defaultEntityFilter && (
+
+ {getEntityFilterIcon(entityFilter)}
+ {entityFilter}
+
+ )}
+ {statusFilter !== defaultStatusFilter && (
+
+ {statusFilter}
+
+ )}
+ {sortConfig.field !== defaultSortField && (
+
+ {sortConfig.direction === 'asc' ? : }
+ Sort: {getSortFieldLabel(sortConfig.field)}
+
+ )}
+
+ );
+};
diff --git a/src/components/moderation/AutoRefreshIndicator.tsx b/src/components/moderation/AutoRefreshIndicator.tsx
new file mode 100644
index 00000000..5343132c
--- /dev/null
+++ b/src/components/moderation/AutoRefreshIndicator.tsx
@@ -0,0 +1,24 @@
+interface AutoRefreshIndicatorProps {
+ enabled: boolean;
+ intervalSeconds: number;
+ mode?: 'polling' | 'realtime';
+}
+
+export const AutoRefreshIndicator = ({
+ enabled,
+ intervalSeconds,
+ mode = 'polling'
+}: AutoRefreshIndicatorProps) => {
+ if (!enabled) return null;
+
+ return (
+
+
+
+
Auto-refresh active
+
+
•
+
Checking every {intervalSeconds}s
+
+ );
+};
diff --git a/src/components/moderation/EmptyQueueState.tsx b/src/components/moderation/EmptyQueueState.tsx
new file mode 100644
index 00000000..efbca20f
--- /dev/null
+++ b/src/components/moderation/EmptyQueueState.tsx
@@ -0,0 +1,51 @@
+import { CheckCircle, LucideIcon } from 'lucide-react';
+import type { EntityFilter, StatusFilter } from '@/types/moderation';
+
+interface EmptyQueueStateProps {
+ entityFilter: EntityFilter;
+ statusFilter: StatusFilter;
+ icon?: LucideIcon;
+ title?: string;
+ customMessage?: string;
+}
+
+const getEmptyStateMessage = (entityFilter: EntityFilter, statusFilter: StatusFilter): string => {
+ const entityLabel = entityFilter === 'all' ? 'items' :
+ entityFilter === 'reviews' ? 'reviews' :
+ entityFilter === 'photos' ? 'photos' : 'submissions';
+
+ switch (statusFilter) {
+ case 'pending':
+ return `No pending ${entityLabel} require moderation at this time.`;
+ case 'partially_approved':
+ return `No partially approved ${entityLabel} found.`;
+ case 'flagged':
+ return `No flagged ${entityLabel} found.`;
+ case 'approved':
+ return `No approved ${entityLabel} found.`;
+ case 'rejected':
+ return `No rejected ${entityLabel} found.`;
+ case 'all':
+ return `No ${entityLabel} found.`;
+ default:
+ return `No ${entityLabel} found for the selected filter.`;
+ }
+};
+
+export const EmptyQueueState = ({
+ entityFilter,
+ statusFilter,
+ icon: Icon = CheckCircle,
+ title = 'No items found',
+ customMessage
+}: EmptyQueueStateProps) => {
+ const message = customMessage || getEmptyStateMessage(entityFilter, statusFilter);
+
+ return (
+
+
+
{title}
+
{message}
+
+ );
+};
diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx
index 570832ee..ad7bd76b 100644
--- a/src/components/moderation/ModerationQueue.tsx
+++ b/src/components/moderation/ModerationQueue.tsx
@@ -1,20 +1,6 @@
import { useState, useEffect, useImperativeHandle, forwardRef, useCallback, useRef, useMemo } from 'react';
-import { CheckCircle, XCircle, Filter, MessageSquare, FileText, Image, X, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap, ArrowUp, ArrowDown } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
+import { CheckCircle, XCircle, AlertTriangle, UserCog, Zap } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
-import { Label } from '@/components/ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import {
- Pagination,
- PaginationContent,
- PaginationEllipsis,
- PaginationItem,
- PaginationLink,
- PaginationNext,
- PaginationPrevious,
-} from '@/components/ui/pagination';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { useUserRole } from '@/hooks/useUserRole';
@@ -25,8 +11,6 @@ import { SubmissionReviewManager } from './SubmissionReviewManager';
import { useIsMobile } from '@/hooks/use-mobile';
import { useAdminSettings } from '@/hooks/useAdminSettings';
import { useModerationQueue } from '@/hooks/useModerationQueue';
-import { Progress } from '@/components/ui/progress';
-import { QueueStatsDashboard } from './QueueStatsDashboard';
import { EscalationDialog } from './EscalationDialog';
import { ReassignDialog } from './ReassignDialog';
import { smartMergeArray } from '@/lib/smartStateUpdate';
@@ -35,6 +19,13 @@ 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 {
ModerationItem,
EntityFilter,
@@ -1818,28 +1809,6 @@ export const ModerationQueue = forwardRef((props, ref) => {
}
};
- const getEmptyStateMessage = (entityFilter: EntityFilter, statusFilter: StatusFilter) => {
- const entityLabel = entityFilter === 'all' ? 'items' :
- entityFilter === 'reviews' ? 'reviews' :
- entityFilter === 'photos' ? 'photos' : 'submissions';
-
- switch (statusFilter) {
- case 'pending':
- return `No pending ${entityLabel} require moderation at this time.`;
- case 'partially_approved':
- return `No partially approved ${entityLabel} found.`;
- case 'flagged':
- return `No flagged ${entityLabel} found.`;
- case 'approved':
- return `No approved ${entityLabel} found.`;
- case 'rejected':
- return `No rejected ${entityLabel} found.`;
- case 'all':
- return `No ${entityLabel} found.`;
- default:
- return `No ${entityLabel} found for the selected filter.`;
- }
- };
// Sort items function
const sortItems = useCallback((items: ModerationItem[], config: SortConfig): ModerationItem[] => {
@@ -1932,13 +1901,10 @@ export const ModerationQueue = forwardRef((props, ref) => {
if (items.length === 0) {
return (
-
-
-
No items found
-
- {getEmptyStateMessage(activeEntityFilter, activeStatusFilter)}
-
-
+
);
}
@@ -2002,15 +1968,34 @@ export const ModerationQueue = forwardRef((props, ref) => {
setSortConfig({ field: 'created_at', direction: 'asc' });
};
- const getEntityFilterIcon = (filter: EntityFilter) => {
- switch (filter) {
- case 'reviews': return ;
- case 'submissions': return ;
- case 'photos': return ;
- default: return ;
+ const handleEntityFilterChange = (filter: EntityFilter) => {
+ setActiveEntityFilter(filter);
+ setLoadingState('loading');
+ };
+
+ const handleStatusFilterChange = (filter: StatusFilter) => {
+ setActiveStatusFilter(filter);
+ setLoadingState('loading');
+ };
+
+ const handleSortConfigChange = (config: SortConfig) => {
+ setSortConfig(config);
+ };
+
+ const handleShowNewItems = () => {
+ if (pendingNewItems.length > 0) {
+ setItems(prev => [...pendingNewItems, ...prev]);
+ setPendingNewItems([]);
+ setNewItemsCount(0);
+ console.log('✅ New items merged into queue:', pendingNewItems.length);
}
};
+ const hasActiveFilters =
+ activeEntityFilter !== 'all' ||
+ activeStatusFilter !== 'pending' ||
+ sortConfig.field !== 'created_at';
+
// Handle claim next action
const handleClaimNext = async () => {
await queue.claimNext();
@@ -2024,25 +2009,7 @@ export const ModerationQueue = forwardRef((props, ref) => {
- {/* Stats Grid */}
-
-
-
{queue.queueStats.pendingCount}
-
Pending
-
-
-
{queue.queueStats.assignedToMe}
-
Assigned to Me
-
-
-
- {queue.queueStats.avgWaitHours.toFixed(1)}h
-
-
Avg Wait
-
-
-
- {/* Claim/Lock Status */}
+
((props, ref) => {
)}
{/* Filter Bar */}
-
-
-
Moderation Queue
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending' || sortConfig.field !== 'created_at') && (
-
-
-
- )}
-
+
{/* Active Filters Display */}
- {(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending' || sortConfig.field !== 'created_at') && (
-
-
Active filters:
- {activeEntityFilter !== 'all' && (
-
- {getEntityFilterIcon(activeEntityFilter)}
- {activeEntityFilter}
-
- )}
- {activeStatusFilter !== 'pending' && (
-
- {activeStatusFilter}
-
- )}
- {sortConfig.field !== 'created_at' && (
-
- {sortConfig.direction === 'asc' ? : }
- Sort: {sortConfig.field === 'username' ? 'Submitter' :
- sortConfig.field === 'submission_type' ? 'Type' :
- sortConfig.field === 'escalated' ? 'Escalated' :
- sortConfig.field === 'status' ? 'Status' : 'Date'}
-
- )}
-
- )}
+
{/* Auto-refresh Status Indicator */}
- {refreshMode === 'auto' && (
-
-
-
-
Auto-refresh active
-
-
•
-
Checking every {Math.round(pollInterval / 1000)}s
-
- )}
+
- {/* New Items Notification - Enhanced */}
- {newItemsCount > 0 && (
-
-
-
- New Items Available
-
- {newItemsCount} new {newItemsCount === 1 ? 'submission' : 'submissions'} pending review
-
-
-
-
- )}
+ {/* New Items Notification */}
+
{/* Queue Content */}
{/* Pagination Controls */}
- {totalPages > 1 && loadingState === 'ready' && (
-
-
-
- Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, totalCount)} of {totalCount} items
-
- {!isMobile && (
- <>
- •
-
- >
- )}
-
-
- {isMobile ? (
-
-
-
- Page {currentPage} of {totalPages}
-
-
-
- ) : (
-
-
-
- {
- 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'}
- />
-
-
- {currentPage > 3 && (
- <>
-
- {
- setLoadingState('loading');
- setCurrentPage(1);
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }}
- isActive={currentPage === 1}
- >
- 1
-
-
- {currentPage > 4 && }
- >
- )}
-
- {Array.from({ length: totalPages }, (_, i) => i + 1)
- .filter(page => page >= currentPage - 2 && page <= currentPage + 2)
- .map(page => (
-
- {
- setLoadingState('loading');
- setCurrentPage(page);
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }}
- isActive={currentPage === page}
- >
- {page}
-
-
- ))
- }
-
- {currentPage < totalPages - 2 && (
- <>
- {currentPage < totalPages - 3 && }
-
- {
- setLoadingState('loading');
- setCurrentPage(totalPages);
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }}
- isActive={currentPage === totalPages}
- >
- {totalPages}
-
-
- >
- )}
-
-
- {
- 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'}
- />
-
-
-
- )}
-
+ {loadingState === 'ready' && (
+ {
+ setLoadingState('loading');
+ setCurrentPage(page);
+ }}
+ onPageSizeChange={(size) => {
+ setLoadingState('loading');
+ setPageSize(size);
+ setCurrentPage(1);
+ }}
+ />
)}
{/* Photo Modal */}
diff --git a/src/components/moderation/NewItemsAlert.tsx b/src/components/moderation/NewItemsAlert.tsx
new file mode 100644
index 00000000..8c5b8a89
--- /dev/null
+++ b/src/components/moderation/NewItemsAlert.tsx
@@ -0,0 +1,34 @@
+import { AlertCircle, RefreshCw } from 'lucide-react';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { Button } from '@/components/ui/button';
+
+interface NewItemsAlertProps {
+ count: number;
+ onShowNewItems: () => void;
+ visible?: boolean;
+}
+
+export const NewItemsAlert = ({ count, onShowNewItems, visible = true }: NewItemsAlertProps) => {
+ if (!visible || count === 0) return null;
+
+ return (
+
+
+
+ New Items Available
+
+ {count} new {count === 1 ? 'submission' : 'submissions'} pending review
+
+
+
+
+ );
+};
diff --git a/src/components/moderation/QueueFilters.tsx b/src/components/moderation/QueueFilters.tsx
new file mode 100644
index 00000000..c47bee32
--- /dev/null
+++ b/src/components/moderation/QueueFilters.tsx
@@ -0,0 +1,140 @@
+import { Filter, MessageSquare, FileText, Image, X } from 'lucide-react';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import { QueueSortControls } from './QueueSortControls';
+import type { EntityFilter, StatusFilter, SortConfig } from '@/types/moderation';
+
+interface QueueFiltersProps {
+ activeEntityFilter: EntityFilter;
+ activeStatusFilter: StatusFilter;
+ sortConfig: SortConfig;
+ isMobile: boolean;
+ onEntityFilterChange: (filter: EntityFilter) => void;
+ onStatusFilterChange: (filter: StatusFilter) => void;
+ onSortConfigChange: (config: SortConfig) => void;
+ onClearFilters: () => void;
+ showClearButton: boolean;
+}
+
+const getEntityFilterIcon = (filter: EntityFilter) => {
+ switch (filter) {
+ case 'reviews': return ;
+ case 'submissions': return ;
+ case 'photos': return ;
+ default: return ;
+ }
+};
+
+export const QueueFilters = ({
+ activeEntityFilter,
+ activeStatusFilter,
+ sortConfig,
+ isMobile,
+ onEntityFilterChange,
+ onStatusFilterChange,
+ onSortConfigChange,
+ onClearFilters,
+ showClearButton
+}: QueueFiltersProps) => {
+ return (
+
+
+
Moderation Queue
+
+
+
+ {/* Entity Type Filter */}
+
+
+
+
+
+ {/* Status Filter */}
+
+
+
+
+
+ {/* Sort Controls */}
+
+
+
+ {/* Clear Filters Button */}
+ {showClearButton && (
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/moderation/QueuePagination.tsx b/src/components/moderation/QueuePagination.tsx
new file mode 100644
index 00000000..f1950808
--- /dev/null
+++ b/src/components/moderation/QueuePagination.tsx
@@ -0,0 +1,161 @@
+import { Button } from '@/components/ui/button';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from '@/components/ui/pagination';
+
+interface QueuePaginationProps {
+ currentPage: number;
+ totalPages: number;
+ pageSize: number;
+ totalCount: number;
+ isMobile: boolean;
+ onPageChange: (page: number) => void;
+ onPageSizeChange: (size: number) => void;
+}
+
+export const QueuePagination = ({
+ currentPage,
+ totalPages,
+ pageSize,
+ totalCount,
+ isMobile,
+ onPageChange,
+ onPageSizeChange
+}: QueuePaginationProps) => {
+ if (totalPages <= 1) return null;
+
+ const handlePageChange = (page: number) => {
+ onPageChange(page);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const handlePageSizeChange = (size: number) => {
+ onPageSizeChange(size);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const startItem = ((currentPage - 1) * pageSize) + 1;
+ const endItem = Math.min(currentPage * pageSize, totalCount);
+
+ return (
+
+ {/* Item Count & Page Size Selector */}
+
+
+ Showing {startItem} - {endItem} of {totalCount} items
+
+ {!isMobile && (
+ <>
+ •
+
+ >
+ )}
+
+
+ {/* Pagination Controls */}
+ {isMobile ? (
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+ ) : (
+
+
+
+ handlePageChange(Math.max(1, currentPage - 1))}
+ className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
+ />
+
+
+ {currentPage > 3 && (
+ <>
+
+ handlePageChange(1)}
+ isActive={currentPage === 1}
+ >
+ 1
+
+
+ {currentPage > 4 && }
+ >
+ )}
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1)
+ .filter(page => page >= currentPage - 2 && page <= currentPage + 2)
+ .map(page => (
+
+ handlePageChange(page)}
+ isActive={currentPage === page}
+ >
+ {page}
+
+
+ ))
+ }
+
+ {currentPage < totalPages - 2 && (
+ <>
+ {currentPage < totalPages - 3 && }
+
+ handlePageChange(totalPages)}
+ isActive={currentPage === totalPages}
+ >
+ {totalPages}
+
+
+ >
+ )}
+
+
+ handlePageChange(Math.min(totalPages, currentPage + 1))}
+ className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
+ />
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/moderation/QueueSortControls.tsx b/src/components/moderation/QueueSortControls.tsx
new file mode 100644
index 00000000..3e2dd352
--- /dev/null
+++ b/src/components/moderation/QueueSortControls.tsx
@@ -0,0 +1,84 @@
+import { ArrowUp, ArrowDown } from 'lucide-react';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import type { SortConfig, SortField } from '@/types/moderation';
+
+interface QueueSortControlsProps {
+ sortConfig: SortConfig;
+ onSortChange: (config: SortConfig) => void;
+ isMobile?: boolean;
+ variant?: 'inline' | 'standalone';
+ showLabel?: boolean;
+}
+
+const getSortFieldLabel = (field: SortField): string => {
+ switch (field) {
+ case 'created_at': return 'Date Created';
+ case 'username': return 'Submitter';
+ case 'submission_type': return 'Type';
+ case 'status': return 'Status';
+ case 'escalated': return 'Escalated';
+ default: return field;
+ }
+};
+
+export const QueueSortControls = ({
+ sortConfig,
+ onSortChange,
+ isMobile = false,
+ variant = 'inline',
+ showLabel = true
+}: QueueSortControlsProps) => {
+ const handleFieldChange = (field: SortField) => {
+ onSortChange({ ...sortConfig, field });
+ };
+
+ const handleDirectionToggle = () => {
+ onSortChange({
+ ...sortConfig,
+ direction: sortConfig.direction === 'asc' ? 'desc' : 'asc'
+ });
+ };
+
+ return (
+
+ {showLabel && (
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/moderation/QueueStats.tsx b/src/components/moderation/QueueStats.tsx
new file mode 100644
index 00000000..ad756dce
--- /dev/null
+++ b/src/components/moderation/QueueStats.tsx
@@ -0,0 +1,29 @@
+interface QueueStatsProps {
+ stats: {
+ pendingCount: number;
+ assignedToMe: number;
+ avgWaitHours: number;
+ };
+ isMobile?: boolean;
+}
+
+export const QueueStats = ({ stats, isMobile }: QueueStatsProps) => {
+ return (
+
+
+
{stats.pendingCount}
+
Pending
+
+
+
{stats.assignedToMe}
+
Assigned to Me
+
+
+
+ {stats.avgWaitHours.toFixed(1)}h
+
+
Avg Wait
+
+
+ );
+};