191 Commits

Author SHA1 Message Date
5643153a05 feat: add Magenta (#a01357) to Pinks & Reds color family
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 10:41:50 -04:00
07ae012aa3 feat: add Green Tea (#8a9f7f) to Greens color family
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 10:39:34 -04:00
ed5db69a90 fix: replace placeholder phone number in privacy policy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:44:17 -04:00
e2af78ff55 fix: nginx waits for estore healthcheck before starting
Nginx resolves upstream hostnames at boot time; if estore isn't
registered in Docker DNS yet it crashes in a restart loop.
Using service_healthy lets nginx wait until the Next.js app
passes its healthcheck before nginx attempts to start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:36:04 -04:00
57cc5840b9 feat: obfuscate email with click-to-reveal across all pages
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>
2026-05-08 10:23:50 -04:00
3330c47af2 fix: secure admin API endpoints with Bearer token auth
- main-site/server.js: add requireAuth middleware to POST /api/update-status
- gallery-backend/routes/photos.js: add requireAuth to upload, delete, and update routes
- admin/admin.js: send Authorization: Bearer header on all mutating requests (fetch + XHR upload); handle 401 on update-status and photo save
- docker-compose.yml: pass ADMIN_PASSWORD to gallery-backend; remove MongoDB public port mapping (27017:27017)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:30:58 -04:00
c40db43c04 fix: replace literal \u2014 escape with em dash in notes placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 07:57:27 -04:00
f969e5d242 feat: configurable booking lead time in admin (default 48h)
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>
2026-05-08 07:48:13 -04:00
134705792c fix: pass OSRM_URL from root .env into estore container
env_file only reads estore/.env; the root .env value wasn't reaching
the container. Wiring it through compose environment: fixes this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:40:53 -04:00
c240ec4ce6 fix: disable Next.js fetch cache for OSRM requests
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>
2026-05-05 16:19:50 -04:00
5bebd51ac4 fix: log OSRM failure reason and URL instead of silent fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:15:40 -04:00
175305a28f fix: calendar event UTC times and slot query range
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>
2026-05-05 14:52:06 -04:00
bb6c8a03a7 fix: calendar newlines, admin delivery window setting
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>
2026-05-05 10:57:37 -04:00
ffd07e35bd fix: vinyl order attribution and 1-hour customer delivery window
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>
2026-05-05 10:51:18 -04:00
2be379a029 fix: delivery slot required; redirect user back if missing; email fallback on error
- 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>
2026-05-05 10:47:44 -04:00
bc0540d36a fix: delivery order failing with MISSING_REQUIRED_PARAMETER from Square
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>
2026-05-05 10:42:25 -04:00
ec748c75a9 perf+fix: lazy images, API caching, iOS scroll lock, color name wrapping
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>
2026-05-05 10:13:50 -04:00
0d95cf93b3 fix: correct shine.svg path and revert balloon mask to /color/ routes
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>
2026-05-05 09:36:31 -04:00
9d02417059 fix: pre-launch audit, calendar closed days, delivery rate reset, and swatch paths
- 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>
2026-05-05 09:22:42 -04:00
68a987a921 fix: force-dynamic on admin items route to prevent stale cached responses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 15:25:50 -04:00
7d7d46af32 Make vinyl an optional add-on with checkbox and additive pricing
- Vinyl is now opt-in via a checkbox (unchecked by default)
- Item's base price is preserved; vinyl cost is added on top
- Checkbox label explains it's lettering on a separate 18" foil balloon
- Price breakdown shows vinyl as an add-on, not a replacement
- Validation only requires text/font when the checkbox is checked
- Editing a vinyl cart entry pre-checks the checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:14:42 -04:00
be7f98a347 Add admin backup and restore for all config files
Download button exports item-overrides, delivery-rates, categories-display,
occasions, hours, and vinyl-config as a single JSON file. Restore button
applies a previously downloaded backup (skips vinyl-config to avoid
overwriting it). Both accessible from the admin header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:09:34 -04:00
1dc8a087b6 Add vinyl configurator feature and admin sync from balloons-shop
- vinyl-config route + data file for shape/font/pricing config
- CatalogItem: vinylEnabled, vinylPromo fields
- ItemOverride: vinylEnabled, vinylPromo fields
- catalog route: applies vinylEnabled/vinylPromo overrides
- ColorPicker: full vinyl configurator UI (shape picker, text/font, pricing)
- CartContext: vinyl cart fields (vinylText, vinylFontId, vinylShape, etc.)
- CartDrawer: vinyl line items flatMap (shape balloon + custom vinyl service)
- admin/items route: synced more-complete version from balloons-shop
- admin page: vinyl configurator and promo note checkboxes in ItemEditor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:01:28 -04:00
7bc84cea75 fix: shop catalog always reflects latest data after admin changes
- Add force-dynamic to /api/catalog so Next.js never serves a
  stale cached route response to the shop
- Add invalidateCatalogCache() to catalog-cache lib to drop the
  30s in-process memory cache on demand
- Call invalidateCatalogCache() after every admin PATCH/DELETE on
  an item so override saves are reflected on the very next shop
  request (no 30s delay)

Refresh from Square already updated the shared disk + memory cache;
force-dynamic ensures the shop route handler actually runs each time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:12:41 -04:00
27093bcd54 fix: multi-category checkboxes in admin + requires-delivery toggle
- Category selector replaced with checkboxes — items can now be
  assigned to multiple categories directly in admin (not just Square).
  Each category shows a "Square" label if it came from the Square
  assignment. Saves as categoriesOverride[] (array of category names).
- categoriesOverride takes precedence over old categoryOverride in the
  catalog route; old overrides still work as fallback.
- Requires-delivery toggle and custom rate fields were already in the
  code but needed container rebuild to appear — no logic change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 09:44:00 -04:00
0ea1b98a1f feat: required delivery toggle with custom rates per item
Items can now be marked as "requires delivery" in admin — these items
cannot be picked up and must be delivered (and struck).

- Admin item editor: "Requires delivery" checkbox + custom base/per-mile
  rate fields that appear when the toggle is on
- ProductCard: "Delivery & setup required" note on the card
- CartDrawer: pickup toggle is hidden and replaced with an explanation
  when any cart item requires delivery; the quote call passes the
  item's custom rate override (highest base + highest per-mile wins
  when multiple requires-delivery items are in the cart)
- delivery-quote API: accepts optional rateOverride to apply per-item
  pricing on top of the inferred tier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 09:31:29 -04:00
107ef43a0e fix: hide category tab when it's already shown as an occasion tab
If a "Mothers Day" or "Graduation" occasion is active and its
squareCategorySlug matches a product category, suppress the duplicate
regular category tab so it doesn't appear twice in the bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:45:46 -04:00
623b237826 feat: multi-category items and fix new items not appearing
Items can now belong to multiple Square categories and appear in all
matching tabs (e.g. a Mother's Day balloon also shows under Easter).

Also fixes new items not appearing when the Square account has no
"online" category — previously this caused zero items to load; now
it falls back to showing all items.

Changes:
- CatalogItem gains categories[] + categoryLabels[] (multi-category)
- square.ts collects all non-skip categories per item; "online" filter
  is now optional (show all if category doesn't exist in Square)
- catalog/route.ts propagates categoryOverride into categories[0]
- FeaturedProducts: tabs and filter use the full categories array
- Admin CategoryDisplayEditor sees all categories from multi-cat items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:39:31 -04:00
84ab6bef2d feat: featured items — admin toggle, badge, sorted to top
- Add featured to ItemOverride so it can be set per-item in admin
- Catalog API applies the override and sorts featured items before
  non-featured (within each group, sortOrder still applies)
- ProductCard shows a teal Featured badge on the image when featured
  and not sold out
- Admin item editor has a  Featured checkbox beside Hidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:21:33 -04:00
6705293e50 fix/feat: hex conflict, scroll-to-top, search all, admin error emails
- Fix Chrome Rose Gold hex (#B76E79 → #C17F87) so it no longer
  conflicts with Classic Rose Gold; image still used for display
- ScrollToTop hides when cart drawer is open and uses z-index 98
  (below the drawer); uses drawerOpen from CartContext
- Search now switches to All tab automatically so results span every
  item, not just the active category
- Add sendAdminErrorAlert() to notify.ts; checkout route emails
  admin@beachpartyballoons.com on unexpected server errors and on
  critical calendar-write failures; card decline errors are not
  forwarded (customers can self-resolve those)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:19:29 -04:00
01c908e919 fix: color picker selection keyed on name instead of hex
Classic Rose Gold and Chrome Rose Gold share the same hex (#B76E79),
so clicking one would deselect the other. Switched all selection
checks (toggle, remove, highlight) to use color.name which is unique.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:08:57 -04:00
6865d2d437 fix: lock body scroll when any modal or drawer is open
Add useLockBodyScroll hook (sets overflow:hidden on body, restores on
unmount) and apply it to ColorPicker, AdminColorFilter, WelcomeModal,
and GuidedTour. CartDrawer uses an inline effect keyed on drawerOpen
since it is always mounted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:12:07 -04:00
e95ec68931 feat: admin color availability filter per item
- Add disabledColors field to ItemOverride and CatalogItem
- Propagate through catalog API applyOverrides
- ColorPicker filters disabled colors out before showing to customers
- New AdminColorFilter modal: same collapsible family layout and balloon
  swatches as the customer view; click to hide/show individual colors;
  Enable all / Disable all shortcuts; badge shows count of hidden colors
- Button appears in the color limits section for color-enabled items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:00:32 -04:00
1861e10d6d fix: restore missing next/server imports + add force-dynamic to admin routes
A botched sed command stripped the first import line from every admin
route file, breaking NextRequest/NextResponse references. Restored all
imports and added export const dynamic = 'force-dynamic' to all admin
GET handlers so Next.js 14 never serves a stale cached response after
a save — this was the root cause of changes appearing not to save.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 08:50:34 -04:00
f2fa8e3c17 fix: zoom chrome/metallic preview dots to 220% background-size
Image-based colors (chrome/metallic) have a balloon silhouette against
a transparent bg, so cover was fitting the whole image including
whitespace. 220% zooms into the center where the finish actually is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:51:24 -04:00
c22b668bc5 fix: update /color-picker/ → /color/ in estore ColorPicker and CSS
ColorPicker.tsx was constructing image URLs with the old /color-picker/
prefix. globals.css had the same for the balloon-mask.svg SVG mask.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:47:16 -04:00
0576677523 feat: scroll-to-top button in estore; fix JS/CSS cache headers on main site
- Add ScrollToTop component matching main site's green Top button
  (appears after 130px scroll, same styling and font)
- Fix main-site server.js: JS/CSS now use max-age=3600 + must-revalidate
  instead of 30d immutable — changes reach users within 1 hour instead
  of being stuck in browser cache for a month
- Images/fonts keep 30d immutable (safe, as they are content-addressed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:41:42 -04:00
c6d5a0265f fix: tour init on All tab + 11" Latex card; fix modal title truncation
- Tour now switches to the All tab and clears search on start, ensuring
  the 11" Latex product is always visible and the exit overlay works
- data-tour="first-card" now targets the 11" Latex item by name instead
  of whichever card happens to be first in the filtered list
- Modal header title now truncates with ellipsis so the X close button
  is never pushed off screen by a long product name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:39:45 -04:00
6fea1f2be1 fix: hide delivery line in order summary when pickup is selected
quote was non-null after entering a delivery address, so the delivery
fee row showed even after switching back to pickup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:34:38 -04:00
c130f9bcdf nginx: redirect /color-picker/* to /color/*
Browsers with cached pages from the old /color-picker/ path resolve
relative image URLs against that base, causing 404s after the rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:32:25 -04:00
e2d9ae7541 nginx: redirects for legal pages, gzip, security headers
- 301 redirects /privacy|terms|refund → /shop/* (pages live in estore)
- gzip compression for HTML/CSS/JS/JSON/SVG
- X-Frame-Options, X-Content-Type-Options, Referrer-Policy headers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:44:56 -04:00
f4b1f7722e Fix data dir permissions and legal doc links
- Dockerfile: create /app/data owned by nextjs before USER switch so fresh
  deployments work without manual chown. Existing servers need:
    sudo chown -R 1001:1001 estore/data
- nav.js: fix footer legal links to point to /shop/privacy|terms|refund
  (pages live in estore, not main site)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:28:20 -04:00
215a8f2e3f Add Plausible Analytics to color page and estore
Both were missing tracking. All pages now report to beachpartyballoons.com
in Plausible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 21:25:06 -04:00
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
9f9f326af9 Add root docker-compose and osrm data directory 2026-04-13 19:27:07 -04:00
668ee46ba6 Add root .gitignore 2026-04-13 19:22:46 -04:00
c984c14085 Remove terms page — now lives in estore footer 2026-04-13 19:22:36 -04:00
f58ae2c5f7 Add 'main-site/color-picker/' from commit '248d73a619ea4fbdca711a516f464cd0a505bfae'
git-subtree-dir: main-site/color-picker
git-subtree-mainline: 21ebb9667b34023f8d563bf8fa2abf7f838f51d7
git-subtree-split: 248d73a619ea4fbdca711a516f464cd0a505bfae
2026-04-13 19:22:30 -04:00
21ebb9667b Add 'estore/' from commit 'e34dfc397c94025670baa2b73b482c01f3033a6a'
git-subtree-dir: estore
git-subtree-mainline: 746868d720b9be1003a2f783b7a12d526d8eea60
git-subtree-split: e34dfc397c94025670baa2b73b482c01f3033a6a
2026-04-13 19:22:23 -04:00
746868d720 Add 'main-site/' from commit '5cefb4d1618bc54ae0e86830421a8c911900302c'
git-subtree-dir: main-site
git-subtree-mainline: 4d1daa39101c0a85ca6d916f1c31139faf39632a
git-subtree-split: 5cefb4d1618bc54ae0e86830421a8c911900302c
2026-04-13 19:22:17 -04:00