mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
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<typeof banFormSchema>;
|
|
|
|
interface BanUserDialogProps {
|
|
profile: {
|
|
user_id: string;
|
|
username: string;
|
|
banned: boolean;
|
|
};
|
|
onBanComplete: () => void;
|
|
onBanUser: (userId: string, ban: boolean, reason?: string, expiresAt?: Date | null) => Promise<void>;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function BanUserDialog({ profile, onBanComplete, onBanUser, disabled }: BanUserDialogProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const form = useForm<BanFormValues>({
|
|
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 (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm" disabled={disabled}>
|
|
<UserCheck className="w-4 h-4 mr-2" />
|
|
Unban
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Unban User</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to unban @{profile.username}? They will be able to access the application again.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleUnban} disabled={isSubmitting}>
|
|
{isSubmitting ? 'Unbanning...' : 'Unban User'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
// For banning, use detailed form
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="destructive" size="sm" disabled={disabled}>
|
|
<Ban className="w-4 h-4 mr-2" />
|
|
Ban
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Ban User</DialogTitle>
|
|
<DialogDescription>
|
|
Ban @{profile.username} from accessing the application. You must provide a reason and duration.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="reason_type"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Ban Reason</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a reason" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{BAN_REASONS.map((reason) => (
|
|
<SelectItem key={reason.value} value={reason.value}>
|
|
{reason.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Choose the primary reason for this ban
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{watchReasonType === 'other' && (
|
|
<FormField
|
|
control={form.control}
|
|
name="custom_reason"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Custom Reason</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Provide a detailed reason for the ban..."
|
|
className="min-h-[100px] resize-none"
|
|
maxLength={500}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{field.value?.length || 0}/500 characters
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="duration"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Ban Duration</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select duration" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{BAN_DURATIONS.map((duration) => (
|
|
<SelectItem key={duration.value} value={duration.value}>
|
|
{duration.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
How long should this ban last?
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Alert>
|
|
<AlertDescription>
|
|
<strong>User will see:</strong> Your account has been suspended. Reason:{' '}
|
|
{watchReasonType === 'other' && form.getValues('custom_reason')
|
|
? form.getValues('custom_reason')
|
|
: BAN_REASONS.find(r => r.value === watchReasonType)?.label || 'Policy violation'}
|
|
.{' '}
|
|
{watchDuration === 'permanent'
|
|
? 'This is a permanent ban.'
|
|
: `This ban will expire in ${BAN_DURATIONS.find(d => d.value === watchDuration)?.label.toLowerCase()}.`}
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setOpen(false)}
|
|
disabled={isSubmitting}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" variant="destructive" disabled={isSubmitting}>
|
|
{isSubmitting ? 'Banning...' : 'Ban User'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|