feat: Implement client-side sorting for queues

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 17:06:39 +00:00
parent 74fbd116cb
commit 83ccc51f61
2 changed files with 263 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useImperativeHandle, forwardRef, useCallback, useRef } from 'react';
import { CheckCircle, XCircle, Filter, MessageSquare, FileText, Image, X, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react';
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 { Card, CardContent } from '@/components/ui/card';
@@ -66,6 +66,13 @@ interface ModerationItem {
type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos';
type StatusFilter = 'all' | 'pending' | 'partially_approved' | 'flagged' | 'approved' | 'rejected';
type QueueTab = 'mainQueue' | 'archive';
type SortField = 'created_at' | 'username' | 'submission_type' | 'status' | 'escalated';
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
export interface ModerationQueueRef {
refresh: () => void;
@@ -124,6 +131,19 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const [pageSize, setPageSize] = useState(25);
const [totalCount, setTotalCount] = useState(0);
const totalPages = Math.ceil(totalCount / pageSize);
// Sort state
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
const saved = localStorage.getItem('moderationQueue_sortConfig');
if (saved) {
try {
return JSON.parse(saved);
} catch {
return { field: 'created_at', direction: 'asc' as SortDirection };
}
}
return { field: 'created_at', direction: 'asc' as SortDirection };
});
// Get admin settings for polling configuration
const {
@@ -159,6 +179,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
isAdminRef.current = isAdmin;
isSuperuserRef.current = isSuperuser;
}, [refreshStrategy, preserveInteraction, user, toast, isAdmin, isSuperuser]);
// Persist sort configuration
useEffect(() => {
localStorage.setItem('moderationQueue_sortConfig', JSON.stringify(sortConfig));
}, [sortConfig]);
// Only sync itemsRef (not loadedIdsRef) to avoid breaking silent polling logic
useEffect(() => {
@@ -1731,6 +1756,57 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
}
};
// Sort items function
const sortItems = useCallback((items: ModerationItem[], config: SortConfig): ModerationItem[] => {
const sorted = [...items];
sorted.sort((a, b) => {
let compareA: any;
let compareB: any;
switch (config.field) {
case 'created_at':
compareA = new Date(a.created_at).getTime();
compareB = new Date(b.created_at).getTime();
break;
case 'username':
compareA = (a.user_profile?.username || '').toLowerCase();
compareB = (b.user_profile?.username || '').toLowerCase();
break;
case 'submission_type':
compareA = a.submission_type || '';
compareB = b.submission_type || '';
break;
case 'status':
compareA = a.status;
compareB = b.status;
break;
case 'escalated':
compareA = a.escalated ? 1 : 0;
compareB = b.escalated ? 1 : 0;
break;
default:
return 0;
}
let result = 0;
if (typeof compareA === 'string' && typeof compareB === 'string') {
result = compareA.localeCompare(compareB);
} else if (typeof compareA === 'number' && typeof compareB === 'number') {
result = compareA - compareB;
}
return config.direction === 'asc' ? result : -result;
});
return sorted;
}, []);
// Memoized callbacks
const handleNoteChange = useCallback((id: string, value: string) => {
setNotes(prev => ({ ...prev, [id]: value }));
@@ -1777,13 +1853,18 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
);
}
// Apply client-side sorting
const sortedItems = useMemo(() => {
return sortItems(items, sortConfig);
}, [items, sortConfig]);
return (
<div
className="flex flex-col gap-6"
data-initial-load={!hasRenderedOnce ? "true" : "false"}
style={{ willChange: 'transform' }}
>
{items.map((item) => (
{sortedItems.map((item) => (
<QueueItem
key={item.id}
item={item}
@@ -1815,6 +1896,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const clearFilters = () => {
setActiveEntityFilter('all');
setActiveStatusFilter('pending');
setSortConfig({ field: 'created_at', direction: 'asc' });
};
const getEntityFilterIcon = (filter: EntityFilter) => {
@@ -1986,9 +2068,47 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
</SelectContent>
</Select>
</div>
<div className={`space-y-2 ${isMobile ? 'w-full' : 'min-w-[180px]'}`}>
<Label className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>Sort By</Label>
<div className="flex gap-2">
<Select
value={sortConfig.field}
onValueChange={(value) => setSortConfig(prev => ({ ...prev, field: value as SortField }))}
>
<SelectTrigger className={isMobile ? "h-10 flex-1" : "flex-1"}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at">Date Created</SelectItem>
<SelectItem value="username">Submitter</SelectItem>
<SelectItem value="submission_type">Type</SelectItem>
<SelectItem value="status">Status</SelectItem>
<SelectItem value="escalated">Escalated</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size={isMobile ? "default" : "sm"}
onClick={() => setSortConfig(prev => ({
...prev,
direction: prev.direction === 'asc' ? 'desc' : 'asc'
}))}
className={isMobile ? "h-10" : ""}
title={sortConfig.direction === 'asc' ? 'Sort Descending' : 'Sort Ascending'}
>
{sortConfig.direction === 'asc' ? (
<ArrowUp className="w-4 h-4" />
) : (
<ArrowDown className="w-4 h-4" />
)}
</Button>
</div>
</div>
</div>
{(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending') && (
{(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending' || sortConfig.field !== 'created_at') && (
<div className={isMobile ? "" : "flex items-end"}>
<Button
variant="outline"
@@ -2004,7 +2124,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
</div>
{/* Active Filters Display */}
{(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending') && (
{(activeEntityFilter !== 'all' || activeStatusFilter !== 'pending' || sortConfig.field !== 'created_at') && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Active filters:</span>
{activeEntityFilter !== 'all' && (
@@ -2018,6 +2138,15 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
{activeStatusFilter}
</Badge>
)}
{sortConfig.field !== 'created_at' && (
<Badge variant="secondary" className="flex items-center gap-1">
{sortConfig.direction === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />}
Sort: {sortConfig.field === 'username' ? 'Submitter' :
sortConfig.field === 'submission_type' ? 'Type' :
sortConfig.field === 'escalated' ? 'Escalated' :
sortConfig.field === 'status' ? 'Status' : 'Date'}
</Badge>
)}
</div>
)}