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

208 lines
7.3 KiB
JavaScript

let statusInterval;
// Burger toggle and footer year are handled by nav.js
document.addEventListener("load", function () {
// Your code goes here
document.querySelectorAll("formFooter").style.display = "none";
});
document.addEventListener("DOMContentLoaded", function () {
// Close lightbox when clicking outside the image
document.querySelectorAll(".lightbox").forEach(function (lightbox) {
lightbox.addEventListener("click", function (event) {
if (event.target === lightbox) {
window.location.hash = ""; // Close lightbox
}
});
});
// Close lightbox when pressing Escape key
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
window.location.hash = ""; // Close lightbox
}
});
});
document.addEventListener("DOMContentLoaded", function () {
if (document.getElementById('photo-gallery')) {
return;
}
let images = Array.from(document.querySelectorAll(".gallery-item a"));
let lightboxImages = images.map(img => img.getAttribute("href"));
let currentIndex = 0;
function openLightbox(index) {
currentIndex = index;
let lightbox = document.getElementById("lightbox");
if (!lightbox) {
lightbox = document.createElement("div");
lightbox.id = "lightbox";
lightbox.className = "lightbox";
lightbox.innerHTML = `
<a href="#" class="close">&times;</a>
<a href="#" class="prev">&lsaquo;</a>
<a href="#" class="next">&rsaquo;</a>
<img id="lightbox-img" src="" alt="">
<p id="lightbox-caption" class="caption" style="color: white; text-align: center; margin-top: 10px;"></p>
`;
document.body.appendChild(lightbox);
}
// Ensure the close button listener is always attached when lightbox is opened
document.querySelector(".lightbox .close").addEventListener("click", function(event) {
event.preventDefault();
document.getElementById("lightbox").style.display = "none";
});
let lightboxImg = document.getElementById("lightbox-img");
let lightboxCaption = document.getElementById("lightbox-caption");
let caption = images[currentIndex].parentElement.querySelector(".caption").textContent;
lightboxImg.src = images[currentIndex].querySelector("img").src;
lightboxCaption.textContent = caption;
lightbox.style.display = "flex";
document.querySelector(".lightbox .prev").onclick = () => navigateLightbox(-1);
document.querySelector(".lightbox .next").onclick = () => navigateLightbox(1);
}
function navigateLightbox(direction) {
currentIndex += direction;
if (currentIndex < 0) currentIndex = images.length - 1;
if (currentIndex >= images.length) currentIndex = 0;
let lightboxImg = document.getElementById("lightbox-img");
let lightboxCaption = document.getElementById("lightbox-caption");
lightboxImg.src = images[currentIndex].querySelector("img").src;
lightboxCaption.textContent = images[currentIndex].parentElement.querySelector(".caption").textContent;
}
// Open lightbox when clicking a thumbnail
images.forEach((img, index) => {
img.addEventListener("click", (event) => {
event.preventDefault();
openLightbox(index);
});
});
// Close lightbox when clicking outside the image
document.addEventListener("click", (event) => {
if (event.target.classList.contains("lightbox")) {
document.getElementById("lightbox").style.display = "none";
}
});
// Close lightbox with Escape key, navigate with Left/Right arrows
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
document.getElementById("lightbox").style.display = "none";
} else if (event.key === "ArrowRight") {
navigateLightbox(1);
} else if (event.key === "ArrowLeft") {
navigateLightbox(-1);
}
});
const lightboxClose = document.querySelector(".lightbox .close");
if (lightboxClose) {
lightboxClose.addEventListener("click", function(event) {
event.preventDefault();
document.querySelector(".lightbox").style.display = "none";
});
}
// Swipe gestures for mobile
let touchStartX = 0;
let touchEndX = 0;
document.addEventListener("touchstart", (event) => {
touchStartX = event.changedTouches[0].screenX;
});
document.addEventListener("touchend", (event) => {
touchEndX = event.changedTouches[0].screenX;
handleSwipe();
});
function handleSwipe() {
let swipeDistance = touchStartX - touchEndX;
if (swipeDistance > 50) {
navigateLightbox(1); // Swipe left → Next image
} else if (swipeDistance < -50) {
navigateLightbox(-1); // Swipe right → Previous image
}
}
});
// Back-to-top button (guard for pages without the element)
(() => {
const mybutton = document.getElementById("top");
// Provide a global handler for legacy onclick usage even if button is absent
window.topFunction = () => {
document.documentElement.classList.remove('is-clipped');
document.body.classList.remove('modal-open');
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
// Ensure both scrolling contexts reset (covers iOS/Android quirks)
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
};
if (!mybutton) return;
function scrollFunction() {
if (document.body.classList.contains('modal-open')) {
mybutton.style.display = "none";
return;
}
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
if (scrollTop > 130) {
mybutton.style.display = "block";
} else {
mybutton.style.display = "none";
}
}
window.addEventListener('scroll', scrollFunction);
mybutton.addEventListener('click', (e) => {
e.preventDefault();
window.topFunction();
});
})();
// The open/closed sign function is now in update.js
function injectPlausibleScript() {
const plausibleDomain = "beachpartyballoons.com";
const scriptUrl = "https://metrics.beachpartyballoons.com/js/script.hash.outbound-links.js";
// Create the main script element
const script = document.createElement('script');
script.setAttribute('defer', '');
script.setAttribute('data-domain', plausibleDomain);
script.setAttribute('src', scriptUrl);
// Create the global plausible function snippet
const functionScript = document.createElement('script');
const functionContent = `
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) };
`;
functionScript.textContent = functionContent;
// Append both scripts to the document's <head>
document.head.appendChild(script);
document.head.appendChild(functionScript);
console.log('Plausible script and function have been injected into the head.');
}
// Run the function when the DOM is ready.
document.addEventListener('DOMContentLoaded', injectPlausibleScript);