diff --git a/.gitignore b/.gitignore index 7adc878..a833ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .* +!.env !.sh !.gitignore output/ diff --git a/VERSION b/VERSION index 10bf840..50aea0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 \ No newline at end of file +2.1.0 \ No newline at end of file diff --git a/config/site.yaml b/config/site.yaml index 166b129..2545965 100644 --- a/config/site.yaml +++ b/config/site.yaml @@ -1,4 +1,3 @@ -# Please change this by your settings. info: title: subtitle: @@ -9,8 +8,8 @@ info: social: instagram_url: - thumbnail: - + thumbnail: '' + menu: items: - label: Home @@ -18,19 +17,17 @@ menu: footer: copyright: Copyright © 2025 - legal_link: '/legals/' + legal_link: /legals/ legal_label: Legal notice -# Build parameters build: - theme: modern # choose a theme in config/theme folder - convert_images: true # true to enable image conversion - resize_images: true # true to enable image resizing - -# Change this by your legals + theme: modern + convert_images: true + resize_images: true + legals: hoster_name: - hoster_adress: + hoster_address: hoster_contact: intellectual_property: - - paragraph: "" + - paragraph: '' diff --git a/config/themes/modern/theme.css b/config/themes/modern/theme.css index 84c8a73..c5cfab5 100644 --- a/config/themes/modern/theme.css +++ b/config/themes/modern/theme.css @@ -35,16 +35,13 @@ img, tag { #footer { box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5); - margin: auto; } @media (max-width: 768px) { #footer { box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5); - border-radius: 15px 15px 0 0; max-width: 1140px; - margin: 0; } .tag { diff --git a/config/themes/modern/theme.yaml b/config/themes/modern/theme.yaml index 26cfa8a..db895de 100644 --- a/config/themes/modern/theme.yaml +++ b/config/themes/modern/theme.yaml @@ -1,32 +1,31 @@ -#-----------------------------------# -# 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..32fde01 100644 --- a/config/themes/typewriter/theme.yaml +++ b/config/themes/typewriter/theme.yaml @@ -1,21 +1,19 @@ -#-----------------------------------# -# 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/demo/config/themes/modern/theme.yaml b/demo/config/themes/modern/theme.yaml index 3f331f5..9142466 100644 --- a/demo/config/themes/modern/theme.yaml +++ b/demo/config/themes/modern/theme.yaml @@ -1,32 +1,31 @@ -#-----------------------------------# -# 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: '#fff' - browser_color: '#fff' + 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: '' + weights: + - '200' + - '400' + - '700' +- family: Montserrat + weights: + - '200' + - '400' + - '700' diff --git a/demo/config/themes/typewriter/theme.yaml b/demo/config/themes/typewriter/theme.yaml index 7a30379..32fde01 100644 --- a/demo/config/themes/typewriter/theme.yaml +++ b/demo/config/themes/typewriter/theme.yaml @@ -1,21 +1,19 @@ -#-----------------------------------# -# 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: '#fff' - browser_color: '#fff' + 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/docker/.env b/docker/.env new file mode 100644 index 0000000..bcc894c --- /dev/null +++ b/docker/.env @@ -0,0 +1,2 @@ +PREVIEW_PORT=3000 +WEBUI_PORT=5000 \ No newline at end of file diff --git a/docker/.sh/entrypoint.sh b/docker/.sh/entrypoint.sh index 77d9eb2..131ea41 100644 --- a/docker/.sh/entrypoint.sh +++ b/docker/.sh/entrypoint.sh @@ -5,12 +5,12 @@ CYAN="\033[1;36m" NC="\033[0m" copy_default_config() { - echo "Checking configuration directory..." + echo "[~] Checking configuration directory..." if [ ! -d "/app/config" ]; then mkdir -p /app/config fi - echo "Checking if default config files need to be copied..." + echo "[~] Checking if default config files need to be copied..." files_copied=false for file in /app/default/*; do @@ -18,16 +18,16 @@ copy_default_config() { target="/app/config/$filename" if [ ! -e "$target" ]; then - echo "Copying default config file: $filename" + echo "[→] Copying default config file: $filename" cp -r "$file" "$target" files_copied=true fi done if [ "$files_copied" = true ]; then - echo "Default configuration files copied successfully." + echo "[✓] Default configuration files copied successfully." else - echo "No default files needed to be copied." + echo "[✓] No default files needed to be copied." fi } @@ -43,11 +43,13 @@ start_server() { cat /tmp/build_logs_fifo >&2 & cat /tmp/build_logs_fifo2 >&2 & - echo "Starting preview HTTP server on port 3000..." + PREVIEW_PORT="${PREVIEW_PORT:-3000}" + echo "[~]Starting preview HTTP server on port 3000..." + echo "[i] Preview host port is set to: ${PREVIEW_PORT}" python3 -u -m http.server 3000 -d /app/output & SERVER_PID=$! - echo "Starting Lumeex Flask webui..." + echo "[~] Starting Lumeex Flask webui..." python3 -u -m src.py.webui.webui & WEBUI_PID=$! @@ -72,15 +74,15 @@ fi case "$1" in build) - echo "Running build.py..." + echo "[~] Running build.py..." python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo ;; gallery) - echo "Running gallery.py..." + echo "[~] Running gallery.py..." python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2 ;; *) - echo "Unknown command: $1" + echo "[!] Unknown command: $1" exec "$@" ;; esac diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index f48000c..793d5af 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2,10 +2,15 @@ services: lumeex: container_name: lmx build: .. + env_file: + - .env + environment: + - PREVIEW_PORT=${PREVIEW_PORT:-3000} # port for preview server - set it in .env file + - WEBUI_PORT=${WEBUI_PORT:-5000} # port for webui server - set it in .env file volumes: - ../config:/app/config # mount config directory - ../output:/app/output # mount output directory ports: - - "3000:3000" - - "5000:5000" + - "${PREVIEW_PORT:-3000}:3000" + - "${WEBUI_PORT:-5000}:5000" \ No newline at end of file diff --git a/src/public/js/lazy.js b/src/public/js/lazy.js index 0dc1c3d..e31518e 100644 --- a/src/public/js/lazy.js +++ b/src/public/js/lazy.js @@ -9,7 +9,6 @@ window.addEventListener("DOMContentLoaded", () => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; - console.log("Lazy-loading image:", img.dataset.src); img.src = img.dataset.src; img.onload = () => { img.classList.add("loaded"); diff --git a/src/public/style/style.css b/src/public/style/style.css index ce22718..5cf76de 100644 --- a/src/public/style/style.css +++ b/src/public/style/style.css @@ -78,6 +78,8 @@ html,body { font-weight: 400; line-height:1.5; color:var(--color-primary-dark); + display: flex; + flex-direction: column; } html { @@ -179,8 +181,8 @@ h2 { /* animation */ .appear { - -webkit-transition: all 0.3s; - transition: all 0.3s; + -webkit-transition: all 1s; + transition: all 1s; opacity: 0; -webkit-transform: translateY(20px); transform: translateY(20px); @@ -237,12 +239,23 @@ h2 { /* Hero */ #hero { - height: 100%; - width: 100%; + min-height: 100vh; + flex: 1 0 auto; + display: flex; + flex-direction: column; } + +#hero .section { + height: 100%; + display: flex; + flex-direction: column; + width: 100%; +} + #hero .content-wrapper, #hero .section { height:100%; } + .hero-background { height: 66%; width: 100%; @@ -303,7 +316,6 @@ h2 { font-size: 22px; } - .gallery { padding-top: 15px; } @@ -341,6 +353,11 @@ h2 { /* Footer */ +#footer { + margin: auto 0 0 0; + width: 100%; +} + .navigation { text-align: center; padding-top: 40px; @@ -470,8 +487,7 @@ h2 { #legals.content-wrapper { max-width: 90%; - margin: auto; - margin-top: 50px; + margin: 50px auto; } .legals-content { diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 75969ac..dbf7810 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -30,6 +30,8 @@ app = Flask( static_url_path="" ) +WEBUI_PORT = int(os.getenv("WEBUI_PORT", 5000)) + # --- Config paths --- SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml" PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos" @@ -72,9 +74,14 @@ def get_local_fonts(theme_name): def index(): return render_template("index.html") +PREVIEW_PORT = int(os.getenv("PREVIEW_PORT", 3000)) + @app.context_processor def inject_version(): - return dict(lumeex_version=lumeex_version) + return dict( + lumeex_version=lumeex_version, + preview_port=PREVIEW_PORT + ) # --- Gallery & Hero API --- @app.route("/gallery-editor") @@ -203,9 +210,21 @@ def get_site_info(): @app.route("/api/site-info", methods=["POST"]) def update_site_info(): """Update site info YAML.""" - data = request.json + new_data = request.json + with open(SITE_YAML, "r") as f: + old_data = yaml.safe_load(f) or {} + + def deep_merge(old, new): + for k, v in new.items(): + if isinstance(v, dict) and isinstance(old.get(k), dict): + old[k] = deep_merge(old[k], v) + else: + old[k] = v + return old + + merged = deep_merge(old_data, new_data) with open(SITE_YAML, "w") as f: - yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True) + yaml.safe_dump(merged, f, sort_keys=False, allow_unicode=True) return jsonify({"status": "ok"}) # --- Theme management --- @@ -487,5 +506,6 @@ def download_output_zip(): # --- Run server --- if __name__ == "__main__": - logging.info("Starting WebUI at http://0.0.0.0:5000") + logging.info("[~] Starting WebUI at http://0.0.0.0:5000") + logging.info(f"[i] WebUI host port is set to {WEBUI_PORT}") app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/src/webui/gallery-editor/index.html b/src/webui/gallery-editor/index.html index 329a65c..3960ad7 100644 --- a/src/webui/gallery-editor/index.html +++ b/src/webui/gallery-editor/index.html @@ -18,7 +18,16 @@ +
+ +${imagesToShow.length} photos
`; + } + + // Update gallery count (bottom) + const galleryCountBottom = document.getElementById('gallery-count-bottom'); + if (galleryCountBottom) { + galleryCountBottom.innerHTML = `${imagesToShow.length} photos
`; + } + + // Show/hide Remove All button (top) const removeAllBtn = document.getElementById('remove-all-gallery'); if (removeAllBtn) { - removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none'; + removeAllBtn.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none'; } + + // Show/hide bottom upload row + const bottomGalleryUpload = document.getElementById('bottom-gallery-upload'); + if (bottomGalleryUpload) { + bottomGalleryUpload.style.display = imagesToShow.length > 0 ? 'flex' : 'none'; + } + + // Show/hide Remove All button (bottom) + const removeAllBtnBottom = document.getElementById('remove-all-gallery-bottom'); + if (removeAllBtnBottom) { + removeAllBtnBottom.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none'; + } + + // Fade-in effect for loaded images + const fadeImages = document.querySelectorAll("img.fade-in-img"); + fadeImages.forEach(img => { + const onLoad = () => { + img.classList.add("loaded"); + }; + if (img.complete && img.naturalHeight !== 0) { + onLoad(); + } else { + img.addEventListener("load", onLoad, { once: true }); + img.addEventListener("error", () => { + console.warn("Image failed to load:", img.dataset.src || img.src); + }); + } + }); } // --- Render tags for a single image --- @@ -233,7 +282,7 @@ function renderHero() { div.className = 'photo flex-item flex-column'; div.innerHTML = `${heroImages.length} photos
`; + } + + // Update hero count (bottom) + const heroCountBottom = document.getElementById('hero-count-bottom'); + if (heroCountBottom) { + heroCountBottom.innerHTML = `${heroImages.length} photos
`; + } + + // Show/hide Remove All button (top) const removeAllBtn = document.getElementById('remove-all-hero'); if (removeAllBtn) { removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none'; } + + // Show/hide bottom upload row + const bottomHeroUpload = document.getElementById('bottom-hero-upload'); + if (bottomHeroUpload) { + bottomHeroUpload.style.display = heroImages.length > 0 ? 'flex' : 'none'; + } + + // Show/hide Remove All button (bottom) + const removeAllBtnBottom = document.getElementById('remove-all-hero-bottom'); + if (removeAllBtnBottom) { + removeAllBtnBottom.style.display = heroImages.length > 0 ? 'inline-block' : 'none'; + } } // --- Save gallery to server --- @@ -427,12 +500,37 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('delete-modal-cancel').onclick = hideDeleteModal; document.getElementById('delete-modal-confirm').onclick = confirmDelete; + // --- Radio toggle for gallery filter --- + const showAllRadio = document.getElementById('show-all-radio'); + const showUntaggedRadio = document.getElementById('show-untagged-radio'); + + if (showAllRadio && showUntaggedRadio) { + showAllRadio.addEventListener('change', () => { + if (showAllRadio.checked) { + showOnlyUntagged = false; + renderGallery(); + } + }); + showUntaggedRadio.addEventListener('change', () => { + if (showUntaggedRadio.checked) { + showOnlyUntagged = true; + renderGallery(); + } + }); + } + // Bulk delete buttons const removeAllGalleryBtn = document.getElementById('remove-all-gallery'); + const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom'); const removeAllHeroBtn = document.getElementById('remove-all-hero'); + const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom'); if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all'); + if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all'); if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all'); + if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all'); }); + + // --- Initialize --- loadData(); \ No newline at end of file diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index 3af7338..f68067c 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,67 @@ document.addEventListener("DOMContentLoaded", () => { } deleteThemeModal.style.display = "none"; themeToDelete = null; + updateSectionStatus("build"); }; } - // Fetch theme list and populate select + // --- 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) { fetch("/api/themes") .then(res => res.json()) @@ -298,148 +360,268 @@ document.addEventListener("DOMContentLoaded", () => { option.textContent = theme; themeSelect.appendChild(option); }); - // Set selected value after loading config - fetch("/api/site-info") - .then(res => res.json()) - .then(data => { - themeSelect.value = data.build?.theme || ""; - }); + // Now load config and update build status after theme select is ready + loadConfigAndUpdateBuildStatus(); }); + } else { + // If no theme select, just load config + loadConfigAndUpdateBuildStatus(); } - // 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 || ""; - 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; - } - }); - } - - // 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] || {}; + function normalizeMenuItems(items) { + return (items || []).map(item => ({ + label: item.label || "", + href: item.href || "" + })); + } + 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(normalizeMenuItems(values.items)) === JSON.stringify(normalizeMenuItems(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; // Only check theme is present + 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 +629,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/js/upload.js b/src/webui/js/upload.js index d2a45f1..8316b6e 100644 --- a/src/webui/js/upload.js +++ b/src/webui/js/upload.js @@ -11,6 +11,7 @@ function hideLoader() { if (loader) loader.classList.remove("active"); } + // --- Upload gallery images --- const galleryInput = document.getElementById('upload-gallery'); if (galleryInput) { @@ -47,6 +48,58 @@ if (heroInput) { const formData = new FormData(); for (const file of files) formData.append('files', file); + try { + const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); + const data = await res.json(); + hideLoader(); + if (res.ok) { + showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success"); + if (typeof refreshHero === "function") refreshHero(); + } else showToast('Error: ' + data.error, "error"); + } catch(err) { + hideLoader(); + console.error(err); + showToast('Server error!', "error"); + } finally { e.target.value = ''; } + }); +} + +// --- Upload gallery images (bottom button) --- +const galleryInputBottom = document.getElementById('upload-gallery-bottom'); +if (galleryInputBottom) { + galleryInputBottom.addEventListener('change', async (e) => { + const files = e.target.files; + if (!files.length) return; + showLoader("Uploading photos..."); + const formData = new FormData(); + for (const file of files) formData.append('files', file); + + try { + const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData }); + const data = await res.json(); + hideLoader(); + if (res.ok) { + showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success"); + if (typeof refreshGallery === "function") refreshGallery(); + } else showToast('Error: ' + data.error, "error"); + } catch(err) { + hideLoader(); + console.error(err); + showToast('Server error!', "error"); + } finally { e.target.value = ''; } + }); +} + +// --- Upload hero images (bottom button) --- +const heroInputBottom = document.getElementById('upload-hero-bottom'); +if (heroInputBottom) { + heroInputBottom.addEventListener('change', async (e) => { + const files = e.target.files; + if (!files.length) return; + showLoader("Uploading hero photos..."); + const formData = new FormData(); + for (const file of files) formData.append('files', file); + try { const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); const data = await res.json(); diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html index 564e31b..eaf5d05 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -4,176 +4,204 @@ {% block content %} -Follow the steps to generate your static gallery
-Follow the steps to generate your static gallery
+