Better ui tag system
This commit is contained in:
@ -1,12 +1,14 @@
|
||||
// --- Arrays to store gallery and hero images ---
|
||||
let galleryImages = [];
|
||||
let heroImages = [];
|
||||
let allTags = []; // global tag list
|
||||
|
||||
// --- Load images from server on page load ---
|
||||
async function loadData() {
|
||||
try {
|
||||
const galleryRes = await fetch('/api/gallery');
|
||||
galleryImages = await galleryRes.json();
|
||||
updateAllTags();
|
||||
renderGallery();
|
||||
|
||||
const heroRes = await fetch('/api/hero');
|
||||
@ -14,10 +16,20 @@ async function loadData() {
|
||||
renderHero();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
alert("Error loading images!");
|
||||
showToast("Error loading images!", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update global tag list from galleryImages ---
|
||||
function updateAllTags() {
|
||||
allTags = [];
|
||||
galleryImages.forEach(img => {
|
||||
if (img.tags) img.tags.forEach(t => {
|
||||
if (!allTags.includes(t)) allTags.push(t);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Render gallery images with tags and delete buttons ---
|
||||
function renderGallery() {
|
||||
const container = document.getElementById('gallery');
|
||||
@ -27,14 +39,132 @@ function renderGallery() {
|
||||
div.className = 'photo';
|
||||
div.innerHTML = `
|
||||
<img src="/photos/${img.src}">
|
||||
<input type="text" value="${(img.tags || []).join(', ')}"
|
||||
onchange="updateTags(${i}, this.value)">
|
||||
<div class="tag-input" data-index="${i}"></div>
|
||||
<button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
renderTags(div.querySelector('.tag-input'), img.tags || [], i);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Render tags for a single image ---
|
||||
function renderTags(container, tags, imgIndex) {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Render existing tags
|
||||
tags.forEach(tag => {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'tag';
|
||||
span.textContent = tag;
|
||||
|
||||
const remove = document.createElement('span');
|
||||
remove.className = 'remove-tag';
|
||||
remove.textContent = '×';
|
||||
remove.onclick = () => {
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
updateTags(imgIndex, tags);
|
||||
renderTags(container, tags, imgIndex);
|
||||
};
|
||||
|
||||
span.appendChild(remove);
|
||||
container.appendChild(span);
|
||||
});
|
||||
|
||||
// Input for new tags
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Add tag...';
|
||||
container.appendChild(input);
|
||||
|
||||
// Suggestion dropdown
|
||||
const suggestionBox = document.createElement('ul');
|
||||
suggestionBox.className = 'suggestions';
|
||||
suggestionBox.style.fontStyle = 'italic';
|
||||
suggestionBox.style.textAlign = 'left';
|
||||
container.appendChild(suggestionBox);
|
||||
|
||||
let selectedIndex = -1;
|
||||
|
||||
const addTag = (tag) => {
|
||||
tag = tag.trim();
|
||||
if (!tag) return;
|
||||
if (!tags.includes(tag)) tags.push(tag);
|
||||
updateAllTags();
|
||||
updateTags(imgIndex, tags);
|
||||
renderTags(container, tags, imgIndex);
|
||||
};
|
||||
|
||||
const updateSuggestions = () => {
|
||||
const value = input.value.toLowerCase();
|
||||
const suggestions = allTags.filter(t => !tags.includes(t) && t.toLowerCase().startsWith(value));
|
||||
|
||||
suggestionBox.innerHTML = '';
|
||||
selectedIndex = -1;
|
||||
|
||||
if (suggestions.length) {
|
||||
suggestionBox.style.display = 'block';
|
||||
suggestions.forEach((s, idx) => {
|
||||
const li = document.createElement('li');
|
||||
const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
|
||||
const rest = s.substring(input.value.length);
|
||||
li.innerHTML = boldPart + rest;
|
||||
li.onclick = () => addTag(s);
|
||||
li.onmouseover = () => selectedIndex = idx;
|
||||
suggestionBox.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
suggestionBox.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('input', updateSuggestions);
|
||||
input.addEventListener('focus', updateSuggestions);
|
||||
|
||||
// Keyboard navigation
|
||||
input.addEventListener('keydown', (e) => {
|
||||
const items = suggestionBox.querySelectorAll('li');
|
||||
if (items.length) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex + 1) % items.length;
|
||||
items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
||||
items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0) addTag(items[selectedIndex].textContent.replace(/×$/,''));
|
||||
else addTag(input.value);
|
||||
} else if (e.key === 'Escape') {
|
||||
suggestionBox.style.display = 'none';
|
||||
} else if ([' ', ','].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
addTag(input.value);
|
||||
}
|
||||
} else if (['Enter', ' ', ','].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
addTag(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => {
|
||||
if (input.value.trim()) addTag(input.value);
|
||||
suggestionBox.style.display = 'none';
|
||||
}, 100);
|
||||
});
|
||||
|
||||
input.focus();
|
||||
}
|
||||
|
||||
// --- Update tags in galleryImages array ---
|
||||
function updateTags(index, tags) {
|
||||
galleryImages[index].tags = tags;
|
||||
saveGallery();
|
||||
}
|
||||
|
||||
// --- Render hero images with delete buttons ---
|
||||
function renderHero() {
|
||||
const container = document.getElementById('hero');
|
||||
@ -50,11 +180,6 @@ function renderHero() {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Update tags for gallery image ---
|
||||
function updateTags(index, value) {
|
||||
galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
|
||||
}
|
||||
|
||||
// --- Delete gallery image ---
|
||||
async function deleteGalleryImage(index) {
|
||||
const img = galleryImages[index];
|
||||
@ -69,9 +194,11 @@ async function deleteGalleryImage(index) {
|
||||
galleryImages.splice(index, 1);
|
||||
renderGallery();
|
||||
await saveGallery();
|
||||
} else alert("Error: " + data.error);
|
||||
showToast("✅ Gallery image deleted!", "success");
|
||||
} else showToast("Error: " + data.error, "error");
|
||||
} catch(err) {
|
||||
console.error(err); alert("Server error!");
|
||||
console.error(err);
|
||||
showToast("Server error!", "error");
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,9 +216,11 @@ async function deleteHeroImage(index) {
|
||||
heroImages.splice(index, 1);
|
||||
renderHero();
|
||||
await saveHero();
|
||||
} else alert("Error: " + data.error);
|
||||
showToast("✅ Hero image deleted!", "success");
|
||||
} else showToast("Error: " + data.error, "error");
|
||||
} catch(err) {
|
||||
console.error(err); alert("Server error!");
|
||||
console.error(err);
|
||||
showToast("Server error!", "error");
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,21 +246,39 @@ async function saveHero() {
|
||||
async function saveChanges() {
|
||||
await saveGallery();
|
||||
await saveHero();
|
||||
alert('✅ Changes saved!');
|
||||
showToast('✅ Changes saved!', "success");
|
||||
}
|
||||
|
||||
// --- Refresh gallery from folder ---
|
||||
async function refreshGallery() {
|
||||
await fetch('/api/gallery/refresh', { method: 'POST' });
|
||||
await loadData();
|
||||
alert('🔄 Gallery updated from photos/gallery folder');
|
||||
showToast('🔄 Gallery updated from photos/gallery folder', "success");
|
||||
}
|
||||
|
||||
// --- Refresh hero from folder ---
|
||||
async function refreshHero() {
|
||||
await fetch('/api/hero/refresh', { method: 'POST' });
|
||||
await loadData();
|
||||
alert('🔄 Hero updated from photos/hero folder');
|
||||
showToast('🔄 Hero updated from photos/hero folder', "success");
|
||||
}
|
||||
|
||||
// --- Show toast notification ---
|
||||
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);
|
||||
}
|
||||
|
||||
// --- Initialize ---
|
||||
|
@ -10,11 +10,12 @@ document.getElementById('upload-gallery').addEventListener('change', async (e) =
|
||||
const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
alert(`✅ ${data.uploaded.length} gallery image(s) uploaded!`);
|
||||
showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
|
||||
refreshGallery();
|
||||
} else alert('Error: ' + data.error);
|
||||
} else showToast('Error: ' + data.error, "error");
|
||||
} catch(err) {
|
||||
console.error(err); alert('Server error!');
|
||||
console.error(err);
|
||||
showToast('Server error!', "error");
|
||||
} finally { e.target.value = ''; }
|
||||
});
|
||||
|
||||
@ -30,10 +31,11 @@ document.getElementById('upload-hero').addEventListener('change', async (e) => {
|
||||
const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
alert(`✅ ${data.uploaded.length} hero image(s) uploaded!`);
|
||||
showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
|
||||
refreshHero();
|
||||
} else alert('Error: ' + data.error);
|
||||
} else showToast('Error: ' + data.error, "error");
|
||||
} catch(err) {
|
||||
console.error(err); alert('Server error!');
|
||||
console.error(err);
|
||||
showToast('Server error!', "error");
|
||||
} finally { e.target.value = ''; }
|
||||
});
|
||||
|
Reference in New Issue
Block a user