document.addEventListener('DOMContentLoaded', () => { // General Admin Elements const loginModal = document.getElementById('login-modal'); const loginForm = document.getElementById('loginForm'); const passwordInput = document.getElementById('passwordInput'); const loginButton = document.getElementById('loginButton'); const adminContent = document.getElementById('admin-content'); // Tabs const tabs = document.querySelectorAll('.tabs li'); const tabContents = document.querySelectorAll('.tab-content'); // Photo Gallery Elements const uploadForm = document.getElementById('uploadForm'); const uploadButton = document.getElementById('uploadButton'); const uploadStatus = document.getElementById('uploadStatus'); const uploadProgress = document.getElementById('uploadProgress'); const tagsInput = document.getElementById('tagsInput'); const tagSuggestions = document.getElementById('tagSuggestions'); const quickTagButtons = document.getElementById('quickTagButtons'); const captionInput = document.getElementById('captionInput'); const captionToTagsButton = document.getElementById('captionToTags'); const manageGallery = document.getElementById('manage-gallery'); const editModal = document.getElementById('editModal'); const editPhotoId = document.getElementById('editPhotoId'); const editCaption = document.getElementById('editCaption'); const editTags = document.getElementById('editTags'); const saveChanges = document.getElementById('saveChanges'); const modalCloseButton = editModal.querySelector('.delete'); const modalCancelButton = editModal.querySelector('.modal-card-foot .button:not(.is-success)'); // Bulk Delete Modal const bulkDeleteModal = document.getElementById('bulkDeleteModal'); const confirmBulkDeleteBtn = document.getElementById('confirmBulkDelete'); const cancelBulkDeleteBtn = document.getElementById('cancelBulkDelete'); const bulkDeleteModalCloseBtn = bulkDeleteModal.querySelector('.delete'); const bulkDeleteCountEl = document.getElementById('bulk-delete-count'); const bulkCaption = document.getElementById('bulkCaption'); const bulkTags = document.getElementById('bulkTags'); const bulkAppendTags = document.getElementById('bulkAppendTags'); const applyBulkEdits = document.getElementById('applyBulkEdits'); const bulkDelete = document.getElementById('bulkDelete'); const selectAllPhotosBtn = document.getElementById('selectAllPhotos'); const clearSelectionBtn = document.getElementById('clearSelection'); const selectedCountEl = document.getElementById('selectedCount'); const bulkPanel = document.getElementById('bulkPanel'); let selectedPhotoIds = new Set(); let photos = []; // Store Status Elements const messageInput = document.getElementById('scrollingMessageInput'); const isClosedCheckbox = document.getElementById('isClosedCheckbox'); const closedMessageInput = document.getElementById('closedMessageInput'); const updateButton = document.getElementById('updateButton'); const responseDiv = document.getElementById('response'); const backendUrl = (() => { const { protocol } = window.location; const backendHostname = 'photobackend.beachpartyballoons.com'; return `${protocol}//${backendHostname}`; // No explicit port needed as it's on 443 })(); const LAST_TAGS_KEY = 'bpb-last-tags'; let adminPassword = ''; const storedPassword = localStorage.getItem('bpb-admin-password'); const getAdminPassword = () => adminPassword || localStorage.getItem('bpb-admin-password') || ''; const showAdmin = () => { adminContent.style.display = 'block'; loginModal.classList.remove('is-active'); }; const showLogin = (message) => { if (message) { passwordInput.value = ''; passwordInput.placeholder = message; } loginModal.classList.add('is-active'); }; const handleUnauthorized = () => { localStorage.removeItem('bpb-admin-password'); adminPassword = ''; showLogin('Enter password to continue'); }; // --- Password Protection --- function login(event) { event.preventDefault(); const passwordVal = passwordInput.value.trim(); if (!passwordVal) return; adminPassword = passwordVal; localStorage.setItem('bpb-admin-password', adminPassword); showAdmin(); fetchPhotos(); fetchStatus(); preloadLastTags(); } loginForm.addEventListener('submit', login); loginButton.addEventListener('click', login); if (storedPassword) { adminPassword = storedPassword; passwordInput.value = storedPassword; showAdmin(); fetchPhotos(); fetchStatus(); preloadLastTags(); } else { showLogin(); } // --- Tab Switching --- tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(item => item.classList.remove('is-active')); tab.classList.add('is-active'); const target = document.getElementById(tab.dataset.tab); tabContents.forEach(content => content.style.display = 'none'); target.style.display = 'block'; }); }); // --- Photo Management --- async function fetchPhotos() { try { const response = await fetch(`${backendUrl}/photos`); if (response.status === 401) { handleUnauthorized(); return; } photos = await response.json(); const validIds = new Set(photos.map(p => p._id)); selectedPhotoIds = new Set(Array.from(selectedPhotoIds).filter(id => validIds.has(id))); updateTagSuggestions(); updateQuickTags(); renderManageGallery(); updateBulkUI(); } catch (error) { console.error('Error fetching photos:', error); } } function renderManageGallery() { manageGallery.innerHTML = ''; if (!photos.length) { manageGallery.innerHTML = '

No photos yet. Upload a photo to get started.

'; return; } photos.forEach(photo => { const photoCard = `
${photo.tags.length} tag${photo.tags.length === 1 ? '' : 's'}
${photo.caption}

Caption: ${photo.caption}

Tags: ${photo.tags.join(', ')}

`; manageGallery.innerHTML += photoCard; }); } function openEditModal(photoId) { const photo = photos.find(p => p._id === photoId); if (photo) { editPhotoId.value = photo._id; editCaption.value = photo.caption; editTags.value = photo.tags.join(', '); editModal.classList.add('is-active'); } } function closeEditModal() { editModal.classList.remove('is-active'); } function updateBulkUI() { const count = selectedPhotoIds.size; selectedCountEl.textContent = `${count} selected`; const disabled = count === 0; applyBulkEdits.disabled = disabled; bulkDelete.disabled = disabled; if (bulkPanel) { bulkPanel.style.display = count ? 'block' : 'none'; } } function toggleSelectAll() { if (selectedPhotoIds.size === photos.length) { selectedPhotoIds.clear(); } else { photos.forEach(p => selectedPhotoIds.add(p._id)); } renderManageGallery(); updateBulkUI(); } function clearSelection() { selectedPhotoIds.clear(); renderManageGallery(); updateBulkUI(); } async function handleSaveChanges() { const photoId = editPhotoId.value; const updatedPhoto = { caption: editCaption.value, tags: editTags.value }; try { const response = await fetch(`${backendUrl}/photos/update/${photoId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updatedPhoto) }); if (response.ok) { closeEditModal(); fetchPhotos(); // Refresh the gallery } else { alert('Failed to save changes.'); } } catch (error) { console.error('Error saving changes:', error); alert('An error occurred while saving. Please try again.'); } } async function deletePhoto(id) { if (confirm('Are you sure you want to delete this photo?')) { try { await fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE' }); fetchPhotos(); } catch (error) { console.error('Error deleting photo:', error); } } } function openBulkDeleteModal() { const count = selectedPhotoIds.size; if (count === 0) return; bulkDeleteCountEl.textContent = `You are about to delete ${count} photo(s).`; bulkDeleteModal.classList.add('is-active'); } function closeBulkDeleteModal() { bulkDeleteModal.classList.remove('is-active'); } async function handleConfirmBulkDelete() { const ids = Array.from(selectedPhotoIds); if (ids.length === 0) { closeBulkDeleteModal(); return; } confirmBulkDeleteBtn.classList.add('is-loading'); try { await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE' }))); clearSelection(); fetchPhotos(); closeBulkDeleteModal(); } catch (error) { console.error('Error deleting photos:', error); alert('Some deletions may have failed. Please refresh and check.'); } finally { confirmBulkDeleteBtn.classList.remove('is-loading'); } } function bulkDeletePhotos() { openBulkDeleteModal(); } function parseTagsString(str) { return str.split(',').map(t => t.trim()).filter(Boolean); } async function bulkApplyEdits() { if (!selectedPhotoIds.size) return; const newCaption = bulkCaption.value.trim(); const tagStr = bulkTags.value.trim(); const hasCaption = newCaption.length > 0; const hasTags = tagStr.length > 0; if (!hasCaption && !hasTags) { alert('Enter a caption and/or tags to apply.'); return; } const ids = Array.from(selectedPhotoIds); const append = bulkAppendTags.checked; try { await Promise.all(ids.map(async (id) => { const photo = photos.find(p => p._id === id); if (!photo) return; const existingTags = Array.isArray(photo.tags) ? photo.tags : []; let finalTags = existingTags; if (hasTags) { const incoming = parseTagsString(tagStr); finalTags = append ? Array.from(new Set([...existingTags, ...incoming])) : incoming; } const payload = { caption: hasCaption ? newCaption : photo.caption, tags: (hasTags ? finalTags : existingTags).join(', ') }; await fetch(`${backendUrl}/photos/update/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); })); fetchPhotos(); clearSelection(); bulkCaption.value = ''; bulkTags.value = ''; bulkAppendTags.checked = false; } catch (error) { console.error('Error applying bulk edits:', error); alert('Some edits may have failed. Please refresh and verify.'); } } manageGallery.addEventListener('click', (e) => { if (e.target.classList.contains('edit-button')) { e.preventDefault(); const photoId = e.target.closest('.card').dataset.photoId; openEditModal(photoId); } if (e.target.classList.contains('delete-button')) { e.preventDefault(); const photoId = e.target.closest('.card').dataset.photoId; deletePhoto(photoId); } if (e.target.classList.contains('select-photo-checkbox')) { const id = e.target.dataset.photoId; if (e.target.checked) { selectedPhotoIds.add(id); } else { selectedPhotoIds.delete(id); } updateBulkUI(); } }); manageGallery.addEventListener('change', (e) => { if (e.target.classList.contains('select-photo-checkbox')) { const id = e.target.dataset.photoId; if (e.target.checked) { selectedPhotoIds.add(id); } else { selectedPhotoIds.delete(id); } updateBulkUI(); } }); selectAllPhotosBtn.addEventListener('click', (e) => { e.preventDefault(); toggleSelectAll(); }); clearSelectionBtn.addEventListener('click', (e) => { e.preventDefault(); clearSelection(); }); uploadForm.addEventListener('submit', (e) => { e.preventDefault(); const photoInput = document.getElementById('photoInput'); const captionInput = document.getElementById('captionInput'); uploadStatus.textContent = ''; uploadStatus.className = 'help mt-3'; uploadProgress.style.display = 'none'; uploadProgress.value = 0; const files = photoInput.files ? Array.from(photoInput.files) : []; if (!files.length) { uploadStatus.textContent = 'Please choose an image before uploading.'; uploadStatus.classList.add('has-text-danger'); return; } const formData = new FormData(); files.forEach(file => formData.append('photos', file)); formData.append('caption', captionInput.value); formData.append('tags', tagsInput.value); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; uploadProgress.value = percentComplete; } }); xhr.addEventListener('load', () => { uploadButton.classList.remove('is-loading'); uploadProgress.style.display = 'none'; if (xhr.status === 401) { handleUnauthorized(); uploadStatus.textContent = 'Session expired. Please log in again.'; uploadStatus.classList.add('has-text-danger'); return; } if (xhr.status < 200 || xhr.status >= 300) { const errorText = xhr.responseText; uploadStatus.textContent = `Upload failed: ${errorText || xhr.statusText}`; uploadStatus.classList.add('has-text-danger'); return; } try { const result = JSON.parse(xhr.responseText); const uploadedCount = Array.isArray(result?.uploaded) ? result.uploaded.length : 0; const skippedCount = Array.isArray(result?.skipped) ? result.skipped.length : 0; if (result?.success === false) { uploadStatus.textContent = result?.error || 'Upload failed.'; uploadStatus.classList.add('has-text-danger'); return; } uploadStatus.textContent = result?.message || `Uploaded ${uploadedCount || files.length} photo${(uploadedCount || files.length) === 1 ? '' : 's'} successfully!` + (skippedCount ? ` Skipped ${skippedCount} duplicate${skippedCount === 1 ? '' : 's'}.` : ''); uploadStatus.classList.add('has-text-success'); localStorage.setItem(LAST_TAGS_KEY, tagsInput.value.trim()); fetchPhotos(); uploadForm.reset(); preloadLastTags(); } catch (jsonError) { console.error('Error parsing upload response:', jsonError); uploadStatus.textContent = 'Received an invalid response from the server.'; uploadStatus.classList.add('has-text-danger'); } }); xhr.addEventListener('error', () => { uploadButton.classList.remove('is-loading'); uploadProgress.style.display = 'none'; uploadStatus.textContent = 'An unexpected error occurred during upload.'; uploadStatus.classList.add('has-text-danger'); }); xhr.addEventListener('abort', () => { uploadButton.classList.remove('is-loading'); uploadProgress.style.display = 'none'; uploadStatus.textContent = 'Upload cancelled.'; uploadStatus.classList.add('has-text-grey'); }); xhr.open('POST', `${backendUrl}/photos/upload`); uploadButton.classList.add('is-loading'); uploadProgress.style.display = 'block'; xhr.send(formData); }); function updateTagSuggestions() { if (!tagSuggestions) return; const uniqueTags = new Set(); photos.forEach(photo => { const rawTags = Array.isArray(photo.tags) ? photo.tags : String(photo.tags || '').split(','); rawTags.map(t => t.trim()).filter(Boolean).forEach(t => uniqueTags.add(t)); }); tagSuggestions.innerHTML = ''; Array.from(uniqueTags).sort().forEach(tag => { const option = document.createElement('option'); option.value = tag; tagSuggestions.appendChild(option); }); } function updateQuickTags() { if (!quickTagButtons) return; const tagCounts = {}; photos.forEach(photo => { const rawTags = Array.isArray(photo.tags) ? photo.tags : String(photo.tags || '').split(','); rawTags.map(t => t.trim()).filter(Boolean).forEach(tag => { tagCounts[tag] = (tagCounts[tag] || 0) + 1; }); }); const sorted = Object.entries(tagCounts) .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) .slice(0, 8) .map(entry => entry[0]); quickTagButtons.innerHTML = sorted.map(tag => ``).join(''); } function normalizeTagsInput(value) { return String(value || '') .split(',') .map(t => t.trim()) .filter(Boolean); } function addTagToInput(tag) { const existing = normalizeTagsInput(tagsInput.value); if (!existing.includes(tag)) { existing.push(tag); tagsInput.value = existing.join(', '); } } function preloadLastTags() { const last = localStorage.getItem(LAST_TAGS_KEY); if (last && tagsInput && !tagsInput.value) { tagsInput.value = last; } } if (quickTagButtons) { quickTagButtons.addEventListener('click', (e) => { const btn = e.target.closest('button[data-tag]'); if (!btn) return; addTagToInput(btn.dataset.tag); }); } if (captionToTagsButton) { captionToTagsButton.addEventListener('click', () => { const caption = captionInput.value || ''; const words = (caption.match(/[A-Za-z0-9]+/g) || []) .map(w => w.toLowerCase()) .filter(w => w.length > 2); const unique = Array.from(new Set(words)); unique.forEach(addTagToInput); uploadStatus.textContent = unique.length ? 'Tags pulled from caption.' : 'No words found to convert to tags.'; uploadStatus.className = 'help mt-3 ' + (unique.length ? 'has-text-success' : 'has-text-grey'); }); } updateBulkUI(); saveChanges.addEventListener('click', handleSaveChanges); modalCloseButton.addEventListener('click', closeEditModal); modalCancelButton.addEventListener('click', closeEditModal); applyBulkEdits.addEventListener('click', bulkApplyEdits); bulkDelete.addEventListener('click', bulkDeletePhotos); confirmBulkDeleteBtn.addEventListener('click', handleConfirmBulkDelete); cancelBulkDeleteBtn.addEventListener('click', closeBulkDeleteModal); bulkDeleteModalCloseBtn.addEventListener('click', closeBulkDeleteModal); // --- Store Status Management --- async function fetchStatus() { try { const response = await fetch('../update.json'); const data = await response.json(); const currentStatus = data[0]; messageInput.value = currentStatus.message; isClosedCheckbox.checked = currentStatus.isClosed; closedMessageInput.value = currentStatus.closedMessage; } catch (error) { console.error('Error fetching current status:', error); responseDiv.textContent = 'Error fetching current status.'; responseDiv.classList.add('is-danger'); } } updateButton.addEventListener('click', async () => { const data = [ { message: messageInput.value, isClosed: isClosedCheckbox.checked, closedMessage: closedMessageInput.value } ]; try { const response = await fetch('/api/update-status', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data }) }); const result = await response.json(); if (result.success) { responseDiv.textContent = 'Status updated successfully!'; responseDiv.classList.remove('is-danger'); responseDiv.classList.add('is-success'); } else { responseDiv.textContent = `Error: ${result.message}`; responseDiv.classList.remove('is-success'); responseDiv.classList.add('is-danger'); } } catch (error) { console.error('Error updating status:', error); responseDiv.textContent = 'An unexpected error occurred.'; responseDiv.classList.remove('is-success'); responseDiv.classList.add('is-danger'); } }); });