Redone form save logic

This commit is contained in:
Djeex
2025-09-02 14:44:54 +02:00
parent f8bebb9c95
commit 795f5fbd13
7 changed files with 925 additions and 574 deletions

View File

@ -1,32 +1,28 @@
#-----------------------------------#
# Modern theme for Lumeex #
# https://git.djeex.fr/Djeex/lumeex #
#-----------------------------------#
colors: colors:
primary: '#0065a1' primary: '#0065A1'
primary_dark: '#005384' primary_dark: '#005384'
secondary: '#00b0f0' secondary: '#00B0F0'
accent: '#ffc700' accent: '#FFC700'
text_dark: '#616161' text_dark: '#616161'
background: '#ffffff' background: '#FFFFFF'
browser_color: '#ffffff' browser_color: '#FFFFFF'
favicon: favicon:
path: favicon.png path: favicon.png
google_fonts:
- family: Lato
weights:
- '200'
- '400'
- '700'
- family: Montserrat
weights:
- '200'
- '400'
- '700'
fonts: fonts:
primary: primary:
name: Lato
fallback: sans-serif fallback: sans-serif
name: Lato
secondary: secondary:
name: Montserrat
fallback: serif fallback: serif
name: Montserrat
google_fonts:
- family: Lato
weights:
- '200'
- '400'
- '700'
- family: Montserrat
weights:
- '200'
- '400'
- '700'

View File

@ -1,21 +1,17 @@
#-----------------------------------#
# Typewriter theme for Lumeex #
# https://git.djeex.fr/Djeex/lumeex #
#-----------------------------------#
colors: colors:
primary: '#0065a1' accent: '#FFC700'
background: '#FFFFFF'
browser_color: '#FFFFFF'
primary: '#0065A1'
primary_dark: '#005384' primary_dark: '#005384'
secondary: '#00b0f0' secondary: '#00B0F0'
accent: '#ffc700'
text_dark: '#616161' text_dark: '#616161'
background: '#ffffff'
browser_color: '#ffffff'
favicon: favicon:
path: favicon.png path: favicon.png
fonts: fonts:
primary: primary:
name: Trixie name: trixie.woff
fallback: sans-serif fallback: sans-serif
secondary: secondary:
name: Trixie name: trixie.woff
fallback: serif fallback: serif

View File

