const fileInput = document.querySelector("#file-upload"); const fileCount = document.querySelector("#upload-file-count"); const fileList = document.querySelector(".upload-file-list"); const dropzone = document.querySelector(".upload-dropzone"); const uploadForm = document.querySelector(".upload-form"); const uploadStatus = document.querySelector(".upload-statusbar span:first-child"); const boxStatus = document.querySelector(".upload-statusbar span:last-child"); let selectedFiles = []; let statusTimer = null; function formatBytes(bytes) { const units = ["B", "KB", "MB", "GB"]; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } if (unitIndex === 0) { return `${size} ${units[unitIndex]}`; } return `${size.toFixed(1)} ${units[unitIndex]}`; } function updateStatus(message) { if (uploadStatus) { uploadStatus.textContent = message; } } function stopStatusAnimation() { if (statusTimer) { clearInterval(statusTimer); statusTimer = null; } } function animateUploadStatus(getPrefix) { let dotCount = 0; stopStatusAnimation(); statusTimer = setInterval(() => { dotCount = (dotCount % 3) + 1; updateStatus(`${getPrefix()} Uploading${".".repeat(dotCount)}`); }, 350); } function setBoxStatus(message) { if (boxStatus) { boxStatus.textContent = message; boxStatus.title = message; } } function updateFileCount() { if (fileCount) { fileCount.textContent = `${selectedFiles.length} ${selectedFiles.length === 1 ? "file" : "files"}`; } } function setRowProgress(row, percent) { const progressBar = row.querySelector(".upload-progress-bar"); if (progressBar) { progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`; } } function createFileRow(selectedFile) { const row = document.createElement("div"); row.className = "upload-file-row"; const icon = document.createElement("span"); icon.className = "upload-file-icon"; icon.setAttribute("aria-hidden", "true"); const name = document.createElement("span"); name.className = "upload-file-name"; name.textContent = selectedFile.file.name; name.title = selectedFile.file.name; const size = document.createElement("span"); size.className = "upload-file-size"; size.textContent = formatBytes(selectedFile.file.size); const progress = document.createElement("span"); progress.className = "upload-progress"; progress.setAttribute("aria-hidden", "true"); const progressBar = document.createElement("span"); progressBar.className = "upload-progress-bar"; progress.append(progressBar); row.append(icon, name, size, progress); selectedFile.row = row; return row; } function updateSelectedFiles(files) { selectedFiles = Array.from(files || []).map((file) => ({ file, row: null, uploaded: false, failed: false, })); updateFileCount(); if (!fileList) { return; } fileList.replaceChildren(); if (!selectedFiles.length) { const emptyState = document.createElement("p"); emptyState.className = "upload-empty-state"; emptyState.textContent = "No files selected"; fileList.append(emptyState); updateStatus("Ready"); setBoxStatus("WarpBox"); return; } const fragment = document.createDocumentFragment(); selectedFiles.forEach((selectedFile) => { fragment.append(createFileRow(selectedFile)); }); fileList.append(fragment); updateStatus("Files selected"); setBoxStatus("WarpBox"); } async function createBox() { const response = await fetch("/box", { method: "POST" }); if (!response.ok) { throw new Error("Could not create upload box"); } return response.json(); } function uploadFile(boxID, selectedFile, onComplete) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", selectedFile.file); xhr.open("POST", `/box/${boxID}/upload`); xhr.upload.addEventListener("loadstart", () => { setRowProgress(selectedFile.row, 2); }); xhr.upload.addEventListener("progress", (event) => { if (!event.lengthComputable) { return; } setRowProgress(selectedFile.row, (event.loaded / event.total) * 100); }); xhr.addEventListener("load", () => { if (xhr.status < 200 || xhr.status >= 300) { selectedFile.failed = true; selectedFile.row.classList.add("is-failed"); reject(new Error("Upload failed")); return; } selectedFile.uploaded = true; selectedFile.row.classList.add("is-uploaded"); setRowProgress(selectedFile.row, 100); onComplete(); resolve(); }); xhr.addEventListener("error", () => { selectedFile.failed = true; selectedFile.row.classList.add("is-failed"); reject(new Error("Upload failed")); }); xhr.send(formData); }); } if (fileInput) { fileInput.addEventListener("change", () => { stopStatusAnimation(); updateSelectedFiles(fileInput.files); }); } if (fileInput && dropzone) { dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); }); dropzone.addEventListener("dragleave", () => { dropzone.classList.remove("is-dragging"); }); dropzone.addEventListener("drop", (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); if (!event.dataTransfer.files.length) { return; } fileInput.files = event.dataTransfer.files; stopStatusAnimation(); updateSelectedFiles(fileInput.files); }); } if (uploadForm) { uploadForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!selectedFiles.length) { updateStatus("Choose files first"); return; } let completedCount = 0; const totalCount = selectedFiles.length; const statusPrefix = () => `${completedCount}/${totalCount}`; selectedFiles.forEach((selectedFile) => { selectedFile.uploaded = false; selectedFile.failed = false; selectedFile.row.classList.remove("is-uploaded", "is-failed"); setRowProgress(selectedFile.row, 0); }); updateStatus(`${statusPrefix()} Uploading.`); animateUploadStatus(statusPrefix); try { const box = await createBox(); setBoxStatus(box.box_url); await Promise.allSettled(selectedFiles.map((selectedFile) => { return uploadFile(box.box_id, selectedFile, () => { completedCount += 1; }); })); stopStatusAnimation(); const failedCount = selectedFiles.filter((selectedFile) => selectedFile.failed).length; if (failedCount > 0) { updateStatus(`${completedCount}/${totalCount} Uploaded, ${failedCount} failed`); return; } updateStatus(`${completedCount}/${totalCount} Uploaded`); } catch (error) { stopStatusAnimation(); updateStatus("Upload failed"); } }); }