Navigation & layout - Replace per-page hardcoded nav/footer with shared nav.js (client-side injection) - Add nginx reverse proxy back to docker-compose for clean localhost routing - Rename /color-picker/ to /color/ across nav, directory, and references eStore admin - Add variation hiding controls (mirrors existing modifier hiding) - Add delivery rate editor (base fee + per-mile per tier, persisted to data/) - Fix all missing BASE prefix on fetch calls (admin PATCH/DELETE, availability, slots, colors) - Mount estore/data/ as a Docker volume so admin config survives rebuilds Booking & calendar - Set pickup calendar events to TRANSPARENT (free) so they don't block delivery slots - Skip CANCELLED events in busy-time calculation - Re-check slot availability at checkout before charging (409 on conflict) Phone & email validation - Auto-format phone as (XXX) XXX-XXXX as user types - Require exactly 10 digits; tighten email regex Confirmation emails (store alert + customer) - Full item detail per line: name, price, add-ons, colors, note - Charges breakdown: subtotal, delivery fee, tax, total - Delivery window: simplified M/D/YY h:mm – h:mm AM/PM format - .ics calendar attachment on customer confirmation Delivery rates - Extract configurable rates to delivery-rates.ts (server-only, no fs in client bundle) - calcDelivery() accepts optional rates param; delivery-quote route passes configured rates Content - Change all "40+ latex colors" references to "70+" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
307 lines
17 KiB
HTML
307 lines
17 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="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-4">
|
|
<div class="control has-icons-left">
|
|
<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>
|
|
<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">Edit Photo</p>
|
|
<button class="delete" aria-label="close"></button>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
<input type="hidden" id="editPhotoId">
|
|
<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">
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<footer class="modal-card-foot">
|
|
<button id="saveChanges" class="button is-success">Save changes</button>
|
|
<button class="button">Cancel</button>
|
|
</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>
|