From 2776ac5a1df9b10dcd6ddef9265ea9296831dfdc Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 10 Jun 2026 12:25:01 -0400 Subject: [PATCH] feat(upload): support multiple CV uploads concurrently with progress list --- app/(dashboard)/candidates/page.tsx | 141 +++++++++++++++++++------ app/(dashboard)/jobs/page.tsx | 154 +++++++++++++++++++++------- 2 files changed, 224 insertions(+), 71 deletions(-) diff --git a/app/(dashboard)/candidates/page.tsx b/app/(dashboard)/candidates/page.tsx index 0425db5..b2e082f 100644 --- a/app/(dashboard)/candidates/page.tsx +++ b/app/(dashboard)/candidates/page.tsx @@ -28,12 +28,19 @@ interface Candidate { created_at: string; } +interface UploadFileStatus { + name: string; + status: "uploading" | "success" | "error"; + errorMessage?: string; +} + export default function CandidatesPage() { const [candidates, setCandidates] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [uploadSuccess, setUploadSuccess] = useState(null); + const [uploadStatuses, setUploadStatuses] = useState([]); const fetchCandidates = () => { fetch("/api/candidates") @@ -55,43 +62,73 @@ export default function CandidatesPage() { fetchCandidates(); }, []); - // Upload PDF CV in a vacuum + // Upload PDF CVs in a vacuum const handleFileUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + const files = e.target.files; + if (!files || files.length === 0) return; - if (file.type !== "application/pdf") { - setUploadError("Please upload a PDF file"); - return; - } + const fileList = Array.from(files); + + // Set initial status + const initialStatuses = fileList.map((file) => ({ + name: file.name, + status: "uploading" as const, + })); + setUploadStatuses(initialStatuses); + setUploading(true); + setUploadError(null); + setUploadSuccess(null); - try { - setUploading(true); - setUploadError(null); - setUploadSuccess(null); - - const formData = new FormData(); - formData.append("file", file); - - const res = await fetch("/candidates/api/parse-cv", { - method: "POST", - body: formData, - }); - - if (!res.ok) { - const errData = await res.json(); - throw new Error(errData.error || "Failed to process CV"); + const uploadPromises = fileList.map(async (file, index) => { + if (file.type !== "application/pdf") { + setUploadStatuses((prev) => + prev.map((status, idx) => + idx === index + ? { ...status, status: "error" as const, errorMessage: "Only PDF files are allowed" } + : status + ) + ); + return; } - const resData = await res.json(); - setUploadSuccess(`CV for ${resData.candidateName || file.name} successfully parsed!`); - fetchCandidates(); - } catch (err: unknown) { - setUploadError(err instanceof Error ? err.message : "Error uploading CV"); - } finally { - setUploading(false); - e.target.value = ""; - } + try { + const formData = new FormData(); + formData.append("file", file); + + const res = await fetch("/candidates/api/parse-cv", { + method: "POST", + body: formData, + }); + + if (!res.ok) { + const errData = await res.json(); + throw new Error(errData.error || "Failed to process CV"); + } + + await res.json(); + setUploadStatuses((prev) => + prev.map((status, idx) => + idx === index + ? { ...status, status: "success" as const } + : status + ) + ); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : "Error uploading CV"; + setUploadStatuses((prev) => + prev.map((status, idx) => + idx === index + ? { ...status, status: "error" as const, errorMessage: msg } + : status + ) + ); + } + }); + + await Promise.all(uploadPromises); + setUploading(false); + fetchCandidates(); + e.target.value = ""; }; return ( @@ -115,10 +152,11 @@ export default function CandidatesPage() { No job position will be associated initially, keeping the data isolated.