From 69d303c7a1dec108cc88e385aa45ced129ba65c8 Mon Sep 17 00:00:00 2001 From: Djeex Date: Thu, 4 Sep 2025 12:28:01 +0200 Subject: [PATCH] Refactored site-info --- src/webui/js/site-info.js | 406 ++++++++++++++------------------------ 1 file changed, 147 insertions(+), 259 deletions(-) diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index f68067c..ec58b1e 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -12,7 +12,6 @@ function showToast(message, type = "success", duration = 3000) { }, duration); } -// --- Loader helpers --- function showLoader(text = "Uploading...") { const loader = document.getElementById("global-loader"); if (loader) { @@ -27,77 +26,65 @@ function hideLoader() { document.addEventListener("DOMContentLoaded", () => { // --- Section Forms --- - const infoForm = document.getElementById("info-form"); - const socialForm = document.getElementById("social-form"); - const menuForm = document.getElementById("menu-form"); - const footerForm = document.getElementById("footer-form"); - const legalsForm = document.getElementById("legals-form"); - const buildForm = document.getElementById("build-form"); + const forms = { + info: document.getElementById("info-form"), + social: document.getElementById("social-form"), + menu: document.getElementById("menu-form"), + footer: document.getElementById("footer-form"), + legals: document.getElementById("legals-form"), + build: document.getElementById("build-form") + }; // --- Menu logic --- const menuList = document.getElementById("menu-items-list"); const addMenuBtn = document.getElementById("add-menu-item"); let menuItems = []; - function renderMenuItems() { menuList.innerHTML = ""; menuItems.forEach((item, idx) => { - const div = document.createElement("div"); - div.style.display = "flex"; - div.style.gap = "8px"; - div.style.marginBottom = "6px"; - div.innerHTML = ` - - - + menuList.innerHTML += ` +
+ + + +
`; - menuList.appendChild(div); }); } - function updateMenuItemsFromInputs() { const inputs = menuList.querySelectorAll("input"); - const items = []; + menuItems = []; for (let i = 0; i < inputs.length; i += 2) { const label = inputs[i].value.trim(); const href = inputs[i + 1].value.trim(); - if (label || href) items.push({ label, href }); + if (label || href) menuItems.push({ label, href }); } - menuItems = items; } // --- Intellectual property paragraphs logic --- const ipList = document.getElementById("ip-list"); const addIpBtn = document.getElementById("add-ip-paragraph"); let ipParagraphs = []; - function renderIpParagraphs() { ipList.innerHTML = ""; ipParagraphs.forEach((item, idx) => { - const div = document.createElement("div"); - div.style.display = "flex"; - div.style.gap = "8px"; - div.style.marginBottom = "6px"; - div.innerHTML = ` - - + ipList.innerHTML += ` +
+ + +
`; - ipList.appendChild(div); }); } - function updateIpParagraphsFromInputs() { - const textareas = ipList.querySelectorAll("textarea"); - ipParagraphs = Array.from(textareas).map(textarea => ({ - paragraph: textarea.value.trim() - })).filter(item => item.paragraph !== ""); + ipParagraphs = Array.from(ipList.querySelectorAll("textarea")) + .map(textarea => ({ paragraph: textarea.value.trim() })) + .filter(item => item.paragraph !== ""); } - // --- Build options --- + // --- Build options & Theme select --- const convertImagesCheckbox = document.getElementById("convert-images-checkbox"); const resizeImagesCheckbox = document.getElementById("resize-images-checkbox"); - - // --- Theme select --- const themeSelect = document.getElementById("theme-select"); // --- Thumbnail upload and modal logic --- @@ -107,40 +94,30 @@ document.addEventListener("DOMContentLoaded", () => { const thumbnailPreview = document.getElementById("thumbnail-preview"); const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn"); - // --- Modal elements for delete confirmation --- - const deleteModal = document.getElementById("delete-modal"); - const deleteModalClose = document.getElementById("delete-modal-close"); - const deleteModalConfirm = document.getElementById("delete-modal-confirm"); - const deleteModalCancel = document.getElementById("delete-modal-cancel"); + // --- Modal helpers --- + function setupModal(modal, closeBtn, confirmBtn, cancelBtn, onConfirm) { + if (!modal) return; + if (closeBtn) closeBtn.onclick = () => modal.style.display = "none"; + if (cancelBtn) cancelBtn.onclick = () => modal.style.display = "none"; + window.addEventListener("click", (e) => { + if (e.target === modal) modal.style.display = "none"; + }); + if (confirmBtn && onConfirm) confirmBtn.onclick = onConfirm; + } - // --- Modal elements for theme deletion --- - const deleteThemeModal = document.getElementById("delete-theme-modal"); - const deleteThemeModalClose = document.getElementById("delete-theme-modal-close"); - const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm"); - const deleteThemeModalCancel = document.getElementById("delete-theme-modal-cancel"); - const deleteThemeModalText = document.getElementById("delete-theme-modal-text"); - let themeToDelete = null; - - // --- Show/hide thumbnail preview, remove button, and choose button --- + // --- Thumbnail preview logic --- function updateThumbnailPreview(src) { if (thumbnailPreview) { thumbnailPreview.src = src || ""; thumbnailPreview.style.display = src ? "block" : "none"; } - if (removeThumbnailBtn) { - removeThumbnailBtn.style.display = src ? "inline-block" : "none"; - } - if (chooseThumbnailBtn) { - chooseThumbnailBtn.style.display = src ? "none" : "inline-block"; - } + if (removeThumbnailBtn) removeThumbnailBtn.style.display = src ? "inline-block" : "none"; + if (chooseThumbnailBtn) chooseThumbnailBtn.style.display = src ? "none" : "inline-block"; } - // --- Choose thumbnail button triggers file input --- if (chooseThumbnailBtn && thumbnailUpload) { chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click()); } - - // --- Handle thumbnail upload and refresh preview (with cache busting) --- if (thumbnailUpload) { thumbnailUpload.addEventListener("change", async (e) => { const file = e.target.files[0]; @@ -161,25 +138,17 @@ document.addEventListener("DOMContentLoaded", () => { updateSectionStatus("social"); }); } - - // --- Remove thumbnail button triggers modal --- if (removeThumbnailBtn) { removeThumbnailBtn.addEventListener("click", () => { - deleteModal.style.display = "flex"; + document.getElementById("delete-modal").style.display = "flex"; }); } - - // --- Modal logic for thumbnail deletion --- - if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) { - deleteModalClose.onclick = deleteModalCancel.onclick = () => { - deleteModal.style.display = "none"; - }; - window.onclick = function(event) { - if (event.target === deleteModal) { - deleteModal.style.display = "none"; - } - }; - deleteModalConfirm.onclick = async () => { + setupModal( + document.getElementById("delete-modal"), + document.getElementById("delete-modal-close"), + document.getElementById("delete-modal-confirm"), + document.getElementById("delete-modal-cancel"), + async () => { const res = await fetch("/api/thumbnail/remove", { method: "POST" }); const result = await res.json(); if (result.status === "ok") { @@ -189,41 +158,28 @@ document.addEventListener("DOMContentLoaded", () => { } else { showToast("❌ Error removing thumbnail", "error"); } - deleteModal.style.display = "none"; + document.getElementById("delete-modal").style.display = "none"; updateSectionStatus("social"); - }; - } + } + ); - // --- Theme upload logic (custom theme folder) --- + // --- Theme upload logic --- const themeUpload = document.getElementById("theme-upload"); const chooseThemeBtn = document.getElementById("choose-theme-btn"); if (chooseThemeBtn && themeUpload) { chooseThemeBtn.addEventListener("click", () => themeUpload.click()); themeUpload.addEventListener("change", async (e) => { const files = Array.from(e.target.files); - if (files.length === 0) return; + if (!files.length) return; showLoader("Uploading theme..."); const formData = new FormData(); - files.forEach(file => { - formData.append("files", file, file.webkitRelativePath || file.name); - }); + 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 - fetch("/api/themes") - .then(res => res.json()) - .then(themes => { - themeSelect.innerHTML = ""; - themes.forEach(theme => { - const option = document.createElement("option"); - option.value = theme; - option.textContent = theme; - themeSelect.appendChild(option); - }); - }); + refreshThemes(); } else { showToast("❌ Error uploading theme", "error"); } @@ -231,8 +187,9 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // --- Remove theme button triggers modal --- + // --- Remove theme logic --- const removeThemeBtn = document.getElementById("remove-theme-btn"); + let themeToDelete = null; if (removeThemeBtn && themeSelect) { removeThemeBtn.addEventListener("click", () => { const theme = themeSelect.value; @@ -242,24 +199,16 @@ document.addEventListener("DOMContentLoaded", () => { return; } themeToDelete = theme; - deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`; - deleteThemeModal.style.display = "flex"; + document.getElementById("delete-theme-modal-text").textContent = `Are you sure you want to remove theme "${theme}"?`; + document.getElementById("delete-theme-modal").style.display = "flex"; }); } - - // --- Modal logic for theme deletion --- - if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) { - deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => { - deleteThemeModal.style.display = "none"; - themeToDelete = null; - }; - window.onclick = function(event) { - if (event.target === deleteThemeModal) { - deleteThemeModal.style.display = "none"; - themeToDelete = null; - } - }; - deleteThemeModalConfirm.onclick = async () => { + setupModal( + document.getElementById("delete-theme-modal"), + document.getElementById("delete-theme-modal-close"), + document.getElementById("delete-theme-modal-confirm"), + document.getElementById("delete-theme-modal-cancel"), + async () => { if (!themeToDelete) return; showLoader("Removing theme..."); const res = await fetch("/api/theme/remove", { @@ -271,85 +220,18 @@ document.addEventListener("DOMContentLoaded", () => { hideLoader(); if (result.status === "ok") { showToast("✅ Theme removed!", "success"); - // Refresh theme select - fetch("/api/themes") - .then(res => res.json()) - .then(themes => { - themeSelect.innerHTML = ""; - themes.forEach(theme => { - const option = document.createElement("option"); - option.value = theme; - option.textContent = theme; - themeSelect.appendChild(option); - }); - }); + refreshThemes(); } else { showToast(result.error || "❌ Error removing theme", "error"); } - deleteThemeModal.style.display = "none"; + document.getElementById("delete-theme-modal").style.display = "none"; themeToDelete = null; updateSectionStatus("build"); - }; - } + } + ); - // --- Fetch theme list and populate select, then load config and update build status --- - let loadedConfig = {}; - function loadConfigAndUpdateBuildStatus() { - fetch("/api/site-info") - .then(res => res.json()) - .then(data => { - loadedConfig = data; - // Info - if (infoForm) { - infoForm.elements["info.title"].value = data.info?.title || ""; - infoForm.elements["info.subtitle"].value = data.info?.subtitle || ""; - infoForm.elements["info.description"].value = data.info?.description || ""; - infoForm.elements["info.canonical"].value = data.info?.canonical || ""; - infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || ""); - infoForm.elements["info.author"].value = data.info?.author || ""; - } - // Social - if (socialForm) { - socialForm.elements["social.instagram_url"].value = data.social?.instagram_url || ""; - if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || ""; - updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : ""); - } - // Menu - menuItems = Array.isArray(data.menu?.items) ? data.menu.items : []; - renderMenuItems(); - // Footer - if (footerForm) { - footerForm.elements["footer.copyright"].value = data.footer?.copyright || ""; - footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || ""; - } - // Legals - ipParagraphs = Array.isArray(data.legals?.intellectual_property) - ? data.legals.intellectual_property - : []; - renderIpParagraphs(); - if (legalsForm) { - legalsForm.elements["legals.hoster_name"].value = data.legals?.hoster_name || ""; - legalsForm.elements["legals.hoster_address"].value = data.legals?.hoster_address || ""; - legalsForm.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || ""; - } - // Build - if (themeSelect) { - themeSelect.value = data.build?.theme || ""; - } - if (convertImagesCheckbox) { - convertImagesCheckbox.checked = !!data.build?.convert_images; - } - if (resizeImagesCheckbox) { - resizeImagesCheckbox.checked = !!data.build?.resize_images; - } - // Initial status update for all sections except build - ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus); - // For build, update status after theme select is set - updateSectionStatus("build"); - }); - } - - if (themeSelect) { + // --- Theme select refresh --- + function refreshThemes() { fetch("/api/themes") .then(res => res.json()) .then(themes => { @@ -360,24 +242,67 @@ document.addEventListener("DOMContentLoaded", () => { option.textContent = theme; themeSelect.appendChild(option); }); - // Now load config and update build status after theme select is ready loadConfigAndUpdateBuildStatus(); }); - } else { - // If no theme select, just load config - loadConfigAndUpdateBuildStatus(); } - // --- Add menu item --- - if (addMenuBtn) { - addMenuBtn.addEventListener("click", () => { - menuItems.push({ label: "", href: "" }); - renderMenuItems(); - updateSectionStatus("menu"); - }); + // --- Config loading --- + let loadedConfig = {}; + function loadConfigAndUpdateBuildStatus() { + fetch("/api/site-info") + .then(res => res.json()) + .then(data => { + loadedConfig = data; + // Info + if (forms.info) { + forms.info.elements["info.title"].value = data.info?.title || ""; + forms.info.elements["info.subtitle"].value = data.info?.subtitle || ""; + forms.info.elements["info.description"].value = data.info?.description || ""; + forms.info.elements["info.canonical"].value = data.info?.canonical || ""; + forms.info.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || ""); + forms.info.elements["info.author"].value = data.info?.author || ""; + } + // Social + if (forms.social) { + forms.social.elements["social.instagram_url"].value = data.social?.instagram_url || ""; + if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || ""; + updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : ""); + } + // Menu + menuItems = Array.isArray(data.menu?.items) ? data.menu.items : []; + renderMenuItems(); + // Footer + if (forms.footer) { + forms.footer.elements["footer.copyright"].value = data.footer?.copyright || ""; + forms.footer.elements["footer.legal_label"].value = data.footer?.legal_label || ""; + } + // Legals + ipParagraphs = Array.isArray(data.legals?.intellectual_property) + ? data.legals.intellectual_property + : []; + renderIpParagraphs(); + if (forms.legals) { + forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || ""; + forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || ""; + forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || ""; + } + // Build + if (themeSelect) themeSelect.value = data.build?.theme || ""; + if (convertImagesCheckbox) convertImagesCheckbox.checked = !!data.build?.convert_images; + if (resizeImagesCheckbox) resizeImagesCheckbox.checked = !!data.build?.resize_images; + ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus); + updateSectionStatus("build"); + }); } + if (themeSelect) refreshThemes(); + else loadConfigAndUpdateBuildStatus(); - // --- Remove menu item --- + // --- Add/remove menu items --- + if (addMenuBtn) addMenuBtn.addEventListener("click", () => { + menuItems.push({ label: "", href: "" }); + renderMenuItems(); + updateSectionStatus("menu"); + }); menuList.addEventListener("click", (e) => { if (e.target.classList.contains("remove-menu-item")) { const idx = parseInt(e.target.getAttribute("data-idx")); @@ -386,23 +311,17 @@ document.addEventListener("DOMContentLoaded", () => { updateSectionStatus("menu"); } }); - - // --- Update menuItems on input change --- menuList.addEventListener("input", () => { updateMenuItemsFromInputs(); updateSectionStatus("menu"); }); - // --- Add paragraph --- - if (addIpBtn) { - addIpBtn.addEventListener("click", () => { - ipParagraphs.push({ paragraph: "" }); - renderIpParagraphs(); - updateSectionStatus("legals"); - }); - } - - // --- Remove paragraph --- + // --- Add/remove IP paragraphs --- + if (addIpBtn) addIpBtn.addEventListener("click", () => { + ipParagraphs.push({ paragraph: "" }); + renderIpParagraphs(); + updateSectionStatus("legals"); + }); ipList.addEventListener("click", (e) => { if (e.target.classList.contains("remove-ip-paragraph")) { const idx = parseInt(e.target.getAttribute("data-idx")); @@ -411,8 +330,6 @@ document.addEventListener("DOMContentLoaded", () => { updateSectionStatus("legals"); } }); - - // --- Update ipParagraphs on input change --- ipList.addEventListener("input", () => { updateIpParagraphsFromInputs(); updateSectionStatus("legals"); @@ -423,16 +340,16 @@ document.addEventListener("DOMContentLoaded", () => { switch (section) { case "info": return { - title: infoForm.elements["info.title"].value, - subtitle: infoForm.elements["info.subtitle"].value, - description: infoForm.elements["info.description"].value, - canonical: infoForm.elements["info.canonical"].value, - keywords: infoForm.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean), - author: infoForm.elements["info.author"].value + title: forms.info.elements["info.title"].value, + subtitle: forms.info.elements["info.subtitle"].value, + description: forms.info.elements["info.description"].value, + canonical: forms.info.elements["info.canonical"].value, + keywords: forms.info.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean), + author: forms.info.elements["info.author"].value }; case "social": return { - instagram_url: socialForm.elements["social.instagram_url"].value, + instagram_url: forms.social.elements["social.instagram_url"].value, thumbnail: thumbnailInput ? thumbnailInput.value : "" }; case "menu": @@ -440,15 +357,15 @@ document.addEventListener("DOMContentLoaded", () => { return { items: menuItems }; case "footer": return { - copyright: footerForm.elements["footer.copyright"].value, - legal_label: footerForm.elements["footer.legal_label"].value + copyright: forms.footer.elements["footer.copyright"].value, + legal_label: forms.footer.elements["footer.legal_label"].value }; case "legals": updateIpParagraphsFromInputs(); return { - hoster_name: legalsForm.elements["legals.hoster_name"].value, - hoster_address: legalsForm.elements["legals.hoster_address"].value, - hoster_contact: legalsForm.elements["legals.hoster_contact"].value, + hoster_name: forms.legals.elements["legals.hoster_name"].value, + hoster_address: forms.legals.elements["legals.hoster_address"].value, + hoster_contact: forms.legals.elements["legals.hoster_contact"].value, intellectual_property: ipParagraphs }; case "build": @@ -533,7 +450,7 @@ document.addEventListener("DOMContentLoaded", () => { values.intellectual_property.every(ip => ip.paragraph) ); case "build": - return !!values.theme; // Only check theme is present + return !!values.theme; default: return true; } @@ -561,46 +478,22 @@ document.addEventListener("DOMContentLoaded", () => { } // --- Listen for changes in each section --- - [ - { form: infoForm, section: "info" }, - { form: socialForm, section: "social" }, - { form: menuForm, section: "menu" }, - { form: footerForm, section: "footer" }, - { form: legalsForm, section: "legals" }, - { form: buildForm, section: "build" } - ].forEach(({ form, section }) => { + Object.entries(forms).forEach(([section, form]) => { if (!form) return; form.addEventListener("input", () => updateSectionStatus(section)); form.addEventListener("change", () => updateSectionStatus(section)); - }); - - // --- Save section handler (form submit) --- - [ - { form: infoForm, section: "info" }, - { form: socialForm, section: "social" }, - { form: menuForm, section: "menu" }, - { form: footerForm, section: "footer" }, - { form: legalsForm, section: "legals" }, - { form: buildForm, section: "build" } - ].forEach(({ form, section }) => { - if (!form) return; form.addEventListener("submit", async (e) => { e.preventDefault(); - // Native browser validation if (!form.reportValidity()) { showToast("❌ Please fill all required fields before saving.", "error"); updateSectionStatus(section); return; } - // Social section: check thumbnail - if (section === "social") { - if (!thumbnailInput || !thumbnailInput.value) { - showToast("❌ Thumbnail is required.", "error"); - updateSectionStatus(section); - return; - } + if (section === "social" && (!thumbnailInput || !thumbnailInput.value)) { + showToast("❌ Thumbnail is required.", "error"); + updateSectionStatus(section); + return; } - // Menu section: check all menu items if (section === "menu") { updateMenuItemsFromInputs(); if (!menuItems.length || !menuItems.every(item => item.label && item.href)) { @@ -609,7 +502,6 @@ document.addEventListener("DOMContentLoaded", () => { return; } } - // Legals section: check all paragraphs if (section === "legals") { updateIpParagraphsFromInputs(); if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) { @@ -618,10 +510,8 @@ document.addEventListener("DOMContentLoaded", () => { return; } } - // Build payload for this section only let payload = {}; payload[section] = getSectionValues(section); - const res = await fetch("/api/site-info", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -630,7 +520,6 @@ document.addEventListener("DOMContentLoaded", () => { const result = await res.json(); if (result.status === "ok") { showToast("✅ Section saved!", "success"); - // Reload config for this section fetch("/api/site-info") .then(res => res.json()) .then(data => { @@ -642,5 +531,4 @@ document.addEventListener("DOMContentLoaded", () => { } }); }); - }); \ No newline at end of file