Refactor: Implement moderation queue enhancements

This commit is contained in:
gpt-engineer-app[bot]
2025-10-15 13:15:46 +00:00
parent eada34698c
commit db082cc9e3
4 changed files with 86 additions and 55 deletions

View File

@@ -2,6 +2,7 @@ import { memo, useState, useCallback } from 'react';
import { CheckCircle, XCircle, Eye, Calendar, MessageSquare, FileText, Image, ListTree, RefreshCw, AlertCircle, Lock, Trash2, AlertTriangle, Edit } from 'lucide-react';
import { usePhotoSubmissionItems } from '@/hooks/usePhotoSubmissionItems';
import { PhotoGrid } from '@/components/common/PhotoGrid';
import { normalizePhotoData } from '@/lib/photoHelpers';
import type { PhotoItem } from '@/types/photos';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -86,6 +87,11 @@ export const QueueItem = memo(({
item.submission_type === 'photo' ? item.id : undefined
);
// Check if submission has any moderator-edited items
const hasModeratorEdits = item.submission_items?.some(
si => si.original_data && Object.keys(si.original_data).length > 0
);
const handleValidationChange = useCallback((result: ValidationResult) => {
setValidationResult(result);
}, []);
@@ -95,6 +101,8 @@ export const QueueItem = memo(({
key={item.id}
className={`border-l-4 transition-all duration-300 ${
item._removing ? 'opacity-0 scale-95 pointer-events-none' : ''
} ${
hasModeratorEdits ? 'ring-2 ring-blue-200 dark:ring-blue-800' : ''
} ${
validationResult?.blockingErrors && validationResult.blockingErrors.length > 0 ? 'border-l-red-600' :
item.status === 'flagged' ? 'border-l-red-500' :
@@ -134,6 +142,22 @@ export const QueueItem = memo(({
{item.status === 'partially_approved' ? 'Partially Approved' :
item.status.charAt(0).toUpperCase() + item.status.slice(1)}
</Badge>
{hasModeratorEdits && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="secondary"
className="bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 border border-blue-300 dark:border-blue-700"
>
<Edit className="w-3 h-3 mr-1" />
Edited
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>This submission has been modified by a moderator</p>
</TooltipContent>
</Tooltip>
)}
{item.status === 'partially_approved' && (
<Badge variant="outline" className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-300 dark:border-yellow-700">
<AlertCircle className="w-3 h-3 mr-1" />
@@ -231,36 +255,29 @@ export const QueueItem = memo(({
)}
</div>
)}
{item.content.photos && item.content.photos.length > 0 && (
<div className="mt-3">
<div className="text-sm font-medium mb-2">Attached Photos:</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{item.content.photos.map((photo: any, index: number) => (
<div key={index} className="relative cursor-pointer" onClick={() => {
onOpenPhotos(item.content.photos.map((p: any, i: number) => ({
id: `${item.id}-${i}`,
url: p.url,
filename: `Review photo ${i + 1}`,
caption: p.caption
})), index);
}}>
<img
src={photo.url}
alt={`Review photo ${index + 1}`}
className="w-full h-20 object-cover rounded border bg-muted/30 hover:opacity-80 transition-opacity"
onError={(e) => {
console.error('Failed to load review photo:', photo.url);
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white text-xs opacity-0 hover:opacity-100 transition-opacity rounded">
<Eye className="w-4 h-4" />
</div>
</div>
))}
{item.content.photos && item.content.photos.length > 0 && (() => {
const reviewPhotos: PhotoItem[] = normalizePhotoData({
type: 'review',
photos: item.content.photos
});
return (
<div className="mt-3">
<div className="text-sm font-medium mb-2">Attached Photos:</div>
<PhotoGrid
photos={reviewPhotos}
onPhotoClick={onOpenPhotos}
maxDisplay={isMobile ? 3 : 4}
className="grid-cols-2 md:grid-cols-3"
/>
{item.content.photos[0]?.caption && (
<p className="text-sm text-muted-foreground mt-2">
{item.content.photos[0].caption}
</p>
)}
</div>
</div>
)}
);
})()}
</div>
) : item.submission_type === 'photo' ? (
<div>
@@ -286,10 +303,10 @@ export const QueueItem = memo(({
photos={photoItems.map(photo => ({
id: photo.id,
url: photo.cloudflare_image_url,
filename: (photo as any).filename || `Photo ${photo.order_index + 1}`,
filename: photo.filename || `Photo ${photo.order_index + 1}`,
caption: photo.caption,
title: photo.title,
date_taken: (photo as any).date_taken,
date_taken: photo.date_taken,
}))}
onPhotoClick={onOpenPhotos}
/>
@@ -370,16 +387,37 @@ export const QueueItem = memo(({
<div className={`flex gap-2 pt-2 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}>
{/* Show Review Items button for content submissions */}
{item.type === 'content_submission' && (
<Button
onClick={() => onOpenReviewManager(item.id)}
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
variant="outline"
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
>
<ListTree className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
Review Items
</Button>
<>
<Button
onClick={() => onOpenReviewManager(item.id)}
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
variant="outline"
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
>
<ListTree className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
Review Items
</Button>
{isAdmin && !isLockedByOther && currentLockSubmissionId === item.id && (
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => onOpenReviewManager(item.id)}
disabled={actionLoading === item.id}
variant="ghost"
className={isMobile ? 'h-11' : ''}
size={isMobile ? "default" : "default"}
>
<Edit className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
{!isMobile && "Edit"}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Edit submission items directly as a moderator</p>
</TooltipContent>
</Tooltip>
)}
</>
)}
<Button

View File

@@ -75,10 +75,10 @@ export function normalizePhotoData(source: PhotoDataSource): PhotoItem[] {
return source.items.map((item) => ({
id: item.id,
url: item.cloudflare_image_url,
filename: (item as any).filename || `Photo ${item.order_index + 1}`,
filename: item.filename || `Photo ${item.order_index + 1}`,
caption: item.caption,
title: item.title,
date_taken: (item as any).date_taken,
date_taken: item.date_taken,
}));
default:
@@ -95,10 +95,10 @@ export function normalizePhotoSubmissionItems(
return items.map((item) => ({
id: item.id,
url: item.cloudflare_image_url,
filename: (item as any).filename || `Photo ${item.order_index + 1}`,
filename: item.filename || `Photo ${item.order_index + 1}`,
caption: item.caption,
title: item.title,
date_taken: (item as any).date_taken,
date_taken: item.date_taken,
order_index: item.order_index,
}));
}

View File

@@ -80,6 +80,7 @@ export interface ModerationItem {
id: string;
item_type: string;
item_data: any;
original_data?: any;
status: string;
}>;
}

View File

@@ -2,6 +2,8 @@
* Photo-related type definitions
*/
import type { PhotoSubmissionItem } from './photo-submissions';
// Core photo display interface
export interface PhotoItem {
id: string;
@@ -30,13 +32,3 @@ export type PhotoDataSource =
| { type: 'review'; photos: any[] }
| { type: 'submission_jsonb'; photos: any[] }
| { type: 'submission_items'; items: PhotoSubmissionItem[] };
export interface PhotoSubmissionItem {
id: string;
cloudflare_image_id: string;
cloudflare_image_url: string;
title?: string;
caption?: string;
date_taken?: string;
order_index: number;
}