chris 50680a323f Major overhaul: shared nav, admin improvements, email enhancements, routing fixes
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>
2026-04-14 21:14:06 -04:00

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>