diff --git a/src/components/moderation/EditHistoryAccordion.tsx b/src/components/moderation/EditHistoryAccordion.tsx new file mode 100644 index 00000000..a35598e2 --- /dev/null +++ b/src/components/moderation/EditHistoryAccordion.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { fetchEditHistory } from '@/lib/submissionItemsService'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; +import { Button } from '@/components/ui/button'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { EditHistoryEntry } from './EditHistoryEntry'; +import { History, Loader2, AlertCircle } from 'lucide-react'; + +interface EditHistoryAccordionProps { + submissionId: string; +} + +const INITIAL_LOAD = 20; +const LOAD_MORE_INCREMENT = 10; + +export function EditHistoryAccordion({ submissionId }: EditHistoryAccordionProps) { + const [limit, setLimit] = useState(INITIAL_LOAD); + + const { data: editHistory, isLoading, error } = useQuery({ + queryKey: ['edit-history', submissionId, limit], + queryFn: async () => { + const { supabase } = await import('@/integrations/supabase/client'); + + // Fetch edit history with user profiles + const { data, error } = await supabase + .from('item_edit_history') + .select(` + id, + item_id, + edited_at, + edited_by, + previous_data, + new_data, + edit_reason, + changed_fields, + profiles:edited_by ( + username, + avatar_url + ) + `) + .eq('item_id', submissionId) + .order('edited_at', { ascending: false }) + .limit(limit); + + if (error) throw error; + return data || []; + }, + staleTime: 5 * 60 * 1000, // 5 minutes + }); + + const loadMore = () => { + setLimit(prev => prev + LOAD_MORE_INCREMENT); + }; + + const hasMore = editHistory && editHistory.length === limit; + + return ( + + + +
+ + Edit History + {editHistory && editHistory.length > 0 && ( + + ({editHistory.length} edit{editHistory.length !== 1 ? 's' : ''}) + + )} +
+
+ + {isLoading && ( +
+ +
+ )} + + {error && ( + + + + Failed to load edit history: {error instanceof Error ? error.message : 'Unknown error'} + + + )} + + {!isLoading && !error && editHistory && editHistory.length === 0 && ( + + + No edit history found for this submission. + + + )} + + {!isLoading && !error && editHistory && editHistory.length > 0 && ( +
+ +
+ {editHistory.map((entry: any) => ( + + ))} +
+
+ + {hasMore && ( +
+ +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/moderation/EditHistoryEntry.tsx b/src/components/moderation/EditHistoryEntry.tsx new file mode 100644 index 00000000..71785033 --- /dev/null +++ b/src/components/moderation/EditHistoryEntry.tsx @@ -0,0 +1,131 @@ +import { formatDistanceToNow } from 'date-fns'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card } from '@/components/ui/card'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { ChevronDown, Edit, User } from 'lucide-react'; +import { useState } from 'react'; + +interface EditHistoryEntryProps { + editId: string; + editorName: string; + editorAvatar?: string; + timestamp: string; + changedFields: string[]; + editReason?: string; + beforeData?: Record; + afterData?: Record; +} + +export function EditHistoryEntry({ + editId, + editorName, + editorAvatar, + timestamp, + changedFields, + editReason, + beforeData, + afterData, +}: EditHistoryEntryProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const getFieldValue = (data: Record | undefined, field: string): string => { + if (!data || !(field in data)) return '—'; + const value = data[field]; + if (value === null || value === undefined) return '—'; + if (typeof value === 'object') return JSON.stringify(value, null, 2); + return String(value); + }; + + return ( + + +
+ {/* Editor Avatar */} + + + + + + + + {/* Edit Info */} +
+
+
+ {editorName} + + + {changedFields.length} field{changedFields.length !== 1 ? 's' : ''} + +
+ + {formatDistanceToNow(new Date(timestamp), { addSuffix: true })} + +
+ + {/* Changed Fields Summary */} +
+ {changedFields.slice(0, 3).map((field) => ( + + {field} + + ))} + {changedFields.length > 3 && ( + + +{changedFields.length - 3} more + + )} +
+ + {/* Edit Reason */} + {editReason && ( +

+ "{editReason}" +

+ )} + + {/* Expand/Collapse Button */} + + + +
+
+ + {/* Detailed Changes */} + + {changedFields.map((field) => { + const beforeValue = getFieldValue(beforeData, field); + const afterValue = getFieldValue(afterData, field); + + return ( +
+
+ {field} +
+
+
+
Before
+
+ {beforeValue} +
+
+
+
After
+
+ {afterValue} +
+
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/components/moderation/QueueFilters.tsx b/src/components/moderation/QueueFilters.tsx index 8804a125..1b34771d 100644 --- a/src/components/moderation/QueueFilters.tsx +++ b/src/components/moderation/QueueFilters.tsx @@ -1,8 +1,11 @@ -import { Filter, MessageSquare, FileText, Image, X } from 'lucide-react'; +import { Filter, MessageSquare, FileText, Image, X, ChevronDown } 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 { Badge } from '@/components/ui/badge'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { QueueSortControls } from './QueueSortControls'; +import { useFilterPanelState } from '@/hooks/useFilterPanelState'; import type { EntityFilter, StatusFilter, SortConfig } from '@/types/moderation'; interface QueueFiltersProps { @@ -39,107 +42,161 @@ export const QueueFilters = ({ onClearFilters, showClearButton }: QueueFiltersProps) => { + const { isCollapsed, toggle } = useFilterPanelState(); + + // Count active filters + const activeFilterCount = [ + activeEntityFilter !== 'all' ? 1 : 0, + activeStatusFilter !== 'all' ? 1 : 0, + ].reduce((sum, val) => sum + val, 0); + return ( -
-
-

Moderation Queue

-
- -
- {/* Entity Type Filter */} -
- - +
+ toggle()}> + {/* Header with collapse trigger on mobile */} +
+
+

Moderation Queue

+ {isCollapsed && activeFilterCount > 0 && ( + + {activeFilterCount} active + + )} +
+ {isMobile && ( + + + + )}
- {/* Status Filter */} -
- - + + +
+ {getEntityFilterIcon(activeEntityFilter)} + {activeEntityFilter === 'all' ? 'All Items' : activeEntityFilter} +
+
+
+ + +
+ + All Items +
+
+ +
+ + Reviews +
+
+ +
+ + Submissions +
+
+ +
+ + Photos +
+
+
+ +
+ + {/* Status Filter */} +
+ + +
+ + {/* Sort Controls */} + +
+ + {/* Clear Filters & Apply Buttons (mobile only) */} + {isMobile && ( +
+ {showClearButton && ( + )} - Approved - Rejected - - -
+ +
+ )} + + - {/* Sort Controls */} - -
- - {/* Clear Filters Button */} - {showClearButton && ( -
+ {/* Clear Filters Button (desktop only) */} + {!isMobile && showClearButton && ( +