Improve gallery sharing and admin tagging
This commit is contained in:
parent
3a679eb03c
commit
a3b8593133
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = '<div class="column"><p class="has-text-grey">No photos yet. Upload a photo to get started.</p></div>';
|
||||
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 = `<div class="column"><p class="has-text-grey">${message}</p></div>`;
|
||||
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 => `<button type="button" class="button is-light is-rounded" data-preset="${preset.name}">${preset.name} preset</button>`);
|
||||
const mainButtons = (tagMeta.main || []).map(tag => `<button type="button" class="button is-link is-light is-rounded" data-tag="${tag.slug}">${tag.label}</button>`);
|
||||
const otherButtons = (tagMeta.other || []).map(tag => `<button type="button" class="button is-light is-rounded" data-tag="${tag.slug}">${tag.label}</button>`);
|
||||
const mainButtons = [...(tagMeta.main || [])]
|
||||
.sort(sortTagsByCount)
|
||||
.map(tag => `<button type="button" class="button is-link is-light is-rounded" data-tag="${tag.slug}">${tag.label}</button>`);
|
||||
const otherButtons = [...(tagMeta.other || [])]
|
||||
.sort(sortTagsByCount)
|
||||
.map(tag => `<button type="button" class="button is-light is-rounded" data-tag="${tag.slug}">${tag.label}</button>`);
|
||||
quickTagButtons.innerHTML = [...presetButtons, ...mainButtons, ...otherButtons].join('');
|
||||
}
|
||||
|
||||
|
||||
@ -73,8 +73,8 @@
|
||||
<div class="field">
|
||||
<label class="label">Photo</label>
|
||||
<div class="control">
|
||||
<input class="input has-background-light has-text-black" type="file" id="photoInput" accept="image/*" multiple required>
|
||||
<p class="help is-size-7 has-text-grey">Select one or many images; each will be converted to WebP automatically.</p>
|
||||
<input class="input has-background-light has-text-black" type="file" id="photoInput" accept="image/*,.heic,.heif" multiple required>
|
||||
<p class="help is-size-7 has-text-grey">Select one or many images (including HEIC/HEIF); each will be converted to WebP automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@ -111,6 +111,12 @@
|
||||
</div>
|
||||
<span class="tag is-info is-light"><i class="fas fa-images mr-2"></i>Gallery</span>
|
||||
</div>
|
||||
<div class="field mb-4">
|
||||
<label class="label is-size-7 has-text-dark">Search by caption or tag</label>
|
||||
<div class="control">
|
||||
<input class="input is-small has-background-light has-text-dark" type="text" id="manageSearchInput" placeholder="e.g. classic, wedding, arch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box has-background-light mb-4" id="bulkPanel" style="display: none;">
|
||||
<div class="columns is-vcentered is-mobile">
|
||||
<div class="column is-narrow">
|
||||
|
||||
@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
];
|
||||
let photos = [];
|
||||
let tagMeta = { labels: {}, tags: [] };
|
||||
let tagMeta = { labels: {}, tags: [], aliases: {} };
|
||||
const tagLabel = (slug) => {
|
||||
if (!slug) return '';
|
||||
if (tagMeta.labels && tagMeta.labels[slug]) return tagMeta.labels[slug];
|
||||
@ -56,6 +56,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const slugifyTag = (value) => String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
const resolveTagSlug = (value) => {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
const lowerRaw = raw.toLowerCase();
|
||||
const aliases = tagMeta.aliases || {};
|
||||
if (aliases[lowerRaw]) {
|
||||
return aliases[lowerRaw];
|
||||
}
|
||||
const labels = tagMeta.labels || {};
|
||||
for (const [slug, label] of Object.entries(labels)) {
|
||||
if (label && label.toLowerCase() === lowerRaw) {
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
return slugifyTag(raw) || lowerRaw;
|
||||
};
|
||||
|
||||
const apiBaseCandidates = (() => {
|
||||
const protocol = window.location.protocol;
|
||||
@ -88,7 +108,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const response = await fetchWithTimeout(`${baseUrl}/photos/tags`, 3000);
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
tagMeta = { labels: {}, tags: [], ...data };
|
||||
tagMeta = { labels: {}, tags: [], aliases: {}, ...data };
|
||||
} catch (err) {
|
||||
// Metadata is optional; fall back to raw tag text if unavailable.
|
||||
}
|
||||
@ -117,7 +137,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
photos = fallbackPhotos;
|
||||
rebuildFilterButtons();
|
||||
}
|
||||
renderFlatGallery(photos);
|
||||
const hashTag = getHashTag();
|
||||
if (hashTag) {
|
||||
applyTagFilter(hashTag, false);
|
||||
} else {
|
||||
applyTagFilter('all', false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateResultCount(count) {
|
||||
@ -138,9 +163,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
filterBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tag = btn.dataset.tag;
|
||||
filterByTag(tag.toLowerCase());
|
||||
filterBtns.forEach(otherBtn => otherBtn.classList.remove('is-active'));
|
||||
btn.classList.add('is-active');
|
||||
applyTagFilter(tag, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -211,10 +234,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
].filter(Boolean).join(', ')
|
||||
: '';
|
||||
const photoTags = normalizeTags(photo.tags);
|
||||
const readableTags = photoTags.map(tagLabel);
|
||||
const photoCard = document.createElement('div');
|
||||
photoCard.className = 'gallery-item';
|
||||
const tagBadges = readableTags.map(tag => `<span class="tag-chip" data-tag="${tag}"><i class="fa-solid fa-wand-magic-sparkles"></i>${tag}</span>`).join('');
|
||||
const tagBadges = photoTags
|
||||
.map(tag => `<span class="tag-chip" data-tag="${tag}"><i class="fa-solid fa-wand-magic-sparkles"></i>${tagLabel(tag)}</span>`)
|
||||
.join('');
|
||||
photoCard.innerHTML = `
|
||||
<div class="gallery-photo">
|
||||
<img loading="lazy" ${srcset ? `srcset="${srcset}" sizes="(min-width: 1024px) 33vw, (min-width: 768px) 45vw, 90vw"` : ''} src="${src}" alt="${photo.caption}" data-caption="${photo.caption}" data-tags="${photoTags.join(',')}" data-full-src="${src}" decoding="async">
|
||||
@ -248,11 +272,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
chip.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const tagText = chip.dataset.tag || '';
|
||||
const slug = normalizeTags(tagText)[0] || tagText.toLowerCase();
|
||||
filterByTag(slug);
|
||||
const matchingBtn = Array.from(filterBtns).find(btn => btn.dataset.tag === slug);
|
||||
filterBtns.forEach(btn => btn.classList.remove('is-active'));
|
||||
if (matchingBtn) matchingBtn.classList.add('is-active');
|
||||
applyTagFilter(tagText, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -260,6 +280,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function filterPhotos() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const normalizedSearch = resolveTagSlug(searchTerm);
|
||||
// Deactivate tag buttons when searching
|
||||
filterBtns.forEach(btn => btn.classList.remove('is-active'));
|
||||
if (searchTerm) {
|
||||
@ -268,7 +289,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const captionMatch = photo.caption.toLowerCase().includes(searchTerm);
|
||||
const tagMatch = photoTags.some(tag => {
|
||||
const label = tagLabel(tag).toLowerCase();
|
||||
return tag.toLowerCase().includes(searchTerm) || label.includes(searchTerm);
|
||||
return tag.toLowerCase().includes(searchTerm)
|
||||
|| label.includes(searchTerm)
|
||||
|| (normalizedSearch && tag.toLowerCase() === normalizedSearch);
|
||||
});
|
||||
return captionMatch || tagMatch;
|
||||
});
|
||||
@ -281,17 +304,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function filterByTag(tag) {
|
||||
searchInput.value = '';
|
||||
if (tag === 'all') {
|
||||
renderFlatGallery(photos);
|
||||
function setActiveFilterButton(tag) {
|
||||
filterBtns.forEach(btn => btn.classList.toggle('is-active', btn.dataset.tag === tag));
|
||||
}
|
||||
|
||||
function setHashTag(tag) {
|
||||
const url = new URL(window.location.href);
|
||||
if (!tag || tag === 'all') {
|
||||
url.hash = '';
|
||||
} else {
|
||||
url.hash = encodeURIComponent(tag);
|
||||
}
|
||||
history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
function filterByTag(tag, updateHash = true) {
|
||||
searchInput.value = '';
|
||||
const slug = resolveTagSlug(tag);
|
||||
if (!slug || slug === 'all') {
|
||||
renderFlatGallery(photos);
|
||||
setActiveFilterButton('all');
|
||||
if (updateHash) setHashTag('');
|
||||
return;
|
||||
}
|
||||
const filteredPhotos = photos.filter(photo => {
|
||||
const photoTags = normalizeTags(photo.tags);
|
||||
return photoTags.some(t => t.toLowerCase() === tag);
|
||||
return photoTags.some(t => t.toLowerCase() === slug);
|
||||
});
|
||||
renderFlatGallery(filteredPhotos);
|
||||
setActiveFilterButton(slug);
|
||||
if (updateHash) setHashTag(slug);
|
||||
}
|
||||
|
||||
function getHashTag() {
|
||||
const hash = window.location.hash || '';
|
||||
if (!hash) return '';
|
||||
return resolveTagSlug(decodeURIComponent(hash.replace(/^#/, '')).trim());
|
||||
}
|
||||
|
||||
function applyTagFilter(tag, updateHash = true) {
|
||||
filterByTag(tag, updateHash);
|
||||
}
|
||||
|
||||
function openModal(imageElement) {
|
||||
@ -321,11 +373,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
chip.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const tagText = chip.dataset.tag || '';
|
||||
const slug = normalizeTags(tagText)[0] || tagText.toLowerCase();
|
||||
filterByTag(slug);
|
||||
const matchingBtn = Array.from(filterBtns).find(btn => btn.dataset.tag === slug);
|
||||
filterBtns.forEach(btn => btn.classList.remove('is-active'));
|
||||
if (matchingBtn) matchingBtn.classList.add('is-active');
|
||||
applyTagFilter(tagText, true);
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
@ -373,5 +421,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
renderSkeletonLoader();
|
||||
window.addEventListener('hashchange', () => {
|
||||
const tag = getHashTag() || 'all';
|
||||
applyTagFilter(tag, false);
|
||||
});
|
||||
|
||||
fetchPhotos();
|
||||
});
|
||||
|
||||
@ -2,6 +2,8 @@ const MAIN_TAGS = [
|
||||
{ slug: 'arch', label: 'Arch', aliases: ['arches', 'archway'] },
|
||||
{ slug: 'garland', label: 'Garland', aliases: ['organic', 'organic-garland'] },
|
||||
{ slug: 'columns', label: 'Columns', aliases: ['pillars'] },
|
||||
{ slug: 'centerpiece', label: 'Centerpiece', aliases: ['table', 'tablescape'] },
|
||||
{ slug: 'sculpture', label: 'Sculpture', aliases: ['sculpt'] },
|
||||
{ slug: 'birthday', label: 'Birthday', aliases: ['bday', 'birthday-party'] },
|
||||
{ slug: 'baby-shower', label: 'Baby Shower', aliases: ['baby', 'shower'] },
|
||||
{ slug: 'gifts', label: 'Gifts', aliases: ['presents'] },
|
||||
@ -11,27 +13,54 @@ const MAIN_TAGS = [
|
||||
const OTHER_TAGS = [
|
||||
{ slug: 'classic', label: 'Classic', aliases: [] },
|
||||
{ slug: 'organic', label: 'Organic', aliases: [] },
|
||||
{ slug: 'sculpture', label: 'Sculpture', aliases: ['sculpt'] },
|
||||
{ slug: 'hoop', label: 'Hoop', aliases: ['ring'] },
|
||||
{ slug: 'helium', label: 'Helium', aliases: [] },
|
||||
{ slug: 'air-filled', label: 'Air-filled', aliases: ['airfilled', 'air'] },
|
||||
{ slug: 'reunion', label: 'Reunion', aliases: [] },
|
||||
{ slug: 'corporate', label: 'Corporate', aliases: ['business', 'office'] },
|
||||
{ slug: 'holiday', label: 'Holiday', aliases: ['christmas', 'halloween', 'easter'] },
|
||||
{ slug: 'holiday', label: 'Holiday', aliases: ['holidays'] },
|
||||
{ slug: 'christmas', label: 'Christmas', aliases: ['xmas', 'x-mas'] },
|
||||
{ slug: 'halloween', label: 'Halloween', aliases: [] },
|
||||
{ slug: 'easter', label: 'Easter', aliases: [] },
|
||||
{ slug: 'valentines', label: "Valentine's Day", aliases: ['valentine', 'valentine-day', 'valentines-day'] },
|
||||
{ slug: 'new-years', label: "New Year's", aliases: ['new-year', 'nye', 'new-years-eve', 'new-year-eve'] },
|
||||
{ slug: 'thanksgiving', label: 'Thanksgiving', aliases: ['turkey-day'] },
|
||||
{ slug: 'july-4th', label: 'July 4th', aliases: ['fourth-of-july', '4th-of-july', 'fourth', 'independence-day'] },
|
||||
{ slug: 'st-patricks', label: "St. Patrick's Day", aliases: ['st-pattys', 'st-paddys', 'st-patricks-day'] },
|
||||
{ slug: 'mothers-day', label: "Mother's Day", aliases: ['mothers', 'mom-day'] },
|
||||
{ slug: 'fathers-day', label: "Father's Day", aliases: ['fathers', 'dad-day'] },
|
||||
{ slug: 'graduation-party', label: 'Graduation Party', aliases: ['grad-party', 'graduation-party'] },
|
||||
{ slug: 'marquee', label: 'Marquee Letters', aliases: ['letters', 'marquee-letters'] },
|
||||
{ slug: 'delivery', label: 'Delivery', aliases: ['deliver', 'delivered'] },
|
||||
{ slug: 'pickup', label: 'Pickup', aliases: ['pick-up', 'collect'] },
|
||||
{ slug: 'neon', label: 'Neon', aliases: ['led', 'light', 'lights'] },
|
||||
];
|
||||
|
||||
const TAG_DEFINITIONS = [...MAIN_TAGS, ...OTHER_TAGS];
|
||||
|
||||
const TAG_PRESETS = [
|
||||
{ name: 'Birthday', tags: ['birthday', 'arch', 'garland'] },
|
||||
{ name: 'Baby Shower', tags: ['baby-shower', 'garland', 'gifts'] },
|
||||
{ name: 'Graduation', tags: ['graduation', 'arch', 'classic'] },
|
||||
{ name: 'Corporate', tags: ['corporate', 'columns', 'delivery'] },
|
||||
{ name: 'Holiday', tags: ['holiday', 'garland', 'marquee'] }
|
||||
{ name: 'Arch - Classic', tags: ['arch', 'classic'] },
|
||||
{ name: 'Arch - Organic', tags: ['arch', 'organic'] },
|
||||
{ name: 'Arch - Hoop', tags: ['arch', 'hoop'] },
|
||||
{ name: 'Garland - Organic', tags: ['garland', 'organic'] },
|
||||
{ name: 'Columns', tags: ['columns'] },
|
||||
{ name: 'Centerpiece - Helium', tags: ['centerpiece', 'helium'] },
|
||||
{ name: 'Centerpiece - Air-filled', tags: ['centerpiece', 'air-filled'] },
|
||||
{ name: 'Sculpture', tags: ['sculpture'] },
|
||||
{ name: 'Marquee', tags: ['marquee'] }
|
||||
];
|
||||
|
||||
const MAX_TAGS = 8;
|
||||
const HOLIDAY_SUBTAGS = new Set([
|
||||
'christmas',
|
||||
'halloween',
|
||||
'easter',
|
||||
'valentines',
|
||||
'new-years',
|
||||
'thanksgiving',
|
||||
'july-4th',
|
||||
'st-patricks',
|
||||
'mothers-day',
|
||||
'fathers-day'
|
||||
]);
|
||||
|
||||
const slugifyTag = (tag) => {
|
||||
return String(tag || '')
|
||||
@ -68,6 +97,11 @@ const normalizeTags = (incomingTags = []) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (normalized.some(tag => HOLIDAY_SUBTAGS.has(tag)) && !seen.has('holiday')) {
|
||||
normalized.push('holiday');
|
||||
seen.add('holiday');
|
||||
}
|
||||
|
||||
return { normalized, rejected: [] };
|
||||
};
|
||||
|
||||
|
||||
@ -66,7 +66,15 @@ router.route('/').get((req, res) => {
|
||||
|
||||
router.route('/tags').get(async (_req, res) => {
|
||||
try {
|
||||
const existing = await Photo.distinct('tags');
|
||||
const tagCountsArray = await Photo.aggregate([
|
||||
{ $unwind: '$tags' },
|
||||
{ $group: { _id: '$tags', count: { $sum: 1 } } }
|
||||
]);
|
||||
const tagCounts = tagCountsArray.reduce((acc, item) => {
|
||||
acc[item._id] = item.count;
|
||||
return acc;
|
||||
}, {});
|
||||
const existing = tagCountsArray.map(item => item._id);
|
||||
res.json({
|
||||
tags: TAG_DEFINITIONS,
|
||||
main: MAIN_TAGS,
|
||||
@ -76,6 +84,7 @@ router.route('/tags').get(async (_req, res) => {
|
||||
maxTags: MAX_TAGS,
|
||||
labels: labelLookup,
|
||||
existing: existing || [],
|
||||
tagCounts,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching tag metadata:', err);
|
||||
@ -88,6 +97,7 @@ router.route('/tags').get(async (_req, res) => {
|
||||
maxTags: MAX_TAGS,
|
||||
labels: labelLookup,
|
||||
existing: [],
|
||||
tagCounts: {},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,6 +10,8 @@ const whitelist = [
|
||||
'https://beachpartyballoons.com',
|
||||
'https://www.beachpartyballoons.com',
|
||||
'https://photobackend.beachpartyballoons.com', // Dedicated backend hostname
|
||||
'http://localhost:3052',
|
||||
'http://127.0.0.1:3052',
|
||||
'http://localhost:3050',
|
||||
'http://127.0.0.1:3050',
|
||||
'http://localhost:8080' // Common local dev port
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user