267 Commits

Author SHA1 Message Date
5e1db823cb Fix bouquet builder: bypass Online filter via dedicated API endpoint
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>
2026-06-18 13:09:13 -04:00
0e1d8e5af4 BouquetPicker: show category slugs in empty state for debugging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:04:24 -04:00
9779198d46 Fix Set spread TS error: use Array.from instead of spread syntax
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:54:37 -04:00
6f6a941007 BouquetPicker: log category slugs to help diagnose build filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:52:30 -04:00
b3df81fb25 Bouquet builder: pull all items from Build category only
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>
2026-06-18 12:42:48 -04:00
52a9828b96 Bouquet builder: mylars from Build category, latex from full catalog
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>
2026-06-18 12:37:23 -04:00
7b7c3efb65 Fix CartDrawer build error: move grouping logic out of JSX
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>
2026-06-18 12:08:34 -04:00
c72699da16 Fix bouquet builder detection: use name match instead of tags
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>
2026-06-18 12:01:54 -04:00
528fb90303 Add Build Your Own Bouquet feature
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>
2026-06-18 11:42:21 -04:00
f1537402b5 Disable sold-out variants in picker; default to first in-stock option
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>
2026-06-18 11:08:17 -04:00
904fa91bad Fix sold-out logic: only mark product sold out when all variants are exhausted
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>
2026-06-18 10:59:41 -04:00
699c5cbaa1 Fix HEIC crash: skip unsupported images gracefully
Sharp in this container lacks HEIF support, causing the entire form
submission to fail when someone uploads a HEIC file. Wrap each file
conversion in try/catch so unsupported formats are skipped with a
warning and the message still goes through.

Also remove HEIC from the advertised accepted formats in the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:44:37 -04:00
14fc9df9d2 Add server-side content filtering to block spam
- Require message to have at least 3 words — catches single-token
  random strings like 'EhdRpaTrHsSahuiuz'
