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>
With basePath=/shop the Next.js app can't serve /.well-known/ at the
domain root. Mount the file into the nginx container and serve it
directly instead.
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>
- Click anywhere on the top bar of a photo card to toggle selection
(replaces the tiny checkbox)
- Drag across the grid to rubber-band select multiple photos at once
- Selected cards show a blue ring + tinted header + solid checkmark icon
- Cards swept during drag show a green ring preview before releasing
- Fixed innerHTML += perf issue (now builds all cards then sets once)
- Thumbnails used in grid so page loads faster
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restructured presets as single-tag accumulators (click multiple to build up tags)
- Added 6 new tags: bridal-shower, cocktail, signature, indoor, outdoor, mitzvah
- Fixed organic/garland alias conflict
- Presets stored in data/presets.json with full CRUD API (add, edit, delete from admin)
- Edit modal shows photo thumbnail, prev/next navigation, preset buttons
- Keyboard shortcuts: Ctrl+Enter to save, arrow keys to navigate, Esc to close
- "Needs tagging" filter in manage view shows only uncategorized/low-tag photos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Photos reseeded from disk now sort by their original upload time
instead of all getting the same insertion timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously all .webp files were indexed including thumbnails and medium
variants, causing each photo to appear three times in the gallery.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Loads bpb-watermark.svg at startup, converts black fills to semi-transparent
white (35% opacity), and composites it centered at 45% image width over every
uploaded photo. Removes the old diagonal "BEACH PARTY BALLOONS" text overlay.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gallery backend: replace origin whitelist with wildcard CORS — NPMplus
was stripping the Allow-Origin header; wildcard passes through reliably
and is appropriate for a public photo gallery
- gallery.js: hardcode photobackend.beachpartyballoons.com as the API base
(NPMplus already routes this subdomain) and remove dead port fallbacks
- nginx.conf: add /photos and /uploads proxy routes to gallery-backend
(kept for direct-nginx access; NPMplus handles external traffic)
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
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>
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>
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>