diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 2cc791ef..228ce606 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -3,6 +3,7 @@ import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileT import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; @@ -10,6 +11,7 @@ import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { format } from 'date-fns'; +import { PhotoModal } from './PhotoModal'; interface ModerationItem { id: string; @@ -22,7 +24,10 @@ interface ModerationItem { user_profile?: { username: string; display_name?: string; + avatar_url?: string; }; + entity_name?: string; + park_name?: string; } type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos'; @@ -39,6 +44,9 @@ export const ModerationQueue = forwardRef((props, ref) => { const [notes, setNotes] = useState>({}); const [activeEntityFilter, setActiveEntityFilter] = useState('all'); const [activeStatusFilter, setActiveStatusFilter] = useState('pending'); + const [photoModalOpen, setPhotoModalOpen] = useState(false); + const [selectedPhotos, setSelectedPhotos] = useState([]); + const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0); const { toast } = useToast(); const { isAdmin, isSuperuser } = useUserRole(); @@ -83,7 +91,7 @@ export const ModerationQueue = forwardRef((props, ref) => { submissionStatuses = ['pending']; } - // Fetch reviews + // Fetch reviews with entity data let reviews = []; if ((entityFilter === 'all' || entityFilter === 'reviews') && reviewStatuses.length > 0) { const { data: reviewsData, error: reviewsError } = await supabase @@ -96,7 +104,18 @@ export const ModerationQueue = forwardRef((props, ref) => { created_at, user_id, moderation_status, - photos + photos, + park_id, + ride_id, + parks:park_id ( + name + ), + rides:ride_id ( + name, + parks:park_id ( + name + ) + ) `) .in('moderation_status', reviewStatuses) .order('created_at', { ascending: false }); @@ -105,7 +124,7 @@ export const ModerationQueue = forwardRef((props, ref) => { reviews = reviewsData || []; } - // Fetch content submissions + // Fetch content submissions with entity data let submissions = []; if ((entityFilter === 'all' || entityFilter === 'submissions' || entityFilter === 'photos') && submissionStatuses.length > 0) { let query = supabase @@ -131,7 +150,47 @@ export const ModerationQueue = forwardRef((props, ref) => { .order('created_at', { ascending: false }); if (submissionsError) throw submissionsError; - submissions = submissionsData || []; + + // Get entity data for photo submissions + let submissionsWithEntities = submissionsData || []; + for (const submission of submissionsWithEntities) { + if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') { + const contentObj = submission.content as any; + const context = contentObj.content?.context; + const rideId = contentObj.content?.ride_id; + const parkId = contentObj.content?.park_id; + + if (context === 'ride' && rideId) { + const { data: rideData } = await supabase + .from('rides') + .select(` + name, + parks:park_id ( + name + ) + `) + .eq('id', rideId) + .single(); + + if (rideData) { + (submission as any).entity_name = rideData.name; + (submission as any).park_name = rideData.parks?.name; + } + } else if (context === 'park' && parkId) { + const { data: parkData } = await supabase + .from('parks') + .select('name') + .eq('id', parkId) + .single(); + + if (parkData) { + (submission as any).entity_name = parkData.name; + } + } + } + } + + submissions = submissionsWithEntities; } // Get unique user IDs to fetch profiles @@ -140,25 +199,39 @@ export const ModerationQueue = forwardRef((props, ref) => { ...submissions.map(s => s.user_id) ]; - // Fetch profiles for all users + // Fetch profiles for all users with avatars const { data: profiles } = await supabase .from('profiles') - .select('user_id, username, display_name') + .select('user_id, username, display_name, avatar_url') .in('user_id', userIds); const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []); // Combine and format items const formattedItems: ModerationItem[] = [ - ...reviews.map(review => ({ - id: review.id, - type: 'review' as const, - content: review, - created_at: review.created_at, - user_id: review.user_id, - status: review.moderation_status, - user_profile: profileMap.get(review.user_id), - })), + ...reviews.map(review => { + let entity_name = ''; + let park_name = ''; + + if ((review as any).rides) { + entity_name = (review as any).rides.name; + park_name = (review as any).rides.parks?.name; + } else if ((review as any).parks) { + entity_name = (review as any).parks.name; + } + + return { + id: review.id, + type: 'review' as const, + content: review, + created_at: review.created_at, + user_id: review.user_id, + status: review.moderation_status, + user_profile: profileMap.get(review.user_id), + entity_name, + park_name, + }; + }), ...submissions.map(submission => ({ id: submission.id, type: 'content_submission' as const, @@ -168,6 +241,8 @@ export const ModerationQueue = forwardRef((props, ref) => { status: submission.status, submission_type: submission.submission_type, user_profile: profileMap.get(submission.user_id), + entity_name: (submission as any).entity_name, + park_name: (submission as any).park_name, })), ]; @@ -524,16 +599,23 @@ export const ModerationQueue = forwardRef((props, ref) => { {item.user_profile && ( -
- - - {item.user_profile.display_name || item.user_profile.username} - - {item.user_profile.display_name && ( - - @{item.user_profile.username} +
+ + + + {(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} + + )} +
)} @@ -556,19 +638,28 @@ export const ModerationQueue = forwardRef((props, ref) => {
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'; }} onLoad={() => console.log('Review photo loaded:', photo.url)} /> -
- Photo {index + 1} +
+
))} @@ -604,11 +695,20 @@ export const ModerationQueue = forwardRef((props, ref) => {
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; @@ -625,6 +725,9 @@ export const ModerationQueue = forwardRef((props, ref) => { }} onLoad={() => console.log('Photo submission loaded:', photo.url)} /> +
+ +
@@ -666,16 +769,16 @@ export const ModerationQueue = forwardRef((props, ref) => { Context: {item.content.content.context}
- {item.content.content.ride_id && ( + {item.entity_name && (
- Ride ID: - {item.content.content.ride_id} + {item.content.content.context === 'ride' ? 'Ride:' : 'Park:'} + {item.entity_name}
)} - {item.content.content.park_id && ( + {item.park_name && item.content.content.context === 'ride' && (
- Park ID: - {item.content.content.park_id} + Park: + {item.park_name}
)}
@@ -865,6 +968,14 @@ export const ModerationQueue = forwardRef((props, ref) => { {/* Queue Content */} + + {/* Photo Modal */} + setPhotoModalOpen(false)} + />
); }); \ No newline at end of file diff --git a/src/components/moderation/PhotoModal.tsx b/src/components/moderation/PhotoModal.tsx new file mode 100644 index 00000000..263b3812 --- /dev/null +++ b/src/components/moderation/PhotoModal.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { X, ChevronLeft, ChevronRight } from 'lucide-react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; + +interface PhotoModalProps { + photos: Array<{ + id: string; + url: string; + filename?: string; + caption?: string; + }>; + initialIndex: number; + isOpen: boolean; + onClose: () => void; +} + +export function PhotoModal({ photos, initialIndex, isOpen, onClose }: PhotoModalProps) { + const [currentIndex, setCurrentIndex] = useState(initialIndex); + const currentPhoto = photos[currentIndex]; + + const goToPrevious = () => { + setCurrentIndex((prev) => (prev > 0 ? prev - 1 : photos.length - 1)); + }; + + const goToNext = () => { + setCurrentIndex((prev) => (prev < photos.length - 1 ? prev + 1 : 0)); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowLeft') goToPrevious(); + if (e.key === 'ArrowRight') goToNext(); + if (e.key === 'Escape') onClose(); + }; + + return ( + + +
+ {/* Header */} +
+
+
+

+ {currentPhoto?.filename || `Photo ${currentIndex + 1}`} +

+ {photos.length > 1 && ( +

+ {currentIndex + 1} of {photos.length} +

+ )} +
+ +
+
+ + {/* Image */} +
+ {currentPhoto?.caption +
+ + {/* Navigation */} + {photos.length > 1 && ( + <> + + + + )} + + {/* Caption */} + {currentPhoto?.caption && ( +
+

{currentPhoto.caption}

+
+ )} +
+
+
+ ); +} \ No newline at end of file