From 1859c60c95d9310fa49e1aac0a7c9da8eb312fd5 Mon Sep 17 00:00:00 2001 From: Roni bhakta Date: Wed, 19 Nov 2025 20:02:07 +0530 Subject: [PATCH] feat: update UploadForm component to handle upload status and messages --- apps/web/components/UploadForm.tsx | 162 ++++++++++++++++++++++++----- apps/web/package.json | 2 +- 2 files changed, 137 insertions(+), 27 deletions(-) diff --git a/apps/web/components/UploadForm.tsx b/apps/web/components/UploadForm.tsx index 57a50fa..cb4f24a 100644 --- a/apps/web/components/UploadForm.tsx +++ b/apps/web/components/UploadForm.tsx @@ -1,17 +1,20 @@ "use client" import { useState } from "react" import axios from "axios" -import { UploadIcon } from "lucide-react" +import { UploadIcon, CheckCircle2, XCircle, Loader2, AlertCircle } from "lucide-react" import { Label } from "@workspace/ui/components/label" import { RadioGroup, RadioGroupItem } from "@workspace/ui/components/radio-group" import { Input } from "@workspace/ui/components/input" import { Button } from "@workspace/ui/components/button" +type UploadStatus = "idle" | "uploading" | "success" | "error" | "conflict" + export default function UploadForm() { const [editionId, setEditionId] = useState("") const [file, setFile] = useState(null) const [encryption, setEncryption] = useState("no") - const [isUploading, setIsUploading] = useState(false) + const [uploadStatus, setUploadStatus] = useState("idle") + const [statusMessage, setStatusMessage] = useState("") const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { @@ -21,18 +24,21 @@ export default function UploadForm() { const handleUpload = async () => { if (!file) { - alert("Please select a file") + setUploadStatus("error") + setStatusMessage("Please select a file") return } if (!editionId) { - alert("Please enter an Open Library Edition ID") + setUploadStatus("error") + setStatusMessage("Please enter an Open Library Edition ID") return } const numericId = editionId.replace(/\D/g, "") if (!numericId || isNaN(Number(numericId))) { - alert("Please enter a valid Open Library Edition ID (e.g., OL60638966M)") + setUploadStatus("error") + setStatusMessage("Please enter a valid Open Library Edition ID (e.g., OL60638966M)") return } @@ -41,33 +47,96 @@ export default function UploadForm() { formData.append("openlibrary_edition", numericId) formData.append("encrypted", encryption === "yes" ? "true" : "false") - setIsUploading(true) + setUploadStatus("uploading") + setStatusMessage("Uploading file to server...") try { - // Use environment variable or fallback to localhost for development - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080" - const response = await axios.post(`${apiUrl}/v1/api/upload`, formData, { + // Resolve API base URL for the browser. + // NOTE: when the frontend is built inside Docker we may have NEXT_PUBLIC_API_URL set to + // an internal Docker hostname (eg. http://lenny_api:80) which is not reachable from the user's + // browser. Prefer these strategies in order: + // 1. If NEXT_PUBLIC_API_URL is set and doesn't look like an internal Docker hostname, use it. + // 2. If NEXT_PUBLIC_API_URL points to an internal Docker hostname (contains "lenny_api"), + // and we're running in the browser on localhost, use http://localhost:8080. + // 3. Otherwise use the current origin (window.location.origin) and post to a relative path + // so a reverse-proxy (nginx) can forward the request to the API. + const envApi = process.env.NEXT_PUBLIC_API_URL + let apiBase = "http://localhost:8080" + if (envApi) { + const isInternalDockerHost = envApi.includes("lenny_api") || envApi.includes("127.0.0.1") + if (!isInternalDockerHost) { + apiBase = envApi + } else if (typeof window !== "undefined") { + // If user is running the browser on localhost, call the mapped host port. + if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") { + apiBase = "http://localhost:8080" + } else { + // If the site is served from a host (not localhost), use relative path so the + // front-facing proxy can route to the API container. + apiBase = window.location.origin + } + } + } else if (typeof window !== "undefined") { + apiBase = window.location.origin + } + + const uploadUrl = apiBase === window.location.origin ? "/v1/api/upload" : `${apiBase}/v1/api/upload` + + const response = await axios.post(uploadUrl, formData, { headers: { "Content-Type": "multipart/form-data", }, }) - console.log("File uploaded successfully:", response.data) - alert("File uploaded successfully!") - // Reset form - setEditionId("") - setFile(null) - setEncryption("no") - // Clear file input - const fileInput = document.getElementById("file") as HTMLInputElement - if (fileInput) fileInput.value = "" + + // Check if response is successful (status 200) + if (response.status === 200) { + setUploadStatus("success") + setStatusMessage("File uploaded successfully! Processing complete.") + + // Reset form after 3 seconds + setTimeout(() => { + setEditionId("") + setFile(null) + setEncryption("no") + setUploadStatus("idle") + setStatusMessage("") + // Clear file input + const fileInput = document.getElementById("file") as HTMLInputElement + if (fileInput) fileInput.value = "" + }, 3000) + } } catch (error) { - console.error("Error uploading file:", error) + // Log full error for debugging (network / CORS / response body) + console.error("Error uploading file (detailed):", error) + if (axios.isAxiosError(error) && error.response) { - alert(`Error: ${error.response.data.detail || error.response.statusText}`) + const status = error.response.status + + if (status === 409) { + setUploadStatus("conflict") + setStatusMessage("This file already exists on the server. Edition ID already has content.") + } else if (status === 413) { + setUploadStatus("error") + setStatusMessage("File is too large. Maximum size is 50MB.") + } else if (status === 503) { + setUploadStatus("error") + setStatusMessage("Uploader not allowed. Check your IP permissions.") + } else if (status === 400) { + setUploadStatus("error") + setStatusMessage("Invalid file. Please upload a valid EPUB or PDF.") + } else { + setUploadStatus("error") + setStatusMessage(error.response.data?.detail || "Error uploading file. Please try again.") + } + } else if (axios.isAxiosError(error) && error.request) { + // The request was made but no response was received (often CORS or network error) + setUploadStatus("error") + setStatusMessage( + "Network or CORS error: no response received from the API. Check browser console for CORS errors and ensure the API is reachable from the browser." + ) } else { - alert("Error uploading file. Please try again.") + setUploadStatus("error") + setStatusMessage("Unexpected error. Check console for details.") } - } finally { - setIsUploading(false) } } @@ -139,14 +208,55 @@ export default function UploadForm() { -
+
+ + {/* Status Messages */} + {uploadStatus !== "idle" && statusMessage && ( +
+ {uploadStatus === "success" && ( + + )} + {uploadStatus === "error" && ( + + )} + {uploadStatus === "conflict" && ( + + )} + {uploadStatus === "uploading" && ( + + )} +
+

+ {statusMessage} +

+
+
+ )}
diff --git a/apps/web/package.json b/apps/web/package.json index 7d9654c..19c21c0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "type": "module", "private": true, "scripts": { - "dev": "next dev --turbopack --port 3000", + "dev": "next dev --turbopack --port 3002", "build": "next build", "start": "next start", "lint": "next lint",