```html

Building the Sailor Board: Photo Gallery Pipeline, Guest Upload Flow, and Deep-Link Booking Modal

What Was Done

During this session, we completed a full-stack implementation of the photo upload and moderation system for chartered event guest pages, with three critical fixes:

  • Verified and extended the photo upload pipeline (Lambda + S3 + DynamoDB) to support same-day Instagram hashtag ingestion and real-time gallery updates
  • Deployed the Sailor Board — a dedicated page at /sailor-board/index.html that aggregates photo galleries across charters and implements search/filtering
  • Fixed the "Book a Sail" button to open the booking modal immediately (via deep-link) instead of navigating to the homepage
  • Created and deployed a standalone booking widget (booking-widget.js) to enable embeddable checkout flows across multiple pages
  • Backfilled thumbnail images for existing photo uploads to improve gallery load performance

Technical Details: Photo Upload & Moderation Pipeline

The core upload flow lives in /tools/shipcaptaincrew/lambda_function.py. Here's the architecture:

Upload Handler (Lambda)

When a guest on a charter page (e.g., /g/2026-05-24-keely-afternoon) submits photos, the client-side handler in the guest page HTML calls a presigned S3 POST endpoint. The Lambda function:

  • Validates the event code (line ~424 in the guest page JS): If provided, photos are flagged for instant publish. If not, they enter a DynamoDB review queue for moderation.
  • Accepts 24 files simultaneously (enforced client-side: Array.from(files).slice(0, 24), line 515 in guest HTML). This prevents spam and keeps uploads manageable.
  • Stores metadata in DynamoDB: `{event_id, guest_id, file_key, status, timestamp, ig_source}`
  • Publishes success notifications: The Lambda returns copy like "Your photo is live. Welcome to the sailor board." (line 2030 in `lambda_function.py`) for approved uploads.

Same-Day Instagram Hashtag Ingestion

The guest page requests photos via:

GET /api/g/{event_id}/photos

This endpoint in the Lambda handler fetches:

  • Published guest uploads from S3 (keyed by event_id)
  • Instagram posts matching #jada or #queenofsandiego hashtags posted on the same calendar day as the charter

The IG fetch is handled by the Instagram Graph API integration in the Lambda. The response object includes an instagram array that gets rendered into the guest page's #ig-grid (lines 452–459 in guest HTML).

S3 & Bucket Structure

Photo files are stored in a structure like:

s3://queenofsandiego-charter-uploads/
  ├── events/
  │   └── {event_id}/
  │       └── {guest_id}/
  │           ├── {timestamp}-{filename}.jpg
  │           ├── {timestamp}-{filename}_thumb.jpg
  │           └── {timestamp}-{filename}.mp4
  └── metadata/
      └── {event_id}/
          └── index.json

CORS Configuration: S3 CORS was updated to allow presigned POST uploads from queenofsandiego.com origins. Without this, browser preflight requests would fail and block uploads.

The Sailor Board: Aggregation & Search

Previously, "Sailor Board" was just marketing copy—each charter had its own guest page with a local photo gallery. We've now built an actual cross-charter aggregation page:

  • File: /sailor-board/index.html
  • Endpoint: https://queenofsandiego.com/sailor-board
  • Data Source: Calls /api/charters/photos to fetch published photos across all past/current charters
  • Features:
    • Timeline view of all charters
    • Filter by hashtag (#jada, #queenofsandiego, etc.)
    • Infinite scroll / pagination
    • Lightbox preview for photos & videos

The page pulls from a DynamoDB query that scans across all `event_id` partitions and sorts by upload timestamp (descending).

Thumbnail Backfill & Performance

Existing photo uploads didn't have thumbnails generated. We created /tmp/backfill-thumbs.py to:

  • Enumerate all S3 objects in the uploads bucket
  • For each image, use Pillow to generate a 300×300px thumbnail (JPEG, quality 85)
  • Write the thumbnail back to S3 with a _thumb suffix

This reduced initial page load for the Sailor Board and guest galleries, since the client now loads small thumbnails first, then fetches full-res on demand.

Booking Modal Deep-Linking

Previously, the "Book a Sail" button on the homepage and guest pages navigated to /, forcing users to manually trigger the booking modal. We fixed this:

Changes to Homepage (index.html)

Added a URL hash fragment listener:

if (window.location.hash === '#book') {
  document.getElementById('booking-modal').style.display = 'block';
}

Updated all "Book a Sail" button links to point to #book instead of just /.

Booking Widget Module (booking-widget.js)

Extracted the booking modal, Stripe integration, and form handling into a reusable module:

  • Export: `window.QOSBookingWidget` — can be instantiated on any page
  • Dependencies: Stripe.js (loaded dynamically), Moment.js for date formatting
  • Usage: Guest pages and the Sailor Board can now embed the booking flow without duplicating code

The widget accepts a config object:

new QOSBookingWidget({
  containerId: 'booking-modal',
  stripePublishableKey: '...',
  onSuccess: (checkoutSession) => { /* redirect */ }
})

Infrastructure