"use client"; // Uploading a product (or a new revision of one): the hub gives an upload token, then the browser sends the file to // the catalog itself, so files never pass through the website's server. import { useRouter } from "next/navigation"; import { useState } from "react"; import { ErrorText } from "@/components/forms"; import { Button } from "@/components/ui/button"; import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"; import { Textarea } from "@/components/ui/textarea"; import { startUpload } from "@/app/products/actions"; type Option = { id: string; name: string }; /** Sends form to url with the token, reporting progress; resolves with the catalog's status and JSON answer. */ function send(url: string, token: string, form: FormData, progress: (fraction: number) => void) { return new Promise<{ status: number; body: unknown }>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", url); xhr.setRequestHeader("Authorization", `Bearer ${token}`); xhr.upload.onprogress = (e) => e.lengthComputable && progress(e.loaded / e.total); xhr.onload = () => { let body: unknown = null; try { body = JSON.parse(xhr.responseText); } catch {} resolve({ status: xhr.status, body }); }; xhr.onerror = () => reject(new Error("network")); xhr.send(form); }); } export function UploadForm({ catalogs, product }: { catalogs: Option[]; product?: string }) { const router = useRouter(); const [error, setError] = useState(""); const [progress, setProgress] = useState(null); async function submit(form: FormData) { setError(""); setProgress(0); const start = await startUpload(String(form.get("catalog")), product); if ("error" in start) { setError(start.error); setProgress(null); return; } const body = new FormData(); body.set("file", form.get("file") as File); body.set("notes", String(form.get("notes") ?? "")); try { const answer = await send(start.url, start.token, body, setProgress); const value = answer.body as { product?: unknown; message?: unknown } | null; if (answer.status === 201 && typeof value?.product === "string") { router.push(`/products/${value.product}`); router.refresh(); return; } setError( typeof value?.message === "string" && value.message ? value.message : `The catalog refused the upload (${answer.status}).`, ); } catch { setError("Couldn't reach the catalog. Try again in a moment."); } setProgress(null); } const busy = progress !== null; return (
{ e.preventDefault(); void submit(new FormData(e.currentTarget)); }} > Catalog {catalogs.map((c) => ( {c.name} ))} File A Create Mode .chkn, or a zip of the product's files. {product ? "What changed" : "Notes"}