diff --git a/src/components/admin/BanUserDialog.tsx b/src/components/admin/BanUserDialog.tsx new file mode 100644 index 00000000..70143c83 --- /dev/null +++ b/src/components/admin/BanUserDialog.tsx @@ -0,0 +1,312 @@ +import { useState } from 'react'; +import { Ban, UserCheck } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +const BAN_REASONS = [ + { value: 'spam', label: 'Spam or advertising' }, + { value: 'harassment', label: 'Harassment or bullying' }, + { value: 'inappropriate_content', label: 'Inappropriate content' }, + { value: 'violation_tos', label: 'Terms of Service violation' }, + { value: 'abuse', label: 'Abuse of platform features' }, + { value: 'fake_info', label: 'Posting false information' }, + { value: 'copyright', label: 'Copyright infringement' }, + { value: 'multiple_accounts', label: 'Multiple account abuse' }, + { value: 'other', label: 'Other (specify below)' } +] as const; + +const BAN_DURATIONS = [ + { value: '1', label: '1 Day', days: 1 }, + { value: '7', label: '7 Days (1 Week)', days: 7 }, + { value: '30', label: '30 Days (1 Month)', days: 30 }, + { value: '90', label: '90 Days (3 Months)', days: 90 }, + { value: 'permanent', label: 'Permanent', days: null } +] as const; + +const banFormSchema = z.object({ + reason_type: z.enum([ + 'spam', + 'harassment', + 'inappropriate_content', + 'violation_tos', + 'abuse', + 'fake_info', + 'copyright', + 'multiple_accounts', + 'other' + ]), + custom_reason: z.string().max(500).optional(), + duration: z.enum(['1', '7', '30', '90', 'permanent']) +}).refine( + (data) => data.reason_type !== 'other' || (data.custom_reason && data.custom_reason.trim().length > 0), + { + message: "Please provide a custom reason", + path: ["custom_reason"] + } +); + +type BanFormValues = z.infer; + +interface BanUserDialogProps { + profile: { + user_id: string; + username: string; + banned: boolean; + }; + onBanComplete: () => void; + onBanUser: (userId: string, ban: boolean, reason?: string, expiresAt?: Date | null) => Promise; + disabled?: boolean; +} + +export function BanUserDialog({ profile, onBanComplete, onBanUser, disabled }: BanUserDialogProps) { + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const form = useForm({ + resolver: zodResolver(banFormSchema), + defaultValues: { + reason_type: 'violation_tos', + custom_reason: '', + duration: '7' + } + }); + + const watchReasonType = form.watch('reason_type'); + const watchDuration = form.watch('duration'); + + const onSubmit = async (values: BanFormValues) => { + setIsSubmitting(true); + try { + // Construct the ban reason + let banReason: string; + if (values.reason_type === 'other' && values.custom_reason) { + banReason = values.custom_reason.trim(); + } else { + const selectedReason = BAN_REASONS.find(r => r.value === values.reason_type); + banReason = selectedReason?.label || 'Policy violation'; + } + + // Calculate expiration date + let expiresAt: Date | null = null; + if (values.duration !== 'permanent') { + const durationConfig = BAN_DURATIONS.find(d => d.value === values.duration); + if (durationConfig?.days) { + expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + durationConfig.days); + } + } + + await onBanUser(profile.user_id, true, banReason, expiresAt); + setOpen(false); + form.reset(); + onBanComplete(); + } catch (error) { + // Error handling is done by the parent component + } finally { + setIsSubmitting(false); + } + }; + + const handleUnban = async () => { + setIsSubmitting(true); + try { + await onBanUser(profile.user_id, false); + setOpen(false); + onBanComplete(); + } catch (error) { + // Error handling is done by the parent component + } finally { + setIsSubmitting(false); + } + }; + + // For unbanning, use simpler dialog + if (profile.banned) { + return ( + + + + + + + Unban User + + Are you sure you want to unban @{profile.username}? They will be able to access the application again. + + + + + + + + + ); + } + + // For banning, use detailed form + return ( + + + + + + + Ban User + + Ban @{profile.username} from accessing the application. You must provide a reason and duration. + + + +
+ + ( + + Ban Reason + + + Choose the primary reason for this ban + + + + )} + /> + + {watchReasonType === 'other' && ( + ( + + Custom Reason + +