Spaces:
Sleeping
Sleeping
| import { useState, useCallback } from "react"; | |
| /* ================= TYPES ================= */ | |
| export interface UploadItem { | |
| id: string; | |
| file: File; | |
| progress: number; // 0 → 100 | |
| status: "idle" | "uploading" | "done" | "error"; | |
| url?: string; | |
| error?: string; | |
| } | |
| /* ================= HOOK ================= */ | |
| export function useFileUpload() { | |
| const [uploads, setUploads] = useState<UploadItem[]>([]); | |
| /* ---------- ADD FILE ---------- */ | |
| const addFiles = useCallback((files: FileList | File[]) => { | |
| const list = Array.from(files).map((file) => ({ | |
| id: crypto.randomUUID(), | |
| file, | |
| progress: 0, | |
| status: "idle" as const, | |
| })); | |
| setUploads((prev) => [...prev, ...list]); | |
| }, []); | |
| /* ---------- UPLOAD SINGLE FILE ---------- */ | |
| const uploadFile = useCallback(async (item: UploadItem) => { | |
| const form = new FormData(); | |
| form.append("file", item.file); | |
| setUploads((prev) => | |
| prev.map((u) => | |
| u.id === item.id | |
| ? { ...u, status: "uploading", progress: 0 } | |
| : u | |
| ) | |
| ); | |
| try { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open("POST", "/api/upload"); | |
| xhr.upload.onprogress = (e) => { | |
| if (!e.lengthComputable) return; | |
| const percent = Math.round((e.loaded / e.total) * 100); | |
| setUploads((prev) => | |
| prev.map((u) => | |
| u.id === item.id ? { ...u, progress: percent } : u | |
| ) | |
| ); | |
| }; | |
| const result: any = await new Promise((resolve, reject) => { | |
| xhr.onload = () => { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| resolve(JSON.parse(xhr.responseText)); | |
| } else { | |
| reject(new Error("Upload failed")); | |
| } | |
| }; | |
| xhr.onerror = () => reject(new Error("Network error")); | |
| xhr.send(form); | |
| }); | |
| setUploads((prev) => | |
| prev.map((u) => | |
| u.id === item.id | |
| ? { | |
| ...u, | |
| status: "done", | |
| progress: 100, | |
| url: result.url, | |
| } | |
| : u | |
| ) | |
| ); | |
| } catch (err: any) { | |
| setUploads((prev) => | |
| prev.map((u) => | |
| u.id === item.id | |
| ? { | |
| ...u, | |
| status: "error", | |
| error: err.message || "Upload error", | |
| } | |
| : u | |
| ) | |
| ); | |
| } | |
| }, []); | |
| /* ---------- UPLOAD ALL ---------- */ | |
| const uploadAll = useCallback(async () => { | |
| for (const item of uploads) { | |
| if (item.status === "idle") { | |
| await uploadFile(item); | |
| } | |
| } | |
| }, [uploads, uploadFile]); | |
| /* ---------- REMOVE FILE ---------- */ | |
| const remove = useCallback((id: string) => { | |
| setUploads((prev) => prev.filter((u) => u.id !== id)); | |
| }, []); | |
| /* ---------- RESET ---------- */ | |
| const reset = useCallback(() => { | |
| setUploads([]); | |
| }, []); | |
| return { | |
| uploads, | |
| addFiles, | |
| uploadFile, | |
| uploadAll, | |
| remove, | |
| reset, | |
| }; | |
| } | |