Spaces:
Sleeping
Sleeping
Update frontend/hooks/useFileUpload.ts
Browse files- frontend/hooks/useFileUpload.ts +79 -81
frontend/hooks/useFileUpload.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
-
import {
|
| 2 |
|
| 3 |
/* ================= TYPES ================= */
|
| 4 |
|
| 5 |
-
export interface
|
| 6 |
id: string;
|
| 7 |
file: File;
|
| 8 |
-
|
|
|
|
| 9 |
status: "idle" | "uploading" | "done" | "error";
|
| 10 |
url?: string;
|
| 11 |
error?: string;
|
|
@@ -14,111 +15,108 @@ export interface UploadItem {
|
|
| 14 |
/* ================= HOOK ================= */
|
| 15 |
|
| 16 |
export function useFileUpload() {
|
| 17 |
-
const [
|
|
|
|
| 18 |
|
| 19 |
-
/* ---------- ADD
|
| 20 |
-
const addFiles = useCallback((
|
| 21 |
-
const list = Array.from(
|
| 22 |
id: crypto.randomUUID(),
|
| 23 |
file,
|
|
|
|
| 24 |
progress: 0,
|
| 25 |
-
status: "idle"
|
| 26 |
}));
|
| 27 |
|
| 28 |
-
|
| 29 |
}, []);
|
| 30 |
|
| 31 |
-
/* ----------
|
| 32 |
-
const
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
)
|
| 42 |
);
|
|
|
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
const xhr = new XMLHttpRequest();
|
| 46 |
xhr.open("POST", "/api/upload");
|
| 47 |
|
| 48 |
xhr.upload.onprogress = (e) => {
|
| 49 |
if (!e.lengthComputable) return;
|
| 50 |
const percent = Math.round((e.loaded / e.total) * 100);
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
| 54 |
)
|
| 55 |
);
|
| 56 |
};
|
| 57 |
|
| 58 |
-
|
| 59 |
-
xhr.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
} catch (err: any) {
|
| 83 |
-
setUploads((prev) =>
|
| 84 |
-
prev.map((u) =>
|
| 85 |
-
u.id === item.id
|
| 86 |
-
? {
|
| 87 |
-
...u,
|
| 88 |
-
status: "error",
|
| 89 |
-
error: err.message || "Upload error",
|
| 90 |
-
}
|
| 91 |
-
: u
|
| 92 |
-
)
|
| 93 |
-
);
|
| 94 |
-
}
|
| 95 |
}, []);
|
| 96 |
|
| 97 |
-
/* ---------- UPLOAD
|
| 98 |
-
const
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
-
}, [
|
| 105 |
-
|
| 106 |
-
/* ---------- REMOVE FILE ---------- */
|
| 107 |
-
const remove = useCallback((id: string) => {
|
| 108 |
-
setUploads((prev) => prev.filter((u) => u.id !== id));
|
| 109 |
-
}, []);
|
| 110 |
-
|
| 111 |
-
/* ---------- RESET ---------- */
|
| 112 |
-
const reset = useCallback(() => {
|
| 113 |
-
setUploads([]);
|
| 114 |
-
}, []);
|
| 115 |
|
|
|
|
| 116 |
return {
|
| 117 |
-
|
|
|
|
| 118 |
addFiles,
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
reset,
|
| 123 |
};
|
| 124 |
-
}
|
|
|
|
| 1 |
+
import { useCallback, useMemo, useState } from "react";
|
| 2 |
|
| 3 |
/* ================= TYPES ================= */
|
| 4 |
|
| 5 |
+
export interface FileItem {
|
| 6 |
id: string;
|
| 7 |
file: File;
|
| 8 |
+
path: string;
|
| 9 |
+
progress: number;
|
| 10 |
status: "idle" | "uploading" | "done" | "error";
|
| 11 |
url?: string;
|
| 12 |
error?: string;
|
|
|
|
| 15 |
/* ================= HOOK ================= */
|
| 16 |
|
| 17 |
export function useFileUpload() {
|
| 18 |
+
const [files, setFiles] = useState<FileItem[]>([]);
|
| 19 |
+
const [isUploading, setIsUploading] = useState(false);
|
| 20 |
|
| 21 |
+
/* ---------- ADD FILES ---------- */
|
| 22 |
+
const addFiles = useCallback((input: FileList | File[]) => {
|
| 23 |
+
const list = Array.from(input).map<FileItem>((file) => ({
|
| 24 |
id: crypto.randomUUID(),
|
| 25 |
file,
|
| 26 |
+
path: file.name, // mặc định
|
| 27 |
progress: 0,
|
| 28 |
+
status: "idle",
|
| 29 |
}));
|
| 30 |
|
| 31 |
+
setFiles((prev) => [...prev, ...list]);
|
| 32 |
}, []);
|
| 33 |
|
| 34 |
+
/* ---------- REMOVE FILE ---------- */
|
| 35 |
+
const removeFile = useCallback((id: string) => {
|
| 36 |
+
setFiles((prev) => prev.filter((f) => f.id !== id));
|
| 37 |
+
}, []);
|
| 38 |
|
| 39 |
+
/* ---------- UPDATE PATH ---------- */
|
| 40 |
+
const updateFilePath = useCallback((id: string, path: string) => {
|
| 41 |
+
setFiles((prev) =>
|
| 42 |
+
prev.map((f) =>
|
| 43 |
+
f.id === id ? { ...f, path } : f
|
| 44 |
)
|
| 45 |
);
|
| 46 |
+
}, []);
|
| 47 |
|
| 48 |
+
/* ---------- UPLOAD SINGLE ---------- */
|
| 49 |
+
const uploadOne = useCallback(async (item: FileItem) => {
|
| 50 |
+
const form = new FormData();
|
| 51 |
+
form.append("file", item.file);
|
| 52 |
+
form.append("path", item.path);
|
| 53 |
+
|
| 54 |
+
return new Promise<void>((resolve, reject) => {
|
| 55 |
const xhr = new XMLHttpRequest();
|
| 56 |
xhr.open("POST", "/api/upload");
|
| 57 |
|
| 58 |
xhr.upload.onprogress = (e) => {
|
| 59 |
if (!e.lengthComputable) return;
|
| 60 |
const percent = Math.round((e.loaded / e.total) * 100);
|
| 61 |
+
|
| 62 |
+
setFiles((prev) =>
|
| 63 |
+
prev.map((f) =>
|
| 64 |
+
f.id === item.id
|
| 65 |
+
? { ...f, progress: percent, status: "uploading" }
|
| 66 |
+
: f
|
| 67 |
)
|
| 68 |
);
|
| 69 |
};
|
| 70 |
|
| 71 |
+
xhr.onload = () => {
|
| 72 |
+
if (xhr.status >= 200 && xhr.status < 300) {
|
| 73 |
+
const res = JSON.parse(xhr.responseText);
|
| 74 |
+
setFiles((prev) =>
|
| 75 |
+
prev.map((f) =>
|
| 76 |
+
f.id === item.id
|
| 77 |
+
? {
|
| 78 |
+
...f,
|
| 79 |
+
progress: 100,
|
| 80 |
+
status: "done",
|
| 81 |
+
url: res.url,
|
| 82 |
+
}
|
| 83 |
+
: f
|
| 84 |
+
)
|
| 85 |
+
);
|
| 86 |
+
resolve();
|
| 87 |
+
} else {
|
| 88 |
+
reject(new Error("Upload failed"));
|
| 89 |
+
}
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
xhr.onerror = () => reject(new Error("Network error"));
|
| 93 |
+
xhr.send(form);
|
| 94 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}, []);
|
| 96 |
|
| 97 |
+
/* ---------- START UPLOAD (App.tsx dùng) ---------- */
|
| 98 |
+
const startUpload = useCallback(async () => {
|
| 99 |
+
if (isUploading) return;
|
| 100 |
+
setIsUploading(true);
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
for (const f of files) {
|
| 104 |
+
if (f.status === "idle") {
|
| 105 |
+
await uploadOne(f);
|
| 106 |
+
}
|
| 107 |
}
|
| 108 |
+
} finally {
|
| 109 |
+
setIsUploading(false);
|
| 110 |
}
|
| 111 |
+
}, [files, uploadOne, isUploading]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
/* ---------- RETURN (KHỚP App.tsx) ---------- */
|
| 114 |
return {
|
| 115 |
+
files, // ✅ App.tsx dùng
|
| 116 |
+
isUploading, // ✅ App.tsx dùng
|
| 117 |
addFiles,
|
| 118 |
+
removeFile, // ✅ App.tsx dùng
|
| 119 |
+
updateFilePath, // ✅ App.tsx dùng
|
| 120 |
+
startUpload, // ✅ App.tsx dùng
|
|
|
|
| 121 |
};
|
| 122 |
+
}
|