2.0 - WebUI builder ("Cielight" merge) #9
@@ -4,6 +4,19 @@
 | 
				
			|||||||
 * @param {string} type - "success" or "error".
 | 
					 * @param {string} type - "success" or "error".
 | 
				
			||||||
 * @param {number} duration - Duration in ms.
 | 
					 * @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) {
 | 
					function showToast(message, type = "success", duration = 3000) {
 | 
				
			||||||
  const container = document.getElementById("toast-container");
 | 
					  const container = document.getElementById("toast-container");
 | 
				
			||||||
  if (!container) return;
 | 
					  if (!container) return;
 | 
				
			||||||
@@ -29,9 +42,11 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Build action handler
 | 
					  // Build action handler
 | 
				
			||||||
  async function handleBuildClick() {
 | 
					  async function handleBuildClick() {
 | 
				
			||||||
 | 
					    showLoader("Building static site...");
 | 
				
			||||||
    // Trigger build on backend
 | 
					    // Trigger build on backend
 | 
				
			||||||
    const res = await fetch("/api/build", { method: "POST" });
 | 
					    const res = await fetch("/api/build", { method: "POST" });
 | 
				
			||||||
    const result = await res.json();
 | 
					    const result = await res.json();
 | 
				
			||||||
 | 
					    hideLoader();
 | 
				
			||||||
    if (result.status === "ok") {
 | 
					    if (result.status === "ok") {
 | 
				
			||||||
      // Show build success modal
 | 
					      // Show build success modal
 | 
				
			||||||
      if (buildModal) buildModal.style.display = "flex";
 | 
					      if (buildModal) buildModal.style.display = "flex";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,19 @@ function showToast(message, type = "success", duration = 3000) {
 | 
				
			|||||||
  }, duration);
 | 
					  }, 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", () => {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
  // Form and menu logic
 | 
					  // Form and menu logic
 | 
				
			||||||
  const form = document.getElementById("site-info-form");
 | 
					  const form = document.getElementById("site-info-form");
 | 
				
			||||||
@@ -130,10 +143,12 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    thumbnailUpload.addEventListener("change", async (e) => {
 | 
					    thumbnailUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const file = e.target.files[0];
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
      if (!file) return;
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					      showLoader("Uploading thumbnail...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
					        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
				
			||||||
        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
					        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
@@ -183,12 +198,14 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    themeUpload.addEventListener("change", async (e) => {
 | 
					    themeUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const files = Array.from(e.target.files);
 | 
					      const files = Array.from(e.target.files);
 | 
				
			||||||
      if (files.length === 0) return;
 | 
					      if (files.length === 0) return;
 | 
				
			||||||
 | 
					      showLoader("Uploading theme...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      files.forEach(file => {
 | 
					      files.forEach(file => {
 | 
				
			||||||
        formData.append("files", file, file.webkitRelativePath || file.name);
 | 
					        formData.append("files", file, file.webkitRelativePath || file.name);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme uploaded!", "success");
 | 
					        showToast("✅ Theme uploaded!", "success");
 | 
				
			||||||
        // Refresh theme select after upload
 | 
					        // Refresh theme select after upload
 | 
				
			||||||
@@ -239,12 +256,14 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    deleteThemeModalConfirm.onclick = async () => {
 | 
					    deleteThemeModalConfirm.onclick = async () => {
 | 
				
			||||||
      if (!themeToDelete) return;
 | 
					      if (!themeToDelete) return;
 | 
				
			||||||
 | 
					      showLoader("Removing theme...");
 | 
				
			||||||
      const res = await fetch("/api/theme/remove", {
 | 
					      const res = await fetch("/api/theme/remove", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme: themeToDelete })
 | 
					        body: JSON.stringify({ theme: themeToDelete })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme removed!", "success");
 | 
					        showToast("✅ Theme removed!", "success");
 | 
				
			||||||
        // Refresh theme select
 | 
					        // Refresh theme select
 | 
				
			||||||
@@ -379,7 +398,9 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // Check if thumbnail is set before saving (uploaded or present in input)
 | 
					      // Check if thumbnail is set before saving (uploaded or present in input)
 | 
				
			||||||
      if (!thumbnailInput || !thumbnailInput.value) {
 | 
					      if (!thumbnailInput || !thumbnailInput.value) {
 | 
				
			||||||
 | 
					        showLoader("Saving...");
 | 
				
			||||||
        showToast("❌ Thumbnail is required.", "error");
 | 
					        showToast("❌ Thumbnail is required.", "error");
 | 
				
			||||||
 | 
					        hideLoader();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -417,6 +438,8 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          intellectual_property: ipParagraphs
 | 
					          intellectual_property: ipParagraphs
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					      // --- REMOVE loader for save ---
 | 
				
			||||||
 | 
					      // showLoader("Saving...");
 | 
				
			||||||
      const res = await fetch("/api/site-info", {
 | 
					      const res = await fetch("/api/site-info", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,19 @@ function showToast(message, type = "success", duration = 3000) {
 | 
				
			|||||||
  }, duration);
 | 
					  }, 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) {
 | 
					function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			||||||
  const colorInput = document.getElementById(colorId);
 | 
					  const colorInput = document.getElementById(colorId);
 | 
				
			||||||
  const colorBtn = document.getElementById(btnId);
 | 
					  const colorBtn = document.getElementById(btnId);
 | 
				
			||||||
@@ -40,7 +53,6 @@ function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			|||||||
  colorBtn.style.background = initial;
 | 
					  colorBtn.style.background = initial;
 | 
				
			||||||
  textInput.value = initial.toUpperCase();
 | 
					  textInput.value = initial.toUpperCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Color input is positioned over the button and is clickable
 | 
					 | 
				
			||||||
  colorInput.addEventListener("input", () => {
 | 
					  colorInput.addEventListener("input", () => {
 | 
				
			||||||
    colorBtn.style.background = colorInput.value;
 | 
					    colorBtn.style.background = colorInput.value;
 | 
				
			||||||
    textInput.value = colorInput.value.toUpperCase();
 | 
					    textInput.value = colorInput.value.toUpperCase();
 | 
				
			||||||
@@ -178,11 +190,13 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
        showToast("Only .woff and .woff2 fonts are allowed.", "error");
 | 
					        showToast("Only .woff and .woff2 fonts are allowed.", "error");
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      showLoader("Uploading font...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      formData.append("theme", themeInfo.theme_name);
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Font uploaded!", "success");
 | 
					        showToast("✅ Font uploaded!", "success");
 | 
				
			||||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
@@ -219,9 +233,11 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    deleteFontModalConfirm.onclick = async () => {
 | 
					    deleteFontModalConfirm.onclick = async () => {
 | 
				
			||||||
      if (!fontToDelete) return;
 | 
					      if (!fontToDelete) return;
 | 
				
			||||||
 | 
					      showLoader("Removing font...");
 | 
				
			||||||
      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
					      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("Font removed!", "✅ success");
 | 
					        showToast("Font removed!", "success");
 | 
				
			||||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
        refreshLocalFonts();
 | 
					        refreshLocalFonts();
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@@ -272,11 +288,13 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
        showToast("Invalid file type for favicon.", "error");
 | 
					        showToast("Invalid file type for favicon.", "error");
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      showLoader("Uploading favicon...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      formData.append("theme", themeInfo.theme_name);
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        faviconInput.value = result.filename;
 | 
					        faviconInput.value = result.filename;
 | 
				
			||||||
        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
					        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
@@ -303,12 +321,14 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    deleteFaviconModalConfirm.onclick = async () => {
 | 
					    deleteFaviconModalConfirm.onclick = async () => {
 | 
				
			||||||
 | 
					      showLoader("Removing favicon...");
 | 
				
			||||||
      const res = await fetch("/api/favicon/remove", {
 | 
					      const res = await fetch("/api/favicon/remove", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
					        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        faviconInput.value = "";
 | 
					        faviconInput.value = "";
 | 
				
			||||||
        updateFaviconPreview("");
 | 
					        updateFaviconPreview("");
 | 
				
			||||||
@@ -335,13 +355,11 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
  if (addGoogleFontBtn) {
 | 
					  if (addGoogleFontBtn) {
 | 
				
			||||||
    addGoogleFontBtn.addEventListener("click", async () => {
 | 
					    addGoogleFontBtn.addEventListener("click", async () => {
 | 
				
			||||||
      googleFonts.push({ family: "", weights: [] });
 | 
					      googleFonts.push({ family: "", weights: [] });
 | 
				
			||||||
      // Save immediately to backend
 | 
					 | 
				
			||||||
      await fetch("/api/theme-google-fonts", {
 | 
					      await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					        body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
      const updatedThemeInfo = await fetchThemeInfo();
 | 
					      const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
      const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					      const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
      googleFonts.length = 0;
 | 
					      googleFonts.length = 0;
 | 
				
			||||||
@@ -353,13 +371,11 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
					  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
				
			||||||
  if (googleFontsFields) {
 | 
					  if (googleFontsFields) {
 | 
				
			||||||
    // Save on blur for family/weights fields
 | 
					 | 
				
			||||||
    googleFontsFields.addEventListener("blur", async (e) => {
 | 
					    googleFontsFields.addEventListener("blur", async (e) => {
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        e.target.name &&
 | 
					        e.target.name &&
 | 
				
			||||||
        (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
 | 
					        (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        // Update googleFonts array from the form fields
 | 
					 | 
				
			||||||
        const fontFields = googleFontsFields.querySelectorAll(".input-field");
 | 
					        const fontFields = googleFontsFields.querySelectorAll(".input-field");
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
        fontFields.forEach(field => {
 | 
					        fontFields.forEach(field => {
 | 
				
			||||||
@@ -368,13 +384,11 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
            .split(",").map(w => w.trim()).filter(Boolean);
 | 
					            .split(",").map(w => w.trim()).filter(Boolean);
 | 
				
			||||||
          googleFonts.push({ family, weights });
 | 
					          googleFonts.push({ family, weights });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Save immediately to backend
 | 
					 | 
				
			||||||
        await fetch("/api/theme-google-fonts", {
 | 
					        await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
          method: "POST",
 | 
					          method: "POST",
 | 
				
			||||||
          headers: { "Content-Type": "application/json" },
 | 
					          headers: { "Content-Type": "application/json" },
 | 
				
			||||||
          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
        const updatedThemeInfo = await fetchThemeInfo();
 | 
					        const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
@@ -382,20 +396,17 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
        renderGoogleFonts(googleFonts);
 | 
					        renderGoogleFonts(googleFonts);
 | 
				
			||||||
        refreshFontDropdowns();
 | 
					        refreshFontDropdowns();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }, true); // Use capture phase to catch blur from children
 | 
					    }, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Delegate remove button click for Google Fonts
 | 
					 | 
				
			||||||
    googleFontsFields.addEventListener("click", async (e) => {
 | 
					    googleFontsFields.addEventListener("click", async (e) => {
 | 
				
			||||||
      if (e.target.classList.contains("remove-google-font")) {
 | 
					      if (e.target.classList.contains("remove-google-font")) {
 | 
				
			||||||
        const idx = Number(e.target.dataset.idx);
 | 
					        const idx = Number(e.target.dataset.idx);
 | 
				
			||||||
        googleFonts.splice(idx, 1);
 | 
					        googleFonts.splice(idx, 1);
 | 
				
			||||||
        // Save immediately to backend
 | 
					 | 
				
			||||||
        await fetch("/api/theme-google-fonts", {
 | 
					        await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
          method: "POST",
 | 
					          method: "POST",
 | 
				
			||||||
          headers: { "Content-Type": "application/json" },
 | 
					          headers: { "Content-Type": "application/json" },
 | 
				
			||||||
          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
        const updatedThemeInfo = await fetchThemeInfo();
 | 
					        const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
@@ -406,9 +417,9 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Form submit
 | 
					 | 
				
			||||||
  document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
 | 
					  document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    showLoader("Saving theme...");
 | 
				
			||||||
    const data = {};
 | 
					    const data = {};
 | 
				
			||||||
    data.colors = {
 | 
					    data.colors = {
 | 
				
			||||||
      primary: document.getElementById("color-primary-text").value,
 | 
					      primary: document.getElementById("color-primary-text").value,
 | 
				
			||||||
@@ -445,6 +456,7 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
      headers: { "Content-Type": "application/json" },
 | 
					      headers: { "Content-Type": "application/json" },
 | 
				
			||||||
      body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
 | 
					      body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    hideLoader();
 | 
				
			||||||
    if (res.ok) {
 | 
					    if (res.ok) {
 | 
				
			||||||
      showToast("✅ Theme saved!", "success");
 | 
					      showToast("✅ Theme saved!", "success");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 ---
 | 
					// --- Upload gallery images ---
 | 
				
			||||||
document.getElementById('upload-gallery').addEventListener('change', async (e) => {
 | 
					const galleryInput = document.getElementById('upload-gallery');
 | 
				
			||||||
 | 
					if (galleryInput) {
 | 
				
			||||||
 | 
					  galleryInput.addEventListener('change', async (e) => {
 | 
				
			||||||
    const files = e.target.files;
 | 
					    const files = e.target.files;
 | 
				
			||||||
    if (!files.length) return;
 | 
					    if (!files.length) return;
 | 
				
			||||||
 | 
					    showLoader("Uploading photos...");
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					    for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
					      const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
				
			||||||
      const data = await res.json();
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (res.ok) {
 | 
					      if (res.ok) {
 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
					        showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
				
			||||||
      refreshGallery();
 | 
					        if (typeof refreshGallery === "function") refreshGallery();
 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					      } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
    } catch(err) {
 | 
					    } catch(err) {
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      console.error(err);
 | 
					      console.error(err);
 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					      showToast('Server error!', "error");
 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					    } finally { e.target.value = ''; }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Upload hero images ---
 | 
					// --- Upload hero images ---
 | 
				
			||||||
document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
					const heroInput = document.getElementById('upload-hero');
 | 
				
			||||||
 | 
					if (heroInput) {
 | 
				
			||||||
 | 
					  heroInput.addEventListener('change', async (e) => {
 | 
				
			||||||
    const files = e.target.files;
 | 
					    const files = e.target.files;
 | 
				
			||||||
    if (!files.length) return;
 | 
					    if (!files.length) return;
 | 
				
			||||||
 | 
					    showLoader("Uploading hero photos...");
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					    for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
					      const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
				
			||||||
      const data = await res.json();
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (res.ok) {
 | 
					      if (res.ok) {
 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
					        showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
				
			||||||
      refreshHero();
 | 
					        if (typeof refreshHero === "function") refreshHero();
 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					      } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
    } catch(err) {
 | 
					    } catch(err) {
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      console.error(err);
 | 
					      console.error(err);
 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					      showToast('Server error!', "error");
 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					    } finally { e.target.value = ''; }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -65,6 +65,16 @@
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        <!-- Loader -->
 | 
				
			||||||
 | 
					        <div id="global-loader" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;background:rgba(0,0,0,0.4);align-items:center;justify-content:center;">
 | 
				
			||||||
 | 
					            <div style="background:#222;padding:32px 48px;border-radius:16px;box-shadow:0 2px 24px #000;display:flex;flex-direction:column;align-items:center;">
 | 
				
			||||||
 | 
					                <div class="loader-spinner" style="width:48px;height:48px;border:6px solid #55c3ec;border-top:6px solid #222;border-radius:50%;animation:spin 1s linear infinite;"></div>
 | 
				
			||||||
 | 
					                <div style="margin-top:18px;color:#fff;font-size:18px;" id="loader-text">Uploading...</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <style>
 | 
				
			||||||
 | 
					        @keyframes spin { 100% { transform: rotate(360deg); } }
 | 
				
			||||||
 | 
					        </style>
 | 
				
			||||||
        <!-- Scripts -->
 | 
					        <!-- Scripts -->
 | 
				
			||||||
        <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
					        <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
				
			||||||
        {% block scripts %}{% endblock %}
 | 
					        {% block scripts %}{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user