'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 COMMON_MIME_PRESETS: { label: string; value: string }[] = [ { label: 'PDF only', value: 'application/pdf' }, { label: 'Images', value: 'image/png, image/jpeg, image/webp' }, { label: 'Video', value: 'video/mp4, video/quicktime, video/webm' }, { label: 'Documents', value: 'application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document' }, { label: 'Spreadsheets', value: 'application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/csv' }, { label: 'Presentations', value: 'application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation' }, { label: 'Any file', value: '' }, ] 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 || []).join(', '), maxSizeMB: req.maxSizeMB?.toString() || '', isRequired: req.isRequired ?? true, }) setEditingId(req.id) setDialogOpen(true) } const handleSubmit = () => { const mimeTypes = form.acceptedMimeTypes .split(',') .map((s) => s.trim()) .filter(Boolean) const maxSize = form.maxSizeMB ? parseInt(form.maxSizeMB, 10) : undefined if (editingId) { updateMutation.mutate({ id: editingId, name: form.name, description: form.description || null, acceptedMimeTypes: mimeTypes, maxSizeMB: maxSize ?? null, isRequired: form.isRequired, }) } else { createMutation.mutate({ roundId, name: form.name, description: form.description || undefined, acceptedMimeTypes: mimeTypes, 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) => { if (t === 'application/pdf') return 'PDF' if (t.startsWith('image/')) return t.replace('image/', '').toUpperCase() if (t.startsWith('video/')) return t.replace('video/', '').toUpperCase() return t.split('/').pop()?.toUpperCase() || t }).join(', ')} ) : ( 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 }))} />