diff --git a/src/webui/js/build.js b/src/webui/js/build.js index 587bcfa..d48408c 100644 --- a/src/webui/js/build.js +++ b/src/webui/js/build.js @@ -4,6 +4,19 @@ * @param {string} type - "success" or "error". * @param {number} duration - Duration in ms. */ + +function showLoader(text = "Building...") { + const loader = document.getElementById("global-loader"); + if (loader) { + loader.style.display = "flex"; + document.getElementById("loader-text").textContent = text; + } +} +function hideLoader() { + const loader = document.getElementById("global-loader"); + if (loader) loader.style.display = "none"; +} + function showToast(message, type = "success", duration = 3000) { const container = document.getElementById("toast-container"); if (!container) return; @@ -29,9 +42,11 @@ document.addEventListener("DOMContentLoaded", () => { // Build action handler async function handleBuildClick() { + showLoader("Building static site..."); // Trigger build on backend const res = await fetch("/api/build", { method: "POST" }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { // Show build success modal if (buildModal) buildModal.style.display = "flex"; diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index a65caaf..d0af38e 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -12,6 +12,19 @@ function showToast(message, type = "success", duration = 3000) { }, duration); } +// --- Loader helpers --- +function showLoader(text = "Uploading...") { + const loader = document.getElementById("global-loader"); + if (loader) { + loader.style.display = "flex"; + document.getElementById("loader-text").textContent = text; + } +} +function hideLoader() { + const loader = document.getElementById("global-loader"); + if (loader) loader.style.display = "none"; +} + document.addEventListener("DOMContentLoaded", () => { // Form and menu logic const form = document.getElementById("site-info-form"); @@ -130,10 +143,12 @@ document.addEventListener("DOMContentLoaded", () => { thumbnailUpload.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; + showLoader("Uploading thumbnail..."); const formData = new FormData(); formData.append("file", file); const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { if (thumbnailInput) thumbnailInput.value = result.filename; updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`); @@ -183,12 +198,14 @@ document.addEventListener("DOMContentLoaded", () => { themeUpload.addEventListener("change", async (e) => { const files = Array.from(e.target.files); if (files.length === 0) return; + showLoader("Uploading theme..."); const formData = new FormData(); files.forEach(file => { formData.append("files", file, file.webkitRelativePath || file.name); }); const res = await fetch("/api/theme/upload", { method: "POST", body: formData }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { showToast("✅ Theme uploaded!", "success"); // Refresh theme select after upload @@ -239,12 +256,14 @@ document.addEventListener("DOMContentLoaded", () => { }; deleteThemeModalConfirm.onclick = async () => { if (!themeToDelete) return; + showLoader("Removing theme..."); const res = await fetch("/api/theme/remove", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme: themeToDelete }) }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { showToast("✅ Theme removed!", "success"); // Refresh theme select @@ -379,7 +398,9 @@ document.addEventListener("DOMContentLoaded", () => { // Check if thumbnail is set before saving (uploaded or present in input) if (!thumbnailInput || !thumbnailInput.value) { + showLoader("Saving..."); showToast("❌ Thumbnail is required.", "error"); + hideLoader(); return; } @@ -417,6 +438,8 @@ document.addEventListener("DOMContentLoaded", () => { intellectual_property: ipParagraphs } }; + // --- REMOVE loader for save --- + // showLoader("Saving..."); const res = await fetch("/api/site-info", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/src/webui/js/theme-editor.js b/src/webui/js/theme-editor.js index ea047bf..c51a00d 100644 --- a/src/webui/js/theme-editor.js +++ b/src/webui/js/theme-editor.js @@ -31,6 +31,19 @@ function showToast(message, type = "success", duration = 3000) { }, duration); } +// --- Loader helpers --- +function showLoader(text = "Uploading...") { + const loader = document.getElementById("global-loader"); + if (loader) { + loader.style.display = "flex"; + document.getElementById("loader-text").textContent = text; + } +} +function hideLoader() { + const loader = document.getElementById("global-loader"); + if (loader) loader.style.display = "none"; +} + function setupColorPicker(colorId, btnId, textId, initial) { const colorInput = document.getElementById(colorId); const colorBtn = document.getElementById(btnId); @@ -40,7 +53,6 @@ function setupColorPicker(colorId, btnId, textId, initial) { colorBtn.style.background = initial; textInput.value = initial.toUpperCase(); - // Color input is positioned over the button and is clickable colorInput.addEventListener("input", () => { colorBtn.style.background = colorInput.value; textInput.value = colorInput.value.toUpperCase(); @@ -178,11 +190,13 @@ document.addEventListener("DOMContentLoaded", async () => { showToast("Only .woff and .woff2 fonts are allowed.", "error"); return; } + showLoader("Uploading font..."); const formData = new FormData(); formData.append("file", file); formData.append("theme", themeInfo.theme_name); const res = await fetch("/api/font/upload", { method: "POST", body: formData }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { showToast("✅ Font uploaded!", "success"); localFonts = await fetchLocalFonts(themeInfo.theme_name); @@ -219,9 +233,11 @@ document.addEventListener("DOMContentLoaded", async () => { }; deleteFontModalConfirm.onclick = async () => { if (!fontToDelete) return; + showLoader("Removing font..."); const result = await removeFont(themeInfo.theme_name, fontToDelete); + hideLoader(); if (result.status === "ok") { - showToast("Font removed!", "✅ success"); + showToast("Font removed!", "success"); localFonts = await fetchLocalFonts(themeInfo.theme_name); refreshLocalFonts(); } else { @@ -272,11 +288,13 @@ document.addEventListener("DOMContentLoaded", async () => { showToast("Invalid file type for favicon.", "error"); return; } + showLoader("Uploading favicon..."); const formData = new FormData(); formData.append("file", file); formData.append("theme", themeInfo.theme_name); const res = await fetch("/api/favicon/upload", { method: "POST", body: formData }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { faviconInput.value = result.filename; updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`); @@ -303,12 +321,14 @@ document.addEventListener("DOMContentLoaded", async () => { } }; deleteFaviconModalConfirm.onclick = async () => { + showLoader("Removing favicon..."); const res = await fetch("/api/favicon/remove", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme: themeInfo.theme_name }) }); const result = await res.json(); + hideLoader(); if (result.status === "ok") { faviconInput.value = ""; updateFaviconPreview(""); @@ -335,13 +355,11 @@ document.addEventListener("DOMContentLoaded", async () => { if (addGoogleFontBtn) { addGoogleFontBtn.addEventListener("click", async () => { googleFonts.push({ family: "", weights: [] }); - // Save immediately to backend await fetch("/api/theme-google-fonts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) }); - // Fetch updated theme info and refresh dropdowns const updatedThemeInfo = await fetchThemeInfo(); const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; googleFonts.length = 0; @@ -353,13 +371,11 @@ document.addEventListener("DOMContentLoaded", async () => { const googleFontsFields = document.getElementById("google-fonts-fields"); if (googleFontsFields) { - // Save on blur for family/weights fields googleFontsFields.addEventListener("blur", async (e) => { if ( e.target.name && (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]")) ) { - // Update googleFonts array from the form fields const fontFields = googleFontsFields.querySelectorAll(".input-field"); googleFonts.length = 0; fontFields.forEach(field => { @@ -368,13 +384,11 @@ document.addEventListener("DOMContentLoaded", async () => { .split(",").map(w => w.trim()).filter(Boolean); googleFonts.push({ family, weights }); }); - // Save immediately to backend await fetch("/api/theme-google-fonts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) }); - // Fetch updated theme info and refresh dropdowns const updatedThemeInfo = await fetchThemeInfo(); const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; googleFonts.length = 0; @@ -382,20 +396,17 @@ document.addEventListener("DOMContentLoaded", async () => { renderGoogleFonts(googleFonts); refreshFontDropdowns(); } - }, true); // Use capture phase to catch blur from children + }, true); - // Delegate remove button click for Google Fonts googleFontsFields.addEventListener("click", async (e) => { if (e.target.classList.contains("remove-google-font")) { const idx = Number(e.target.dataset.idx); googleFonts.splice(idx, 1); - // Save immediately to backend await fetch("/api/theme-google-fonts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) }); - // Fetch updated theme info and refresh dropdowns const updatedThemeInfo = await fetchThemeInfo(); const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; googleFonts.length = 0; @@ -406,9 +417,9 @@ document.addEventListener("DOMContentLoaded", async () => { }); } - // Form submit document.getElementById("theme-editor-form").addEventListener("submit", async (e) => { e.preventDefault(); + showLoader("Saving theme..."); const data = {}; data.colors = { primary: document.getElementById("color-primary-text").value, @@ -445,6 +456,7 @@ document.addEventListener("DOMContentLoaded", async () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data }) }); + hideLoader(); if (res.ok) { showToast("✅ Theme saved!", "success"); } else { diff --git a/src/webui/js/upload.js b/src/webui/js/upload.js index a8d76bc..81c30a0 100644 --- a/src/webui/js/upload.js +++ b/src/webui/js/upload.js @@ -1,41 +1,64 @@ +// --- Loader helpers --- +function showLoader(text = "Uploading...") { + const loader = document.getElementById("global-loader"); + if (loader) { + loader.style.display = "flex"; + document.getElementById("loader-text").textContent = text; + } +} +function hideLoader() { + const loader = document.getElementById("global-loader"); + if (loader) loader.style.display = "none"; +} + // --- Upload gallery images --- -document.getElementById('upload-gallery').addEventListener('change', async (e) => { - const files = e.target.files; - if (!files.length) return; +const galleryInput = document.getElementById('upload-gallery'); +if (galleryInput) { + galleryInput.addEventListener('change', async (e) => { + const files = e.target.files; + if (!files.length) return; + showLoader("Uploading photos..."); + const formData = new FormData(); + for (const file of files) formData.append('files', file); - const formData = new FormData(); - for (const file of files) formData.append('files', file); - - try { - const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData }); - const data = await res.json(); - if (res.ok) { - showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success"); - refreshGallery(); - } else showToast('Error: ' + data.error, "error"); - } catch(err) { - console.error(err); - showToast('Server error!', "error"); - } finally { e.target.value = ''; } -}); + try { + const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData }); + const data = await res.json(); + hideLoader(); + if (res.ok) { + showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success"); + if (typeof refreshGallery === "function") refreshGallery(); + } else showToast('Error: ' + data.error, "error"); + } catch(err) { + hideLoader(); + console.error(err); + showToast('Server error!', "error"); + } finally { e.target.value = ''; } + }); +} // --- Upload hero images --- -document.getElementById('upload-hero').addEventListener('change', async (e) => { - const files = e.target.files; - if (!files.length) return; +const heroInput = document.getElementById('upload-hero'); +if (heroInput) { + heroInput.addEventListener('change', async (e) => { + const files = e.target.files; + if (!files.length) return; + showLoader("Uploading hero photos..."); + const formData = new FormData(); + for (const file of files) formData.append('files', file); - const formData = new FormData(); - for (const file of files) formData.append('files', file); - - try { - const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); - const data = await res.json(); - if (res.ok) { - showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success"); - refreshHero(); - } else showToast('Error: ' + data.error, "error"); - } catch(err) { - console.error(err); - showToast('Server error!', "error"); - } finally { e.target.value = ''; } -}); + try { + const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); + const data = await res.json(); + hideLoader(); + if (res.ok) { + showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success"); + if (typeof refreshHero === "function") refreshHero(); + } else showToast('Error: ' + data.error, "error"); + } catch(err) { + hideLoader(); + console.error(err); + showToast('Server error!', "error"); + } finally { e.target.value = ''; } + }); +} \ No newline at end of file diff --git a/src/webui/template/base.html b/src/webui/template/base.html index 0bede5b..8645784 100644 --- a/src/webui/template/base.html +++ b/src/webui/template/base.html @@ -65,6 +65,16 @@ + +
+ {% block scripts %}{% endblock %}