- Require message to be at least 10 characters
- Validate email format server-side (was client-side only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:21:58 -04:00
eef6d0cb7d Revert gallery/server workarounds — fixed in NPMplus instead
The root cause (wrong port 5001 vs 5002 in NPMplus /uploads location)
is now fixed. Remove the window.location.origin fallback and the
Express proxy routes that were added while diagnosing the issue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:05:29 -04:00
ca4e52336a Proxy /photos and /uploads through main-site to gallery backend
NPMplus routes beachpartyballoons.com directly to main-site Express,
bypassing Docker nginx. Adding proxy routes here ensures same-origin
image URLs, eliminating OpaqueResponseBlocking in the browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 07:55:56 -04:00
cdeb2f5406 Fix gallery images blocked by OpaqueResponseBlocking
Try same-origin (/photos, /uploads via main nginx) before falling back
to photobackend subdomain, which returns 502 for static files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:21:19 -04:00
8b1e89cf04 Lower ALTCHA cost to 2000
50000 was taking >1 minute on some devices. 2000 should be ~1-2s in
most browsers while still being meaningful work for bots. The HMAC
key and expiration are the real security — cost is just friction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:23:55 -04:00
f59d19f3c2 Set ALTCHA to auto-solve on page load
auto="onload" starts the proof-of-work in the background as soon as
the page loads. By the time a user fills out the form it's already
verified — no manual click required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:21:10 -04:00
08ac545567 Harden ALTCHA: raise cost and add expiration
cost 100 → 50000: was solvable in milliseconds by a bot, now takes
~1-2s in a real browser, making mass automation impractical.

expiresAt 10min: embeds expiry in the HMAC-signed challenge so the
server rejects replayed tokens without needing to store seen challenges.

ALTCHA_HMAC_KEY must be set to a strong secret in production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:11:19 -04:00
10f9511c76 Add robots.txt and sitemap.xml
robots.txt blocks /admin/ from crawlers and points to the sitemap.
sitemap.xml lists all six public pages with appropriate priorities.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:09:02 -04:00
d20a2804ff Fix contact tile height stretching
align-items: flex-start lets each tile shrink to its content height
instead of stretching to match the tallest tile (Shop Hours).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 19:48:58 -04:00
ad5c984a46 Mirror live open/closed status to contact page
Add #status and #hours-* IDs to the contact page hours tile and load
update.js so the same live open/closed logic from the homepage runs
on the contact page. Guard against missing #message element so update.js
works on pages that don't have the marquee banner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 19:45:21 -04:00
f8ff7f0d94 Revert "Mirror contact tiles to homepage, consolidate hours into tile"
This reverts commit 483b36999b6bde786b47ef1d841a235475e26cfd.
2026-06-12 19:44:38 -04:00
483b36999b Mirror contact tiles to homepage, consolidate hours into tile
Replace plain-text address/phone/hours on the homepage with the same
three-tile card layout used on the contact page. Consolidates the
separate hours section into the tile, removing the duplicate. Dynamic
#status and #hours-* IDs are preserved inside the tile for script.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 19:43:29 -04:00
5cd7b29b63 Improve contact page layout and visual design
Add a gradient hero header, three icon-based contact tiles (address,
phone, hours), and a "Send a Message" heading above the form to give
the page more structure and visual interest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 19:38:39 -04:00
418fef1a15 Remove email obfuscation from contact page
The base64 atob() pattern is trivially decoded by scrapers, making it
ineffective. The contact form is the preferred channel anyway.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 19:36:54 -04:00
01cad11472 Fix ALTCHA widget not loading or submitting
Two bugs prevented form submission entirely:
1. `challengeurl` attribute was renamed to `challenge` in altcha v3 — the
   widget silently ignored the old name so it never fetched a challenge.
2. `altchaWidget.value` is not an exposed property on the v3 custom element;
   read the solved payload from the hidden `<input name="altcha">` the widget
   renders in light DOM instead.

Also clears the err-altcha error message at the start of each submit attempt
so it doesn't linger after the user completes verification and retries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:44:05 -04:00
5e6b201336 Add Cache-Control: no-store to ALTCHA challenge endpoint 2026-06-12 09:20:17 -04:00
c503d6dd75 Easter egg: trigger on copyright symbol click only
Previously fired on 5 rapid taps anywhere on the page.
Now triggers with a single click on the © in the footer.
Added id="bpb-copyright" to the symbol span in nav.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 06:03:15 -04:00
2ebbe4fbe0 Fix ALTCHA CDN path for v3 widget
v3 moved the widget from dist/altcha.min.js to dist/main/altcha.min.js;
the old path served a cached v2 widget which expected a 'challenge'
string field and threw split-on-undefined against v3's parameters format.
Pin to @3.1.0 to prevent future surprise upgrades.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 06:01:45 -04:00
c00f2de338 Upgrade main-site to Node 20 for Web Crypto globals
Node 18 requires --experimental-global-webcrypto for crypto.subtle /
crypto.getRandomValues as globals; Node 20 LTS exposes them by default,
which altcha/lib needs for createChallenge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 05:56:40 -04:00
e7af5bca4a Fix store-status route always returning 401
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>
2026-06-11 14:53:39 -04:00
9cac3a8e8a Fix altcha require path for Node 18 compatibility
Node 18 enforces the package exports map; the deep path
'altcha/dist/lib/index.umd.cjs' is not exported, but 'altcha/lib' is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:43:53 -04:00
0652339539 Add ALTCHA widget to main page contact form
The widget was only on the /contact/ page; main page form was unprotected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:25:27 -04:00
a89788f531 Add ALTCHA proof-of-work spam protection to contact form
- Server: /api/altcha generates a SHA-256 challenge (v3 API); /api/contact
  verifies the widget payload before processing the submission
- Widget: added <altcha-widget> from CDN above the submit button
- contact-form.js: blocks submission if altcha value is missing and
  appends it to FormData
- docker-compose.yml: passes ALTCHA_HMAC_KEY env var to main-site container
- package.json: added altcha@3.1.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:23:42 -04:00
548c19f3fa Add UX improvements: thumbnails, auto-quote, shareable cart, order status
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>
2026-06-10 08:26:22 -04:00
9dd4aff35e Remove dead code: unused components, duplicate logic, orphaned route
- 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>
2026-06-10 08:01:39 -04:00
f0b60f123d Fix store closure message rendering in dark mode
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>
2026-06-09 21:05:13 -04:00
781f990541 Add store kill switch to admin panel and estore
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>
2026-06-07 00:52:31 -04:00
2e5f253580 Polish: meta/OG tags, JSON-LD, 404 pages, typo fix
- 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>
2026-06-07 00:47:26 -04:00
0d57760df1 Contact form: set min date to today on event date picker 2026-06-07 00:35:32 -04:00
77318fb477 Contact form: split name into first/last, add ntfy notification
- First and last name are now separate required fields
- Server combines them into a full name for emails
- Sends a push notification to NTFY_URL on new inquiry (fire-and-forget)
- NTFY_URL env var wired through docker-compose

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:32:06 -04:00
cd18bd3937 Contact form: add emoji to subject, show event date instead of type 2026-06-07 00:27:50 -04:00
d026bc8217 Fix balloon animation: use Web Animations API instead of CSS custom properties in keyframes 2026-06-06 21:38:40 -04:00
75b20e6ca2 Add debug logging to easter egg trigger 2026-06-06 21:29:51 -04:00
181195dbbc Fix easter egg trigger: use touchstart on mobile, 5 taps in 3s
click events are unreliable on mobile due to scroll handling. Use
touchstart (fires immediately) for mobile and click for desktop with
deduplication. Lower threshold to 5 taps, widen window to 3 seconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:23:05 -04:00
066364d2b7 Fix easter egg trigger: 7 quick taps on non-interactive area
Long press on the logo link was unreliable. Switch to detecting 7 rapid
taps on any non-link/button area within 2 seconds — works on mobile and
desktop without conflicting with navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:15:26 -04:00
2002d7f35a Add balloon easter egg — long press logo to trigger
Hold the logo for 0.6s (works on mobile touch and desktop mouse) to launch
22 balloon silhouettes floating up from the bottom. Balloons drift with a
slight sway, have a shine highlight, and naturally disappear under the navbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:07:06 -04:00
5900ce817e Fix dark mode: add data-theme=light to all main-site HTML pages
Bulma 1.0 follows system dark mode preference if no theme is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:01:38 -04:00
6a2bf1f30b Fix nginx port: bind to 3000 instead of 80 for NPM compatibility
NPM proxies beachpartyballoons.com → host:3000. Binding to 80 conflicts
with NPM which owns that port.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:59:04 -04:00