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(({
>
)}
-
-
+
- {actionLoading === item.id ? (
- <>
-
- Processing...
- >
- ) : (
- <>
-
- Reject
- >
- )}
-
+ 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 (
+
+ );
+};