diff --git a/.gitignore b/.gitignore index 7ed83aa..3a74a2a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,11 +27,15 @@ lerna-debug.log* .DS_Store Thumbs.db -/assets/pics/gallery/centerpiece/ │ -/assets/pics/gallery/sculpture/ │ -/assets/pics/gallery/classic/ │ -/assets/pics/gallery/organic/ │ -gallery/centerpiece/index.html │ -gallery/organic/index.html │ -gallery/classic/index.html │ -gallery/sculpture/index.html \ No newline at end of file +/assets/pics/gallery/centerpiece/ +/assets/pics/gallery/sculpture/ +/assets/pics/gallery/classic/ +/assets/pics/gallery/organic/ +gallery/centerpiece/index.html +gallery/organic/index.html +gallery/classic/index.html +gallery/sculpture/index.html + +# Build artifacts and backups +public/build/ +backups/ diff --git a/Dockerfile b/Dockerfile index 38e6708..005af04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,9 @@ RUN npm install # Bundle app source COPY . . +# Build optimized frontend assets +RUN npm run build + # Make port 3050 available to the world outside this container EXPOSE 3050 diff --git a/admin/admin.css b/admin/admin.css index 4d2b49b..7c1c567 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -1,3 +1,7 @@ #clearSelection:hover { color: #f14668; } + +.low-tag-card { + box-shadow: 0 0 0 2px #ffdd57 inset; +} diff --git a/admin/admin.js b/admin/admin.js index bb3a8b8..f2478f2 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -60,9 +60,48 @@ document.addEventListener('DOMContentLoaded', () => { return `${protocol}//${backendHostname}`; // No explicit port needed as it's on 443 })(); const LAST_TAGS_KEY = 'bpb-last-tags'; + const DEFAULT_MAX_TAGS = 8; + let tagMeta = { + tags: [], + main: [], + other: [], + aliases: {}, + presets: [], + labels: {}, + maxTags: DEFAULT_MAX_TAGS, + existing: [] + }; let adminPassword = ''; const storedPassword = localStorage.getItem('bpb-admin-password'); const getAdminPassword = () => adminPassword || localStorage.getItem('bpb-admin-password') || ''; + const slugifyTag = (tag) => String(tag || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').trim(); + const canonicalizeTag = (tag) => { + const slug = slugifyTag(tag); + const mapped = tagMeta.aliases?.[slug] || slug; + return mapped; + }; + const displayTag = (slug) => { + if (!slug) return ''; + if (tagMeta.labels && tagMeta.labels[slug]) return tagMeta.labels[slug]; + return slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + }; + const canonicalToDisplayString = (canonicalArr) => canonicalArr.map(displayTag).join(', '); + const normalizeTagsInput = (value) => { + const raw = String(value || '') + .split(',') + .map(t => t.trim()) + .filter(Boolean); + const seen = new Set(); + const canonical = []; + raw.forEach(tag => { + const mapped = canonicalizeTag(tag); + if (mapped && !seen.has(mapped) && canonical.length < (tagMeta.maxTags || DEFAULT_MAX_TAGS)) { + seen.add(mapped); + canonical.push(mapped); + } + }); + return canonical; + }; const showAdmin = () => { adminContent.style.display = 'block'; @@ -91,6 +130,7 @@ document.addEventListener('DOMContentLoaded', () => { adminPassword = passwordVal; localStorage.setItem('bpb-admin-password', adminPassword); showAdmin(); + fetchTagMeta(); fetchPhotos(); fetchStatus(); preloadLastTags(); @@ -103,6 +143,7 @@ document.addEventListener('DOMContentLoaded', () => { adminPassword = storedPassword; passwordInput.value = storedPassword; showAdmin(); + fetchTagMeta(); fetchPhotos(); fetchStatus(); preloadLastTags(); @@ -110,6 +151,30 @@ document.addEventListener('DOMContentLoaded', () => { showLogin(); } + async function fetchTagMeta() { + try { + const response = await fetch(`${backendUrl}/photos/tags`); + if (!response.ok) return; + const data = await response.json(); + tagMeta = { + tags: [], + main: [], + other: [], + aliases: {}, + presets: [], + labels: {}, + maxTags: DEFAULT_MAX_TAGS, + existing: [], + ...data + }; + updateTagSuggestions(); + updateQuickTags(); + preloadLastTags(); + } catch (error) { + console.error('Error fetching tag metadata:', error); + } + } + // --- Tab Switching --- tabs.forEach(tab => { tab.addEventListener('click', () => { @@ -149,15 +214,19 @@ document.addEventListener('DOMContentLoaded', () => { return; } photos.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' : ''; + const readableTags = (Array.isArray(photo.tags) ? photo.tags : []).map(displayTag); const photoCard = `
Caption: ${photo.caption}
-Tags: ${photo.tags.join(', ')}
+Tags: ${readableTags.join(', ')}