diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 12be5670..670c6656 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -322,6 +322,8 @@ export const ModerationQueue = forwardRef {/* Active Filters Display */} diff --git a/src/components/moderation/NewItemsAlert.tsx b/src/components/moderation/NewItemsAlert.tsx index 8c5b8a89..03164aa4 100644 --- a/src/components/moderation/NewItemsAlert.tsx +++ b/src/components/moderation/NewItemsAlert.tsx @@ -1,6 +1,6 @@ -import { AlertCircle, RefreshCw } from 'lucide-react'; +import { AlertCircle } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; +import { ShowNewItemsButton } from './show-new-items-button'; interface NewItemsAlertProps { count: number; @@ -18,15 +18,10 @@ export const NewItemsAlert = ({ count, onShowNewItems, visible = true }: NewItem 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 index 1b34771d..188068bd 100644 --- a/src/components/moderation/QueueFilters.tsx +++ b/src/components/moderation/QueueFilters.tsx @@ -4,6 +4,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { RefreshButton } from '@/components/ui/refresh-button'; import { QueueSortControls } from './QueueSortControls'; import { useFilterPanelState } from '@/hooks/useFilterPanelState'; import type { EntityFilter, StatusFilter, SortConfig } from '@/types/moderation'; @@ -19,6 +20,8 @@ interface QueueFiltersProps { onSortChange: (config: SortConfig) => void; onClearFilters: () => void; showClearButton: boolean; + onRefresh?: () => void; + isRefreshing?: boolean; } const getEntityFilterIcon = (filter: EntityFilter) => { @@ -40,7 +43,9 @@ export const QueueFilters = ({ onStatusFilterChange, onSortChange, onClearFilters, - showClearButton + showClearButton, + onRefresh, + isRefreshing = false }: QueueFiltersProps) => { const { isCollapsed, toggle } = useFilterPanelState(); @@ -189,19 +194,28 @@ export const QueueFilters = ({ - {/* Clear Filters Button (desktop only) */} - {!isMobile && showClearButton && ( -
- + {/* Clear Filters & Manual Refresh (desktop only) */} + {!isMobile && (showClearButton || onRefresh) && ( +
+ {onRefresh && ( + + )} + {showClearButton && ( + + )}
)}
diff --git a/src/components/moderation/ValidationSummary.tsx b/src/components/moderation/ValidationSummary.tsx index 18583dd2..f4e20ec2 100644 --- a/src/components/moderation/ValidationSummary.tsx +++ b/src/components/moderation/ValidationSummary.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useMemo } from 'react'; -import { AlertCircle, CheckCircle, Info, AlertTriangle, RefreshCw } from 'lucide-react'; +import { AlertCircle, CheckCircle, Info, AlertTriangle } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; +import { RefreshButton } from '@/components/ui/refresh-button'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { validateEntityData, ValidationResult } from '@/lib/entityValidationSchemas'; @@ -25,6 +25,7 @@ export function ValidationSummary({ item, onValidationChange, compact = false, v const [isLoading, setIsLoading] = useState(true); const [isExpanded, setIsExpanded] = useState(false); const [manualTriggerCount, setManualTriggerCount] = useState(0); + const [isRevalidating, setIsRevalidating] = useState(false); // Helper to extract the correct entity ID based on entity type const getEntityId = ( @@ -225,15 +226,24 @@ export function ValidationSummary({ item, onValidationChange, compact = false, v )} - + {/* Detailed Issues */} diff --git a/src/components/moderation/renderers/QueueItemActions.tsx b/src/components/moderation/renderers/QueueItemActions.tsx index 7ad80ae1..bd46f3ac 100644 --- a/src/components/moderation/renderers/QueueItemActions.tsx +++ b/src/components/moderation/renderers/QueueItemActions.tsx @@ -1,10 +1,10 @@ import { memo, useCallback } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { - CheckCircle, XCircle, RefreshCw, AlertCircle, Lock, Trash2, - Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar + AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { ActionButton } from '@/components/ui/action-button'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; @@ -159,24 +159,14 @@ export const QueueItemActions = memo(({
Claim this submission to lock it for 15 minutes while you review - + />
@@ -274,46 +264,29 @@ export const QueueItemActions = memo(({ )} - - + isMobile={isMobile} + /> @@ -329,15 +302,13 @@ export const QueueItemActions = memo(({

You can reset it to pending to re-review and approve it.

- + /> )} @@ -361,23 +332,22 @@ export const QueueItemActions = memo(({ Review Items - - + isLoading={actionLoading === item.id} + className="flex-1" + /> )} @@ -461,27 +431,30 @@ export const QueueItemActions = memo(({ />
{item.status === 'approved' && ( - + )} {item.status === 'rejected' && ( - + )}
@@ -491,16 +464,17 @@ export const QueueItemActions = memo(({ {/* Delete button for rejected submissions (admin/superadmin only) */} {item.status === 'rejected' && item.type === 'content_submission' && (isAdmin || isSuperuser) && (
- +
)} diff --git a/src/components/moderation/show-new-items-button.tsx b/src/components/moderation/show-new-items-button.tsx new file mode 100644 index 00000000..665dca2f --- /dev/null +++ b/src/components/moderation/show-new-items-button.tsx @@ -0,0 +1,30 @@ +import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ShowNewItemsButtonProps { + count: number; + onShow: () => void | Promise; + isLoading?: boolean; +} + +export const ShowNewItemsButton = ({ + count, + onShow, + isLoading = false +}: ShowNewItemsButtonProps) => { + const itemText = count === 1 ? 'item' : 'items'; + + return ( + + ); +}; diff --git a/src/components/ui/action-button.tsx b/src/components/ui/action-button.tsx new file mode 100644 index 00000000..edc48d30 --- /dev/null +++ b/src/components/ui/action-button.tsx @@ -0,0 +1,105 @@ +import { CheckCircle, XCircle, Lock, RefreshCw, Trash2 } from 'lucide-react'; +import { Button, ButtonProps } from './button'; + +type ActionType = 'approve' | 'reject' | 'delete' | 'claim' | 'reset' | 'retry'; + +interface ActionConfig { + icon: React.ReactNode; + defaultLabel: string; + loadingText: string; + variant: ButtonProps['variant']; + className?: string; +} + +const ACTION_CONFIGS: Record = { + approve: { + icon: , + defaultLabel: 'Approve', + loadingText: 'Processing...', + variant: 'default', + }, + reject: { + icon: , + defaultLabel: 'Reject', + loadingText: 'Processing...', + variant: 'destructive', + }, + claim: { + icon: , + defaultLabel: 'Claim Submission', + loadingText: 'Claiming...', + variant: 'default', + }, + reset: { + icon: , + defaultLabel: 'Reset to Pending', + loadingText: 'Resetting...', + variant: 'outline', + }, + retry: { + icon: , + defaultLabel: 'Retry Failed', + loadingText: 'Retrying...', + variant: 'default', + className: 'bg-yellow-600 hover:bg-yellow-700', + }, + delete: { + icon: , + defaultLabel: 'Delete', + loadingText: 'Deleting...', + variant: 'destructive', + }, +}; + +interface ActionButtonProps extends Omit { + action: ActionType; + isLoading?: boolean; + loadingText?: string; + variant?: ButtonProps['variant']; + children?: React.ReactNode; + isMobile?: boolean; +} + +export const ActionButton = ({ + action, + isLoading = false, + loadingText, + variant, + size = 'default', + className, + children, + isMobile = false, + ...props +}: ActionButtonProps) => { + const config = ACTION_CONFIGS[action]; + const iconClassName = isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"; + + // Clone the icon with mobile-appropriate size + const icon = isMobile && config.icon + ? // Dynamic sizing handled per action below + : config.icon; + + return ( + + ); +}; diff --git a/src/components/ui/icon-button.tsx b/src/components/ui/icon-button.tsx new file mode 100644 index 00000000..04f5ea8d --- /dev/null +++ b/src/components/ui/icon-button.tsx @@ -0,0 +1,28 @@ +import { Button, ButtonProps } from './button'; + +interface IconButtonProps extends Omit { + icon: React.ReactNode; + label: string; // For accessibility + isLoading?: boolean; +} + +export const IconButton = ({ + icon, + label, + isLoading = false, + variant = 'ghost', + size = 'icon', + ...props +}: IconButtonProps) => { + return ( + + ); +}; diff --git a/src/components/ui/refresh-button.tsx b/src/components/ui/refresh-button.tsx new file mode 100644 index 00000000..a561d5fe --- /dev/null +++ b/src/components/ui/refresh-button.tsx @@ -0,0 +1,34 @@ +import { RefreshCw } from 'lucide-react'; +import { Button, ButtonProps } from './button'; + +interface RefreshButtonProps extends Omit { + onRefresh: () => void | Promise; + isLoading?: boolean; + children?: React.ReactNode; +} + +export const RefreshButton = ({ + onRefresh, + isLoading = false, + size = 'default', + variant = 'outline', + children = 'Refresh', + className, + ...props +}: RefreshButtonProps) => { + return ( + + ); +};