diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 7e03ba7f..f55fe499 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -1,10 +1,11 @@ import { useState, useEffect } from 'react'; -import { CheckCircle, XCircle, Eye, Calendar, User } from 'lucide-react'; +import { CheckCircle, XCircle, Eye, Calendar, User, Filter } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { format } from 'date-fns'; @@ -27,47 +28,88 @@ export function ModerationQueue() { const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [notes, setNotes] = useState>({}); + const [activeFilter, setActiveFilter] = useState('pending'); const { toast } = useToast(); - const fetchItems = async () => { + const fetchItems = async (filter: string = 'pending') => { try { - // Fetch pending reviews with profile data - const { data: reviews, error: reviewsError } = await supabase - .from('reviews') - .select(` - id, - title, - content, - rating, - created_at, - user_id, - moderation_status - `) - .in('moderation_status', ['pending', 'flagged']) - .order('created_at', { ascending: true }); + setLoading(true); + + let reviewStatuses: string[] = []; + let submissionStatuses: string[] = []; + + // Define status filters + switch (filter) { + case 'all': + reviewStatuses = ['pending', 'flagged', 'approved', 'rejected']; + submissionStatuses = ['pending', 'approved', 'rejected']; + break; + case 'pending': + reviewStatuses = ['pending']; + submissionStatuses = ['pending']; + break; + case 'flagged': + reviewStatuses = ['flagged']; + submissionStatuses = []; // Content submissions don't have flagged status + break; + case 'approved': + reviewStatuses = ['approved']; + submissionStatuses = ['approved']; + break; + case 'rejected': + reviewStatuses = ['rejected']; + submissionStatuses = ['rejected']; + break; + default: + reviewStatuses = ['pending', 'flagged']; + submissionStatuses = ['pending']; + } - if (reviewsError) throw reviewsError; + // Fetch reviews + let reviews = []; + if (reviewStatuses.length > 0) { + const { data: reviewsData, error: reviewsError } = await supabase + .from('reviews') + .select(` + id, + title, + content, + rating, + created_at, + user_id, + moderation_status + `) + .in('moderation_status', reviewStatuses) + .order('created_at', { ascending: false }); - // Fetch pending content submissions - const { data: submissions, error: submissionsError } = await supabase - .from('content_submissions') - .select(` - id, - content, - submission_type, - created_at, - user_id, - status - `) - .eq('status', 'pending') - .order('created_at', { ascending: true }); + if (reviewsError) throw reviewsError; + reviews = reviewsData || []; + } - if (submissionsError) throw submissionsError; + // Fetch content submissions + let submissions = []; + if (submissionStatuses.length > 0) { + const { data: submissionsData, error: submissionsError } = await supabase + .from('content_submissions') + .select(` + id, + content, + submission_type, + created_at, + user_id, + status + `) + .in('status', submissionStatuses) + .order('created_at', { ascending: false }); + + if (submissionsError) throw submissionsError; + submissions = submissionsData || []; + } // Get unique user IDs to fetch profiles const userIds = [ - ...(reviews || []).map(r => r.user_id), - ...(submissions || []).map(s => s.user_id) + ...reviews.map(r => r.user_id), + ...submissions.map(s => s.user_id) ]; // Fetch profiles for all users @@ -80,7 +122,7 @@ export function ModerationQueue() { // Combine and format items const formattedItems: ModerationItem[] = [ - ...(reviews || []).map(review => ({ + ...reviews.map(review => ({ id: review.id, type: 'review' as const, content: review, @@ -89,7 +131,7 @@ export function ModerationQueue() { status: review.moderation_status, user_profile: profileMap.get(review.user_id), })), - ...(submissions || []).map(submission => ({ + ...submissions.map(submission => ({ id: submission.id, type: 'content_submission' as const, content: submission, @@ -100,8 +142,8 @@ export function ModerationQueue() { })), ]; - // Sort by creation date - formattedItems.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + // Sort by creation date (newest first for better UX) + formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); setItems(formattedItems); } catch (error) { @@ -117,8 +159,8 @@ export function ModerationQueue() { }; useEffect(() => { - fetchItems(); - }, []); + fetchItems(activeFilter); + }, [activeFilter]); const handleModerationAction = async ( item: ModerationItem, @@ -151,8 +193,13 @@ export function ModerationQueue() { description: `The ${item.type} has been ${action}`, }); - // Remove item from queue - setItems(prev => prev.filter(i => i.id !== item.id)); + // Remove item from queue if it's no longer in the active filter + if (activeFilter === 'pending' || activeFilter === 'flagged') { + setItems(prev => prev.filter(i => i.id !== item.id)); + } else { + // Refresh the queue to show updated status + fetchItems(activeFilter); + } // Clear notes setNotes(prev => { @@ -172,120 +219,194 @@ export function ModerationQueue() { } }; - if (loading) { - return ( -
-
-
- ); - } + const getStatusBadgeVariant = (status: string) => { + switch (status) { + case 'pending': + return 'secondary'; + case 'flagged': + return 'destructive'; + case 'approved': + return 'default'; + case 'rejected': + return 'outline'; + default: + return 'secondary'; + } + }; - if (items.length === 0) { - return ( -
- -

Queue is empty

-

- No pending items require moderation at this time. -

-
- ); - } + const getEmptyStateMessage = (filter: string) => { + switch (filter) { + case 'pending': + return 'No pending items require moderation at this time.'; + case 'flagged': + return 'No flagged content found.'; + case 'approved': + return 'No approved content found.'; + case 'rejected': + return 'No rejected content found.'; + case 'all': + return 'No moderation items found.'; + default: + return 'No items found for the selected filter.'; + } + }; - return ( -
- {items.map((item) => ( - - -
-
- - {item.type === 'review' ? 'Review' : 'Submission'} - - {item.status === 'flagged' && ( - Flagged - )} -
-
- - {format(new Date(item.created_at), 'MMM d, yyyy HH:mm')} -
-
- - {item.user_profile && ( -
- - - {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 -
+ const QueueContent = () => { + if (loading) { + return ( +
+
+
+ ); + } + + if (items.length === 0) { + return ( +
+ +

No items found

+

+ {getEmptyStateMessage(activeFilter)} +

+
+ ); + } + + return ( +
+ {items.map((item) => ( + + +
+
+ + {item.type === 'review' ? 'Review' : 'Submission'} + + + {item.status.charAt(0).toUpperCase() + item.status.slice(1)} +
- ) : ( -
-
- Type: {item.content.submission_type} -
-
-                    {JSON.stringify(item.content.content, null, 2)}
-                  
+
+ + {format(new Date(item.created_at), 'MMM d, yyyy HH:mm')} +
+
+ + {item.user_profile && ( +
+ + + {item.user_profile.display_name || item.user_profile.username} + + {item.user_profile.display_name && ( + + @{item.user_profile.username} + + )}
)} -
+
-
- -