```html

Building the Sailor Board: Post-Event Photo Gallery & Real-Time Instagram Integration for Charter Guest Pages

What Was Done

This session delivered a complete photo upload and curation system for Queen of San Diego's per-charter guest pages. The "Sailor Board" is a dynamic gallery that appears on each event's guest page (e.g., /g/2026-05-24-keely-afternoon) immediately after a charter concludes. It combines three data sources: guest-uploaded photos/videos, moderated submissions from a Lambda review queue, and real-time Instagram posts matching charter-specific hashtags.

Key features shipped:

  • Client-side multi-file upload: Accept up to 24 photos/videos per submission; validate file types (JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM)
  • Guest code spam gate: Event-specific code enables instant publish; missing code routes to moderation queue
  • Server-side moderation flow: Lambda review queue with email notifications to event organizer
  • Same-day Instagram hashtag integration: Fetch and render #jada and #queenofsandiego posts from the charter date
  • Booking widget modal: "Book a Sail" link now opens checkout in-modal instead of navigating away

Technical Architecture

File Paths & Structure

Guest page template:

/Users/cb/Documents/repos/sites/queenofsandiego.com/g/{event_id}/index.html

Example: Keely's post-sail guest page lives at queenofsandiego.com/g/2026-05-24-keely-afternoon and is stored in S3 at that path. The local repository does not currently track guest pages as checked-in sources; they are generated and deployed directly to S3 during event setup.

Lambda function handling photo operations:

/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py

This single Lambda handles multiple routes:

  • POST /api/g/{event_id}/photos — Accept file uploads, validate code, enqueue for moderation or publish directly
  • GET /api/g/{event_id}/photos — Retrieve approved photos + Instagram feed for the event date
  • GET /api/g/{event_id}/moderate — Admin review queue
  • POST /api/g/{event_id}/moderate — Approve/reject submissions

Booking widget (newly extracted):

/Users/cb/Documents/repos/sites/queenofsandiego.com/booking-widget.js

This ~15 KB standalone module contains the Stripe checkout modal initialization and payment flow. It's embedded in guest pages via a <script> tag and triggered by "Book a Sail" click handlers.

Upload Flow

Client side (guest page):

  • File input element (#file-input) accepts multiple files; HTML5 accept attribute constrains to image/video MIME types
  • handleFiles() function (line 514 in Keely's page) retrieves files, slices to first 24, and calls uploadFile() for each
  • Each uploadFile() request is a multipart POST to /api/g/{event_id}/photos with fields: file, code (optional event code), uploader_name, uploader_email
  • Guest enters event code in #g-code input if they have it; code determines publish vs. queue behavior

Server side (Lambda):

  • Extract multipart body and validate MIME type against allowlist
  • If code matches event's code field in DynamoDB: bypass moderation, write to S3 photo bucket and DynamoDB gallery table, return success
  • If no code or code mismatch: store in queue DynamoDB table, send email to event organizer with moderation link
  • Generate thumbnail on write via Pillow (Lambda layer) for fast gallery rendering
  • Store metadata (uploader name/email, timestamp, original file size) in DynamoDB for audit and display

Instagram Integration

The Lambda has an embedded Instagram API client (using requests library) that fires during the GET /api/g/{event_id}/photos call:

  • Query Instagram for posts with hashtags #jada and #queenofsandiego from the event's date (e.g., May 24, 2026)
  • Filter results to only posts within the 24-hour window of the charter
  • Download media URLs and thumbnails; store pointers in the same response payload as guest photos
  • Client renders Instagram posts in a dedicated section (#ig-grid) using the same grid styling as guest uploads

This approach avoids client-side API calls (which would require exposing Instagram credentials) and centralizes filtering logic on the backend where rate limits and authentication are managed securely.

S3 & CloudFront Deployment

Photo bucket: s3://qos-guest-uploads/ (event-scoped prefix: 2026-05-24-keely-afternoon/)

Photo metadata DynamoDB table: qos_guest_photos (partition key: event_id, sort key: photo_id)

Moderation queue table: qos_photo_queue (partition key: event_id, sort key: submission_ts)

Guest pages are published to the main S3 website bucket and fronted by CloudFront distribution E2ABCD1234EF5` (staging: E5XYZ9876ABC3`). The Lambda is exposed via API Gateway with base path shipcaptaincrew.queenofsandiego.com and routed through a separate CloudFront distribution for API caching (TTL: 0 for photo POST, 60s for GET).

S3 CORS policy update: To allow guest pages to issue XHR requests to the presigned upload endpoint, CORS was updated to allow Origin: https://queenofsandiego.com and https://www-staging.queenofsandiego.com with methods GET, POST, PUT.

Booking Modal Integration

Previously, "Book a Sail" links on the homepage and guest pages navigated to /book