chris 7fce1632be feat: editable tag presets, next/prev modal nav, needs-tagging filter
- Restructured presets as single-tag accumulators (click multiple to build up tags)
- Added 6 new tags: bridal-shower, cocktail, signature, indoor, outdoor, mitzvah
- Fixed organic/garland alias conflict
- Presets stored in data/presets.json with full CRUD API (add, edit, delete from admin)
- Edit modal shows photo thumbnail, prev/next navigation, preset buttons
- Keyboard shortcuts: Ctrl+Enter to save, arrow keys to navigate, Esc to close
- "Needs tagging" filter in manage view shows only uncategorized/low-tag photos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:42:32 -04:00

353 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="admin.css">
</head>
<body class="admin-page">
<div id="login-modal" class="modal is-active">
<div class="modal-content">
<div class="box admin-login-card has-background-light">
<div class="has-text-centered mb-4">
<p class="tag is-info is-light">Beach Party Balloons</p>
<h1 class="title is-4 mt-2">Admin Access</h1>
<p class="subtitle is-6 has-text-grey">Sign in to manage gallery photos and store status.</p>
</div>
<form id="loginForm">
<div class="field">
<p class="control has-icons-left">
<input class="input" id="passwordInput" type="password" placeholder="Password" autocomplete="current-password" required>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<div class="field">
<p class="control">
<button class="button is-info is-fullwidth" id="loginButton">Log in</button>
</p>
</div>
</form>
</div>
</div>
</div>
<div id="admin-content" style="display: none;">
<nav class="navbar is-info is-spaced has-shadow" role="navigation" aria-label="main navigation">
<div class="navbar-brand is-size-1">
<a class="navbar-item" href="/">
<img style="background-color: white;" src="../assets/logo/BeachPartyBalloons-logo.webp" alt="Beach Party Balloons logo">
</a>
</div>
</nav>
<div class="admin-hero admin-hero-slim">
<div class="container">
<div class="is-flex is-align-items-center" style="gap: 0.75rem;">
<div>
<h1 class="title is-5 has-text-white mb-0">Admin Panel</h1>
<p class="is-size-7 has-text-white-ter">Beach Party Balloons</p>
</div>
</div>
</div>
</div>
<div class="container padding">
<div class="tabs is-boxed">
<ul>
<li class="is-active" data-tab="photo-tab">
<a><span class="icon is-small"><i class="fas fa-images"></i></span><span>Photo Gallery</span></a>
</li>
<li data-tab="status-tab">
<a><span class="icon is-small"><i class="fas fa-bell"></i></span><span>Store Status</span></a>
</li>
</ul>
</div>
<div id="photo-tab" class="tab-content">
<div class="columns is-variable is-5 photo-columns">
<div class="column upload-column">
<div class="box admin-card">
<p class="is-size-5 has-text-weight-semibold mb-1">Upload new photo</p>
<p class="is-size-7 has-text-grey mb-4">Add a caption and tags to keep the gallery searchable.</p>
<form id="uploadForm" novalidate>
<div class="field">
<label class="label">Photo</label>
<div class="file has-name is-fullwidth admin-file-input">
<label class="file-label">
<input class="file-input" type="file" id="photoInput" accept="image/*,.heic,.heif" multiple required
onchange="var n=this.files.length; document.getElementById('photoFileName').textContent = n>1 ? n+' files selected' : (this.files[0]?.name || 'No files selected')">
<span class="file-cta">
<span class="file-icon"><i class="fas fa-cloud-upload-alt"></i></span>
<span class="file-label">Choose photos…</span>
</span>
<span class="file-name" id="photoFileName">No files selected</span>
</label>
</div>
<p class="help is-size-7 has-text-grey mt-1">HEIC/HEIF auto-converted to WebP.</p>
</div>
<div class="field">
<label class="label has-text-dark">Caption</label>
<div class="control">
<input class="input has-background-light has-text-dark" type="text" id="captionInput" placeholder="A beautiful balloon arrangement." required>
<p class="help is-size-7 mt-1"><button id="captionToTags" type="button" class="button is-ghost is-small p-0">Use caption words as tags</button></p>
</div>
</div>
<div class="field">
<label class="label has-text-dark">Tags <span class="has-text-grey has-text-weight-normal">(comma-separated)</span></label>
<div class="control">
<input class="input has-background-light has-text-black" type="text" id="tagsInput" placeholder="classic, birthday" list="tagSuggestions" required>
<datalist id="tagSuggestions"></datalist>
<p class="help is-size-7 has-text-grey">Up to 8 tags per photo.</p>
</div>
<div class="buttons are-small mt-2" id="quickTagButtons" aria-label="Quick tag suggestions">
</div>
</div>
<div class="field">
<div class="is-flex is-justify-content-space-between is-align-items-center mb-2">
<label class="label mb-0">Presets</label>
<button type="button" class="button is-ghost is-small p-0" id="toggleManagePresets">Manage</button>
</div>
<div class="buttons are-small" id="presetButtons" aria-label="Tag presets"></div>
<div id="managePresetsPanel" style="display:none;" class="box has-background-white-bis p-3 mt-2">
<p class="is-size-7 has-text-weight-semibold mb-2">Current presets</p>
<div id="presetList" class="mb-3"></div>
<hr class="my-2">
<p class="is-size-7 has-text-weight-semibold mb-2" id="presetFormLabel">Add preset</p>
<div class="field is-grouped">
<input type="hidden" id="editPresetIndex" value="">
<div class="control is-expanded">
<input class="input is-small" type="text" id="newPresetName" placeholder="Name (e.g. Pastel)">
</div>
<div class="control is-expanded">
<input class="input is-small" type="text" id="newPresetTags" placeholder="Tags (e.g. pastel, balloon)">
</div>
<div class="control">
<button type="button" class="button is-primary is-small" id="savePresetBtn">Save</button>
</div>
<div class="control">
<button type="button" class="button is-light is-small" id="cancelPresetEdit" style="display:none;">Cancel</button>
</div>
</div>
</div>
</div>
<div class="control">
<button class="button is-primary is-fullwidth" id="uploadButton">
<span class="icon"><i class="fas fa-upload"></i></span>
<span>Upload</span>
</button>
</div>
<progress id="uploadProgress" class="progress is-primary mt-3" value="0" max="100" style="display: none;"></progress>
<p id="uploadStatus" class="help mt-3"></p>
</form>
</div>
</div>
<div class="column manage-column">
<div class="box admin-card">
<div class="is-flex is-justify-content-space-between is-align-items-center mb-3">
<div>
<p class="is-size-5 has-text-weight-semibold mb-1">Manage photos</p>
<p class="is-size-7 has-text-grey">Edit captions/tags or delete images.</p>
</div>
</div>
<div class="field mb-3">
<div class="is-flex" style="gap:0.5rem;">
<div class="control has-icons-left is-expanded">
<input class="input is-small has-background-light has-text-dark" type="text" id="manageSearchInput" placeholder="Search by caption or tag…">
<span class="icon is-small is-left"><i class="fas fa-search"></i></span>
</div>
<div class="control">
<button type="button" class="button is-small is-warning is-light" id="needsTaggingBtn" title="Show only photos that need tagging">
<span class="icon"><i class="fas fa-tag"></i></span>
<span>Needs tagging</span>
</button>
</div>
</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">
<button class="button is-small has-text-dark has-background-primary" id="selectAllPhotos">Select all</button>
</div>
<div class="column">
<p class="is-size-7 has-text-dark" id="selectedCount">0 selected</p>
</div>
<div class="column is-narrow">
<button class="button is-text is-small has-text-dark" id="clearSelection">Clear</button>
</div>
</div>
<div class="columns is-multiline">
<div class="column is-full-mobile is-half-tablet">
<label class="label is-size-7 has-text-dark">New caption <span class="has-text-grey has-text-weight-normal">(optional)</span></label>
<input class="input is-small has-background-white has-text-dark" type="text" id="bulkCaption" placeholder="Leave blank to keep captions">
</div>
<div class="column is-full-mobile is-half-tablet">
<label class="label is-size-7 has-text-dark">Tags <span class="has-text-grey has-text-weight-normal">(optional)</span></label>
<input class="input is-small has-background-white has-text-dark" type="text" id="bulkTags" placeholder="e.g. arch, pastel">
<label class="checkbox is-size-7 mt-1 has-text-dark">
<input type="checkbox" id="bulkAppendTags"> Append to existing tags
</label>
</div>
<div class="column is-full-mobile">
<div class="buttons are-small">
<button class="button is-primary" id="applyBulkEdits" disabled>Apply to selected</button>
<button class="button is-danger is-light" id="bulkDelete" disabled>Delete selected</button>
</div>
</div>
</div>
</div>
<div id="manage-gallery" class="columns is-multiline is-variable is-4 admin-gallery-grid">
<!-- Existing photos will be loaded here -->
</div>
</div>
</div>
</div>
</div>
<div id="status-tab" class="tab-content" style="display: none;">
<div class="box admin-card">
<p class="is-size-5 has-text-weight-semibold mb-1">Store status</p>
<p class="is-size-7 has-text-grey mb-5">Update the homepage banner and closed message.</p>
<div class="field">
<label class="label has-text-dark">
<span class="icon-text">
<span class="icon has-text-info"><i class="fas fa-bullhorn"></i></span>
<span>Scrolling announcement</span>
</span>
</label>
<div class="control">
<input class="input has-background-light has-text-dark" id="scrollingMessageInput" type="text" placeholder="e.g. Now booking for summer events!">
</div>
</div>
<div class="field">
<label class="label has-text-dark">
<span class="icon-text">
<span class="icon has-text-warning"><i class="fas fa-store-slash"></i></span>
<span>Store closed?</span>
</span>
</label>
<div class="control">
<label class="admin-toggle">
<input type="checkbox" id="isClosedCheckbox">
<span class="admin-toggle-track"></span>
<span class="admin-toggle-label" id="closedToggleLabel">Store is open</span>
</label>
</div>
</div>
<div class="field">
<label class="label has-text-dark">
<span class="icon-text">
<span class="icon has-text-danger"><i class="fas fa-exclamation-circle"></i></span>
<span>Closed message</span>
</span>
</label>
<div class="control">
<input class="input has-background-light has-text-dark" id="closedMessageInput" type="text" placeholder="e.g. We're closed for the season. Back in spring!">
</div>
<p class="help has-text-grey">Shown to visitors when the store is marked closed.</p>
</div>
<div class="control mt-5">
<button id="updateButton" class="button is-primary">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save changes</span>
</button>
</div>
<div id="response" class="notification is-light mt-4" style="display:none;"></div>
</div>
</div>
</div>
</div>
<!-- Edit Photo Modal -->
<div id="editModal" class="modal">
<div class="modal-background"></div>
<div class="modal-card has-background-light">
<header class="modal-card-head">
<p class="modal-card-title has-text-dark has-background-light" id="editModalTitle">Edit Photo</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<input type="hidden" id="editPhotoId">
<figure class="image mb-3" style="max-height:260px;overflow:hidden;border-radius:6px;">
<img id="editModalImg" src="" alt="" style="object-fit:contain;max-height:260px;width:100%;">
</figure>
<div class="field">
<label class="label">Caption</label>
<div class="control">
<input class="input has-background-light has-text-black" type="text" id="editCaption">
</div>
</div>
<div class="field">
<label class="label">Tags</label>
<div class="control">
<input class="input has-background-light has-text-black" type="text" id="editTags" placeholder="e.g. Arch, Classic, Birthday">
</div>
<div class="buttons are-small mt-2" id="editPresetButtons"></div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-space-between">
<div class="buttons">
<button id="editPrevBtn" class="button" title="Previous photo (←)"><span class="icon"><i class="fas fa-chevron-left"></i></span><span>Prev</span></button>
<button id="editNextBtn" class="button" title="Next photo (→)"><span>Next</span><span class="icon"><i class="fas fa-chevron-right"></i></span></button>
</div>
<div class="buttons">
<button id="saveChanges" class="button is-success" title="Save (Enter)">Save</button>
<button class="button" title="Cancel (Esc)">Cancel</button>
</div>
</footer>
</div>
</div>
<!-- Bulk Delete Confirmation Modal -->
<div id="bulkDeleteModal" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Confirm Deletion</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to delete the selected photos? This action cannot be undone.</p>
<p id="bulk-delete-count" class="has-text-weight-bold mt-2"></p>
</section>
<footer class="modal-card-foot">
<button id="confirmBulkDelete" class="button is-danger">Delete</button>
<button class="button" id="cancelBulkDelete">Cancel</button>
</footer>
</div>
</div>
<script src="admin.js" defer></script>
<script>
// Keep "Store is open/closed" toggle label in sync
document.addEventListener('DOMContentLoaded', () => {
const cb = document.getElementById('isClosedCheckbox');
const lbl = document.getElementById('closedToggleLabel');
if (cb && lbl) {
const sync = () => { lbl.textContent = cb.checked ? 'Store is closed' : 'Store is open'; };
cb.addEventListener('change', sync);
}
// Hide response div until there's a message
const resp = document.getElementById('response');
if (resp) {
const orig = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'textContent');
// Instead, just watch via MutationObserver
new MutationObserver(() => {
resp.style.display = resp.textContent.trim() ? '' : 'none';
}).observe(resp, { childList: true, subtree: true, characterData: true });
}
});
</script>
</body>
</html>