mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 13:11:16 -05:00
Refactor: Implement moderation queue enhancements
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user