From 795f5fbd1389c025285b26bed700e232d603c493 Mon Sep 17 00:00:00 2001 From: Djeex Date: Tue, 2 Sep 2025 14:44:54 +0200 Subject: [PATCH] Redone form save logic --- config/themes/modern/theme.yaml | 40 ++- config/themes/typewriter/theme.yaml | 20 +- src/webui/js/site-info.js | 408 ++++++++++++++++++++-------- src/webui/js/theme-editor.js | 285 ++++++++++++++----- src/webui/site-info/index.html | 354 +++++++++++++----------- src/webui/style/style.css | 53 ++-- src/webui/theme-editor/index.html | 339 ++++++++++++----------- 7 files changed, 925 insertions(+), 574 deletions(-) diff --git a/config/themes/modern/theme.yaml b/config/themes/modern/theme.yaml index 26cfa8a..0b23d8a 100644 --- a/config/themes/modern/theme.yaml +++ b/config/themes/modern/theme.yaml @@ -1,32 +1,28 @@ -#-----------------------------------# -# Modern theme for Lumeex # -# https://git.djeex.fr/Djeex/lumeex # -#-----------------------------------# colors: - primary: '#0065a1' + primary: '#0065A1' primary_dark: '#005384' - secondary: '#00b0f0' - accent: '#ffc700' + secondary: '#00B0F0' + accent: '#FFC700' text_dark: '#616161' - background: '#ffffff' - browser_color: '#ffffff' + background: '#FFFFFF' + browser_color: '#FFFFFF' favicon: path: favicon.png -google_fonts: - - family: Lato - weights: - - '200' - - '400' - - '700' - - family: Montserrat - weights: - - '200' - - '400' - - '700' fonts: primary: - name: Lato fallback: sans-serif + name: Lato secondary: + fallback: serif name: Montserrat - fallback: serif \ No newline at end of file +google_fonts: +- family: Lato + weights: + - '200' + - '400' + - '700' +- family: Montserrat + weights: + - '200' + - '400' + - '700' diff --git a/config/themes/typewriter/theme.yaml b/config/themes/typewriter/theme.yaml index 546e89d..fd251d0 100644 --- a/config/themes/typewriter/theme.yaml +++ b/config/themes/typewriter/theme.yaml @@ -1,21 +1,17 @@ -#-----------------------------------# -# Typewriter theme for Lumeex # -# https://git.djeex.fr/Djeex/lumeex # -#-----------------------------------# colors: - primary: '#0065a1' + accent: '#FFC700' + background: '#FFFFFF' + browser_color: '#FFFFFF' + primary: '#0065A1' primary_dark: '#005384' - secondary: '#00b0f0' - accent: '#ffc700' + secondary: '#00B0F0' text_dark: '#616161' - background: '#ffffff' - browser_color: '#ffffff' favicon: path: favicon.png fonts: primary: - name: Trixie + name: trixie.woff fallback: sans-serif secondary: - name: Trixie - fallback: serif \ No newline at end of file + name: trixie.woff + fallback: serif diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index 3af7338..b349bdb 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -26,14 +26,19 @@ function hideLoader() { } document.addEventListener("DOMContentLoaded", () => { - // Form and menu logic - const form = document.getElementById("site-info-form"); + // --- 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"); + + // --- Menu logic --- const menuList = document.getElementById("menu-items-list"); const addMenuBtn = document.getElementById("add-menu-item"); - let menuItems = []; - // Render menu items function renderMenuItems() { menuList.innerHTML = ""; menuItems.forEach((item, idx) => { @@ -50,7 +55,6 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Update menu items from inputs function updateMenuItemsFromInputs() { const inputs = menuList.querySelectorAll("input"); const items = []; @@ -62,12 +66,11 @@ document.addEventListener("DOMContentLoaded", () => { menuItems = items; } - // Intellectual property paragraphs logic + // --- Intellectual property paragraphs logic --- const ipList = document.getElementById("ip-list"); const addIpBtn = document.getElementById("add-ip-paragraph"); let ipParagraphs = []; - // Render IP paragraphs function renderIpParagraphs() { ipList.innerHTML = ""; ipParagraphs.forEach((item, idx) => { @@ -83,7 +86,6 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Update IP paragraphs from textareas function updateIpParagraphsFromInputs() { const textareas = ipList.querySelectorAll("textarea"); ipParagraphs = Array.from(textareas).map(textarea => ({ @@ -91,27 +93,27 @@ document.addEventListener("DOMContentLoaded", () => { })).filter(item => item.paragraph !== ""); } - // Build options + // --- Build options --- const convertImagesCheckbox = document.getElementById("convert-images-checkbox"); const resizeImagesCheckbox = document.getElementById("resize-images-checkbox"); - // Theme select + // --- Theme select --- const themeSelect = document.getElementById("theme-select"); - // Thumbnail upload and modal logic - const thumbnailInput = form?.elements["social.thumbnail"]; + // --- Thumbnail upload and modal logic --- + const thumbnailInput = document.getElementById("social-thumbnail"); const thumbnailUpload = document.getElementById("thumbnail-upload"); const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn"); const thumbnailPreview = document.getElementById("thumbnail-preview"); const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn"); - // Modal elements for delete confirmation + // --- 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 elements for theme deletion + // --- 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"); @@ -119,7 +121,7 @@ document.addEventListener("DOMContentLoaded", () => { const deleteThemeModalText = document.getElementById("delete-theme-modal-text"); let themeToDelete = null; - // Show/hide thumbnail preview, remove button, and choose button + // --- Show/hide thumbnail preview, remove button, and choose button --- function updateThumbnailPreview(src) { if (thumbnailPreview) { thumbnailPreview.src = src || ""; @@ -133,12 +135,12 @@ document.addEventListener("DOMContentLoaded", () => { } } - // Choose thumbnail button triggers file input + // --- Choose thumbnail button triggers file input --- if (chooseThumbnailBtn && thumbnailUpload) { chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click()); } - // Handle thumbnail upload and refresh preview (with cache busting) + // --- Handle thumbnail upload and refresh preview (with cache busting) --- if (thumbnailUpload) { thumbnailUpload.addEventListener("change", async (e) => { const file = e.target.files[0]; @@ -156,17 +158,18 @@ document.addEventListener("DOMContentLoaded", () => { } else { showToast("❌ Error uploading thumbnail", "error"); } + updateSectionStatus("social"); }); } - // Remove thumbnail button triggers modal + // --- Remove thumbnail button triggers modal --- if (removeThumbnailBtn) { removeThumbnailBtn.addEventListener("click", () => { deleteModal.style.display = "flex"; }); } - // Modal logic for thumbnail deletion + // --- Modal logic for thumbnail deletion --- if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) { deleteModalClose.onclick = deleteModalCancel.onclick = () => { deleteModal.style.display = "none"; @@ -187,10 +190,11 @@ document.addEventListener("DOMContentLoaded", () => { showToast("❌ Error removing thumbnail", "error"); } deleteModal.style.display = "none"; + updateSectionStatus("social"); }; } - // Theme upload logic (custom theme folder) + // --- Theme upload logic (custom theme folder) --- const themeUpload = document.getElementById("theme-upload"); const chooseThemeBtn = document.getElementById("choose-theme-btn"); if (chooseThemeBtn && themeUpload) { @@ -223,10 +227,11 @@ document.addEventListener("DOMContentLoaded", () => { } else { showToast("❌ Error uploading theme", "error"); } + updateSectionStatus("build"); }); } - // Remove theme button triggers modal + // --- Remove theme button triggers modal --- const removeThemeBtn = document.getElementById("remove-theme-btn"); if (removeThemeBtn && themeSelect) { removeThemeBtn.addEventListener("click", () => { @@ -242,7 +247,7 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Modal logic for theme deletion + // --- Modal logic for theme deletion --- if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) { deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => { deleteThemeModal.style.display = "none"; @@ -283,10 +288,11 @@ document.addEventListener("DOMContentLoaded", () => { } deleteThemeModal.style.display = "none"; themeToDelete = null; + updateSectionStatus("build"); }; } - // Fetch theme list and populate select + // --- Fetch theme list and populate select --- if (themeSelect) { fetch("/api/themes") .then(res => res.json()) @@ -307,139 +313,307 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Load config from server and populate form - if (form) { - fetch("/api/site-info") - .then(res => res.json()) - .then(data => { - ipParagraphs = Array.isArray(data.legals?.intellectual_property) - ? data.legals.intellectual_property - : []; - renderIpParagraphs(); - menuItems = Array.isArray(data.menu?.items) ? data.menu.items : []; - renderMenuItems(); - form.elements["info.title"].value = data.info?.title || ""; - form.elements["info.subtitle"].value = data.info?.subtitle || ""; - form.elements["info.description"].value = data.info?.description || ""; - form.elements["info.canonical"].value = data.info?.canonical || ""; - form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || ""); - form.elements["info.author"].value = data.info?.author || ""; - form.elements["social.instagram_url"].value = data.social?.instagram_url || ""; + // --- Load config from server and populate forms --- + let loadedConfig = {}; + 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()}` : ""); - form.elements["footer.copyright"].value = data.footer?.copyright || ""; - form.elements["footer.legal_label"].value = data.footer?.legal_label || ""; - if (themeSelect) { - themeSelect.value = data.build?.theme || ""; - } - form.elements["legals.hoster_name"].value = data.legals?.hoster_name || ""; - form.elements["legals.hoster_address"].value = data.legals?.hoster_address || ""; - form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || ""; - // Build checkboxes - if (convertImagesCheckbox) { - convertImagesCheckbox.checked = !!data.build?.convert_images; - } - if (resizeImagesCheckbox) { - resizeImagesCheckbox.checked = !!data.build?.resize_images; - } - }); - } + } + // 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 + ["info", "social", "menu", "footer", "legals", "build"].forEach(updateSectionStatus); + }); - // Add menu item + // --- Add menu item --- if (addMenuBtn) { addMenuBtn.addEventListener("click", () => { menuItems.push({ label: "", href: "" }); renderMenuItems(); + updateSectionStatus("menu"); }); } - // Remove menu item + // --- Remove menu item --- menuList.addEventListener("click", (e) => { if (e.target.classList.contains("remove-menu-item")) { const idx = parseInt(e.target.getAttribute("data-idx")); menuItems.splice(idx, 1); renderMenuItems(); + updateSectionStatus("menu"); } }); - // Update menuItems on input change + // --- Update menuItems on input change --- menuList.addEventListener("input", () => { updateMenuItemsFromInputs(); + updateSectionStatus("menu"); }); - // Add paragraph + // --- Add paragraph --- if (addIpBtn) { addIpBtn.addEventListener("click", () => { ipParagraphs.push({ paragraph: "" }); renderIpParagraphs(); + updateSectionStatus("legals"); }); } - // Remove paragraph + // --- Remove paragraph --- ipList.addEventListener("click", (e) => { if (e.target.classList.contains("remove-ip-paragraph")) { const idx = parseInt(e.target.getAttribute("data-idx")); ipParagraphs.splice(idx, 1); renderIpParagraphs(); + updateSectionStatus("legals"); } }); - // Update ipParagraphs on input change + // --- Update ipParagraphs on input change --- ipList.addEventListener("input", () => { updateIpParagraphsFromInputs(); + updateSectionStatus("legals"); }); - // Save config to server - if (form) { + // --- Section value helpers --- + function getSectionValues(section) { + 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 + }; + case "social": + return { + instagram_url: socialForm.elements["social.instagram_url"].value, + thumbnail: thumbnailInput ? thumbnailInput.value : "" + }; + case "menu": + updateMenuItemsFromInputs(); + return { items: menuItems }; + case "footer": + return { + copyright: footerForm.elements["footer.copyright"].value, + legal_label: footerForm.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, + intellectual_property: ipParagraphs + }; + case "build": + return { + theme: themeSelect ? themeSelect.value : "", + convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked), + resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked) + }; + default: + return {}; + } + } + + function isSectionSaved(section) { + const values = getSectionValues(section); + const config = loadedConfig[section] || {}; + switch (section) { + case "info": + return Object.keys(values).every( + key => values[key] && ( + key === "keywords" + ? Array.isArray(config.keywords) && values.keywords.join(",") === config.keywords.join(",") + : values[key] === (config[key] || "") + ) + ); + case "social": + return values.instagram_url && values.thumbnail && + values.instagram_url === (config.instagram_url || "") && + values.thumbnail === (config.thumbnail || ""); + case "menu": + return JSON.stringify(values.items) === JSON.stringify(config.items || []); + case "footer": + return values.copyright && values.legal_label && + values.copyright === (config.copyright || "") && + values.legal_label === (config.legal_label || ""); + case "legals": + return values.hoster_name && values.hoster_address && values.hoster_contact && + values.hoster_name === (config.hoster_name || "") && + values.hoster_address === (config.hoster_address || "") && + values.hoster_contact === (config.hoster_contact || "") && + JSON.stringify(values.intellectual_property) === JSON.stringify(config.intellectual_property || []); + case "build": + return values.theme === (config.theme || "") && + !!values.convert_images === !!config.convert_images && + !!values.resize_images === !!config.resize_images; + default: + return true; + } + } + + function isSectionComplete(section) { + const values = getSectionValues(section); + switch (section) { + case "info": + return ( + values.title && + values.subtitle && + values.description && + values.canonical && + values.keywords.length > 0 && + values.author + ); + case "social": + return values.instagram_url && values.thumbnail; + case "menu": + return Array.isArray(values.items) && values.items.every(item => item.label && item.href); + case "footer": + return values.copyright && values.legal_label; + case "legals": + return ( + values.hoster_name && + values.hoster_address && + values.hoster_contact && + Array.isArray(values.intellectual_property) && + values.intellectual_property.length > 0 && + values.intellectual_property.every(ip => ip.paragraph) + ); + case "build": + return values.theme; + default: + return true; + } + } + + function updateSectionStatus(section) { + const statusEl = document.querySelector(`#${section}-section .section-status`); + if (!statusEl) return; + if (!isSectionComplete(section)) { + statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields"; + statusEl.style.color = "#ffc700"; + statusEl.style.display = ""; + statusEl.style.fontStyle = "normal"; + return; + } + if (isSectionSaved(section)) { + statusEl.innerHTML = ""; + statusEl.style.display = "none"; + } else { + statusEl.innerHTML = "⚠️ Section not yet saved"; + statusEl.style.color = "#ffc700"; + statusEl.style.display = ""; + statusEl.style.fontStyle = "normal"; + } +} + + // --- 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 }) => { + 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(); - updateMenuItemsFromInputs(); - updateIpParagraphsFromInputs(); - - // Check if thumbnail is set before saving (uploaded or present in input) - if (!thumbnailInput || !thumbnailInput.value) { - showLoader("Saving..."); - showToast("❌ Thumbnail is required.", "error"); - hideLoader(); + // Native browser validation + if (!form.reportValidity()) { + showToast("❌ Please fill all required fields before saving.", "error"); + updateSectionStatus(section); return; } - - const build = { - theme: themeSelect ? themeSelect.value : "", - convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked), - resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked) - }; - - const payload = { - info: { - title: form.elements["info.title"].value, - subtitle: form.elements["info.subtitle"].value, - description: form.elements["info.description"].value, - canonical: form.elements["info.canonical"].value, - keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean), - author: form.elements["info.author"].value - }, - social: { - instagram_url: form.elements["social.instagram_url"].value, - thumbnail: thumbnailInput ? thumbnailInput.value : "" - }, - menu: { - items: menuItems - }, - footer: { - copyright: form.elements["footer.copyright"].value, - legal_label: form.elements["footer.legal_label"].value - }, - build, - legals: { - hoster_name: form.elements["legals.hoster_name"].value, - hoster_address: form.elements["legals.hoster_address"].value, - hoster_contact: form.elements["legals.hoster_contact"].value, - intellectual_property: ipParagraphs + // Social section: check thumbnail + if (section === "social") { + if (!thumbnailInput || !thumbnailInput.value) { + showToast("❌ Thumbnail is required.", "error"); + updateSectionStatus(section); + return; } - }; - // --- REMOVE loader for save --- - // showLoader("Saving..."); + } + // Menu section: check all menu items + if (section === "menu") { + updateMenuItemsFromInputs(); + if (!menuItems.length || !menuItems.every(item => item.label && item.href)) { + showToast("❌ Please fill all menu item fields.", "error"); + updateSectionStatus(section); + return; + } + } + // Legals section: check all paragraphs + if (section === "legals") { + updateIpParagraphsFromInputs(); + if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) { + showToast("❌ Please fill all intellectual property paragraphs.", "error"); + updateSectionStatus(section); + 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" }, @@ -447,10 +621,18 @@ document.addEventListener("DOMContentLoaded", () => { }); const result = await res.json(); if (result.status === "ok") { - showToast("✅ Site info saved!", "success"); + showToast("✅ Section saved!", "success"); + // Reload config for this section + fetch("/api/site-info") + .then(res => res.json()) + .then(data => { + loadedConfig = data; + updateSectionStatus(section); + }); } else { - showToast("❌ Error saving site info", "error"); + showToast("❌ Error saving section", "error"); } }); - } + }); + }); \ No newline at end of file diff --git a/src/webui/js/theme-editor.js b/src/webui/js/theme-editor.js index f7b45c4..4d853f3 100644 --- a/src/webui/js/theme-editor.js +++ b/src/webui/js/theme-editor.js @@ -116,54 +116,173 @@ function renderLocalFonts(fonts) { }); } +// --- Section helpers --- +function getSectionValues(section) { + switch (section) { + case "colors": + return { + primary: document.getElementById("color-primary-text").value, + primary_dark: document.getElementById("color-primary-dark-text").value, + secondary: document.getElementById("color-secondary-text").value, + accent: document.getElementById("color-accent-text").value, + text_dark: document.getElementById("color-text-dark-text").value, + background: document.getElementById("color-background-text").value, + browser_color: document.getElementById("color-browser-color-text").value + }; + case "google-fonts": + const googleFontsFields = document.getElementById("google-fonts-fields"); + const fonts = []; + if (googleFontsFields) { + googleFontsFields.querySelectorAll(".input-field").forEach(field => { + const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value.trim(); + const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value + .split(",").map(w => w.trim()).filter(Boolean); + if (family) fonts.push({ family, weights }); + }); + } + return fonts; + case "fonts": + return { + primary: { + name: document.getElementById("font-primary").value, + fallback: document.getElementById("font-primary-fallback").value + }, + secondary: { + name: document.getElementById("font-secondary").value, + fallback: document.getElementById("font-secondary-fallback").value + } + }; + case "favicon": + return { + path: document.getElementById("favicon-path").value + }; + default: + return {}; + } +} + +function isSectionComplete(section) { + switch (section) { + case "colors": + const v = getSectionValues("colors"); + return ( + v.primary && + v.primary_dark && + v.secondary && + v.accent && + v.text_dark && + v.background && + v.browser_color + ); + case "google-fonts": + const fonts = getSectionValues("google-fonts"); + return fonts.every(f => f.family); + case "fonts": + const f = getSectionValues("fonts"); + return f.primary.name && f.primary.fallback && f.secondary.name && f.secondary.fallback; + case "favicon": + const fav = getSectionValues("favicon"); + return !!fav.path; + default: + return true; + } +} + +function isSectionSaved(section, loadedConfig) { + switch (section) { + case "colors": + const v = getSectionValues("colors"); + const c = loadedConfig.colors || {}; + return ( + v.primary === c.primary && + v.primary_dark === c.primary_dark && + v.secondary === c.secondary && + v.accent === c.accent && + v.text_dark === c.text_dark && + v.background === c.background && + v.browser_color === c.browser_color + ); + case "google-fonts": + const fonts = getSectionValues("google-fonts"); + const cf = loadedConfig.google_fonts || []; + return JSON.stringify(fonts) === JSON.stringify(cf); + case "fonts": + const f = getSectionValues("fonts"); + const cfnt = loadedConfig.fonts || {}; + return ( + f.primary.name === (cfnt.primary?.name || "") && + f.primary.fallback === (cfnt.primary?.fallback || "") && + f.secondary.name === (cfnt.secondary?.name || "") && + f.secondary.fallback === (cfnt.secondary?.fallback || "") + ); + case "favicon": + const fav = getSectionValues("favicon"); + const cfav = loadedConfig.favicon || {}; + return fav.path === (cfav.path || ""); + default: + return true; + } +} + +function updateSectionStatus(section, loadedConfig) { + const statusEl = document.querySelector(`#${section}-form .section-status`); + if (!statusEl) return; + if (!isSectionComplete(section)) { + statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields"; + statusEl.style.color = "#ffc700"; + statusEl.style.display = ""; + statusEl.style.fontStyle = "normal"; + return; + } + if (isSectionSaved(section, loadedConfig)) { + statusEl.innerHTML = ""; + statusEl.style.display = "none"; + } else { + statusEl.innerHTML = "⚠️ Section not yet saved"; + statusEl.style.color = "#ffc700"; + statusEl.style.display = ""; + statusEl.style.fontStyle = "normal"; + } +} + document.addEventListener("DOMContentLoaded", async () => { const themeInfo = await fetchThemeInfo(); const themeNameSpan = document.getElementById("current-theme"); if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name; - const themeYaml = themeInfo.theme_yaml; - const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : []; + let loadedConfig = themeInfo.theme_yaml; + let googleFonts = loadedConfig.google_fonts ? JSON.parse(JSON.stringify(loadedConfig.google_fonts)) : []; let localFonts = await fetchLocalFonts(themeInfo.theme_name); // Colors - if (themeYaml.colors) { - setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1"); - setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384"); - setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0"); - setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700"); - setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", themeYaml.colors.text_dark || "#616161"); - setupColorPicker("color-background", "color-background-btn", "color-background-text", themeYaml.colors.background || "#fff"); - setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", themeYaml.colors.browser_color || "#fff"); + if (loadedConfig.colors) { + setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", loadedConfig.colors.primary || "#0065a1"); + setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", loadedConfig.colors.primary_dark || "#005384"); + setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", loadedConfig.colors.secondary || "#00b0f0"); + setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", loadedConfig.colors.accent || "#ffc700"); + setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", loadedConfig.colors.text_dark || "#616161"); + setupColorPicker("color-background", "color-background-btn", "color-background-text", loadedConfig.colors.background || "#fff"); + setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", loadedConfig.colors.browser_color || "#fff"); } // Fonts function refreshFontDropdowns() { - setFontDropdown("font-primary", document.getElementById("font-primary").value, [ + setFontDropdown("font-primary", loadedConfig.fonts?.primary?.name || "Lato", [ ...googleFonts.map(f => f.family), ...localFonts ]); - setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [ + setFontDropdown("font-secondary", loadedConfig.fonts?.secondary?.name || "Montserrat", [ ...googleFonts.map(f => f.family), ...localFonts ]); + setFallbackDropdown("font-primary-fallback", loadedConfig.fonts?.primary?.fallback || "sans-serif"); + setFallbackDropdown("font-secondary-fallback", loadedConfig.fonts?.secondary?.fallback || "serif"); } - if (themeYaml.fonts) { - setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [ - ...googleFonts.map(f => f.family), - ...localFonts - ]); - setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif"); - setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [ - ...googleFonts.map(f => f.family), - ...localFonts - ]); - setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif"); - } + refreshFontDropdowns(); // Font upload logic const fontUploadInput = document.getElementById("font-upload"); const chooseFontBtn = document.getElementById("choose-font-btn"); - const fontUploadStatus = document.getElementById("font-upload-status"); const localFontsList = document.getElementById("local-fonts-list"); // Modal logic for font deletion @@ -341,9 +460,9 @@ document.addEventListener("DOMContentLoaded", async () => { }; } - if (themeYaml.favicon && themeYaml.favicon.path) { - faviconInput.value = themeYaml.favicon.path; - updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`); + if (loadedConfig.favicon && loadedConfig.favicon.path) { + faviconInput.value = loadedConfig.favicon.path; + updateFaviconPreview(`/themes/${themeInfo.theme_name}/${loadedConfig.favicon.path}?t=${Date.now()}`); } else { updateFaviconPreview(""); } @@ -367,6 +486,7 @@ document.addEventListener("DOMContentLoaded", async () => { googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); refreshFontDropdowns(); + updateSectionStatus("google-fonts", loadedConfig); }); } @@ -396,6 +516,7 @@ document.addEventListener("DOMContentLoaded", async () => { googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); refreshFontDropdowns(); + updateSectionStatus("google-fonts", loadedConfig); } }, true); @@ -414,54 +535,72 @@ document.addEventListener("DOMContentLoaded", async () => { googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); refreshFontDropdowns(); + updateSectionStatus("google-fonts", loadedConfig); } }); } - 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, - primary_dark: document.getElementById("color-primary-dark-text").value, - secondary: document.getElementById("color-secondary-text").value, - accent: document.getElementById("color-accent-text").value, - text_dark: document.getElementById("color-text-dark-text").value, - background: document.getElementById("color-background-text").value, - browser_color: document.getElementById("color-browser-color-text").value - }; - data.fonts = { - primary: { - name: document.getElementById("font-primary").value, - fallback: document.getElementById("font-primary-fallback").value - }, - secondary: { - name: document.getElementById("font-secondary").value, - fallback: document.getElementById("font-secondary-fallback").value - } - }; - data.favicon = { - path: faviconInput.value - }; - data.google_fonts = []; - document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => { - const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value; - const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value - .split(",").map(w => w.trim()).filter(w => w); - if (family) data.google_fonts.push({ family, weights }); - }); - - const res = await fetch("/api/theme-info", { - method: "POST", - 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 { - showToast("Error saving theme.", "error"); - } + // --- Section status listeners --- + [ + { form: document.getElementById("colors-form"), section: "colors" }, + { form: document.getElementById("google-fonts-form"), section: "google-fonts" }, + { form: document.getElementById("fonts-form"), section: "fonts" }, + { form: document.getElementById("favicon-form"), section: "favicon" } + ].forEach(({ form, section }) => { + if (!form) return; + form.addEventListener("input", () => updateSectionStatus(section, loadedConfig)); + form.addEventListener("change", () => updateSectionStatus(section, loadedConfig)); }); + + // --- Section save handlers --- + [ + { form: document.getElementById("colors-form"), section: "colors" }, + { form: document.getElementById("google-fonts-form"), section: "google-fonts" }, + { form: document.getElementById("fonts-form"), section: "fonts" }, + { form: document.getElementById("favicon-form"), section: "favicon" } + ].forEach(({ form, section }) => { + if (!form) return; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + if (!form.reportValidity() || !isSectionComplete(section)) { + showToast("❌ Please fill all required fields before saving.", "error"); + updateSectionStatus(section, loadedConfig); + return; + } + // Merge with loadedConfig to avoid overwriting other sections + let payload = { ...loadedConfig }; + switch (section) { + case "colors": + payload.colors = getSectionValues("colors"); + break; + case "google-fonts": + payload.google_fonts = getSectionValues("google-fonts"); + break; + case "fonts": + payload.fonts = getSectionValues("fonts"); + break; + case "favicon": + payload.favicon = getSectionValues("favicon"); + break; + } + showLoader("Saving..."); + const res = await fetch("/api/theme-info", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: payload }) + }); + hideLoader(); + if (res.ok) { + showToast("✅ Section saved!", "success"); + const updatedThemeInfo = await fetchThemeInfo(); + loadedConfig = updatedThemeInfo.theme_yaml; + updateSectionStatus(section, loadedConfig); + } else { + showToast("Error saving section.", "error"); + } + }); + }); + + // Initial status update + ["colors", "google-fonts", "fonts", "favicon"].forEach(section => updateSectionStatus(section, loadedConfig)); }); \ No newline at end of file diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html index 8eeebe4..9e86233 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -4,178 +4,204 @@ {% block content %} -