@ -26,14 +26,19 @@ function hideLoader() {
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Form and menu logic // --- Section Forms ---
const form = document.getElementById("site-info-form"); 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 menuList = document.getElementById("menu-items-list");
const addMenuBtn = document.getElementById("add-menu-item"); const addMenuBtn = document.getElementById("add-menu-item");
let menuItems = []; let menuItems = [];
// Render menu items
function renderMenuItems() { function renderMenuItems() {
menuList.innerHTML = ""; menuList.innerHTML = "";
menuItems.forEach((item, idx) => { menuItems.forEach((item, idx) => {
@ -50,7 +55,6 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
// Update menu items from inputs
function updateMenuItemsFromInputs() { function updateMenuItemsFromInputs() {
const inputs = menuList.querySelectorAll("input"); const inputs = menuList.querySelectorAll("input");
const items = []; const items = [];
@ -62,12 +66,11 @@ document.addEventListener("DOMContentLoaded", () => {
menuItems = items; menuItems = items;
} }
// Intellectual property paragraphs logic // --- Intellectual property paragraphs logic ---
const ipList = document.getElementById("ip-list"); const ipList = document.getElementById("ip-list");
const addIpBtn = document.getElementById("add-ip-paragraph"); const addIpBtn = document.getElementById("add-ip-paragraph");
let ipParagraphs = []; let ipParagraphs = [];
// Render IP paragraphs
function renderIpParagraphs() { function renderIpParagraphs() {
ipList.innerHTML = ""; ipList.innerHTML = "";
ipParagraphs.forEach((item, idx) => { ipParagraphs.forEach((item, idx) => {
@ -83,7 +86,6 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
// Update IP paragraphs from textareas
function updateIpParagraphsFromInputs() { function updateIpParagraphsFromInputs() {
const textareas = ipList.querySelectorAll("textarea"); const textareas = ipList.querySelectorAll("textarea");
ipParagraphs = Array.from(textareas).map(textarea => ({ ipParagraphs = Array.from(textareas).map(textarea => ({
@ -91,27 +93,27 @@ document.addEventListener("DOMContentLoaded", () => {
})).filter(item => item.paragraph !== ""); })).filter(item => item.paragraph !== "");
} }
// Build options // --- Build options ---
const convertImagesCheckbox = document.getElementById("convert-images-checkbox"); const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
const resizeImagesCheckbox = document.getElementById("resize-images-checkbox"); const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
// Theme select // --- Theme select ---
const themeSelect = document.getElementById("theme-select"); const themeSelect = document.getElementById("theme-select");
// Thumbnail upload and modal logic // --- Thumbnail upload and modal logic ---
const thumbnailInput = form?.elements["social.thumbnail"]; const thumbnailInput = document.getElementById("social-thumbnail");
const thumbnailUpload = document.getElementById("thumbnail-upload"); const thumbnailUpload = document.getElementById("thumbnail-upload");
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn"); const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
const thumbnailPreview = document.getElementById("thumbnail-preview"); const thumbnailPreview = document.getElementById("thumbnail-preview");
const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn"); const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
// Modal elements for delete confirmation // --- Modal elements for delete confirmation ---
const deleteModal = document.getElementById("delete-modal"); const deleteModal = document.getElementById("delete-modal");
const deleteModalClose = document.getElementById("delete-modal-close"); const deleteModalClose = document.getElementById("delete-modal-close");
const deleteModalConfirm = document.getElementById("delete-modal-confirm"); const deleteModalConfirm = document.getElementById("delete-modal-confirm");
const deleteModalCancel = document.getElementById("delete-modal-cancel"); 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 deleteThemeModal = document.getElementById("delete-theme-modal");
const deleteThemeModalClose = document.getElementById("delete-theme-modal-close"); const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm"); const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
@ -119,7 +121,7 @@ document.addEventListener("DOMContentLoaded", () => {
const deleteThemeModalText = document.getElementById("delete-theme-modal-text"); const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
let themeToDelete = null; let themeToDelete = null;
// Show/hide thumbnail preview, remove button, and choose button // --- Show/hide thumbnail preview, remove button, and choose button ---
function updateThumbnailPreview(src) { function updateThumbnailPreview(src) {
if (thumbnailPreview) { if (thumbnailPreview) {
thumbnailPreview.src = src || ""; 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) { if (chooseThumbnailBtn && thumbnailUpload) {
chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click()); 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) { if (thumbnailUpload) {
thumbnailUpload.addEventListener("change", async (e) => { thumbnailUpload.addEventListener("change", async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@ -156,17 +158,18 @@ document.addEventListener("DOMContentLoaded", () => {
} else { } else {
showToast("❌ Error uploading thumbnail", "error"); showToast("❌ Error uploading thumbnail", "error");
} }
updateSectionStatus("social");
}); });
} }
// Remove thumbnail button triggers modal // --- Remove thumbnail button triggers modal ---
if (removeThumbnailBtn) { if (removeThumbnailBtn) {
removeThumbnailBtn.addEventListener("click", () => { removeThumbnailBtn.addEventListener("click", () => {
deleteModal.style.display = "flex"; deleteModal.style.display = "flex";
}); });
} }
// Modal logic for thumbnail deletion // --- Modal logic for thumbnail deletion ---
if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) { if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
deleteModalClose.onclick = deleteModalCancel.onclick = () => { deleteModalClose.onclick = deleteModalCancel.onclick = () => {
deleteModal.style.display = "none"; deleteModal.style.display = "none";
@ -187,10 +190,11 @@ document.addEventListener("DOMContentLoaded", () => {
showToast("❌ Error removing thumbnail", "error"); showToast("❌ Error removing thumbnail", "error");
} }
deleteModal.style.display = "none"; 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 themeUpload = document.getElementById("theme-upload");
const chooseThemeBtn = document.getElementById("choose-theme-btn"); const chooseThemeBtn = document.getElementById("choose-theme-btn");
if (chooseThemeBtn && themeUpload) { if (chooseThemeBtn && themeUpload) {
@ -223,10 +227,11 @@ document.addEventListener("DOMContentLoaded", () => {
} else { } else {
showToast("❌ Error uploading theme", "error"); showToast("❌ Error uploading theme", "error");
} }
updateSectionStatus("build");
}); });
} }
// Remove theme button triggers modal // --- Remove theme button triggers modal ---
const removeThemeBtn = document.getElementById("remove-theme-btn"); const removeThemeBtn = document.getElementById("remove-theme-btn");
if (removeThemeBtn && themeSelect) { if (removeThemeBtn && themeSelect) {
removeThemeBtn.addEventListener("click", () => { 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) { if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => { deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
deleteThemeModal.style.display = "none"; deleteThemeModal.style.display = "none";
@ -283,10 +288,11 @@ document.addEventListener("DOMContentLoaded", () => {
} }
deleteThemeModal.style.display = "none"; deleteThemeModal.style.display = "none";
themeToDelete = null; themeToDelete = null;
updateSectionStatus("build");
}; };
} }
// Fetch theme list and populate select // --- Fetch theme list and populate select ---
if (themeSelect) { if (themeSelect) {
fetch("/api/themes") fetch("/api/themes")
.then(res => res.json()) .then(res => res.json())
@ -307,139 +313,307 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
// Load config from server and populate form // --- Load config from server and populate forms ---
if (form) { let loadedConfig = {};
fetch("/api/site-info") fetch("/api/site-info")
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
ipParagraphs = Array.isArray(data.legals?.intellectual_property) loadedConfig = data;
? data.legals.intellectual_property // Info
: []; if (infoForm) {
renderIpParagraphs(); infoForm.elements["info.title"].value = data.info?.title || "";
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : []; infoForm.elements["info.subtitle"].value = data.info?.subtitle || "";
renderMenuItems(); infoForm.elements["info.description"].value = data.info?.description || "";
form.elements["info.title"].value = data.info?.title || ""; infoForm.elements["info.canonical"].value = data.info?.canonical || "";
form.elements["info.subtitle"].value = data.info?.subtitle || ""; infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
form.elements["info.description"].value = data.info?.description || ""; infoForm.elements["info.author"].value = data.info?.author || "";
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 || ""); // Social
form.elements["info.author"].value = data.info?.author || ""; if (socialForm) {
form.elements["social.instagram_url"].value = data.social?.instagram_url || ""; socialForm.elements["social.instagram_url"].value = data.social?.instagram_url || "";
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || ""; if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : ""); 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 || ""; // Menu
if (themeSelect) { menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
themeSelect.value = data.build?.theme || ""; renderMenuItems();
} // Footer
form.elements["legals.hoster_name"].value = data.legals?.hoster_name || ""; if (footerForm) {
form.elements["legals.hoster_address"].value = data.legals?.hoster_address || ""; footerForm.elements["footer.copyright"].value = data.footer?.copyright || "";
form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || ""; footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || "";
// Build checkboxes }
if (convertImagesCheckbox) { // Legals
convertImagesCheckbox.checked = !!data.build?.convert_images; ipParagraphs = Array.isArray(data.legals?.intellectual_property)
} ? data.legals.intellectual_property
if (resizeImagesCheckbox) { : [];
resizeImagesCheckbox.checked = !!data.build?.resize_images; 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) { if (addMenuBtn) {
addMenuBtn.addEventListener("click", () => { addMenuBtn.addEventListener("click", () => {
menuItems.push({ label: "", href: "" }); menuItems.push({ label: "", href: "" });
renderMenuItems(); renderMenuItems();
updateSectionStatus("menu");
}); });
} }
// Remove menu item // --- Remove menu item ---
menuList.addEventListener("click", (e) => { menuList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-menu-item")) { if (e.target.classList.contains("remove-menu-item")) {
const idx = parseInt(e.target.getAttribute("data-idx")); const idx = parseInt(e.target.getAttribute("data-idx"));
menuItems.splice(idx, 1); menuItems.splice(idx, 1);
renderMenuItems(); renderMenuItems();
updateSectionStatus("menu");
} }
}); });
// Update menuItems on input change // --- Update menuItems on input change ---
menuList.addEventListener("input", () => { menuList.addEventListener("input", () => {
updateMenuItemsFromInputs(); updateMenuItemsFromInputs();
updateSectionStatus("menu");
}); });
// Add paragraph // --- Add paragraph ---
if (addIpBtn) { if (addIpBtn) {
addIpBtn.addEventListener("click", () => { addIpBtn.addEventListener("click", () => {
ipParagraphs.push({ paragraph: "" }); ipParagraphs.push({ paragraph: "" });
renderIpParagraphs(); renderIpParagraphs();
updateSectionStatus("legals");
}); });
} }
// Remove paragraph // --- Remove paragraph ---
ipList.addEventListener("click", (e) => { ipList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-ip-paragraph")) { if (e.target.classList.contains("remove-ip-paragraph")) {
const idx = parseInt(e.target.getAttribute("data-idx")); const idx = parseInt(e.target.getAttribute("data-idx"));
ipParagraphs.splice(idx, 1); ipParagraphs.splice(idx, 1);
renderIpParagraphs(); renderIpParagraphs();
updateSectionStatus("legals");
} }
}); });
// Update ipParagraphs on input change // --- Update ipParagraphs on input change ---
ipList.addEventListener("input", () => { ipList.addEventListener("input", () => {
updateIpParagraphsFromInputs(); updateIpParagraphsFromInputs();
updateSectionStatus("legals");
}); });
// Save config to server // --- Section value helpers ---
if (form) { 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) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
updateMenuItemsFromInputs(); // Native browser validation
updateIpParagraphsFromInputs(); if (!form.reportValidity()) {
showToast("❌ Please fill all required fields before saving.", "error");
// Check if thumbnail is set before saving (uploaded or present in input) updateSectionStatus(section);
if (!thumbnailInput || !thumbnailInput.value) {
showLoader("Saving...");
showToast("❌ Thumbnail is required.", "error");
hideLoader();
return; return;
} }
// Social section: check thumbnail
const build = { if (section === "social") {
theme: themeSelect ? themeSelect.value : "", if (!thumbnailInput || !thumbnailInput.value) {
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked), showToast("❌ Thumbnail is required.", "error");
resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked) updateSectionStatus(section);
}; return;
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
} }
}; }
// --- REMOVE loader for save --- // Menu section: check all menu items
// showLoader("Saving..."); 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", { const res = await fetch("/api/site-info", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -447,10 +621,18 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
const result = await res.json(); const result = await res.json();
if (result.status === "ok") { 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 { } else {
showToast("❌ Error saving site info", "error"); showToast("❌ Error saving section", "error");
} }
}); });
} });
}); });

View File

@ -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 () => { document.addEventListener("DOMContentLoaded", async () => {
const themeInfo = await fetchThemeInfo(); const themeInfo = await fetchThemeInfo();
const themeNameSpan = document.getElementById("current-theme"); const themeNameSpan = document.getElementById("current-theme");
if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name; if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
const themeYaml = themeInfo.theme_yaml; let loadedConfig = themeInfo.theme_yaml;
const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : []; let googleFonts = loadedConfig.google_fonts ? JSON.parse(JSON.stringify(loadedConfig.google_fonts)) : [];
let localFonts = await fetchLocalFonts(themeInfo.theme_name); let localFonts = await fetchLocalFonts(themeInfo.theme_name);
// Colors // Colors
if (themeYaml.colors) { if (loadedConfig.colors) {
setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1"); 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", themeYaml.colors.primary_dark || "#005384"); 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", themeYaml.colors.secondary || "#00b0f0"); setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", loadedConfig.colors.secondary || "#00b0f0");
setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700"); 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", themeYaml.colors.text_dark || "#616161"); 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", themeYaml.colors.background || "#fff"); 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", themeYaml.colors.browser_color || "#fff"); setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", loadedConfig.colors.browser_color || "#fff");
} }
// Fonts // Fonts
function refreshFontDropdowns() { function refreshFontDropdowns() {
setFontDropdown("font-primary", document.getElementById("font-primary").value, [ setFontDropdown("font-primary", loadedConfig.fonts?.primary?.name || "Lato", [
...googleFonts.map(f => f.family), ...googleFonts.map(f => f.family),
...localFonts ...localFonts
]); ]);
setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [ setFontDropdown("font-secondary", loadedConfig.fonts?.secondary?.name || "Montserrat", [
...googleFonts.map(f => f.family), ...googleFonts.map(f => f.family),
...localFonts ...localFonts
]); ]);
setFallbackDropdown("font-primary-fallback", loadedConfig.fonts?.primary?.fallback || "sans-serif");
setFallbackDropdown("font-secondary-fallback", loadedConfig.fonts?.secondary?.fallback || "serif");
} }
if (themeYaml.fonts) { refreshFontDropdowns();
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");
}
// Font upload logic // Font upload logic
const fontUploadInput = document.getElementById("font-upload"); const fontUploadInput = document.getElementById("font-upload");
const chooseFontBtn = document.getElementById("choose-font-btn"); const chooseFontBtn = document.getElementById("choose-font-btn");
const fontUploadStatus = document.getElementById("font-upload-status");
const localFontsList = document.getElementById("local-fonts-list"); const localFontsList = document.getElementById("local-fonts-list");
// Modal logic for font deletion // Modal logic for font deletion
@ -341,9 +460,9 @@ document.addEventListener("DOMContentLoaded", async () => {
}; };
} }
if (themeYaml.favicon && themeYaml.favicon.path) { if (loadedConfig.favicon && loadedConfig.favicon.path) {
faviconInput.value = themeYaml.favicon.path; faviconInput.value = loadedConfig.favicon.path;
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`); updateFaviconPreview(`/themes/${themeInfo.theme_name}/${loadedConfig.favicon.path}?t=${Date.now()}`);
} else { } else {
updateFaviconPreview(""); updateFaviconPreview("");
} }
@ -367,6 +486,7 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts); googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts); renderGoogleFonts(googleFonts);
refreshFontDropdowns(); refreshFontDropdowns();
updateSectionStatus("google-fonts", loadedConfig);
}); });
} }
@ -396,6 +516,7 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts); googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts); renderGoogleFonts(googleFonts);
refreshFontDropdowns(); refreshFontDropdowns();
updateSectionStatus("google-fonts", loadedConfig);
} }
}, true); }, true);
@ -414,54 +535,72 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts); googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts); renderGoogleFonts(googleFonts);
refreshFontDropdowns(); refreshFontDropdowns();
updateSectionStatus("google-fonts", loadedConfig);
} }
}); });
} }
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => { // --- Section status listeners ---
e.preventDefault(); [
showLoader("Saving theme..."); { form: document.getElementById("colors-form"), section: "colors" },
const data = {}; { form: document.getElementById("google-fonts-form"), section: "google-fonts" },
data.colors = { { form: document.getElementById("fonts-form"), section: "fonts" },
primary: document.getElementById("color-primary-text").value, { form: document.getElementById("favicon-form"), section: "favicon" }
primary_dark: document.getElementById("color-primary-dark-text").value, ].forEach(({ form, section }) => {
secondary: document.getElementById("color-secondary-text").value, if (!form) return;
accent: document.getElementById("color-accent-text").value, form.addEventListener("input", () => updateSectionStatus(section, loadedConfig));
text_dark: document.getElementById("color-text-dark-text").value, form.addEventListener("change", () => updateSectionStatus(section, loadedConfig));
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 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));
}); });

View File

@ -4,178 +4,204 @@
{% block content %} {% block content %}
<h1>Edit Site Info</h1> <h1>Edit Site Info</h1>
<form id="site-info-form">
<button type="submit" class="top-save">Save</button> <!-- Info Section -->
<!-- Info Section --> <form id="info-form" autocomplete="off">
<fieldset> <fieldset id="info-section">
<h2>Info</h2> <h2>Info</h2>
<p>Set the basic information for your site and SEO</p> <p class="section-status"></p>
<div class="fields"> <p>Set the basic information for your site and SEO</p>
<div class="input-field"> <div class="fields">
<label>Title</label> <div class="input-field">
<input type="text" name="info.title" placeholder="Your site title" required> <label>Title</label>
</div> <input type="text" name="info.title" placeholder="Your site title" required>
<div class="input-field">
<label>Subtitle</label>
<input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
</div>
<div class="input-field">
<label>Description</label>
<input type="text" name="info.description" placeholder="Your site description" required>
</div>
<div class="input-field">
<label>Canonical URL</label>
<input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
</div>
<div class="input-field">
<label>Keywords (comma separated)</label>
<input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
</div>
<div class="input-field">
<label>Author</label>
<input type="text" name="info.author" placeholder="Your Name" required>
</div>
</div>
</fieldset>
<!-- Social Section -->
<fieldset>
<h2>Social</h2>
<p>Set your social media links and thumbnail for link sharing</p>
<div class="fields">
<div class="input-field">
<label>Instagram URL</label>
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
<label class="thumbnail-form-label">Thumbnail</label>
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
<div class="thumbnail-form">
<input type="hidden" name="social.thumbnail" id="social-thumbnail">
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
</div>
</div>
</div>
</fieldset>
<!-- Menu Section -->
<fieldset>
<h2>Menu</h2>
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
<div class="fields">
<div class="input-field" style="flex: 1 1 100%;">
<div id="menu-items-list"></div>
<button type="button" id="add-menu-item">+ Add menu item</button>
</div>
</div>
</fieldset>
<!-- Footer Section -->
<fieldset>
<h2>Footer</h2>
<p>Set your copyright informations and legal link name</p>
<div class="fields">
<div class="input-field">
<label>Copyright</label>
<input type="text" name="footer.copyright" required>
</div>
<div class="input-field">
<label>Legal Label</label>
<input type="text" name="footer.legal_label" re>
</div>
</div>
</fieldset>
<!-- Legals Section -->
<fieldset>
<h2>Legals</h2>
<p>Set your legal informations</p>
<div class="fields">
<div class="input-field">
<label>Hoster Name</label>
<input type="text" name="legals.hoster_name" placeholder="Name" required>
</div>
<div class="input-field">
<label>Hoster Address</label>
<input type="text" name="legals.hoster_address" placeholder="Street, Postal Code, City, Country" required>
</div>
<div class="input-field">
<label>Hoster Contact</label>
<input type="text" name="legals.hoster_contact" placeholder="Email or/and Phone" required>
</div>
<div class="input-field" style="flex: 1 1 100%;">
<label>Intellectual Property</label>
<div id="ip-list"></div>
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
</div>
</div>
</fieldset>
<!-- Build Section -->
<fieldset>
<h2>Build</h2>
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
<div class="fields">
<div class="input-field">
<label>Theme</label>
<select name="build.theme" id="theme-select" required></select>
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
<label class="thumbnail-form-label">Images processing</label>
<p>If checked, images will be converted for web and resized to fit the theme</p>
<label>
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
Convert images
</label>
<label>
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
Resize images
</label>
</div>
</div>
</fieldset>
<button type="submit">Save</button>
</form>
<!-- Stepper -->
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a class="step-active" href="/site-info">Configure site info</a></li>
<div></div>
<li><a href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div> </div>
</div> <div class="input-field">
<label>Subtitle</label>
<input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
</div>
<div class="input-field">
<label>Description</label>
<input type="text" name="info.description" placeholder="Your site description" required>
</div>
<div class="input-field">
<label>Canonical URL</label>
<input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
</div>
<div class="input-field">
<label>Keywords (comma separated)</label>
<input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
</div>
<div class="input-field">
<label>Author</label>
<input type="text" name="info.author" placeholder="Your Name" required>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="info">Save</button>
</fieldset>
</form>
<!-- Social Section -->
<form id="social-form" autocomplete="off">
<fieldset id="social-section">
<h2>Social</h2>
<p>Set your social media links and thumbnail for link sharing</p>
<p class="section-status"></p>
<div class="fields">
<div class="input-field">
<label>Instagram URL</label>
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
<label class="thumbnail-form-label">Thumbnail</label>
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
<div class="thumbnail-form">
<input type="hidden" name="social.thumbnail" id="social-thumbnail" required>
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
</div>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="social">Save</button>
</fieldset>
</form>
<!-- Menu Section -->
<form id="menu-form" autocomplete="off">
<fieldset id="menu-section">
<h2>Menu</h2>
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
<p class="section-status"></p>
<div class="fields">
<div class="input-field" style="flex: 1 1 100%;">
<div id="menu-items-list"></div>
<button type="button" id="add-menu-item">+ Add menu item</button>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="menu">Save</button>
</fieldset>
</form>
<!-- Footer Section -->
<form id="footer-form" autocomplete="off">
<fieldset id="footer-section">
<h2>Footer</h2>
<p>Set your copyright informations and legal link name</p>
<p class="section-status"></p>
<div class="fields">
<div class="input-field">
<label>Copyright</label>
<input type="text" name="footer.copyright" required>
</div>
<div class="input-field">
<label>Legal Label</label>
<input type="text" name="footer.legal_label" required>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="footer">Save</button>
</fieldset>
</form>
<!-- Legals Section -->
<form id="legals-form" autocomplete="off">
<fieldset id="legals-section">
<h2>Legals</h2>
<p>Set your legal informations</p>
<p class="section-status"></p>
<div class="fields">
<div class="input-field">
<label>Hoster Name</label>
<input type="text" name="legals.hoster_name" placeholder="Name" required>
</div>
<div class="input-field">
<label>Hoster Address</label>
<input type="text" name="legals.hoster_address" placeholder="Street, Postal Code, City, Country" required>
</div>
<div class="input-field">
<label>Hoster Contact</label>
<input type="text" name="legals.hoster_contact" placeholder="Email or/and Phone" required>
</div>
<div class="input-field" style="flex: 1 1 100%;">
<label>Intellectual Property</label>
<div id="ip-list"></div>
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="legals">Save</button>
</fieldset>
</form>
<!-- Build Section -->
<form id="build-form" autocomplete="off">
<fieldset id="build-section">
<h2>Build</h2>
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
<p class="section-status"></p>
<div class="fields">
<div class="input-field">
<label>Theme</label>
<select name="build.theme" id="theme-select" required></select>
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
<label class="thumbnail-form-label">Images processing</label>
<p>If checked, images will be converted for web and resized to fit the theme</p>
<label>
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
Convert images
</label>
<label>
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
Resize images
</label>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="build">Save</button>
</fieldset>
</form>
<!-- Stepper -->
<div class="section">
<h2>Steps</h2>
<p>Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a class="step-active" href="/site-info">Configure site info</a></li>
<div></div>
<li><a href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
<!-- Delete thumbnail confirmation modal--> <!-- Delete thumbnail confirmation modal-->
<div class="content-inner"> <div class="content-inner">
<div id="delete-modal" class="modal" style="display:none;"> <div id="delete-modal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">
<span id="delete-modal-close" class="modal-close">&times;</span> <span id="delete-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3> <h3>Confirm Deletion</h3>
<p id="delete-modal-text">Are you sure you want to remove this thumbnail?</p> <p id="delete-modal-text">Are you sure you want to remove this thumbnail?</p>
<div class="modal-actions"> <div class="modal-actions">
<button id="delete-modal-confirm" class="modal-btn danger">Remove</button> <button id="delete-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-modal-cancel" class="modal-btn">Cancel</button> <button id="delete-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Delete theme confirmation modal --> </div>
<div class="content-inner"> <!-- Delete theme confirmation modal -->
<div id="delete-theme-modal" class="modal" style="display:none;"> <div class="content-inner">
<div class="modal-content"> <div id="delete-theme-modal" class="modal" style="display:none;">
<span id="delete-theme-modal-close" class="modal-close">&times;</span> <div class="modal-content">
<h3>Confirm Theme Deletion</h3> <span id="delete-theme-modal-close" class="modal-close">&times;</span>
<p id="delete-theme-modal-text">Are you sure you want to remove this theme?</p> <h3>Confirm Theme Deletion</h3>
<div class="modal-actions"> <p id="delete-theme-modal-text">Are you sure you want to remove this theme?</p>
<button id="delete-theme-modal-confirm" class="modal-btn danger">Remove</button> <div class="modal-actions">
<button id="delete-theme-modal-cancel" class="modal-btn">Cancel</button> <button id="delete-theme-modal-confirm" class="modal-btn danger">Remove</button>
</div> <button id="delete-theme-modal-cancel" class="modal-btn">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -610,9 +610,9 @@ label {
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
#site-info-form input, #theme-editor-form input, form input,
#site-info-form textarea, #theme-editor-form textarea, form textarea,
#site-info-form select, #theme-editor-form select { form select {
background: #1f2223; background: #1f2223;
color: #fff; color: #fff;
border: 1px solid #585858; border: 1px solid #585858;
@ -626,10 +626,6 @@ label {
box-shadow: 0 2px 8px rgba(0,0,0,0.07); box-shadow: 0 2px 8px rgba(0,0,0,0.07);
} }
#site-info-form button[type="submit"].top-save, #theme-editor-form button[type="submit"].top-save {
margin-bottom: 0;
}
#theme-editor-form input, #theme-editor-form textarea,#theme-editor-form select { #theme-editor-form input, #theme-editor-form textarea,#theme-editor-form select {
margin-bottom: 18px; margin-bottom: 18px;
} }
@ -642,25 +638,20 @@ label {
margin-bottom: 25px; margin-bottom: 25px;
} }
#site-info-form input::placeholder, form input::placeholder,
#theme-editor-form input::placeholder, form textarea::placeholder {
#site-info-form textarea::placeholder,
#theme-editor-form textarea::placeholder {
color: #585858; color: #585858;
font-style: italic; font-style: italic;
} }
#site-info-form input:focus, form input:focus,
#theme-editor-form input:focus, form textarea:focus,
#site-info-form textarea:focus, form select:focus {
#theme-editor-form textarea:focus,
#site-info-form select:focus,
#theme-editor-form select:focus {
border-color: #585858; border-color: #585858;
background: #161616; background: #161616;
} }
#site-info-form textarea,
#theme-editor-form textarea { form textarea {
min-height: 60px; min-height: 60px;
resize: vertical; resize: vertical;
} }
@ -679,7 +670,7 @@ img#thumbnail-preview {
border: 1px solid #585858; border: 1px solid #585858;
} }
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] { form button[type="submit"] {
background: linear-gradient(135deg, #26c4ff, #016074); background: linear-gradient(135deg, #26c4ff, #016074);
color: #fff; color: #fff;
font-weight: 700; font-weight: 700;
@ -693,11 +684,11 @@ img#thumbnail-preview {
transition: background 0.2s; transition: background 0.2s;
} }
#site-info-form button[type="submit"]:hover, #theme-editor-form button[type="submit"]:hover { form button[type="submit"]:hover {
background: linear-gradient(135deg, #72d9ff, #26657e); background: linear-gradient(135deg, #72d9ff, #26657e);
} }
#site-info-form button[type="button"], #theme-editor-form button[type="button"] { form button[type="button"] {
background: #00000000; background: #00000000;
color: #fff; color: #fff;
border: none; border: none;
@ -710,34 +701,36 @@ img#thumbnail-preview {
border: 1px solid #585858; border: 1px solid #585858;
} }
#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover { form button[type="button"]:hover{
background: #2d2d2d; background: #2d2d2d;
color: #fff; color: #fff;
} }
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph { form button.remove-menu-item,
form button.remove-ip-paragraph {
margin-top: 0px; margin-top: 0px;
margin-bottom: 4px; margin-bottom: 4px;
border-radius: 30px; border-radius: 30px;
background: #2d2d2d; background: #2d2d2d;
} }
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover { form button.remove-menu-item:hover,
form button.remove-ip-paragraph:hover {
background: rgb(121, 26, 19); background: rgb(121, 26, 19);
} }
#site-info-form button.remove-btn, #theme-editor-form button.remove-btn { form button.remove-btn {
border-radius: 30px; border-radius: 30px;
background: #2d2d2d; background: #2d2d2d;
} }
#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover { form button.remove-btn:hover {
background: rgb(121, 26, 19); background: rgb(121, 26, 19);
} }
#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label { form .thumbnail-form-label {
margin-top: 10px; margin-top: 10px;
} }
@ -799,6 +792,10 @@ fieldset p, .section p {
margin-top: 16px; margin-top: 16px;
} }
.section-status {
font-style: normal;
}
/* --- Stepper --- */ /* --- Stepper --- */
#stepper { #stepper {

View File

@ -4,178 +4,193 @@
{% block content %} {% block content %}
<h1>Edit Theme</h1> <h1>Edit Theme</h1>
<!-- Show current theme --> <!-- Show current theme -->
<div class="theme-info"> <div class="theme-info">
<strong>Current theme:</strong> <span id="current-theme"></span> <strong>Current theme:</strong> <span id="current-theme"></span>
</div> </div>
<form id="theme-editor-form"> <div id="theme-editor-form">
<button type="submit" class="top-save">Save</button> <!-- Colors Section -->
<!-- Colors Section --> <form id="colors-form" autocomplete="off">
<fieldset id="color-picker"> <fieldset id="color-picker">
<h2>Colors</h2> <h2>Colors</h2>
<p>Set the color values for your theme</p> <p class="section-status"></p>
<div class="fields"> <p>Set the color values for your theme</p>
<!-- Example for one color field, repeat for all --> <div class="fields">
<div class="input-field"> <div class="input-field">
<label>Primary</label> <label>Primary</label>
<div class="fields color-fields"> <div class="fields color-fields">
<button type="button" id="color-primary-btn" class="color-btn"></button> <button type="button" id="color-primary-btn" class="color-btn"></button>
<input type="color" name="colors.primary" id="color-primary" class="color-input"> <input type="color" name="colors.primary" id="color-primary" class="color-input" required>
<input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;"> <input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;" required>
</div>
</div>
<div class="input-field">
<label>Primary Dark</label>
<div class="fields color-fields">
<button type="button" id="color-primary-dark-btn" class="color-btn"></button>
<input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input">
<input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Secondary</label>
<div class="fields color-fields">
<button type="button" id="color-secondary-btn" class="color-btn"></button>
<input type="color" name="colors.secondary" id="color-secondary" class="color-input">
<input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Accent</label>
<div class="fields color-fields">
<button type="button" id="color-accent-btn" class="color-btn"></button>
<input type="color" name="colors.accent" id="color-accent" class="color-input">
<input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Text Dark</label>
<div class="fields color-fields">
<button type="button" id="color-text-dark-btn" class="color-btn"></button>
<input type="color" name="colors.text_dark" id="color-text-dark" class="color-input">
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Background</label>
<div class="fields color-fields">
<button type="button" id="color-background-btn" class="color-btn"></button>
<input type="color" name="colors.background" id="color-background" class="color-input">
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Browser Color</label>
<div class="fields color-fields">
<button type="button" id="color-browser-color-btn" class="color-btn"></button>
<input type="color" name="colors.browser_color" id="color-browser-color" class="color-input">
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
</div>
</div> </div>
</div> </div>
</fieldset> <div class="input-field">
<!-- Google Fonts Section --> <label>Primary Dark</label>
<fieldset> <div class="fields color-fields">
<h2>Google Fonts</h2> <button type="button" id="color-primary-dark-btn" class="color-btn"></button>
<p>Add Google Fonts to your theme</p> <input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input" required>
<div class="fields" id="google-fonts-fields"> <input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;" required>
<!-- JS will render font family and weights inputs here -->
</div>
<button type="button" id="add-google-font">Add Google Font</button>
</fieldset>
<!-- Custom Font Upload Section -->
<fieldset>
<h2>Upload Custom Font</h2>
<p>Supported formats: .woff, .woff2</p>
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
<div id="local-fonts-list" class="font-list"></div>
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
</fieldset>
<!-- Fonts Section -->
<fieldset>
<h2>Fonts</h2>
<p>Select where to apply your fonts</p>
<div class="fields">
<div class="input-field">
<label>Primary Font</label>
<select name="fonts.primary.name" id="font-primary"></select>
<label>Fallback</label>
<select name="fonts.primary.fallback" id="font-primary-fallback">
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div>
<div class="input-field">
<label>Secondary Font</label>
<select name="fonts.secondary.name" id="font-secondary"></select>
<label>Fallback</label>
<select name="fonts.secondary.fallback" id="font-secondary-fallback">
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div> </div>
</div> </div>
</fieldset> <div class="input-field">
<!-- Favicon Section --> <label>Secondary</label>
<fieldset> <div class="fields color-fields">
<h2>Favicon</h2> <button type="button" id="color-secondary-btn" class="color-btn"></button>
<p>Supported formats: .png, .jpg, .jpeg</p> <input type="color" name="colors.secondary" id="color-secondary" class="color-input" required>
<div class="fields"> <input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;" required>
<div class="input-field"> </div>
<label>Favicon Path</label> </div>
<input type="text" name="favicon.path" id="favicon-path" readonly> <div class="input-field">
<input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;"> <label>Accent</label>
<button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button> <div class="fields color-fields">
<div class="favicon-form"> <button type="button" id="color-accent-btn" class="color-btn"></button>
<img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;"> <input type="color" name="colors.accent" id="color-accent" class="color-input" required>
<button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button> <input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;" required>
</div> </div>
</div>
<div class="input-field">
<label>Text Dark</label>
<div class="fields color-fields">
<button type="button" id="color-text-dark-btn" class="color-btn"></button>
<input type="color" name="colors.text_dark" id="color-text-dark" class="color-input" required>
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;" required>
</div>
</div>
<div class="input-field">
<label>Background</label>
<div class="fields color-fields">
<button type="button" id="color-background-btn" class="color-btn"></button>
<input type="color" name="colors.background" id="color-background" class="color-input" required>
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;" required>
</div>
</div>
<div class="input-field">
<label>Browser Color</label>
<div class="fields color-fields">
<button type="button" id="color-browser-color-btn" class="color-btn"></button>
<input type="color" name="colors.browser_color" id="color-browser-color" class="color-input" required>
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;" required>
</div> </div>
</div> </div>
</fieldset>
<button type="submit">Save</button>
</form>
<!-- Stepper -->
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a href="/site-info">Configure site info</a></li>
<div></div>
<li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
</div>
</div>
<!-- Delete confirmation modal for favicon -->
<div id="delete-favicon-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-favicon-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
<div class="modal-actions">
<button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
</div> </div>
</div> <button type="submit" class="section-save-btn" data-section="colors">Save</button>
</div> </fieldset>
<!-- Delete confirmation modal for font --> </form>
<div id="delete-font-modal" class="modal" style="display:none;"> <!-- Google Fonts Section -->
<div class="modal-content"> <form id="google-fonts-form" autocomplete="off">
<span id="delete-font-modal-close" class="modal-close">&times;</span> <fieldset>
<h3>Confirm Deletion</h3> <h2>Google Fonts</h2>
<p id="delete-font-modal-text">Are you sure you want to remove this font?</p> <p class="section-status"></p>
<div class="modal-actions"> <p>Add Google Fonts to your theme</p>
<button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button> <div class="fields" id="google-fonts-fields">
<button id="delete-font-modal-cancel" class="modal-btn">Cancel</button> <!-- JS will render font family and weights inputs here -->
</div> </div>
<button type="button" id="add-google-font">Add Google Font</button>
<button type="submit" class="section-save-btn" data-section="google-fonts">Save</button>
</fieldset>
</form>
<!-- Custom Font Upload Section -->
<form id="font-upload-form" autocomplete="off">
<fieldset>
<h2>Upload Custom Font</h2>
<p class="section-status"></p>
<p>Supported formats: .woff, .woff2</p>
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
<div id="local-fonts-list" class="font-list"></div>
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
<!-- Save button not needed for upload-only section -->
</fieldset>
</form>
<!-- Fonts Section -->
<form id="fonts-form" autocomplete="off">
<fieldset>
<h2>Fonts</h2>
<p class="section-status"></p>
<p>Select where to apply your fonts</p>
<div class="fields">
<div class="input-field">
<label>Primary Font</label>
<select name="fonts.primary.name" id="font-primary" required></select>
<label>Fallback</label>
<select name="fonts.primary.fallback" id="font-primary-fallback" required>
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div>
<div class="input-field">
<label>Secondary Font</label>
<select name="fonts.secondary.name" id="font-secondary" required></select>
<label>Fallback</label>
<select name="fonts.secondary.fallback" id="font-secondary-fallback" required>
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="fonts">Save</button>
</fieldset>
</form>
<!-- Favicon Section -->
<form id="favicon-form" autocomplete="off">
<fieldset>
<h2>Favicon</h2>
<p class="section-status"></p>
<p>Supported formats: .png, .jpg, .jpeg</p>
<div class="fields">
<div class="input-field">
<label>Favicon Path</label>
<input type="text" name="favicon.path" id="favicon-path" readonly>
<input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;">
<button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button>
<div class="favicon-form">
<img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;">
<button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
</div>
</div>
</div>
<button type="submit" class="section-save-btn" data-section="favicon">Save</button>
</fieldset>
</form>
</div>
<!-- Stepper -->
<div class="section">
<h2>Steps</h2>
<p>Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a href="/site-info">Configure site info</a></li>
<div></div>
<li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
<!-- Delete confirmation modal for favicon -->
<div id="delete-favicon-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-favicon-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
<div class="modal-actions">
<button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
</div> </div>
</div> </div>
</div>
<!-- Delete confirmation modal for font -->
<div id="delete-font-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-font-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-font-modal-text">Are you sure you want to remove this font?</p>
<div class="modal-actions">
<button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
{% endblock %} {% endblock %}