Beta-2.1 - The clearer, the faster #22
@ -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,31 +35,44 @@ 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 = `
|
||||
<div class="flex-item">
|
||||
<img class="fade-in-img" src="/photos/${img.src}">
|
||||
</div>
|
||||
<div class="flex-item">
|
||||
<img class="fade-in-img" src="/photos/${img.src}">
|
||||
</div>
|
||||
<div class="tags-display" data-index="${i}"></div>
|
||||
<div class="flex-item flex-full">
|
||||
<div class="flex-item flex-end">
|
||||
@ -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) {
|
||||
showOnlyUntagged = false;
|
||||
renderGallery();
|
||||
}
|
||||
});
|
||||
showUntaggedRadio.addEventListener('change', () => {
|
||||
if (showUntaggedRadio.checked) {
|
||||
showOnlyUntagged = true;
|
||||
renderGallery();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (showAllRadio) showAllRadio.addEventListener('change', () => {
|
||||
showOnlyUntagged = false;
|
||||
renderGallery();
|
||||
});
|
||||
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();
|
Reference in New Issue
Block a user