Refactored Galley Editor

This commit is contained in:
Djeex
2025-09-04 12:22:52 +02:00
parent 205dcae2bc
commit 1d1d7d8304

View File

@ -1,19 +1,14 @@
// --- Arrays to store gallery and hero images ---
let galleryImages = [];
let heroImages = [];
let allTags = []; // global tag list
let allTags = [];
let showOnlyUntagged = false;
// --- Fade-in helper ---
function applyFadeInImages(container) {
const fadeImages = container.querySelectorAll("img.fade-in-img");
fadeImages.forEach(img => {
const onLoad = () => {
img.classList.add("loaded");
};
if (img.complete && img.naturalHeight !== 0) {
onLoad();
} else {
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);
@ -25,13 +20,10 @@ function applyFadeInImages(container) {
// --- Load images from server on page load ---
async function loadData() {
try {
const galleryRes = await fetch('/api/gallery');
galleryImages = await galleryRes.json();
galleryImages = await (await fetch('/api/gallery')).json();
updateAllTags();
renderGallery();
const heroRes = await fetch('/api/hero');
heroImages = await heroRes.json();
heroImages = await (await fetch('/api/hero')).json();
renderHero();
} catch(err) {
console.error(err);
@ -43,25 +35,38 @@ async function loadData() {
function updateAllTags() {
allTags = [];
galleryImages.forEach(img => {
if (img.tags) img.tags.forEach(t => {
(img.tags || []).forEach(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 ---
function renderGallery() {
const container = document.getElementById('gallery');
container.innerHTML = '';
// Filter images if toggle is on
let imagesToShow = galleryImages;
if (showOnlyUntagged) {
imagesToShow = galleryImages.filter(img => !img.tags || img.tags.length === 0);
}
let imagesToShow = showOnlyUntagged
? galleryImages.filter(img => !img.tags || img.tags.length === 0)
: galleryImages;
imagesToShow.forEach((img) => {
const i = galleryImages.indexOf(img); // Use real index for tag/delete actions
const i = galleryImages.indexOf(img);
const div = document.createElement('div');
div.className = 'photo flex-item flex-column';
div.innerHTML = `
@ -77,41 +82,10 @@ function renderGallery() {
</div>
`;
container.appendChild(div);
renderTags(i, img.tags || []);
});
// Update gallery count (top)
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
updateCountAndButtons('gallery', imagesToShow.length);
applyFadeInImages(container);
}
@ -119,7 +93,6 @@ function renderGallery() {
function renderTags(imgIndex, tags) {
const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
tagsDisplay.innerHTML = '';
inputContainer.innerHTML = '';
@ -127,7 +100,6 @@ function renderTags(imgIndex, tags) {
const span = document.createElement('span');
span.className = 'tag';
span.textContent = tag;
const remove = document.createElement('span');
remove.className = 'remove-tag';
remove.textContent = '×';
@ -136,7 +108,6 @@ function renderTags(imgIndex, tags) {
updateTags(imgIndex, tags);
renderTags(imgIndex, tags);
};
span.appendChild(remove);
tagsDisplay.appendChild(span);
});
@ -146,11 +117,10 @@ function renderTags(imgIndex, tags) {
input.placeholder = 'Add tag...';
inputContainer.appendChild(input);
// --- Validate button ---
const validateBtn = document.createElement('button');
validateBtn.textContent = '✔️';
validateBtn.className = 'validate-tag-btn';
validateBtn.style.display = 'none'; // hidden by default
validateBtn.style.display = 'none';
validateBtn.style.marginLeft = '4px';
inputContainer.appendChild(validateBtn);
@ -170,30 +140,20 @@ function renderTags(imgIndex, tags) {
const updateSuggestions = () => {
const value = input.value.toLowerCase();
const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
const tagCount = {};
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));
suggestionBox.innerHTML = '';
selectedIndex = -1;
if (suggestions.length) {
suggestionBox.style.display = 'block';
suggestions.forEach((s, idx) => {
const li = document.createElement('li');
li.style.fontStyle = 'italic';
li.style.textAlign = 'left';
const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
const rest = s.substring(input.value.length);
li.innerHTML = boldPart + rest;
li.innerHTML = `<b>${s.substring(0, input.value.length)}</b>${s.substring(input.value.length)}`;
li.addEventListener('mousedown', (e) => {
e.preventDefault();
addTag(s);
@ -201,7 +161,6 @@ function renderTags(imgIndex, tags) {
input.focus();
updateSuggestions();
});
li.onmouseover = () => selectedIndex = idx;
suggestionBox.appendChild(li);
});
@ -218,7 +177,6 @@ function renderTags(imgIndex, tags) {
updateSuggestions();
validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
});
input.addEventListener('keydown', (e) => {
const items = suggestionBox.querySelectorAll('li');
if (e.key === 'ArrowDown') {
@ -249,26 +207,22 @@ function renderTags(imgIndex, tags) {
validateBtn.style.display = 'none';
}
});
input.addEventListener('blur', () => {
suggestionBox.style.display = 'none';
input.value = '';
validateBtn.style.display = 'none';
});
// --- Validate button action ---
validateBtn.onclick = () => {
validateBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
if (input.value.trim()) {
addTag(input.value.trim());
input.value = '';
updateSuggestions();
validateBtn.style.display = 'none';
}
};
});
updateSuggestions();
if (!input.value.trim()) {
suggestionBox.style.display = 'none';
}
if (!input.value.trim()) suggestionBox.style.display = 'none';
}
// --- Update tags in galleryImages array ---
@ -296,38 +250,7 @@ function renderHero() {
`;
container.appendChild(div);
});
// 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';
}
// Fade-in effect for loaded images
updateCountAndButtons('hero', heroImages.length);
applyFadeInImages(container);
}
@ -374,33 +297,27 @@ async function refreshHero() {
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
toast.addEventListener("transitionend", () => toast.remove());
}, duration);
}
let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
let pendingDelete = null;
// --- Show delete confirmation modal ---
function showDeleteModal(type, index = null) {
pendingDelete = { type, index };
const modalText = document.getElementById('delete-modal-text');
if (type === 'gallery-all') {
modalText.textContent = "Are you sure you want to delete ALL gallery images?";
} else if (type === 'hero-all') {
modalText.textContent = "Are you sure you want to delete ALL hero images?";
} else {
modalText.textContent = "Are you sure you want to delete this image?";
}
modalText.textContent =
type === 'gallery-all' ? "Are you sure you want to delete ALL gallery images?"
: type === 'hero-all' ? "Are you sure you want to delete ALL hero images?"
: "Are you sure you want to delete this image?";
document.getElementById('delete-modal').style.display = 'flex';
}
@ -413,15 +330,10 @@ function hideDeleteModal() {
// --- Confirm deletion ---
async function confirmDelete() {
if (!pendingDelete) return;
if (pendingDelete.type === 'gallery') {
await actuallyDeleteGalleryImage(pendingDelete.index);
} else if (pendingDelete.type === 'hero') {
await actuallyDeleteHeroImage(pendingDelete.index);
} else if (pendingDelete.type === 'gallery-all') {
await actuallyDeleteAllGalleryImages();
} else if (pendingDelete.type === 'hero-all') {
await actuallyDeleteAllHeroImages();
}
if (pendingDelete.type === 'gallery') await actuallyDeleteGalleryImage(pendingDelete.index);
else if (pendingDelete.type === 'hero') await actuallyDeleteHeroImage(pendingDelete.index);
else if (pendingDelete.type === 'gallery-all') await actuallyDeleteAllGalleryImages();
else if (pendingDelete.type === 'hero-all') await actuallyDeleteAllHeroImages();
hideDeleteModal();
}
@ -503,41 +415,36 @@ async function actuallyDeleteAllHeroImages() {
// --- Modal event listeners and bulk delete buttons ---
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('delete-modal-close').onclick = hideDeleteModal;
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
['delete-modal-close', 'delete-modal-cancel'].forEach(id => {
const el = document.getElementById(id);
if (el) el.onclick = hideDeleteModal;
});
const confirmBtn = document.getElementById('delete-modal-confirm');
if (confirmBtn) confirmBtn.onclick = confirmDelete;
// --- Radio toggle for gallery filter ---
// Gallery filter radios
const showAllRadio = document.getElementById('show-all-radio');
const showUntaggedRadio = document.getElementById('show-untagged-radio');
if (showAllRadio && showUntaggedRadio) {
showAllRadio.addEventListener('change', () => {
if (showAllRadio.checked) {
if (showAllRadio) showAllRadio.addEventListener('change', () => {
showOnlyUntagged = false;
renderGallery();
}
});
showUntaggedRadio.addEventListener('change', () => {
if (showUntaggedRadio.checked) {
if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
showOnlyUntagged = true;
renderGallery();
}
});
}
// Bulk delete buttons
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom');
const removeAllHeroBtn = document.getElementById('remove-all-hero');
const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom');
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all');
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all');
[
['remove-all-gallery', 'gallery-all'],
['remove-all-gallery-bottom', 'gallery-all'],
['remove-all-hero', 'hero-all'],
['remove-all-hero-bottom', 'hero-all']
].forEach(([btnId, type]) => {
const btn = document.getElementById(btnId);
if (btn) btn.onclick = () => showDeleteModal(type);
});
});
// --- Initialize ---
loadData();