Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 136 additions & 26 deletions apps/web/components/UploadForm.tsx
Original file line number Diff line number Diff line change
@@ -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<File | null>(null)
const [encryption, setEncryption] = useState("no")
const [isUploading, setIsUploading] = useState(false)
const [uploadStatus, setUploadStatus] = useState<UploadStatus>("idle")
const [statusMessage, setStatusMessage] = useState("")

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
Expand All @@ -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
}

Expand All @@ -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)
}
}

Expand Down Expand Up @@ -139,14 +208,55 @@ export default function UploadForm() {
</RadioGroup>
</div>

<div className="pt-4">
<div className="pt-4 space-y-4">
<Button
onClick={handleUpload}
disabled={isUploading || !file}
disabled={uploadStatus === "uploading" || !file}
className="w-full"
>
{isUploading ? "Uploading..." : "Upload File"}
{uploadStatus === "uploading" ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Uploading...
</>
) : (
"Upload File"
)}
</Button>

{/* Status Messages */}
{uploadStatus !== "idle" && statusMessage && (
<div className={`
flex items-start gap-3 p-4 rounded-md border
${uploadStatus === "success" ? "bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800" : ""}
${uploadStatus === "error" ? "bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800" : ""}
${uploadStatus === "conflict" ? "bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800" : ""}
${uploadStatus === "uploading" ? "bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800" : ""}
`}>
{uploadStatus === "success" && (
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400 shrink-0 mt-0.5" />
)}
{uploadStatus === "error" && (
<XCircle className="w-5 h-5 text-red-600 dark:text-red-400 shrink-0 mt-0.5" />
)}
{uploadStatus === "conflict" && (
<AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 shrink-0 mt-0.5" />
)}
{uploadStatus === "uploading" && (
<Loader2 className="w-5 h-5 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5 animate-spin" />
)}
<div className="flex-1">
<p className={`text-sm font-medium
${uploadStatus === "success" ? "text-green-900 dark:text-green-100" : ""}
${uploadStatus === "error" ? "text-red-900 dark:text-red-100" : ""}
${uploadStatus === "conflict" ? "text-yellow-900 dark:text-yellow-100" : ""}
${uploadStatus === "uploading" ? "text-blue-900 dark:text-blue-100" : ""}
`}>
{statusMessage}
</p>
</div>
</div>
)}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down