mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -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 { CheckCircle, XCircle, Eye, Calendar, MessageSquare, FileText, Image, ListTree, RefreshCw, AlertCircle, Lock, Trash2, AlertTriangle, Edit } from 'lucide-react';
|
||||||
import { usePhotoSubmissionItems } from '@/hooks/usePhotoSubmissionItems';
|
import { usePhotoSubmissionItems } from '@/hooks/usePhotoSubmissionItems';
|
||||||
import { PhotoGrid } from '@/components/common/PhotoGrid';
|
import { PhotoGrid } from '@/components/common/PhotoGrid';
|
||||||
|
import { normalizePhotoData } from '@/lib/photoHelpers';
|
||||||
import type { PhotoItem } from '@/types/photos';
|
import type { PhotoItem } from '@/types/photos';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -86,6 +87,11 @@ export const QueueItem = memo(({
|
|||||||
item.submission_type === 'photo' ? item.id : undefined
|
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) => {
|
const handleValidationChange = useCallback((result: ValidationResult) => {
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -95,6 +101,8 @@ export const QueueItem = memo(({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className={`border-l-4 transition-all duration-300 ${
|
className={`border-l-4 transition-all duration-300 ${
|
||||||
item._removing ? 'opacity-0 scale-95 pointer-events-none' : ''
|
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' :
|
validationResult?.blockingErrors && validationResult.blockingErrors.length > 0 ? 'border-l-red-600' :
|
||||||
item.status === 'flagged' ? 'border-l-red-500' :
|
item.status === 'flagged' ? 'border-l-red-500' :
|
||||||
@@ -134,6 +142,22 @@ export const QueueItem = memo(({
|
|||||||
{item.status === 'partially_approved' ? 'Partially Approved' :
|
{item.status === 'partially_approved' ? 'Partially Approved' :
|
||||||
item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
||||||
</Badge>
|
</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' && (
|
{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">
|
<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" />
|
<AlertCircle className="w-3 h-3 mr-1" />
|
||||||
@@ -231,36 +255,29 @@ export const QueueItem = memo(({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.content.photos && item.content.photos.length > 0 && (
|
{item.content.photos && item.content.photos.length > 0 && (() => {
|
||||||
<div className="mt-3">
|
const reviewPhotos: PhotoItem[] = normalizePhotoData({
|
||||||
<div className="text-sm font-medium mb-2">Attached Photos:</div>
|
type: 'review',
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
photos: item.content.photos
|
||||||
{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) => ({
|
return (
|
||||||
id: `${item.id}-${i}`,
|
<div className="mt-3">
|
||||||
url: p.url,
|
<div className="text-sm font-medium mb-2">Attached Photos:</div>
|
||||||
filename: `Review photo ${i + 1}`,
|
<PhotoGrid
|
||||||
caption: p.caption
|
photos={reviewPhotos}
|
||||||
})), index);
|
onPhotoClick={onOpenPhotos}
|
||||||
}}>
|
maxDisplay={isMobile ? 3 : 4}
|
||||||
<img
|
className="grid-cols-2 md:grid-cols-3"
|
||||||
src={photo.url}
|
/>
|
||||||
alt={`Review photo ${index + 1}`}
|
{item.content.photos[0]?.caption && (
|
||||||
className="w-full h-20 object-cover rounded border bg-muted/30 hover:opacity-80 transition-opacity"
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
onError={(e) => {
|
{item.content.photos[0].caption}
|
||||||
console.error('Failed to load review photo:', photo.url);
|
</p>
|
||||||
(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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : item.submission_type === 'photo' ? (
|
) : item.submission_type === 'photo' ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -286,10 +303,10 @@ export const QueueItem = memo(({
|
|||||||
photos={photoItems.map(photo => ({
|
photos={photoItems.map(photo => ({
|
||||||
id: photo.id,
|
id: photo.id,
|
||||||
url: photo.cloudflare_image_url,
|
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,
|
caption: photo.caption,
|
||||||
title: photo.title,
|
title: photo.title,
|
||||||
date_taken: (photo as any).date_taken,
|
date_taken: photo.date_taken,
|
||||||
}))}
|
}))}
|
||||||
onPhotoClick={onOpenPhotos}
|
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'}`}>
|
<div className={`flex gap-2 pt-2 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}>
|
||||||
{/* Show Review Items button for content submissions */}
|
{/* Show Review Items button for content submissions */}
|
||||||
{item.type === 'content_submission' && (
|
{item.type === 'content_submission' && (
|
||||||
<Button
|
<>
|
||||||
onClick={() => onOpenReviewManager(item.id)}
|
<Button
|
||||||
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
|
onClick={() => onOpenReviewManager(item.id)}
|
||||||
variant="outline"
|
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
|
||||||
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
|
variant="outline"
|
||||||
size={isMobile ? "default" : "default"}
|
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
|
<ListTree className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
|
||||||
</Button>
|
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
|
<Button
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export function normalizePhotoData(source: PhotoDataSource): PhotoItem[] {
|
|||||||
return source.items.map((item) => ({
|
return source.items.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.cloudflare_image_url,
|
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,
|
caption: item.caption,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
date_taken: (item as any).date_taken,
|
date_taken: item.date_taken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -95,10 +95,10 @@ export function normalizePhotoSubmissionItems(
|
|||||||
return items.map((item) => ({
|
return items.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.cloudflare_image_url,
|
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,
|
caption: item.caption,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
date_taken: (item as any).date_taken,
|
date_taken: item.date_taken,
|
||||||
order_index: item.order_index,
|
order_index: item.order_index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export interface ModerationItem {
|
|||||||
id: string;
|
id: string;
|
||||||
item_type: string;
|
item_type: string;
|
||||||
item_data: any;
|
item_data: any;
|
||||||
|
original_data?: any;
|
||||||
status: string;
|
status: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* Photo-related type definitions
|
* Photo-related type definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PhotoSubmissionItem } from './photo-submissions';
|
||||||
|
|
||||||
// Core photo display interface
|
// Core photo display interface
|
||||||
export interface PhotoItem {
|
export interface PhotoItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,13 +32,3 @@ export type PhotoDataSource =
|
|||||||
| { type: 'review'; photos: any[] }
|
| { type: 'review'; photos: any[] }
|
||||||
| { type: 'submission_jsonb'; photos: any[] }
|
| { type: 'submission_jsonb'; photos: any[] }
|
||||||
| { type: 'submission_items'; items: PhotoSubmissionItem[] };
|
| { 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;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user