"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 { stageId: 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({ stageId, }: FileRequirementsEditorProps) { const utils = trpc.useUtils(); const { data: requirements = [], isLoading } = trpc.file.listRequirements.useQuery({ stageId }); const createMutation = trpc.file.createRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ stageId }); toast.success("Requirement created"); }, onError: (err) => toast.error(err.message), }); const updateMutation = trpc.file.updateRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ stageId }); toast.success("Requirement updated"); }, onError: (err) => toast.error(err.message), }); const deleteMutation = trpc.file.deleteRequirement.useMutation({ onSuccess: () => { utils.file.listRequirements.invalidate({ stageId }); toast.success("Requirement deleted"); }, onError: (err) => toast.error(err.message), }); const reorderMutation = trpc.file.reorderRequirements.useMutation({ onSuccess: () => utils.file.listRequirements.invalidate({ stageId }), 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({ stageId, 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({ stageId, 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" />