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.