Spaces:
Running
Running
| import { useCallback, useMemo, useState } from "react"; | |
| /* ================= TYPES ================= */ | |
| export interface FileItem { | |
| id: string; | |
| file: File; | |
| path: string; | |
| progress: number; | |
| status: "idle" | "uploading" | "done" | "error"; | |
| url?: string; | |
| error?: string; | |
| } | |
| /* ================= HOOK ================= */ | |
| export function useFileUpload() { | |
| const [files, setFiles] = useState<FileItem[]>([]); | |
| const [isUploading, setIsUploading] = useState(false); | |
| /* ---------- ADD FILES ---------- */ | |
| const addFiles = useCallback((input: FileList | File[]) => { | |
| const list = Array.from(input).map<FileItem>((file) => ({ | |
| id: crypto.randomUUID(), | |
| file, | |
| path: file.name, // mặc định | |
| progress: 0, | |
| status: "idle", | |
| })); | |
| setFiles((prev) => [...prev, ...list]); | |
| }, []); | |
| /* ---------- REMOVE FILE ---------- */ | |
| const removeFile = useCallback((id: string) => { | |
| setFiles((prev) => prev.filter((f) => f.id !== id)); | |
| }, []); | |
| /* ---------- UPDATE PATH ---------- */ | |
| const updateFilePath = useCallback((id: string, path: string) => { | |
| setFiles((prev) => | |
| prev.map((f) => | |
| f.id === id ? { ...f, path } : f | |
| ) | |
| ); | |
| }, []); | |
| /* ---------- UPLOAD SINGLE ---------- */ | |
| const uploadOne = useCallback(async (item: FileItem) => { | |
| const form = new FormData(); | |
| form.append("file", item.file); | |
| form.append("path", item.path); | |
| return new Promise<void>((resolve, reject) => { | |
| 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); | |
| setFiles((prev) => | |
| prev.map((f) => | |
| f.id === item.id | |
| ? { ...f, progress: percent, status: "uploading" } | |
| : f | |
| ) | |
| ); | |
| }; | |
| xhr.onload = () => { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| const res = JSON.parse(xhr.responseText); | |
| setFiles((prev) => | |
| prev.map((f) => | |
| f.id === item.id | |
| ? { | |
| ...f, | |
| progress: 100, | |
| status: "done", | |
| url: res.url, | |
| } | |
| : f | |
| ) | |
| ); | |
| resolve(); | |
| } else { | |
| reject(new Error("Upload failed")); | |
| } | |
| }; | |
| xhr.onerror = () => reject(new Error("Network error")); | |
| xhr.send(form); | |
| }); | |
| }, []); | |
| /* ---------- START UPLOAD (App.tsx dùng) ---------- */ | |
| const startUpload = useCallback(async () => { | |
| if (isUploading) return; | |
| setIsUploading(true); | |
| try { | |
| for (const f of files) { | |
| if (f.status === "idle") { | |
| await uploadOne(f); | |
| } | |
| } | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| }, [files, uploadOne, isUploading]); | |
| /* ---------- RETURN (KHỚP App.tsx) ---------- */ | |
| return { | |
| files, // ✅ App.tsx dùng | |
| isUploading, // ✅ App.tsx dùng | |
| addFiles, | |
| removeFile, // ✅ App.tsx dùng | |
| updateFilePath, // ✅ App.tsx dùng | |
| startUpload, // ✅ App.tsx dùng | |
| }; | |
| } | |