1 Commits

Author SHA1 Message Date
147ad1545c Merge pull request 'v2.1 - The clearer, the faster' (#21) from Beta-2.1 into main
Reviewed-on: #21
2025-09-04 00:28:44 +02:00
8 changed files with 539 additions and 276 deletions

View File

@ -50,7 +50,7 @@ img, tag {
.hero-background { .hero-background {
max-width: 90%; max-width: 90%;
margin: 0 auto; margin: auto;
} }
.back-button { .back-button {

View File

@ -6,12 +6,14 @@ colors:
primary_dark: '#005384' primary_dark: '#005384'
secondary: '#00B0F0' secondary: '#00B0F0'
text_dark: '#616161' text_dark: '#616161'
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

@ -10,28 +10,24 @@ copy_default_config() {
mkdir -p /app/config mkdir -p /app/config
fi fi
echo "[~] Checking if default config files/folders need to be copied..." echo "[~] Checking if default config files need to be copied..."
files_copied=false files_copied=false
# Recursively check all files and folders in /app/default for file in /app/default/*; do
while IFS= read -r src; do filename=$(basename "$file")
relpath="${src#/app/default/}" target="/app/config/$filename"
target="/app/config/$relpath"
if [ ! -e "$target" ]; then if [ ! -e "$target" ]; then
echo "[→] Copying: $relpath" echo "[→] Copying default config file: $filename"
if [ -d "$src" ]; then cp -r "$file" "$target"
cp -r "$src" "$target"
else
cp "$src" "$target"
fi
files_copied=true files_copied=true
fi fi
done < <(find /app/default -mindepth 1) done
if [ "$files_copied" = true ]; then if [ "$files_copied" = true ]; then
echo "[✓] Default configuration files/folders copied successfully." echo "[✓] Default configuration files copied successfully."
else else
echo "[✓] No default files/folders needed to be copied." echo "[✓] No default files needed to be copied."
fi fi
} }

View File

@ -415,8 +415,7 @@ def upload_font():
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts" fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
fonts_dir.mkdir(parents=True, exist_ok=True) fonts_dir.mkdir(parents=True, exist_ok=True)
file.save(fonts_dir / file.filename) file.save(fonts_dir / file.filename)
font_basename = Path(file.filename).stem return jsonify({"status": "ok", "filename": file.filename})
return jsonify({"status": "ok", "filename": font_basename})
@app.route("/api/font/remove", methods=["POST"]) @app.route("/api/font/remove", methods=["POST"])
def remove_font(): def remove_font():

View File

@ -1,29 +1,19 @@
// --- Arrays to store gallery and hero images ---
let galleryImages = []; let galleryImages = [];
let heroImages = []; let heroImages = [];
let allTags = []; let allTags = []; // global tag list
let showOnlyUntagged = false; let showOnlyUntagged = false;
// --- Fade-in helper ---
function applyFadeInImages(container) {
container.querySelectorAll("img.fade-in-img").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);
});
}
});
}
// --- Load images from server on page load --- // --- Load images from server on page load ---
async function loadData() { async function loadData() {
try { try {
galleryImages = await (await fetch('/api/gallery')).json(); const galleryRes = await fetch('/api/gallery');
galleryImages = await galleryRes.json();
updateAllTags(); updateAllTags();
renderGallery(); renderGallery();
heroImages = await (await fetch('/api/hero')).json();
const heroRes = await fetch('/api/hero');
heroImages = await heroRes.json();
renderHero(); renderHero();
} catch(err) { } catch(err) {
console.error(err); console.error(err);
@ -35,44 +25,31 @@ async function loadData() {
function updateAllTags() { function updateAllTags() {
allTags = []; allTags = [];
galleryImages.forEach(img => { galleryImages.forEach(img => {
(img.tags || []).forEach(t => { if (img.tags) img.tags.forEach(t => {
if (!allTags.includes(t)) allTags.push(t); if (!allTags.includes(t)) allTags.push(t);
}); });
}); });
} }
// --- Helper: update count and button visibility ---
function updateCountAndButtons(prefix, count) {
const countTop = document.getElementById(`${prefix}-count`);
const countBottom = document.getElementById(`${prefix}-count-bottom`);
if (countTop) countTop.innerHTML = `<p>${count} photos</p>`;
if (countBottom) countBottom.innerHTML = `<p>${count} photos</p>`;
const removeAllBtn = document.getElementById(`remove-all-${prefix}`);
const removeAllBtnBottom = document.getElementById(`remove-all-${prefix}-bottom`);
if (removeAllBtn) removeAllBtn.style.display = count > 0 ? 'inline-block' : 'none';
if (removeAllBtnBottom) removeAllBtnBottom.style.display = count > 0 ? 'inline-block' : 'none';
const bottomUpload = document.getElementById(`bottom-${prefix}-upload`);
if (bottomUpload) bottomUpload.style.display = count > 0 ? 'flex' : 'none';
}
// --- Render gallery images with tags and delete buttons --- // --- Render gallery images with tags and delete buttons ---
function renderGallery() { function renderGallery() {
const container = document.getElementById('gallery'); const container = document.getElementById('gallery');
container.innerHTML = ''; container.innerHTML = '';
let imagesToShow = showOnlyUntagged
? galleryImages.filter(img => !img.tags || img.tags.length === 0) // Filter images if toggle is on
: galleryImages; let imagesToShow = galleryImages;
if (showOnlyUntagged) {
imagesToShow = galleryImages.filter(img => !img.tags || img.tags.length === 0);
}
imagesToShow.forEach((img) => { imagesToShow.forEach((img) => {
const i = galleryImages.indexOf(img); const i = galleryImages.indexOf(img); // Use real index for tag/delete actions
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'photo flex-item flex-column'; div.className = 'photo flex-item flex-column';
div.innerHTML = ` div.innerHTML = `
<div class="flex-item"> <div class="flex-item">
<img class="fade-in-img" src="/photos/${img.src}"> <img class="fade-in-img" src="/photos/${img.src}">
</div> </div>
<div class="tags-display" data-index="${i}"></div> <div class="tags-display" data-index="${i}"></div>
<div class="flex-item flex-full"> <div class="flex-item flex-full">
<div class="flex-item flex-end"> <div class="flex-item flex-end">
@ -82,17 +59,62 @@ function renderGallery() {
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);
renderTags(i, img.tags || []); renderTags(i, img.tags || []);
}); });
updateCountAndButtons('gallery', imagesToShow.length); // Update gallery count (top)
applyFadeInImages(container); const galleryCount = document.getElementById('gallery-count');
if (galleryCount) {
galleryCount.innerHTML = `<p>${imagesToShow.length} photos</p>`;
}
// Update gallery count (bottom)
const galleryCountBottom = document.getElementById('gallery-count-bottom');
if (galleryCountBottom) {
galleryCountBottom.innerHTML = `<p>${imagesToShow.length} photos</p>`;
}
// Show/hide Remove All button (top)
const removeAllBtn = document.getElementById('remove-all-gallery');
if (removeAllBtn) {
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 --- // --- Render tags for a single image ---
function renderTags(imgIndex, tags) { function renderTags(imgIndex, tags) {
const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`); const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`); const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
tagsDisplay.innerHTML = ''; tagsDisplay.innerHTML = '';
inputContainer.innerHTML = ''; inputContainer.innerHTML = '';
@ -100,6 +122,7 @@ function renderTags(imgIndex, tags) {
const span = document.createElement('span'); const span = document.createElement('span');
span.className = 'tag'; span.className = 'tag';
span.textContent = tag; span.textContent = tag;
const remove = document.createElement('span'); const remove = document.createElement('span');
remove.className = 'remove-tag'; remove.className = 'remove-tag';
remove.textContent = '×'; remove.textContent = '×';
@ -108,6 +131,7 @@ function renderTags(imgIndex, tags) {
updateTags(imgIndex, tags); updateTags(imgIndex, tags);
renderTags(imgIndex, tags); renderTags(imgIndex, tags);
}; };
span.appendChild(remove); span.appendChild(remove);
tagsDisplay.appendChild(span); tagsDisplay.appendChild(span);
}); });
@ -117,10 +141,11 @@ function renderTags(imgIndex, tags) {
input.placeholder = 'Add tag...'; input.placeholder = 'Add tag...';
inputContainer.appendChild(input); inputContainer.appendChild(input);
// --- Validate button ---
const validateBtn = document.createElement('button'); const validateBtn = document.createElement('button');
validateBtn.textContent = '✔️'; validateBtn.textContent = '✔️';
validateBtn.className = 'validate-tag-btn'; validateBtn.className = 'validate-tag-btn';
validateBtn.style.display = 'none'; validateBtn.style.display = 'none'; // hidden by default
validateBtn.style.marginLeft = '4px'; validateBtn.style.marginLeft = '4px';
inputContainer.appendChild(validateBtn); inputContainer.appendChild(validateBtn);
@ -140,20 +165,30 @@ function renderTags(imgIndex, tags) {
const updateSuggestions = () => { const updateSuggestions = () => {
const value = input.value.toLowerCase(); const value = input.value.toLowerCase();
const allTagsFlat = galleryImages.flatMap(img => img.tags || []); const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
const tagCount = {}; const tagCount = {};
allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1); allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
const allTagsSorted = Object.keys(tagCount).sort((a, b) => tagCount[b] - tagCount[a]);
const allTagsSorted = Object.keys(tagCount)
.sort((a, b) => tagCount[b] - tagCount[a]);
const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t)); const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
suggestionBox.innerHTML = ''; suggestionBox.innerHTML = '';
selectedIndex = -1; selectedIndex = -1;
if (suggestions.length) { if (suggestions.length) {
suggestionBox.style.display = 'block'; suggestionBox.style.display = 'block';
suggestions.forEach((s, idx) => { suggestions.forEach((s, idx) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.style.fontStyle = 'italic'; li.style.fontStyle = 'italic';
li.style.textAlign = 'left'; li.style.textAlign = 'left';
li.innerHTML = `<b>${s.substring(0, input.value.length)}</b>${s.substring(input.value.length)}`;
const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
const rest = s.substring(input.value.length);
li.innerHTML = boldPart + rest;
li.addEventListener('mousedown', (e) => { li.addEventListener('mousedown', (e) => {
e.preventDefault(); e.preventDefault();
addTag(s); addTag(s);
@ -161,6 +196,7 @@ function renderTags(imgIndex, tags) {
input.focus(); input.focus();
updateSuggestions(); updateSuggestions();
}); });
li.onmouseover = () => selectedIndex = idx; li.onmouseover = () => selectedIndex = idx;
suggestionBox.appendChild(li); suggestionBox.appendChild(li);
}); });
@ -177,6 +213,7 @@ function renderTags(imgIndex, tags) {
updateSuggestions(); updateSuggestions();
validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none'; validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
}); });
input.addEventListener('keydown', (e) => { input.addEventListener('keydown', (e) => {
const items = suggestionBox.querySelectorAll('li'); const items = suggestionBox.querySelectorAll('li');
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
@ -207,22 +244,27 @@ function renderTags(imgIndex, tags) {
validateBtn.style.display = 'none'; validateBtn.style.display = 'none';
} }
}); });
input.addEventListener('blur', () => { input.addEventListener('blur', () => {
suggestionBox.style.display = 'none'; setTimeout(() => {
input.value = ''; suggestionBox.style.display = 'none';
validateBtn.style.display = 'none'; input.value = '';
validateBtn.style.display = 'none';
}, 150);
}); });
validateBtn.addEventListener('mousedown', (e) => {
e.preventDefault(); // --- Validate button action ---
validateBtn.onclick = () => {
if (input.value.trim()) { if (input.value.trim()) {
addTag(input.value.trim()); addTag(input.value.trim());
input.value = ''; input.value = '';
updateSuggestions(); updateSuggestions();
validateBtn.style.display = 'none'; validateBtn.style.display = 'none';
} }
}); };
input.focus();
updateSuggestions(); updateSuggestions();
if (!input.value.trim()) suggestionBox.style.display = 'none';
} }
// --- Update tags in galleryImages array --- // --- Update tags in galleryImages array ---
@ -250,8 +292,36 @@ function renderHero() {
`; `;
container.appendChild(div); container.appendChild(div);
}); });
updateCountAndButtons('hero', heroImages.length);
applyFadeInImages(container); // Update hero count (top)
const heroCount = document.getElementById('hero-count');
if (heroCount) {
heroCount.innerHTML = `<p>${heroImages.length} photos</p>`;
}
// Update hero count (bottom)
const heroCountBottom = document.getElementById('hero-count-bottom');
if (heroCountBottom) {
heroCountBottom.innerHTML = `<p>${heroImages.length} photos</p>`;
}
// 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 --- // --- Save gallery to server ---
@ -297,27 +367,33 @@ async function refreshHero() {
function showToast(message, type = "success", duration = 3000) { function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container"); const container = document.getElementById("toast-container");
if (!container) return; if (!container) return;
const toast = document.createElement("div"); const toast = document.createElement("div");
toast.className = `toast ${type}`; toast.className = `toast ${type}`;
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show")); requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => { setTimeout(() => {
toast.classList.remove("show"); toast.classList.remove("show");
toast.addEventListener("transitionend", () => toast.remove()); toast.addEventListener("transitionend", () => toast.remove());
}, duration); }, duration);
} }
let pendingDelete = null; let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
// --- Show delete confirmation modal --- // --- Show delete confirmation modal ---
function showDeleteModal(type, index = null) { function showDeleteModal(type, index = null) {
pendingDelete = { type, index }; pendingDelete = { type, index };
const modalText = document.getElementById('delete-modal-text'); const modalText = document.getElementById('delete-modal-text');
modalText.textContent = if (type === 'gallery-all') {
type === 'gallery-all' ? "Are you sure you want to delete ALL gallery images?" modalText.textContent = "Are you sure you want to delete ALL gallery images?";
: type === 'hero-all' ? "Are you sure you want to delete ALL hero images?" } else if (type === 'hero-all') {
: "Are you sure you want to delete this image?"; modalText.textContent = "Are you sure you want to delete ALL hero images?";
} else {
modalText.textContent = "Are you sure you want to delete this image?";
}
document.getElementById('delete-modal').style.display = 'flex'; document.getElementById('delete-modal').style.display = 'flex';
} }
@ -330,10 +406,15 @@ function hideDeleteModal() {
// --- Confirm deletion --- // --- Confirm deletion ---
async function confirmDelete() { async function confirmDelete() {
if (!pendingDelete) return; if (!pendingDelete) return;
if (pendingDelete.type === 'gallery') await actuallyDeleteGalleryImage(pendingDelete.index); if (pendingDelete.type === 'gallery') {
else if (pendingDelete.type === 'hero') await actuallyDeleteHeroImage(pendingDelete.index); await actuallyDeleteGalleryImage(pendingDelete.index);
else if (pendingDelete.type === 'gallery-all') await actuallyDeleteAllGalleryImages(); } else if (pendingDelete.type === 'hero') {
else if (pendingDelete.type === 'hero-all') await actuallyDeleteAllHeroImages(); await actuallyDeleteHeroImage(pendingDelete.index);
} else if (pendingDelete.type === 'gallery-all') {
await actuallyDeleteAllGalleryImages();
} else if (pendingDelete.type === 'hero-all') {
await actuallyDeleteAllHeroImages();
}
hideDeleteModal(); hideDeleteModal();
} }
@ -415,36 +496,41 @@ async function actuallyDeleteAllHeroImages() {
// --- Modal event listeners and bulk delete buttons --- // --- Modal event listeners and bulk delete buttons ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
['delete-modal-close', 'delete-modal-cancel'].forEach(id => { document.getElementById('delete-modal-close').onclick = hideDeleteModal;
const el = document.getElementById(id); document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
if (el) el.onclick = hideDeleteModal; document.getElementById('delete-modal-confirm').onclick = confirmDelete;
});
const confirmBtn = document.getElementById('delete-modal-confirm');
if (confirmBtn) confirmBtn.onclick = confirmDelete;
// Gallery filter radios // --- Radio toggle for gallery filter ---
const showAllRadio = document.getElementById('show-all-radio'); const showAllRadio = document.getElementById('show-all-radio');
const showUntaggedRadio = document.getElementById('show-untagged-radio'); const showUntaggedRadio = document.getElementById('show-untagged-radio');
if (showAllRadio) showAllRadio.addEventListener('change', () => {
showOnlyUntagged = false; if (showAllRadio && showUntaggedRadio) {
renderGallery(); showAllRadio.addEventListener('change', () => {
}); if (showAllRadio.checked) {
if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => { showOnlyUntagged = false;
showOnlyUntagged = true; renderGallery();
renderGallery(); }
}); });
showUntaggedRadio.addEventListener('change', () => {
if (showUntaggedRadio.checked) {
showOnlyUntagged = true;
renderGallery();
}
});
}
// Bulk delete buttons // Bulk delete buttons
[ const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
['remove-all-gallery', 'gallery-all'], const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom');
['remove-all-gallery-bottom', 'gallery-all'], const removeAllHeroBtn = document.getElementById('remove-all-hero');
['remove-all-hero', 'hero-all'], const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom');
['remove-all-hero-bottom', 'hero-all'] if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
].forEach(([btnId, type]) => { if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all');
const btn = document.getElementById(btnId); if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
if (btn) btn.onclick = () => showDeleteModal(type); if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all');
});
}); });
// --- Initialize --- // --- Initialize ---
loadData(); loadData();

