- Latex items now filtered to categories.includes('latex') — excludes
garlands, arches, and other non-balloon showColors items
- Latex UI replaced with collapsible color families (same as ColorPicker),
chip palette preview, and per-color remove buttons
- Multiple latex sizes show a size selector tab bar
- Modifiers always rendered on mylar cards (dimmed at qty=0, active when >0)
so Helium Weight and other options are always discoverable
- Added console.log for mylar modifier debug output on load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Latex items now fetched from /api/catalog (Online items with showColors)
instead of requiring Build-category items — latex section always shows
- Mylar/latex counter boxes are sticky so they stay visible while scrolling
- Modifier lists (e.g. Helium Weight) render per mylar item once qty > 0,
with price deltas included in running total and cart entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Items in the Square "Build" category don't need the "Online" category
to appear in the bouquet builder. A new /api/bouquet-items endpoint
calls getSquareCatalog({ filterCategory: 'build' }) which skips the
Online filter and returns only Build-category items directly from Square.
BouquetPicker now fetches from /api/bouquet-items instead of /api/catalog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both mylar and latex items come from the Build category in Square.
Add the 11" latex item to Build and it automatically gets the color
picker. Add any mylar/foil item to Build and it gets a quantity picker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously both mylar and latex items had to be in the Build category.
Now: mylar options = Build category items without showColors; latex
options = all showColors items in the catalog, no extra tagging needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The IIFE with an inline type declaration inside a JSX ternary didn't
compile. Pulled renderEntry and cartGroups into plain variables above
the cartBody const so the JSX stays clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags are never populated from Square, so the tag check never worked.
Any item whose name starts with "Build Your" now opens the bouquet picker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New BouquetPicker modal lets customers assemble a bouquet from catalog
items tagged with the Square "Build" category — up to 6 mylars (with
per-item quantity and variation selection) and 6 latex balloons (with
inline color swatch picker).
Product cards tagged 'bouquet-builder' in Square open the new picker
instead of ColorPicker. Each selected balloon is added to the cart as
its own line item grouped under a "Your Bouquet" header in the drawer,
with a single "Remove all" button for the whole bouquet.
The "build" category is hidden from the main catalog tab bar so
component items don't clutter the shop unless they're also in another
display category.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sold-out variant buttons now show "Sold Out" and can't be selected.
The picker also defaults to the first in-stock variation instead of
always starting on variations[0], which could be out of stock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, if any single variant hit 0 stock, the whole product card
would show as sold out and block ordering. Now it checks all tracked
variants — the product is only sold out when every variant is unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The route had a redundant isAuthed() checking for 'bpb_admin' cookie,
but login sets 'admin_token'. The middleware already guards all
/api/admin/* routes, so the in-route check was just wrong.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
High impact:
- Cart items now show product thumbnail (52px) so customers can visually
confirm what they ordered
- Delivery quote auto-fetches 800ms after the address stops changing,
removing the manual "Check availability" step; also persists across
sessions and restores when the same address is loaded from localStorage
- Calendar error fallback now shows a clear explanation before the
booking request form
Medium impact:
- "Copy shareable cart link" button in cart footer encodes the current
cart as a ?cart= URL param; opening the link re-hydrates the cart from
the catalog so customers can share or continue on another device
- Order status page at /order/[orderId] shows state, fulfillment time,
and items; linked from the post-checkout success screen
- Delivery quote is saved to localStorage and restored automatically
when the same address is loaded in a new session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete Hero, ReviewsSection, TrustedBrands components (never imported)
- Delete /api/admin/orders/[orderId]/complete route (never called; order
state transitions go through /status instead)
- Extract maxColorsFor() from ProductCard and CartDrawer into
src/lib/colors.ts to eliminate the duplicated implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Force explicit light-mode colors with inline styles so Bulma's dark
mode cannot override the closure banner text and background.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin panel shows a prominent open/closed toggle above the tabs. When
closed, the shop displays a branded closure message and the checkout API
returns 503. The closure state persists in data/store-status.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Unique title + meta description on every main-site page
- OpenGraph + Twitter card tags sitewide (hero image on homepage, logo elsewhere)
- LocalBusiness JSON-LD on homepage for Google rich results
- Custom 404 page on main-site (branded, links home + contact)
- Custom not-found page on estore
- Fix typo: "Delivery avalable" → "Delivery available"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace single Mark Complete button with contextual In progress / Ready /
Complete buttons based on current fulfillment state. Adds a general
/api/admin/orders/[orderId]/status endpoint that handles all transitions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Square requires all fulfillments to be COMPLETED before the order can be
marked COMPLETED — include fulfillment state in the same updateOrder call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetches open orders from Square filtered by source=online-shop metadata.
Each order shows customer, fulfillment time/address, items, and total with
a Mark Complete button that updates the order state in Square directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change fulfillment state from RESERVED to PROPOSED (Square rejects RESERVED)
- Return 503 from slots API when CalDAV is unreachable instead of serving empty
busy blocks that made all time slots appear falsely available
- Add BookingRequestPanel and /api/booking-request endpoint: when the calendar
server is down, customers can submit their order and preferred time; server
emails info@beachpartyballoons.com and sends a confirmation to the customer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move domain association file to estore/public/.well-known/ so Next.js
serves it, and add a /.well-known/ location block in nginx so Apple's
servers can reach it at the domain root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PaymentForm now initialises Google Pay and Apple Pay via Square's Web
Payments SDK alongside the existing card form; wallet buttons appear
above the card with an "or pay with card" divider when available
- Apple Pay domain verification file added to public/.well-known/
- square.ts: fix online-category filter to show all items when the
category doesn't exist; support multi-category display per item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nginx: add /photos and /uploads proxy routes to gallery-backend so the
browser can reach the gallery API without needing direct port access
- gallery.js: drop hardcoded port/subdomain fallbacks; use same-origin path
via the new nginx routes
- square.ts: pass buyerEmailAddress to createPayment so Square auto-sends
a payment receipt to the customer on capture
- square.ts: create fulfillments in RESERVED state (was PROPOSED) so staff
can mark orders complete/filled directly from the Square dashboard
- CartDrawer: merge Custom Vinyl into the Shape Balloon line item (one fewer
Square line item per vinyl order); show modifier price deltas in cart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tax was $0 on first production order — catalog items don't have tax
configured in Square Dashboard. Apply it programmatically via an
ad-hoc LINE_ITEM scoped tax on every line item. Delivery remains
untaxed (service charge taxable: false).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace plausible.io with metrics.beachpartyballoons.com across all
main-site pages and estore layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
border-radius was already set but invisible (white image on white background).
Changing the image background to a warm off-white makes the 12px rounded
corners show against the surrounding area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subtotal was always shown alongside Total even for pickup orders with
no additional charges, making both lines identical. Now the breakdown
(Subtotal / Delivery / Tax) only appears when there are fees beyond
the item total. Applies to both customer and store alert emails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all bare info@ occurrences with a click-to-reveal pattern:
- New EmailLink React component (base64 decode on click, never in DOM pre-click)
- privacy, terms, refund pages use EmailLink
- contact/index.html uses a vanilla JS button with the same pattern
- PaymentForm mailto builder uses atob() to keep email out of source literals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds leadTimeHours to HoursConfig. Slot generation, calendar minDate,
and pickup disabled-date precomputation all read from the config.
Admin hours page has a new input to adjust it without a redeploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
next:{revalidate:86400} was caching the OSRM URL in Next.js's on-disk
data cache, so old localhost:5002 requests replayed even after the env
var was updated to osrm:5000. Drive-time lookups need live responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CalDAV DTSTART/DTEND now use UTC (Z-suffix) instead of TZID local
time. Without a VTIMEZONE component, some CalDAV servers strip the
TZID on return, causing ical.js to read the times as UTC — shifting
every event 4 hours early and letting taken slots appear free.
Slot query range changed from ±6h around UTC midnight (36-hour window)
to 3AM–6AM UTC the next day (~27h) which covers the full ET business
day without pulling in afternoon events from the previous day.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CalDAV: joins were using literal '\n' strings which icalEscape then
double-escaped the backslash, so calendar entries showed raw \n. Now
joins use real newline chars which icalEscape converts correctly.
Added deliveryWindowMinutes to HoursConfig (default 60 min). The
checkout route reads this at request time to set both the Square
deliveryWindowDuration and the customer email arrival window. Admin
hours page now has a number input to configure it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vinyl line items now include the parent product name in their notes
("Vinyl add-on for: X" and "Add-on for: X | Text: ...") so the Square
dashboard and receipts show which item the vinyl belongs to.
Confirmation emails now show a 1-hour arrival window (was 2.5 hrs
because jobMin for classic tier was used; jobMin is still sent to
Square as deliveryWindowDuration for internal job scheduling).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert ASAP fallback — SCHEDULED is always correct; server validates before Square
- validateAndContinue now catches a cleared delivery/pickup slot and redirects
back to the delivery step with an inline error message rather than letting
the order reach Square with a missing deliver_at
- PaymentForm onError prop: 400-level checkout errors call onError so CartDrawer
can navigate the user to the right step to fix the problem
- On any payment error, show an "email us your order details" mailto link that
pre-fills name, phone, items, fulfillment, and total so we can issue an invoice
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scheduleType was hardcoded to SCHEDULED even when no deliverAt time was
provided, causing Square to reject with 400. Now uses ASAP when no slot
is present. Also added server-side validation to reject delivery orders
that arrive without a deliverySlotISO before they reach Square.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Performance:
- Add loading="lazy" decoding="async" to product card images
- Preconnect to Square S3 image CDN and fonts.googleapis.com in layout
- Cache-Control headers on catalog (20s), inventory (10s), occasions/categories (5min)
Scroll lock:
- Update useLockBodyScroll to use position:fixed + scroll-restore for iOS Safari
- Apply same fix to CartDrawer's inline scroll lock
Color names:
- Remove word-break:break-word so single words never split across lines;
multi-word names still wrap at spaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Balloon mask and color images correctly live at /color/images/ (served by
main site via nginx). Only the shine image path was wrong — it's at
/color/shine.svg, not /color/images/shine.svg. That missing file was the
actual cause of Safari showing ? placeholders. Previous commit incorrectly
moved everything to /color-picker/ which broke the CSS mask-image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix vinyl add-on checkout: product line item was dropped when vinyl selected; entryUnitPrice also excluded base product price
- Store vinyl per-letter price on cart entry so CartDrawer charges the config price, not hardcoded 65¢
- Fix two bare modifiers.find() calls (use optional chaining) to prevent checkout crash on bad data
- Validate deliveryCents (must be non-negative integer) and customer name fields (no control chars) in checkout API
- Validate rateOverride values are non-negative numbers in delivery-quote API
- Add RFC 5545 iCalendar escaping to SUMMARY/LOCATION/DESCRIPTION fields to prevent calendar injection
- Add public /api/hours route; pickup and delivery calendars now fetch admin-saved hours and pre-grey closed days
- Reset delivery quote and slot when high-rate item is removed from cart
- Change delivery window copy from 2 hours to 1 hour (DeliveryDatePicker + terms page)
- Fix SVG paths: /color/images/ → /color-picker/images/ (balloon mask, shine, color backgrounds); was causing Safari ? placeholders
- Enlarge padlock icon in PaymentForm from 11px to 14px for better alignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>