diff --git a/main-site/admin/admin.js b/main-site/admin/admin.js
index 42933de..97f1a49 100644
--- a/main-site/admin/admin.js
+++ b/main-site/admin/admin.js
@@ -22,13 +22,30 @@ document.addEventListener('DOMContentLoaded', () => {
const captionToTagsButton = document.getElementById('captionToTags');
const manageGallery = document.getElementById('manage-gallery');
const manageSearchInput = document.getElementById('manageSearchInput');
+ const needsTaggingBtn = document.getElementById('needsTaggingBtn');
const editModal = document.getElementById('editModal');
const editPhotoId = document.getElementById('editPhotoId');
const editCaption = document.getElementById('editCaption');
const editTags = document.getElementById('editTags');
+ const editModalImg = document.getElementById('editModalImg');
+ const editModalTitle = document.getElementById('editModalTitle');
+ const editPresetButtons = document.getElementById('editPresetButtons');
+ const editPrevBtn = document.getElementById('editPrevBtn');
+ const editNextBtn = document.getElementById('editNextBtn');
const saveChanges = document.getElementById('saveChanges');
const modalCloseButton = editModal.querySelector('.delete');
const modalCancelButton = editModal.querySelector('.modal-card-foot .button:not(.is-success)');
+ // Preset management elements
+ const presetButtons = document.getElementById('presetButtons');
+ const toggleManagePresets = document.getElementById('toggleManagePresets');
+ const managePresetsPanel = document.getElementById('managePresetsPanel');
+ const presetList = document.getElementById('presetList');
+ const newPresetName = document.getElementById('newPresetName');
+ const newPresetTags = document.getElementById('newPresetTags');
+ const savePresetBtn = document.getElementById('savePresetBtn');
+ const cancelPresetEdit = document.getElementById('cancelPresetEdit');
+ const editPresetIndex = document.getElementById('editPresetIndex');
+ const presetFormLabel = document.getElementById('presetFormLabel');
// Bulk Delete Modal
const bulkDeleteModal = document.getElementById('bulkDeleteModal');
@@ -47,6 +64,8 @@ document.addEventListener('DOMContentLoaded', () => {
const bulkPanel = document.getElementById('bulkPanel');
let selectedPhotoIds = new Set();
let photos = [];
+ let needsTaggingFilter = false;
+ let currentEditIndex = -1;
// Store Status Elements
const messageInput = document.getElementById('scrollingMessageInput');
@@ -225,12 +244,18 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
- function renderManageGallery() {
- manageGallery.innerHTML = '';
+ function getFilteredPhotos() {
const query = String(manageSearchInput?.value || '').trim().toLowerCase();
const normalizedQuery = resolveSearchTag(query);
- const filtered = query
- ? photos.filter(photo => {
+ let list = photos;
+ if (needsTaggingFilter) {
+ list = list.filter(photo => {
+ const tags = Array.isArray(photo.tags) ? photo.tags : [];
+ return tags.includes('uncategorized') || tags.length <= 1;
+ });
+ }
+ if (query) {
+ list = list.filter(photo => {
const caption = String(photo.caption || '').toLowerCase();
const tags = Array.isArray(photo.tags) ? photo.tags : [];
const tagText = tags.map(displayTag).join(' ').toLowerCase();
@@ -238,8 +263,14 @@ document.addEventListener('DOMContentLoaded', () => {
|| tags.some(tag => String(tag || '').toLowerCase().includes(query))
|| (normalizedQuery && tags.some(tag => String(tag || '').toLowerCase() === normalizedQuery))
|| tagText.includes(query);
- })
- : photos;
+ });
+ }
+ return list;
+ }
+
+ function renderManageGallery() {
+ manageGallery.innerHTML = '';
+ const filtered = getFilteredPhotos();
if (!filtered.length) {
const message = query
? 'No photos match your search.'
@@ -282,18 +313,46 @@ document.addEventListener('DOMContentLoaded', () => {
}
function openEditModal(photoId) {
- const photo = photos.find(p => p._id === photoId);
- if (photo) {
- editPhotoId.value = photo._id;
- editCaption.value = photo.caption;
- const readable = (Array.isArray(photo.tags) ? photo.tags : []).map(displayTag).join(', ');
- editTags.value = readable;
- editModal.classList.add('is-active');
+ const filtered = getFilteredPhotos();
+ const idx = filtered.findIndex(p => p._id === photoId);
+ if (idx === -1) return;
+ currentEditIndex = idx;
+ loadEditModalPhoto(filtered[idx], idx, filtered.length);
+ editModal.classList.add('is-active');
+ setTimeout(() => editCaption.focus(), 50);
+ }
+
+ function loadEditModalPhoto(photo, idx, total) {
+ editPhotoId.value = photo._id;
+ editCaption.value = photo.caption;
+ const readable = (Array.isArray(photo.tags) ? photo.tags : []).map(displayTag).join(', ');
+ editTags.value = readable;
+ if (editModalImg) {
+ const src = photo.variants?.thumb
+ ? `${backendUrl}/${photo.variants.thumb}`
+ : `${backendUrl}/${photo.path}`;
+ editModalImg.src = src;
+ editModalImg.alt = photo.caption;
}
+ if (editModalTitle) editModalTitle.textContent = `Photo ${idx + 1} of ${total}`;
+ if (editPrevBtn) editPrevBtn.disabled = idx === 0;
+ if (editNextBtn) editNextBtn.disabled = idx === total - 1;
+ renderEditPresetButtons();
+ }
+
+ function navigateEditModal(delta) {
+ const filtered = getFilteredPhotos();
+ const newIdx = currentEditIndex + delta;
+ if (newIdx < 0 || newIdx >= filtered.length) return;
+ handleSaveChanges(false).then(() => {
+ currentEditIndex = newIdx;
+ loadEditModalPhoto(filtered[newIdx], newIdx, filtered.length);
+ });
}
function closeEditModal() {
editModal.classList.remove('is-active');
+ currentEditIndex = -1;
}
function updateBulkUI() {
@@ -323,7 +382,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateBulkUI();
}
- async function handleSaveChanges() {
+ async function handleSaveChanges(closeAfter = true) {
const photoId = editPhotoId.value;
const canonicalTags = normalizeTagsInput(editTags.value);
if (!canonicalTags.length) {
@@ -351,9 +410,16 @@ document.addEventListener('DOMContentLoaded', () => {
if (response.status === 401) { handleUnauthorized(); return; }
if (response.ok) {
- closeEditModal();
- fetchPhotos();
- fetchTagMeta();
+ const idx = photos.findIndex(p => p._id === photoId);
+ if (idx !== -1) {
+ photos[idx].caption = updatedPhoto.caption;
+ photos[idx].tags = canonicalTags;
+ }
+ if (closeAfter) {
+ closeEditModal();
+ renderManageGallery();
+ fetchTagMeta();
+ }
} else {
alert('Failed to save changes.');
}
@@ -667,24 +733,98 @@ document.addEventListener('DOMContentLoaded', () => {
function updateQuickTags() {
if (!quickTagButtons) return;
- const presetButtons = (tagMeta.presets || []).map(preset => ``);
const mainButtons = [...(tagMeta.main || [])]
.sort(sortTagsByCount)
- .map(tag => ``);
+ .map(tag => ``);
const otherButtons = [...(tagMeta.other || [])]
.sort(sortTagsByCount)
- .map(tag => ``);
- quickTagButtons.innerHTML = [...presetButtons, ...mainButtons, ...otherButtons].join('');
+ .map(tag => ``);
+ quickTagButtons.innerHTML = [...mainButtons, ...otherButtons].join('');
+ renderPresetButtons();
}
- function addTagToInput(tag) {
+ function renderPresetButtons() {
+ if (!presetButtons) return;
+ presetButtons.innerHTML = (tagMeta.presets || [])
+ .map(p => ``)
+ .join('');
+ }
+
+ function renderEditPresetButtons() {
+ if (!editPresetButtons) return;
+ editPresetButtons.innerHTML = (tagMeta.presets || [])
+ .map(p => ``)
+ .join('');
+ }
+
+ function renderPresetManagementList() {
+ if (!presetList) return;
+ const presets = tagMeta.presets || [];
+ if (!presets.length) {
+ presetList.innerHTML = '
No presets yet.
';
+ return;
+ }
+ presetList.innerHTML = presets.map((p, i) => `
+
+ ${p.name}
+ ${(p.tags||[]).join(', ')}
+
+
+
`).join('');
+ }
+
+ async function savePreset() {
+ const name = newPresetName.value.trim();
+ const tags = newPresetTags.value.split(',').map(t => t.trim().toLowerCase().replace(/\s+/g, '-')).filter(Boolean);
+ if (!name || !tags.length) { alert('Enter a name and at least one tag.'); return; }
+ const idx = editPresetIndex.value;
+ const method = idx !== '' ? 'PUT' : 'POST';
+ const url = idx !== '' ? `${backendUrl}/photos/presets/${idx}` : `${backendUrl}/photos/presets`;
+ const res = await fetch(url, {
+ method,
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getAdminPassword()}` },
+ body: JSON.stringify({ name, tags })
+ });
+ if (res.ok) {
+ const data = await res.json();
+ tagMeta.presets = data.presets;
+ newPresetName.value = '';
+ newPresetTags.value = '';
+ editPresetIndex.value = '';
+ presetFormLabel.textContent = 'Add preset';
+ cancelPresetEdit.style.display = 'none';
+ renderPresetButtons();
+ renderPresetManagementList();
+ renderEditPresetButtons();
+ }
+ }
+
+ async function deletePreset(idx) {
+ if (!confirm('Delete this preset?')) return;
+ const res = await fetch(`${backendUrl}/photos/presets/${idx}`, {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${getAdminPassword()}` }
+ });
+ if (res.ok) {
+ const data = await res.json();
+ tagMeta.presets = data.presets;
+ renderPresetButtons();
+ renderPresetManagementList();
+ renderEditPresetButtons();
+ }
+ }
+
+ function addTagToInput(tag, inputEl) {
+ const target = inputEl || tagsInput;
const canonical = canonicalizeTag(tag);
if (!canonical) return;
- const existing = normalizeTagsInput(tagsInput.value);
- if (!existing.includes(canonical)) {
- existing.push(canonical);
- }
- tagsInput.value = canonicalToDisplayString(existing);
+ const existing = normalizeTagsInput(target.value);
+ if (!existing.includes(canonical)) existing.push(canonical);
+ target.value = canonicalToDisplayString(existing);
+ }
+
+ function applyPresetToInput(tagsStr, inputEl) {
+ tagsStr.split(',').forEach(t => addTagToInput(t.trim(), inputEl));
}
function preloadLastTags() {
@@ -695,24 +835,58 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
- function applyPresetTags(presetName) {
- const preset = (tagMeta.presets || []).find(p => p.name === presetName);
- if (!preset) return;
- const canonical = normalizeTagsInput((preset.tags || []).join(','));
- tagsInput.value = canonicalToDisplayString(canonical);
- }
-
if (quickTagButtons) {
quickTagButtons.addEventListener('click', (e) => {
- const presetBtn = e.target.closest('button[data-preset]');
const tagBtn = e.target.closest('button[data-tag]');
- if (presetBtn) {
- applyPresetTags(presetBtn.dataset.preset);
- return;
- }
- if (tagBtn) {
- addTagToInput(tagBtn.dataset.tag);
+ if (tagBtn) addTagToInput(tagBtn.dataset.tag);
+ });
+ }
+ if (presetButtons) {
+ presetButtons.addEventListener('click', (e) => {
+ const btn = e.target.closest('button[data-preset-tags]');
+ if (btn) applyPresetToInput(btn.dataset.presetTags, tagsInput);
+ });
+ }
+ if (editPresetButtons) {
+ editPresetButtons.addEventListener('click', (e) => {
+ const btn = e.target.closest('button[data-edit-preset-tags]');
+ if (btn) applyPresetToInput(btn.dataset.editPresetTags, editTags);
+ });
+ }
+ if (toggleManagePresets) {
+ toggleManagePresets.addEventListener('click', () => {
+ const open = managePresetsPanel.style.display === 'none';
+ managePresetsPanel.style.display = open ? '' : 'none';
+ toggleManagePresets.textContent = open ? 'Done' : 'Manage';
+ if (open) renderPresetManagementList();
+ });
+ }
+ if (savePresetBtn) savePresetBtn.addEventListener('click', savePreset);
+ if (cancelPresetEdit) {
+ cancelPresetEdit.addEventListener('click', () => {
+ newPresetName.value = '';
+ newPresetTags.value = '';
+ editPresetIndex.value = '';
+ presetFormLabel.textContent = 'Add preset';
+ cancelPresetEdit.style.display = 'none';
+ });
+ }
+ if (presetList) {
+ presetList.addEventListener('click', (e) => {
+ const editBtn = e.target.closest('.edit-preset-btn');
+ const delBtn = e.target.closest('.delete-preset-btn');
+ if (editBtn) {
+ const idx = parseInt(editBtn.dataset.idx, 10);
+ const preset = (tagMeta.presets || [])[idx];
+ if (!preset) return;
+ editPresetIndex.value = idx;
+ newPresetName.value = preset.name;
+ newPresetTags.value = (preset.tags || []).join(', ');
+ presetFormLabel.textContent = 'Edit preset';
+ cancelPresetEdit.style.display = '';
+ newPresetName.focus();
}
+ if (delBtn) deletePreset(parseInt(delBtn.dataset.idx, 10));
});
}
@@ -730,15 +904,44 @@ document.addEventListener('DOMContentLoaded', () => {
}
updateBulkUI();
- saveChanges.addEventListener('click', handleSaveChanges);
+ saveChanges.addEventListener('click', () => handleSaveChanges(true));
modalCloseButton.addEventListener('click', closeEditModal);
modalCancelButton.addEventListener('click', closeEditModal);
+ if (editPrevBtn) editPrevBtn.addEventListener('click', () => navigateEditModal(-1));
+ if (editNextBtn) editNextBtn.addEventListener('click', () => navigateEditModal(1));
applyBulkEdits.addEventListener('click', bulkApplyEdits);
bulkDelete.addEventListener('click', bulkDeletePhotos);
confirmBulkDeleteBtn.addEventListener('click', handleConfirmBulkDelete);
cancelBulkDeleteBtn.addEventListener('click', closeBulkDeleteModal);
bulkDeleteModalCloseBtn.addEventListener('click', closeBulkDeleteModal);
+ if (needsTaggingBtn) {
+ needsTaggingBtn.addEventListener('click', () => {
+ needsTaggingFilter = !needsTaggingFilter;
+ needsTaggingBtn.classList.toggle('is-warning', !needsTaggingFilter);
+ needsTaggingBtn.classList.toggle('is-warning is-light', !needsTaggingFilter);
+ needsTaggingBtn.classList.toggle('is-warning', needsTaggingFilter);
+ needsTaggingBtn.style.fontWeight = needsTaggingFilter ? 'bold' : '';
+ renderManageGallery();
+ });
+ }
+
+ // Keyboard shortcuts in edit modal
+ document.addEventListener('keydown', (e) => {
+ if (!editModal.classList.contains('is-active')) return;
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ handleSaveChanges(true);
+ }
+ return;
+ }
+ if (e.key === 'ArrowRight') { e.preventDefault(); navigateEditModal(1); }
+ if (e.key === 'ArrowLeft') { e.preventDefault(); navigateEditModal(-1); }
+ if (e.key === 'Escape') { e.preventDefault(); closeEditModal(); }
+ if (e.key === 'Enter') { e.preventDefault(); handleSaveChanges(true); }
+ });
+
// --- Store Status Management ---
async function fetchStatus() {
diff --git a/main-site/admin/index.html b/main-site/admin/index.html
index 34495e9..297e45f 100644
--- a/main-site/admin/index.html
+++ b/main-site/admin/index.html
@@ -108,6 +108,34 @@
+
+
+
+
+
+
+
+
Current presets
+
+
+
Add preset
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -237,11 +273,14 @@
- Edit Photo
+ Edit Photo
+
+
+
diff --git a/main-site/photo-gallery-app/backend/data/presets.json b/main-site/photo-gallery-app/backend/data/presets.json
new file mode 100644
index 0000000..14310b4
--- /dev/null
+++ b/main-site/photo-gallery-app/backend/data/presets.json
@@ -0,0 +1,16 @@
+[
+ { "name": "Classic", "tags": ["classic"] },
+ { "name": "Organic", "tags": ["organic"] },
+ { "name": "Arch", "tags": ["arch"] },
+ { "name": "Garland", "tags": ["garland"] },
+ { "name": "Columns", "tags": ["columns"] },
+ { "name": "Birthday", "tags": ["birthday"] },
+ { "name": "Baby Shower", "tags": ["baby-shower"] },
+ { "name": "Bridal Shower", "tags": ["bridal-shower"] },
+ { "name": "Graduation", "tags": ["graduation"] },
+ { "name": "Cocktail", "tags": ["cocktail"] },
+ { "name": "Signature", "tags": ["signature"] },
+ { "name": "Indoor", "tags": ["indoor"] },
+ { "name": "Outdoor", "tags": ["outdoor"] },
+ { "name": "Mitzvah", "tags": ["mitzvah"] }
+]
diff --git a/main-site/photo-gallery-app/backend/lib/tagConfig.js b/main-site/photo-gallery-app/backend/lib/tagConfig.js
index 0edd6c2..c6bc72b 100644
--- a/main-site/photo-gallery-app/backend/lib/tagConfig.js
+++ b/main-site/photo-gallery-app/backend/lib/tagConfig.js
@@ -1,12 +1,11 @@
const MAIN_TAGS = [
{ slug: 'arch', label: 'Arch', aliases: ['arches', 'archway'] },
- { slug: 'garland', label: 'Garland', aliases: ['organic', 'organic-garland'] },
+ { slug: 'garland', label: 'Garland', aliases: ['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'] },
{ slug: 'graduation', label: 'Graduation', aliases: ['grad', 'commencement'] },
];
@@ -14,8 +13,14 @@ const OTHER_TAGS = [
{ slug: 'classic', label: 'Classic', aliases: [] },
{ slug: 'organic', label: 'Organic', aliases: [] },
{ slug: 'hoop', label: 'Hoop', aliases: ['ring'] },
+ { slug: 'signature', label: 'Signature', aliases: [] },
{ slug: 'helium', label: 'Helium', aliases: [] },
{ slug: 'air-filled', label: 'Air-filled', aliases: ['airfilled', 'air'] },
+ { slug: 'indoor', label: 'Indoor', aliases: ['inside'] },
+ { slug: 'outdoor', label: 'Outdoor', aliases: ['outside'] },
+ { slug: 'cocktail', label: 'Cocktail', aliases: ['cocktail-party', 'reception'] },
+ { slug: 'bridal-shower', label: 'Bridal Shower', aliases: ['bridal', 'bride-shower'] },
+ { slug: 'mitzvah', label: 'Mitzvah', aliases: ['bar-mitzvah', 'bat-mitzvah', 'bnai-mitzvah'] },
{ slug: 'reunion', label: 'Reunion', aliases: [] },
{ slug: 'corporate', label: 'Corporate', aliases: ['business', 'office'] },
{ slug: 'holiday', label: 'Holiday', aliases: ['holidays'] },
@@ -29,23 +34,28 @@ const OTHER_TAGS = [
{ 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: 'neon', label: 'Neon', aliases: ['led', 'light', 'lights'] },
];
const TAG_DEFINITIONS = [...MAIN_TAGS, ...OTHER_TAGS];
+// Default presets — overridden at runtime by data/presets.json if present
const TAG_PRESETS = [
- { 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: 'Classic', tags: ['classic'] },
+ { name: 'Organic', tags: ['organic'] },
+ { name: 'Arch', tags: ['arch'] },
+ { name: 'Garland', tags: ['garland'] },
{ 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'] }
+ { name: 'Birthday', tags: ['birthday'] },
+ { name: 'Baby Shower', tags: ['baby-shower'] },
+ { name: 'Bridal Shower', tags: ['bridal-shower'] },
+ { name: 'Graduation', tags: ['graduation'] },
+ { name: 'Cocktail', tags: ['cocktail'] },
+ { name: 'Signature', tags: ['signature'] },
+ { name: 'Indoor', tags: ['indoor'] },
+ { name: 'Outdoor', tags: ['outdoor'] },
+ { name: 'Mitzvah', tags: ['mitzvah'] },
];
const MAX_TAGS = 8;
diff --git a/main-site/photo-gallery-app/backend/routes/photos.js b/main-site/photo-gallery-app/backend/routes/photos.js
index a152efd..6dc724e 100644
--- a/main-site/photo-gallery-app/backend/routes/photos.js
+++ b/main-site/photo-gallery-app/backend/routes/photos.js
@@ -80,6 +80,20 @@ async function applyInvisibleWatermark(buffer, payload, filename) {
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
+// Preset file helpers
+const PRESETS_FILE = path.join(__dirname, '..', 'data', 'presets.json');
+function loadPresets() {
+ try {
+ return JSON.parse(fs.readFileSync(PRESETS_FILE, 'utf8'));
+ } catch (_e) {
+ return TAG_PRESETS;
+ }
+}
+function savePresets(presets) {
+ fs.mkdirSync(path.dirname(PRESETS_FILE), { recursive: true });
+ fs.writeFileSync(PRESETS_FILE, JSON.stringify(presets, null, 2));
+}
+
// GET all photos
router.route('/').get((req, res) => {
Photo.find().sort({ createdAt: -1 }) // Sort by newest first
@@ -103,7 +117,7 @@ router.route('/tags').get(async (_req, res) => {
main: MAIN_TAGS,
other: OTHER_TAGS,
aliases: aliasMap,
- presets: TAG_PRESETS,
+ presets: loadPresets(),
maxTags: MAX_TAGS,
labels: labelLookup,
existing: existing || [],
@@ -116,7 +130,7 @@ router.route('/tags').get(async (_req, res) => {
main: MAIN_TAGS,
other: OTHER_TAGS,
aliases: aliasMap,
- presets: TAG_PRESETS,
+ presets: loadPresets(),
maxTags: MAX_TAGS,
labels: labelLookup,
existing: [],
@@ -125,6 +139,40 @@ router.route('/tags').get(async (_req, res) => {
}
});
+// Preset CRUD
+router.route('/presets')
+ .get((_req, res) => res.json(loadPresets()))
+ .post(requireAuth, (req, res) => {
+ const { name, tags } = req.body;
+ if (!name || !Array.isArray(tags) || !tags.length) {
+ return res.status(400).json({ error: 'name and tags[] required' });
+ }
+ const presets = loadPresets();
+ presets.push({ name: String(name).trim(), tags });
+ savePresets(presets);
+ res.json({ success: true, presets });
+ });
+
+router.route('/presets/:index')
+ .put(requireAuth, (req, res) => {
+ const idx = parseInt(req.params.index, 10);
+ const { name, tags } = req.body;
+ const presets = loadPresets();
+ if (idx < 0 || idx >= presets.length) return res.status(404).json({ error: 'Not found' });
+ if (name) presets[idx].name = String(name).trim();
+ if (Array.isArray(tags)) presets[idx].tags = tags;
+ savePresets(presets);
+ res.json({ success: true, presets });
+ })
+ .delete(requireAuth, (req, res) => {
+ const idx = parseInt(req.params.index, 10);
+ const presets = loadPresets();
+ if (idx < 0 || idx >= presets.length) return res.status(404).json({ error: 'Not found' });
+ presets.splice(idx, 1);
+ savePresets(presets);
+ res.json({ success: true, presets });
+ });
+
const parseIncomingTags = (tagsInput) => {
const rawList = Array.isArray(tagsInput)
? tagsInput