View File

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

View File

@ -70,11 +70,9 @@ function setupColorPicker(colorId, btnId, textId, initial) {
function setFontDropdown(selectId, value, options) { function setFontDropdown(selectId, value, options) {
const select = document.getElementById(selectId); const select = document.getElementById(selectId);
if (!select) return; if (!select) return;
select.innerHTML = options.map(opt => { select.innerHTML = options.map(opt =>
// Remove extension if present `<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
const base = opt.replace(/\.(woff2?|ttf|otf)$/, ""); ).join("");
return `<option value="${base}"${base === value ? " selected" : ""}>${base}</option>`;
}).join("");
} }
function setFallbackDropdown(selectId, value) { function setFallbackDropdown(selectId, value) {

View File

@ -11,37 +11,107 @@ function hideLoader() {
if (loader) loader.classList.remove("active"); if (loader) loader.classList.remove("active");
} }
// --- Generic upload handler ---
function setupUpload(inputId, apiUrl, loaderText, successMsg, refreshFn) { // --- Upload gallery images ---
const input = document.getElementById(inputId); const galleryInput = document.getElementById('upload-gallery');
if (!input) return; if (galleryInput) {
input.addEventListener('change', async (e) => { galleryInput.addEventListener('change', async (e) => {
const files = e.target.files; const files = e.target.files;
if (!files.length) return; if (!files.length) return;
showLoader(loaderText); showLoader("Uploading photos...");
const formData = new FormData(); const formData = new FormData();
for (const file of files) formData.append('files', file); for (const file of files) formData.append('files', file);
try { try {
const res = await fetch(apiUrl, { method: 'POST', body: formData }); const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
const data = await res.json(); const data = await res.json();
hideLoader(); hideLoader();
if (res.ok) { if (res.ok) {
showToast(`${data.uploaded.length} ${successMsg}`, "success"); showToast(`${data.uploaded.length} gallery image(s) uploaded!`, "success");
if (typeof refreshFn === "function") refreshFn(); if (typeof refreshGallery === "function") refreshGallery();
} else showToast('Error: ' + data.error, "error"); } else showToast('Error: ' + data.error, "error");
} catch(err) { } catch(err) {
hideLoader(); hideLoader();
console.error(err); console.error(err);
showToast('Server error!', "error"); showToast('Server error!', "error");
} finally { } finally { e.target.value = ''; }
e.target.value = '';
}
}); });
} }
// --- Setup all upload inputs --- // --- Upload hero images ---
setupUpload('upload-gallery', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery); const heroInput = document.getElementById('upload-hero');
setupUpload('upload-hero', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero); if (heroInput) {
setupUpload('upload-gallery-bottom', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery); heroInput.addEventListener('change', async (e) => {
setupUpload('upload-hero-bottom', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero); 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();
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();
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 = ''; }
});
}