```html

Building a Real-Time Event Photo Gallery: Keely's Guest Page Architecture & the Sailor Board Feature

What Was Done

This session focused on shipping Keely's guest charter page — a single-event photo upload and gallery system — and laying groundwork for an aggregate "Sailor Board" feature. The guest page went live on production S3 post-event (2026-05-25 02:11 UTC) and includes four core capabilities: photo/video uploads with a 24-at-once cap, event-code gating to prevent spam, same-day Instagram hashtag integration, and moderation-queue handling. The session also involved shipping a booking-widget refactor to fix a critical UX bug: "Book a Sail" now opens the scheduling modal inline instead of bouncing users back to the homepage.

Technical Architecture: Guest Page Stack

Keely's page lives at a predictable S3 path:

/queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html

The architecture layers are:

  • Static HTML shell (22.5 KB, served from CloudFront / S3)
    • Feature toggle: FLIP_UTC = "2026-05-25T00:00:00Z" controls pre-sail (upload hidden) vs. post-sail (upload visible)
    • Time-based mode switch handles the 5 PM PT event boundary
  • Client-side upload handler (lines 514–550 in Keely's page)
    • Accepts: JPEG, PNG, WebP, HEIC/HEIF, MP4, MOV, AVI, WebM
    • Validates file count: Array.from(files).slice(0, 24).forEach(uploadFile)
    • Presign request flow: POST to /api/g/{event_id}/presign (Lambda endpoint)
  • Lambda photo handler (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py, function: shipcaptaincrew)
    • Presign endpoint: Validates event code, generates signed S3 URLs for client-side direct upload
    • Photo listing endpoint: GET /api/g/{event_id}/photos returns approved photo metadata + Instagram feed
    • Metadata store: DynamoDB table (implicit in Lambda code, likely QueenofsandiegoPhotos or similar)
  • S3 bucket hierarchy
    • s3://qos-guest-uploads/{event_id}/{filename} — raw uploads
    • s3://qos-guest-uploads/{event_id}/thumbs/{filename}.webp — 300×300 thumbnails (backfilled post-event)
    • CORS policy: Updated to allow queenofsandiego.com and staging.queenofsandiego.com origins for presign requests
  • CloudFront distribution (staging alias: staging.queenofsandiego.com)
    • Invalidation after page deploy: /* pattern to bust cache

Spam Prevention: Event Code Gating

The page includes a passive but critical security mechanism. When guests upload without an event code, their submissions enter a moderation queue instead of auto-publishing:

// From Keely's page (line 350–354)
const isCodeVerified = code.length > 0;
if (!isCodeVerified) {
  // Submit to moderation queue instead of direct publish
  metadata.status = 'pending_review';
}

The event code is provided to charter attendees via email (e.g., Keely's pre-sail email body in proposals/keely-email-body.html). This pattern accomplishes two goals:

  1. Reduces spam: Random internet users can't auto-publish to the gallery
  2. Increases social proof: Only guests who attended the event (and have the code) bypass moderation

Copy on the page makes this transparent: "The code keeps strangers from posting to your charter page."

Instagram Hashtag Integration

The page renders an IG feed grid (lines 452–459) pulled from the Lambda endpoint. The endpoint accepts two hashtags — #jada and #queenofsandiego — and searches Instagram's public API for posts from the event day:

GET /api/g/2026-05-24/photos
→ Returns: { photos: [...], instagram: [...] }

The IG feed is server-side fetched by Lambda (details in shipcaptaincrew/lambda_function.py, likely around the Instagram integration logic) and baked into the response. This keeps the client-side logic simple: just render whatever d.instagram contains. Why server-side? Lambda can cache IG results, avoid exposing client-side IG tokens, and batch-fetch across events without hammering the API on every page load.

Photo Thumbnail Backfill

Post-event, the session ran a backfill script (/tmp/backfill-thumbs.py) to generate 300×300 WebP thumbnails for gallery rendering. This happened after all uploads had concluded, keeping the presign/upload hot path lightweight. Thumbnails are stored alongside originals in S3 under a /thumbs/ prefix, and the Lambda endpoint's photo listing handler references them when available.

Booking Widget Refactor: Modal Instead of Redirect

The "Book a Sail" button had a critical flaw: it redirected to the homepage, losing context and creating a disjointed UX. The fix involved:

  1. Extracted booking logic from homepage to standalone booking-widget.js
  2. Embedded modal markup + CSS into Keely's page (and presumably the main homepage)
  3. Wired click handler to open modal inline instead of navigating away
  4. Kept Stripe checkout integration (JS dependencies) to maintain payment flow

The widget is now reusable across multiple pages — any page that includes booking-widget.js and the modal HTML can offer seamless bookings without redirects.

Infrastructure & Deployment

Lambda function lifecycle:

  • Function name: shipcaptaincrew
  • Runtime: Python (version extracted from Lambda config during session)
  • Source: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py