'use client' import { useState } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { Skeleton } from '@/components/ui/skeleton' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Loader2, Plus, Pencil, Trash2, FileText, GripVertical, FileCheck, FileQuestion, } from 'lucide-react' type FileRequirementsEditorProps = { roundId: string windowOpenAt?: Date | string | null windowCloseAt?: Date | string | null } type FormState = { name: string description: string acceptedMimeTypes: string[] maxSizeMB: string isRequired: boolean } const emptyForm: FormState = { name: '', description: '', acceptedMimeTypes: [], maxSizeMB: '', isRequired: true, } const MIME_TYPE_OPTIONS: { label: string; value: string }[] = [ { label: 'PDF', value: 'application/pdf' }, { label: 'Images', value: 'image/*' }, { label: 'Video', value: 'video/*' }, { label: 'Word', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }, { label: 'Excel', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, { label: 'PowerPoint', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }, ] function getMimeLabel(mime: string): string { const preset = MIME_TYPE_OPTIONS.find((p) => p.value === mime) if (preset) return preset.label if (mime.endsWith('/*')) return mime.replace('/*', '') return mime } export function FileRequirementsEditor({ roundId, windowOpenAt, windowCloseAt }: FileRequirementsEditorProps) { const [dialogOpen, setDialogOpen] = useState(false) const [editingId, setEditingId] = useState(null) const [form, setForm] = useState(emptyForm) const utils = trpc.useUtils() const { data: requirements, isLoading } = trpc.file.listRequirements.useQuery({ roundId }) const createMutation = trpc.file.createRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ roundId }) toast.success('Requirement added') closeDialog() }, onError: (err) => toast.error(err.message), }) const updateMutation = trpc.file.updateRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ roundId }) toast.success('Requirement updated') closeDialog() }, onError: (err) => toast.error(err.message), }) const deleteMutation = trpc.file.deleteRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ roundId }) toast.success('Requirement removed') }, onError: (err) => toast.error(err.message), }) const closeDialog = () => { setDialogOpen(false) setEditingId(null) setForm(emptyForm) } const openCreateDialog = () => { setForm(emptyForm) setEditingId(null) setDialogOpen(true) } const openEditDialog = (req: any) => { setForm({ name: req.name, description: req.description || '', acceptedMimeTypes: req.acceptedMimeTypes || [], maxSizeMB: req.maxSizeMB?.toString() || '', isRequired: req.isRequired ?? true, }) setEditingId(req.id) setDialogOpen(true) } const toggleMimeType = (mime: string) => { setForm((prev) => ({ ...prev, acceptedMimeTypes: prev.acceptedMimeTypes.includes(mime) ? prev.acceptedMimeTypes.filter((m) => m !== mime) : [...prev.acceptedMimeTypes, mime], })) } const handleSubmit = () => { const maxSize = form.maxSizeMB ? parseInt(form.maxSizeMB, 10) : undefined if (editingId) { updateMutation.mutate({ id: editingId, name: form.name, description: form.description || null, acceptedMimeTypes: form.acceptedMimeTypes, maxSizeMB: maxSize ?? null, isRequired: form.isRequired, }) } else { createMutation.mutate({ roundId, name: form.name, description: form.description || undefined, acceptedMimeTypes: form.acceptedMimeTypes, maxSizeMB: maxSize, isRequired: form.isRequired, sortOrder: (requirements?.length ?? 0), }) } } const isSaving = createMutation.isPending || updateMutation.isPending if (isLoading) { return (
{[1, 2, 3].map((i) => )}
) } return (
{/* Submission period info */}

Submission Period

Applicants can upload documents during the round's active window

{windowOpenAt || windowCloseAt ? ( <>

{windowOpenAt ? new Date(windowOpenAt).toLocaleDateString() : 'No start'} —{' '} {windowCloseAt ? new Date(windowCloseAt).toLocaleDateString() : 'No deadline'}

Set in the Config tab under round time windows

) : (

No dates configured — set in Config tab

)}
{/* Requirements list */}
Required Documents Define what files applicants must submit for this round
{!requirements || requirements.length === 0 ? (

No Document Requirements

Add requirements to specify what documents applicants must upload during this round.

) : (
{requirements.map((req: any) => (
{req.isRequired ? ( ) : ( )}

{req.name}

{req.isRequired ? 'Required' : 'Optional'}
{req.description && (

{req.description}

)}
{req.acceptedMimeTypes?.length > 0 ? ( req.acceptedMimeTypes.map((t: string) => ( {getMimeLabel(t)} )) ) : ( Any file type )} {req.maxSizeMB && ( Max {req.maxSizeMB} MB )}
Delete requirement? This will remove "{req.name}" from the round. Previously uploaded files will not be deleted. Cancel deleteMutation.mutate({ id: req.id })} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Delete
))}
)}
{/* Create / Edit Dialog */} { if (!open) closeDialog() }}> {editingId ? 'Edit Requirement' : 'Add Document Requirement'} {editingId ? 'Update the document requirement details.' : 'Define a new document that applicants must submit.'}
setForm((f) => ({ ...f, name: e.target.value }))} />