shadcn file upload: wire the dropzone to your storage
components/file-upload.tsx
"use client";
import { useRef } from "react";
import { UploadDropzone, type Uploader, type UploadItem } from "@/components/upload-dropzone/UploadDropzone";
type Signed = { url: string; key: string };
const ALLOWED = ["image/png", "image/jpeg", "image/webp", "application/pdf"];
const MAX_SIZE = 10 * 1024 * 1024;
// PUT the file to a URL your server signed. XHR because fetch can't report upload progress.
function putWithProgress(url: string, file: File, onProgress: (pct: number) => void, signal: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", url);
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress((e.loaded / e.total) * 100);
};
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`Storage said ${xhr.status}`)));
xhr.onerror = () => reject(new Error("Network error"));
xhr.onabort = () => reject(new DOMException("Upload cancelled", "AbortError"));
signal.addEventListener("abort", () => xhr.abort(), { once: true });
xhr.send(file);
});
}
export function FileUpload({ onUploaded }: { onUploaded?: (keys: string[]) => void }) {
// which storage key each File ended up under, so onAllDone can report keys
const keys = useRef(new WeakMap<File, string>());
const upload: Uploader = async (file, onProgress, signal) => {
// accept only filters the file picker; dropped files skip it, so check here too
if (!ALLOWED.includes(file.type)) throw new Error("This file type isn't allowed");
const res = await fetch("/api/uploads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: file.name, type: file.type, size: file.size }),
signal,
});
if (!res.ok) throw new Error("Couldn't get an upload URL");
const { url, key } = (await res.json()) as Signed;
await putWithProgress(url, file, onProgress, signal);
keys.current.set(file, key);
};
const done = (items: UploadItem[]) => {
const uploaded = items.flatMap((i) => (i.file && keys.current.has(i.file) ? [keys.current.get(i.file)!] : []));
onUploaded?.(uploaded);
};
return (
<UploadDropzone
upload={upload}
accept={ALLOWED.join(",")}
maxSize={MAX_SIZE}
title="Drop files here"
hint="PNG, JPG, WEBP or PDF, up to 10 MB each"
chips={["PNG", "JPG", "WEBP", "PDF"]}
onAllDone={done}
/>
);
}