Edit Site Info

-
- - -
-

Info

-

Set the basic information for your site and SEO

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-

Social

-

Set your social media links and thumbnail for link sharing

-
-
- - - - - -
- - - -
-
-
-
- -
-

Menu

-

Manage your site menu items. You can use tag combination to propose custom filters

-
-
- - -
-
-
- -
-

Footer

-

Set your copyright informations and legal link name

-
-
- - -
-
- - -
-
-
- -
-

Legals

-

Set your legal informations

-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
- -
-

Build

-

Select a theme from the dropdown menu or add your custom theme folder

-
-
- - - - - - -

If checked, images will be converted for web and resized to fit the theme

- - -
-
-
- -
- -
-

Steps

-

Follow the steps to generate your static gallery

- +

Edit Site Info

+ + +
+
+

Info

+

+

Set the basic information for your site and SEO

+
+
+ +
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+
+

Social

+

Set your social media links and thumbnail for link sharing

+

+
+
+ + + + + +
+ + + +
+
+
+ +
+
+ + + + + + + + +
+
+

Legals

+

Set your legal informations

+

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+ + +
+
+

Build

+

Select a theme from the dropdown menu or add your custom theme folder

+

+
+
+ + + + + + +

If checked, images will be converted for web and resized to fit the theme

+ + +
+
+ +
+
+ + +
+

Steps

+

Follow the steps to generate your static gallery

+ +
-
-