diff --git a/admin/admin.css b/admin/admin.css index 7c1c567..37a01fd 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -5,3 +5,16 @@ .low-tag-card { box-shadow: 0 0 0 2px #ffdd57 inset; } + +#bulkPanel { + position: sticky; + top: 16px; + z-index: 10; + box-shadow: 0 12px 24px rgba(17, 17, 17, 0.08); +} + +@media (max-width: 768px) { + #bulkPanel { + top: 8px; + } +} diff --git a/admin/admin.js b/admin/admin.js index f2478f2..f1871f8 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => { const captionInput = document.getElementById('captionInput'); const captionToTagsButton = document.getElementById('captionToTags'); const manageGallery = document.getElementById('manage-gallery'); + const manageSearchInput = document.getElementById('manageSearchInput'); const editModal = document.getElementById('editModal'); const editPhotoId = document.getElementById('editPhotoId'); const editCaption = document.getElementById('editCaption'); @@ -55,7 +56,17 @@ document.addEventListener('DOMContentLoaded', () => { const responseDiv = document.getElementById('response'); const backendUrl = (() => { - const { protocol } = window.location; + const { protocol, hostname } = window.location; + const productionHosts = new Set([ + 'beachpartyballoons.com', + 'www.beachpartyballoons.com', + 'preview.beachpartyballoons.com', + 'photobackend.beachpartyballoons.com' + ]); + const isProduction = productionHosts.has(hostname); + if (!isProduction) { + return 'http://localhost:5001'; + } const backendHostname = 'photobackend.beachpartyballoons.com'; return `${protocol}//${backendHostname}`; // No explicit port needed as it's on 443 })(); @@ -69,7 +80,8 @@ document.addEventListener('DOMContentLoaded', () => { presets: [], labels: {}, maxTags: DEFAULT_MAX_TAGS, - existing: [] + existing: [], + tagCounts: {} }; let adminPassword = ''; const storedPassword = localStorage.getItem('bpb-admin-password'); @@ -80,6 +92,11 @@ document.addEventListener('DOMContentLoaded', () => { const mapped = tagMeta.aliases?.[slug] || slug; return mapped; }; + const resolveSearchTag = (value) => { + const slug = slugifyTag(value); + if (!slug) return ''; + return tagMeta.aliases?.[slug] || slug; + }; const displayTag = (slug) => { if (!slug) return ''; if (tagMeta.labels && tagMeta.labels[slug]) return tagMeta.labels[slug]; @@ -165,6 +182,7 @@ document.addEventListener('DOMContentLoaded', () => { labels: {}, maxTags: DEFAULT_MAX_TAGS, existing: [], + tagCounts: {}, ...data }; updateTagSuggestions(); @@ -209,11 +227,27 @@ document.addEventListener('DOMContentLoaded', () => { function renderManageGallery() { manageGallery.innerHTML = ''; - if (!photos.length) { - manageGallery.innerHTML = '

No photos yet. Upload a photo to get started.

'; + const query = String(manageSearchInput?.value || '').trim().toLowerCase(); + const normalizedQuery = resolveSearchTag(query); + const filtered = query + ? photos.filter(photo => { + const caption = String(photo.caption || '').toLowerCase(); + const tags = Array.isArray(photo.tags) ? photo.tags : []; + const tagText = tags.map(displayTag).join(' ').toLowerCase(); + return caption.includes(query) + || tags.some(tag => String(tag || '').toLowerCase().includes(query)) + || (normalizedQuery && tags.some(tag => String(tag || '').toLowerCase() === normalizedQuery)) + || tagText.includes(query); + }) + : photos; + if (!filtered.length) { + const message = query + ? 'No photos match your search.' + : 'No photos yet. Upload a photo to get started.'; + manageGallery.innerHTML = `

${message}

`; return; } - photos.forEach(photo => { + filtered.forEach(photo => { const tagCount = Array.isArray(photo.tags) ? photo.tags.length : 0; const tagStatusClass = tagCount <= 2 ? 'is-warning' : 'is-light'; const lowTagClass = tagCount <= 2 ? 'low-tag-card' : ''; @@ -466,6 +500,10 @@ document.addEventListener('DOMContentLoaded', () => { } }); + if (manageSearchInput) { + manageSearchInput.addEventListener('input', () => renderManageGallery()); + } + selectAllPhotosBtn.addEventListener('click', (e) => { e.preventDefault(); toggleSelectAll(); @@ -582,13 +620,28 @@ document.addEventListener('DOMContentLoaded', () => { xhr.send(formData); }); + const getTagCount = (slug) => Number(tagMeta.tagCounts?.[slug] || 0); + const sortTagsByCount = (a, b) => { + const countDiff = getTagCount(b.slug) - getTagCount(a.slug); + if (countDiff !== 0) return countDiff; + return (a.label || '').localeCompare(b.label || ''); + }; + const sortSlugsByCount = (a, b) => { + const countDiff = getTagCount(b) - getTagCount(a); + if (countDiff !== 0) return countDiff; + return displayTag(a).localeCompare(displayTag(b)); + }; + function updateTagSuggestions() { if (!tagSuggestions) return; tagSuggestions.innerHTML = ''; + const mainSorted = [...(tagMeta.main || [])].sort(sortTagsByCount); + const otherSorted = [...(tagMeta.other || [])].sort(sortTagsByCount); + const existingSorted = [...(tagMeta.existing || [])].sort(sortSlugsByCount); const suggestions = [ - ...(tagMeta.main || []), - ...(tagMeta.other || []), - ...((tagMeta.existing || []).map(slug => ({ slug, label: displayTag(slug) }))) + ...mainSorted, + ...otherSorted, + ...existingSorted.map(slug => ({ slug, label: displayTag(slug) })) ]; const seen = new Set(); suggestions.forEach(tag => { @@ -604,8 +657,12 @@ document.addEventListener('DOMContentLoaded', () => { function updateQuickTags() { if (!quickTagButtons) return; const presetButtons = (tagMeta.presets || []).map(preset => ``); - const mainButtons = (tagMeta.main || []).map(tag => ``); - const otherButtons = (tagMeta.other || []).map(tag => ``); + const mainButtons = [...(tagMeta.main || [])] + .sort(sortTagsByCount) + .map(tag => ``); + const otherButtons = [...(tagMeta.other || [])] + .sort(sortTagsByCount) + .map(tag => ``); quickTagButtons.innerHTML = [...presetButtons, ...mainButtons, ...otherButtons].join(''); } diff --git a/admin/index.html b/admin/index.html index 7960780..b56748c 100644 --- a/admin/index.html +++ b/admin/index.html @@ -73,8 +73,8 @@
- -

Select one or many images; each will be converted to WebP automatically.

+ +

Select one or many images (including HEIC/HEIF); each will be converted to WebP automatically.

@@ -111,6 +111,12 @@
Gallery +
+ +
+ +
+