'use client' import { useState } from 'react' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { toast } from 'sonner' import { Plus, Pencil, Trash2, GripVertical, ArrowUp, ArrowDown, FileText, Loader2, } from 'lucide-react' const MIME_TYPE_PRESETS = [ { label: 'PDF', value: 'application/pdf' }, { label: 'Images', value: 'image/*' }, { label: 'Video', value: 'video/*' }, { label: 'Word Documents', 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_PRESETS.find((p) => p.value === mime) if (preset) return preset.label if (mime.endsWith('/*')) return mime.replace('/*', '') return mime } interface FileRequirementsEditorProps { roundId: string } interface RequirementFormData { name: string description: string acceptedMimeTypes: string[] maxSizeMB: string isRequired: boolean } const emptyForm: RequirementFormData = { name: '', description: '', acceptedMimeTypes: [], maxSizeMB: '', isRequired: true, } export function FileRequirementsEditor({ roundId }: FileRequirementsEditorProps) { 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 created') }, onError: (err) => toast.error(err.message), }) const updateMutation = trpc.file.updateRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ roundId }) toast.success('Requirement updated') }, onError: (err) => toast.error(err.message), }) const deleteMutation = trpc.file.deleteRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ roundId }) toast.success('Requirement deleted') }, onError: (err) => toast.error(err.message), }) const reorderMutation = trpc.file.reorderRequirements.useMutation({ onSuccess: () => utils.file.listRequirements.invalidate({ roundId }), onError: (err) => toast.error(err.message), }) const [dialogOpen, setDialogOpen] = useState(false) const [editingId, setEditingId] = useState(null) const [form, setForm] = useState(emptyForm) const openCreate = () => { setEditingId(null) setForm(emptyForm) setDialogOpen(true) } const openEdit = (req: typeof requirements[number]) => { setEditingId(req.id) setForm({ name: req.name, description: req.description || '', acceptedMimeTypes: req.acceptedMimeTypes, maxSizeMB: req.maxSizeMB?.toString() || '', isRequired: req.isRequired, }) setDialogOpen(true) } const handleSave = async () => { if (!form.name.trim()) { toast.error('Name is required') return } const maxSizeMB = form.maxSizeMB ? parseInt(form.maxSizeMB) : undefined if (editingId) { await updateMutation.mutateAsync({ id: editingId, name: form.name.trim(), description: form.description.trim() || null, acceptedMimeTypes: form.acceptedMimeTypes, maxSizeMB: maxSizeMB || null, isRequired: form.isRequired, }) } else { await createMutation.mutateAsync({ roundId, name: form.name.trim(), description: form.description.trim() || undefined, acceptedMimeTypes: form.acceptedMimeTypes, maxSizeMB, isRequired: form.isRequired, sortOrder: requirements.length, }) } setDialogOpen(false) } const handleDelete = async (id: string) => { await deleteMutation.mutateAsync({ id }) } const handleMove = async (index: number, direction: 'up' | 'down') => { const newOrder = [...requirements] const swapIndex = direction === 'up' ? index - 1 : index + 1 if (swapIndex < 0 || swapIndex >= newOrder.length) return ;[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]] await reorderMutation.mutateAsync({ roundId, orderedIds: newOrder.map((r) => r.id), }) } const toggleMimeType = (mime: string) => { setForm((prev) => ({ ...prev, acceptedMimeTypes: prev.acceptedMimeTypes.includes(mime) ? prev.acceptedMimeTypes.filter((m) => m !== mime) : [...prev.acceptedMimeTypes, mime], })) } const isSaving = createMutation.isPending || updateMutation.isPending return (
File Requirements Define required files applicants must upload for this round
{isLoading ? (
) : requirements.length === 0 ? (
No file requirements defined. Applicants can still upload files freely.
) : (
{requirements.map((req, index) => (
{req.name} {req.isRequired ? 'Required' : 'Optional'}
{req.description && (

{req.description}

)}
{req.acceptedMimeTypes.map((mime) => ( {getMimeLabel(mime)} ))} {req.maxSizeMB && ( Max {req.maxSizeMB}MB )}
))}
)}
{/* Create/Edit Dialog */} {editingId ? 'Edit' : 'Add'} File Requirement Define what file applicants need to upload for this round.
setForm((p) => ({ ...p, name: e.target.value }))} placeholder="e.g., Executive Summary" />