314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import { jsxs, jsx } from "react/jsx-runtime";
|
|
import { B as Button } from "./button-CdJZJLGw.js";
|
|
import { I as Input } from "./input-BsvJqbcd.js";
|
|
import axios from "axios";
|
|
import { CheckCircle, Loader2 } from "lucide-react";
|
|
import { useState, useRef, useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
import { I as InputError } from "./input-error-D1JIzedA.js";
|
|
const ChunkedUploaderInput = ({
|
|
storage,
|
|
isSubmit,
|
|
courseId,
|
|
sectionId,
|
|
filetype,
|
|
delayUpload = false,
|
|
onError,
|
|
onCancelUpload,
|
|
onFileSelected,
|
|
onFileUploaded
|
|
}) => {
|
|
const [file, setFile] = useState(null);
|
|
const [uploadId, setUploadId] = useState(null);
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
const [uploadStatus, setUploadStatus] = useState("idle");
|
|
const fileInputRef = useRef(null);
|
|
const abortControllerRef = useRef(null);
|
|
const maxFileSize = 1024 * 1024 * 1024;
|
|
const chunkSize = 5 * 1024 * 1024;
|
|
useEffect(() => {
|
|
var _a, _b;
|
|
axios.defaults.withCredentials = true;
|
|
const token = (_a = document.cookie.split("; ").find((row) => row.startsWith("XSRF-TOKEN="))) == null ? void 0 : _a.split("=")[1];
|
|
if (token) {
|
|
axios.defaults.headers.common["X-XSRF-TOKEN"] = decodeURIComponent(token);
|
|
} else {
|
|
const metaToken = (_b = document.querySelector('meta[name="csrf-token"]')) == null ? void 0 : _b.getAttribute("content");
|
|
if (metaToken) {
|
|
axios.defaults.headers.common["X-CSRF-TOKEN"] = metaToken;
|
|
}
|
|
}
|
|
}, []);
|
|
useEffect(() => {
|
|
if (isSubmit) {
|
|
initiateUpload();
|
|
}
|
|
}, [isSubmit]);
|
|
const handleFileChange = (event) => {
|
|
if (event.target.files && event.target.files.length > 0) {
|
|
const selectedFile = event.target.files[0];
|
|
if (selectedFile.size > maxFileSize) {
|
|
setErrorMessage(`File is too large. Maximum file size is ${maxFileSize / (1024 * 1024)} MB`);
|
|
return;
|
|
}
|
|
setFile(selectedFile);
|
|
setErrorMessage("");
|
|
setUploadStatus("idle");
|
|
setUploadProgress(0);
|
|
if (delayUpload) {
|
|
if (onFileSelected) {
|
|
onFileSelected(selectedFile);
|
|
}
|
|
} else {
|
|
initiateUpload();
|
|
}
|
|
}
|
|
};
|
|
const initiateUpload = async () => {
|
|
var _a, _b, _c, _d;
|
|
if (!file) return;
|
|
setUploadStatus("initializing");
|
|
setErrorMessage("");
|
|
try {
|
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
const response = await axios.post(
|
|
"/dashboard/uploads/chunked/initialize",
|
|
{
|
|
storage,
|
|
filename: file.name,
|
|
mimetype: file.type,
|
|
filesize: (file.size || 0) / 1024,
|
|
filetype,
|
|
total_chunks: totalChunks,
|
|
course_id: courseId,
|
|
course_section_id: sectionId
|
|
},
|
|
{
|
|
timeout: 12e4
|
|
// 2 minutes timeout for initialization request
|
|
}
|
|
);
|
|
if (response.data.success) {
|
|
setUploadId(response.data.upload_id);
|
|
await uploadChunks(response.data.upload_id, totalChunks);
|
|
} else {
|
|
throw new Error(response.data.message || "Failed to initialize upload");
|
|
}
|
|
} catch (error) {
|
|
setUploadStatus("error");
|
|
setErrorMessage(((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) || error.message || "Failed to initialize upload");
|
|
if (onError) onError(((_d = (_c = error.response) == null ? void 0 : _c.data) == null ? void 0 : _d.message) || error.message || "Failed to initialize upload");
|
|
}
|
|
};
|
|
const uploadChunks = async (uploadId2, totalChunks) => {
|
|
var _a, _b, _c, _d;
|
|
if (!file) return;
|
|
setUploadStatus("uploading");
|
|
abortControllerRef.current = new AbortController();
|
|
const signal = abortControllerRef.current.signal;
|
|
try {
|
|
const uploadedParts = [];
|
|
let uploadedChunks = 0;
|
|
for (let chunkIndex = 0; chunkIndex < totalChunks && !signal.aborted; chunkIndex++) {
|
|
const start = chunkIndex * chunkSize;
|
|
const end = Math.min(start + chunkSize, file.size);
|
|
const chunk = file.slice(start, end);
|
|
const reader = new FileReader();
|
|
const readChunk = new Promise((resolve, reject) => {
|
|
reader.onloadend = () => {
|
|
const base64data2 = reader.result;
|
|
resolve(base64data2);
|
|
};
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsDataURL(chunk);
|
|
});
|
|
const base64data = await readChunk;
|
|
const response = await axios.post(
|
|
`/dashboard/uploads/chunked/${uploadId2}/chunk`,
|
|
{
|
|
storage,
|
|
chunk_data: base64data,
|
|
part_number: chunkIndex + 1,
|
|
filename: file.name,
|
|
mimetype: file.type
|
|
},
|
|
{
|
|
signal,
|
|
timeout: 12e4,
|
|
// 2 minute timeout for chunk uploads
|
|
maxContentLength: Infinity,
|
|
// Allow large content
|
|
maxBodyLength: Infinity
|
|
// Allow large body
|
|
}
|
|
);
|
|
if (response.data.success) {
|
|
uploadedChunks++;
|
|
const progress = Math.round(uploadedChunks / totalChunks * 100);
|
|
setUploadProgress(progress);
|
|
uploadedParts.push({
|
|
PartNumber: chunkIndex + 1,
|
|
ETag: response.data.etag || ""
|
|
// The server should return the ETag
|
|
});
|
|
} else {
|
|
throw new Error(response.data.message || "Failed to upload chunk");
|
|
}
|
|
}
|
|
if (signal.aborted) {
|
|
return;
|
|
}
|
|
await completeUpload(uploadId2);
|
|
} catch (error) {
|
|
if (signal.aborted) {
|
|
setUploadStatus("idle");
|
|
setUploadProgress(0);
|
|
return;
|
|
}
|
|
setUploadStatus("error");
|
|
setErrorMessage(((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) || error.message || "Failed to upload file chunks");
|
|
if (onError) onError(((_d = (_c = error.response) == null ? void 0 : _c.data) == null ? void 0 : _d.message) || error.message || "Failed to upload file chunks");
|
|
}
|
|
};
|
|
const completeUpload = async (uploadId2) => {
|
|
var _a, _b, _c, _d;
|
|
setUploadStatus("completing");
|
|
try {
|
|
const response = await axios.post(
|
|
`/dashboard/uploads/chunked/${uploadId2}/complete`,
|
|
{
|
|
storage
|
|
},
|
|
{
|
|
timeout: 12e4
|
|
// 2 minute timeout for completion request
|
|
}
|
|
);
|
|
if (response.data.success) {
|
|
setUploadStatus("completed");
|
|
const fileData = {
|
|
file_path: response.data.file_path,
|
|
file_url: response.data.file_url,
|
|
signed_url: response.data.signed_url,
|
|
mime_type: response.data.mime_type,
|
|
file_name: response.data.file_name,
|
|
file_size: response.data.file_size
|
|
};
|
|
onFileUploaded == null ? void 0 : onFileUploaded(fileData);
|
|
setTimeout(() => {
|
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
setFile(null);
|
|
setUploadId(null);
|
|
setUploadProgress(0);
|
|
setUploadStatus("idle");
|
|
}, 3e3);
|
|
} else {
|
|
throw new Error(response.data.message || "Failed to complete upload");
|
|
}
|
|
} catch (error) {
|
|
setUploadStatus("error");
|
|
setErrorMessage(((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) || error.message || "Failed to complete upload");
|
|
if (onError) onError(((_d = (_c = error.response) == null ? void 0 : _c.data) == null ? void 0 : _d.message) || error.message || "Failed to complete upload");
|
|
}
|
|
};
|
|
const cancelUpload = async () => {
|
|
if (uploadId && uploadStatus !== "idle" && uploadStatus !== "completed") {
|
|
onCancelUpload == null ? void 0 : onCancelUpload();
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
}
|
|
try {
|
|
await axios.delete(`/dashboard/uploads/chunked/${uploadId}/abort`);
|
|
} catch (error) {
|
|
toast.error("Error aborting upload:" + error);
|
|
}
|
|
setUploadStatus("idle");
|
|
setUploadProgress(0);
|
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
setFile(null);
|
|
}
|
|
};
|
|
const renderStatus = () => {
|
|
switch (uploadStatus) {
|
|
case "initializing":
|
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
|
|
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
|
|
"Initializing upload..."
|
|
] });
|
|
case "uploading":
|
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
|
|
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
|
|
"Uploading file chunks..."
|
|
] });
|
|
case "completing":
|
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
|
|
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
|
|
"Finalizing upload..."
|
|
] });
|
|
case "completed":
|
|
return /* @__PURE__ */ jsxs("div", { className: "text-secondary-foreground flex items-center", children: [
|
|
/* @__PURE__ */ jsx(CheckCircle, { className: "mr-2 h-4 w-4" }),
|
|
"Completed upload"
|
|
] });
|
|
// case 'error':
|
|
// return (
|
|
// <div className="text-destructive flex items-center text-sm">
|
|
// <AlertCircle className="mr-2 h-4 w-4" />
|
|
// Error: {errorMessage}
|
|
// </div>
|
|
// );
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
/* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-sm", children: [
|
|
/* @__PURE__ */ jsx(
|
|
Input,
|
|
{
|
|
type: "file",
|
|
name: "file",
|
|
onChange: (e) => {
|
|
var _a;
|
|
handleFileChange(e);
|
|
const file2 = (_a = e.target.files) == null ? void 0 : _a[0];
|
|
if (file2) {
|
|
setFile(file2);
|
|
onFileSelected == null ? void 0 : onFileSelected(file2);
|
|
}
|
|
}
|
|
}
|
|
),
|
|
uploadStatus === "uploading" && file && /* @__PURE__ */ jsxs("div", { className: "absolute top-0 left-0 z-10 flex h-full w-full items-center justify-between", children: [
|
|
/* @__PURE__ */ jsxs("div", { className: "relative h-full w-full overflow-hidden bg-gray-200", children: [
|
|
/* @__PURE__ */ jsx(
|
|
"div",
|
|
{
|
|
className: "bg-secondary absolute top-0 left-0 h-full transition-all duration-300 ease-in-out",
|
|
style: { width: `${uploadProgress}%` }
|
|
}
|
|
),
|
|
/* @__PURE__ */ jsxs("div", { className: "relative z-10 flex h-full items-center justify-between gap-2 px-2 text-xs", children: [
|
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
uploadProgress,
|
|
"%"
|
|
] }),
|
|
renderStatus(),
|
|
/* @__PURE__ */ jsxs("span", { className: "text-gray-800", children: [
|
|
"Size: (",
|
|
(file ? file.size / (1024 * 1024) : 0).toFixed(2),
|
|
" MB)"
|
|
] })
|
|
] })
|
|
] }),
|
|
/* @__PURE__ */ jsx("div", { className: "bg-gray-200", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", onClick: cancelUpload, className: "text-xs", children: "Cancel" }) })
|
|
] })
|
|
] }),
|
|
errorMessage && /* @__PURE__ */ jsx(InputError, { message: errorMessage })
|
|
] });
|
|
};
|
|
export {
|
|
ChunkedUploaderInput as C
|
|
};
|