diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 6e57c936..3fdaec8e 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useImperativeHandle, forwardRef, useCallback, useR import { CheckCircle, XCircle, Filter, MessageSquare, FileText, Image, X, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { supabase } from '@/integrations/supabase/client'; @@ -1473,731 +1475,6 @@ export const ModerationQueue = forwardRef((props, ref) => { ); }; - key={item.id} - className={`border-l-4 transition-opacity duration-200 ${ - item.status === 'flagged' ? 'border-l-red-500' : - item.status === 'approved' ? 'border-l-green-500' : - item.status === 'rejected' ? 'border-l-red-400' : - item.status === 'partially_approved' ? 'border-l-yellow-500' : - 'border-l-amber-500' - }`} - style={{ - opacity: actionLoading === item.id ? 0.5 : 1, - pointerEvents: actionLoading === item.id ? 'none' : 'auto' - }} - > - -
-
- - {item.type === 'review' ? ( - <> - - Review - - ) : item.submission_type === 'photo' ? ( - <> - - Photo - - ) : ( - <> - - Submission - - )} - - - {item.status === 'partially_approved' ? 'Partially Approved' : - item.status.charAt(0).toUpperCase() + item.status.slice(1)} - - {item.status === 'partially_approved' && ( - - - Needs Retry - - )} - {lockedSubmissions.has(item.id) && item.type === 'content_submission' && ( - - - Locked by Another Moderator - - )} - {queue.currentLock?.submissionId === item.id && item.type === 'content_submission' && ( - - - Claimed by You - - )} -
-
- - {format(new Date(item.created_at), isMobile ? 'MMM d, yyyy' : 'MMM d, yyyy HH:mm')} -
-
- - {item.user_profile && ( -
- - - - {(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()} - - -
- - {item.user_profile.display_name || item.user_profile.username} - - {item.user_profile.display_name && ( - - @{item.user_profile.username} - - )} -
-
- )} -
- - -
- {item.type === 'review' ? ( -
- {item.content.title && ( -

{item.content.title}

- )} - {item.content.content && ( -

{item.content.content}

- )} -
- Rating: {item.content.rating}/5 -
- - {/* Entity Names for Reviews */} - {(item.entity_name || item.park_name) && ( -
- {item.entity_name && ( -
- {item.park_name ? 'Ride:' : 'Park:'} - {item.entity_name} -
- )} - {item.park_name && ( -
- Park: - {item.park_name} -
- )} -
- )} - {item.content.photos && item.content.photos.length > 0 && ( -
-
Attached Photos:
-
- {item.content.photos.map((photo: any, index: number) => ( -
{ - setSelectedPhotos(item.content.photos.map((p: any, i: number) => ({ - id: `${item.id}-${i}`, - url: p.url, - filename: `Review photo ${i + 1}`, - caption: p.caption - }))); - setSelectedPhotoIndex(index); - setPhotoModalOpen(true); - }}> - {`Review { - console.error('Failed to load review photo:', photo.url); - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> -
- -
-
- ))} -
-
- )} -
- ) : item.submission_type === 'photo' ? ( -
-
- Photo Submission -
- - {/* Submission Title */} - {item.content.title && ( -
-
Title:
-

{item.content.title}

-
- )} - - {/* Submission Caption */} - {item.content.content?.caption && ( -
-
Caption:
-

{item.content.content.caption}

-
- )} - - {/* Photos */} - {item.content.content?.photos && item.content.content.photos.length > 0 ? ( -
-
Photos ({item.content.content.photos.length}):
- {item.content.content.photos.map((photo: any, index: number) => ( -
-
{ - setSelectedPhotos(item.content.content.photos.map((p: any, i: number) => ({ - id: `${item.id}-${i}`, - url: p.url, - filename: p.filename, - caption: p.caption - }))); - setSelectedPhotoIndex(index); - setPhotoModalOpen(true); - }}> - {`Photo { - console.error('Failed to load photo submission:', photo); - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - const parent = target.parentElement; - if (parent) { - // Create elements safely using DOM API to prevent XSS - const errorContainer = document.createElement('div'); - errorContainer.className = 'absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs'; - - const errorIcon = document.createElement('div'); - errorIcon.textContent = '⚠️ Image failed to load'; - - const urlDisplay = document.createElement('div'); - urlDisplay.className = 'mt-1 font-mono text-xs break-all px-2'; - // Use textContent to prevent XSS - it escapes HTML automatically - urlDisplay.textContent = photo.url; - - errorContainer.appendChild(errorIcon); - errorContainer.appendChild(urlDisplay); - parent.appendChild(errorContainer); - } - }} - /> -
- -
-
-
-
- URL: - {photo.url} -
-
- Filename: - {photo.filename || 'Unknown'} -
-
- Size: - {photo.size ? `${Math.round(photo.size / 1024)} KB` : 'Unknown'} -
-
- Type: - {photo.type || 'Unknown'} -
- {photo.caption && ( -
-
Caption:
-
{photo.caption}
-
- )} -
-
- ))} -
- ) : ( -
- No photos found in submission -
- )} - - {/* Context Information */} - {item.content.content?.context && ( -
-
- Context: - - {typeof item.content.content.context === 'object' - ? (item.content.content.context.ride_id ? 'ride' : - item.content.content.context.park_id ? 'park' : 'unknown') - : item.content.content.context} - -
- {item.entity_name && ( -
- - {(typeof item.content.content.context === 'object' - ? (item.content.content.context.ride_id ? 'ride' : 'park') - : item.content.content.context) === 'ride' ? 'Ride:' : 'Park:'} - - {item.entity_name} -
- )} - {item.park_name && - (typeof item.content.content.context === 'object' - ? !!item.content.content.context.ride_id - : item.content.content.context === 'ride') && ( -
- Park: - {item.park_name} -
- )} -
- )} -
- ) : ( -
- {/* Composite Submissions (Ride with Manufacturer/Model) */} - {(item.submission_type === 'ride_with_manufacturer' || - item.submission_type === 'ride_with_model' || - item.submission_type === 'ride_with_manufacturer_and_model') ? ( -
- {/* New Manufacturer Card */} - {item.content.new_manufacturer && ( - - -
- - New Manufacturer - - {item.content.new_manufacturer.name} -
-
- - {item.content.new_manufacturer.description && ( -
- Description: - {item.content.new_manufacturer.description} -
- )} -
- {item.content.new_manufacturer.person_type && ( -
- Type: - {item.content.new_manufacturer.person_type} -
- )} - {item.content.new_manufacturer.founded_year && ( -
- Founded: - {item.content.new_manufacturer.founded_year} -
- )} - {item.content.new_manufacturer.headquarters_location && ( -
- HQ: - {item.content.new_manufacturer.headquarters_location} -
- )} - {item.content.new_manufacturer.website_url && ( - - )} -
-
-
- )} - - {/* New Ride Model Card */} - {item.content.new_ride_model && ( - - -
- - New Ride Model - - {item.content.new_ride_model.name} -
-
- -
- Manufacturer: - - {item.content.new_manufacturer - ? item.content.new_manufacturer.name - : 'Existing manufacturer'} - -
-
-
- Category: - {item.content.new_ride_model.category?.replace('_', ' ')} -
-
- Type: - {item.content.new_ride_model.ride_type} -
-
- {item.content.new_ride_model.description && ( -
- Description: - {item.content.new_ride_model.description} -
- )} -
-
- )} - - {/* Ride Details Card */} - - -
- - Ride - - {item.content.ride?.name} -
-
- - {item.content.ride?.description && ( -

{item.content.ride.description}

- )} -
- {item.content.ride?.category && ( -
- Category: - {item.content.ride.category.replace('_', ' ')} -
- )} - {item.content.ride?.status && ( -
- Status: - {item.content.ride.status} -
- )} - {item.content.ride?.max_speed_kmh && ( -
- Max Speed: - - - -
- )} - {item.content.ride?.max_height_meters && ( -
- Max Height: - - - -
- )} -
-
-
-
- ) : (item.submission_type === 'manufacturer' || - item.submission_type === 'designer' || - item.submission_type === 'operator' || - item.submission_type === 'property_owner' || - item.submission_type === 'park' || - item.submission_type === 'ride' || - item.submission_type === 'ride_model' || - item.submission_type === 'photo_delete' || - item.submission_type === 'photo_edit') ? ( - - ) : ( -
-
- - - Unknown Submission Type - -
-
- Type: {item.submission_type} -
- {item.content?.action && ( -
- Action: {item.content.action} -
- )} -
- - View raw data (for developers) - -
-                            {JSON.stringify(item.content, null, 2)}
-                          
-
-
- )} -
- )} -
- - {/* Action buttons based on status */} - {(item.status === 'pending' || item.status === 'flagged') && ( - <> - {/* Claim button for unclaimed submissions */} - {!lockedSubmissions.has(item.id) && queue.currentLock?.submissionId !== item.id && ( -
- - - Unclaimed Submission - -
- Claim this submission to lock it for 15 minutes while you review - -
-
-
-
- )} - -
- -