diff --git a/static/js/alerts.js b/static/js/alerts.js new file mode 100644 index 0000000..4336d9d --- /dev/null +++ b/static/js/alerts.js @@ -0,0 +1,11 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll("[data-toast-close]").forEach((button) => { + button.addEventListener("click", () => { + const toast = button.closest(".toast"); + if (toast) { + toast.classList.add("hidden"); + setTimeout(() => toast.remove(), 200); + } + }); + }); +}); diff --git a/static/js/exports.js b/static/js/exports.js index 5c2a6de..f47e298 100644 --- a/static/js/exports.js +++ b/static/js/exports.js @@ -39,20 +39,67 @@ document.addEventListener("DOMContentLoaded", () => { const formData = new FormData(form); const format = formData.get("format") || "csv"; - const response = await fetch(submitUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - format, - include_metadata: formData.get("include_metadata") === "true", - filters: null, - }), - }); + const submitBtn = form.querySelector("button[type='submit']"); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.classList.add("loading"); + } + + let response; + try { + response = await fetch(submitUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + format, + include_metadata: formData.get("include_metadata") === "true", + filters: null, + }), + }); + } catch (error) { + console.error(error); + NotificationCenter.show({ + message: "Network error during export.", + level: "error", + }); + const errorContainer = form.querySelector("[data-export-error]"); + if (errorContainer) { + errorContainer.textContent = "Network error during export."; + errorContainer.classList.remove("hidden"); + } + submitBtn?.classList.remove("loading"); + submitBtn?.removeAttribute("disabled"); + return; + } if (!response.ok) { - alert("Export failed. Please try again."); + let detail = "Export failed. Please try again."; + try { + const payload = await response.json(); + if (payload?.detail) { + detail = Array.isArray(payload.detail) + ? payload.detail.map((item) => item.msg || item).join("; ") + : payload.detail; + } + } catch (error) { + // ignore JSON parse issues + } + + NotificationCenter.show({ + message: detail, + level: "error", + }); + + const errorContainer = form.querySelector("[data-export-error]"); + if (errorContainer) { + errorContainer.textContent = detail; + errorContainer.classList.remove("hidden"); + } + + submitBtn?.classList.remove("loading"); + submitBtn?.removeAttribute("disabled"); return; } @@ -79,6 +126,14 @@ document.addEventListener("DOMContentLoaded", () => { if (modal) { closeModal(modal); } + + NotificationCenter.show({ + message: `Export ready: ${filename}`, + level: "success", + }); + + submitBtn?.classList.remove("loading"); + submitBtn?.removeAttribute("disabled"); } document.querySelectorAll("[data-export-trigger]").forEach((button) => { @@ -90,7 +145,10 @@ document.addEventListener("DOMContentLoaded", () => { await loadModal(dataset); } catch (error) { console.error(error); - alert("Unable to open export dialog."); + NotificationCenter.show({ + message: "Unable to open export dialog.", + level: "error", + }); } }); }); diff --git a/static/js/imports.js b/static/js/imports.js index f64480a..c6fff40 100644 --- a/static/js/imports.js +++ b/static/js/imports.js @@ -95,6 +95,11 @@ document.addEventListener("DOMContentLoaded", () => { uploadButton.classList.add("loading"); // Actual upload logic handled separately (e.g., fetch). + NotificationCenter?.show({ + message: "Upload started…", + level: "info", + timeout: 2000, + }); }); }); }); diff --git a/static/js/notifications.js b/static/js/notifications.js new file mode 100644 index 0000000..8842ba5 --- /dev/null +++ b/static/js/notifications.js @@ -0,0 +1,38 @@ +(() => { + let container; + + function ensureContainer() { + if (!container) { + container = document.createElement("div"); + container.className = "toast-container"; + document.body.appendChild(container); + } + return container; + } + + function show({ message, level = "info", timeout = 5000 } = {}) { + const root = ensureContainer(); + const toast = document.createElement("div"); + toast.className = `toast toast--${level}`; + toast.setAttribute("role", "alert"); + toast.innerHTML = ` + +
+ + `; + root.appendChild(toast); + + const close = () => { + toast.classList.add("hidden"); + setTimeout(() => toast.remove(), 200); + }; + + toast.querySelector(".toast__close").addEventListener("click", close); + + if (timeout > 0) { + setTimeout(close, timeout); + } + } + + window.NotificationCenter = { show }; +})(); diff --git a/templates/base.html b/templates/base.html index a7c9700..2169b4d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,7 @@ +