# Plan: Zoé & Shaldon Wedding Site

## Goal
QR-code-accessible site for save-the-date, RSVP, and guest photo uploads. Wedding date: **7 November 2026**.

## Stack
- **Frontend**: SvelteKit, `adapter-static`, prerendered. No SSR needed.
- **UI/theming**: Tailwind CSS + [Skeleton](https://skeleton.dev) (Svelte), custom theme for the blue/yellow palette — Skeleton handles light/dark switching itself, no hand-rolled CSS variables needed.
- **Backend**: Go, single binary, `embed.FS` serves the built static site + JSON API on one port.
- **Storage**: SQLite via `modernc.org/sqlite` (pure Go, no cgo — keeps the Docker build static/`CGO_ENABLED=0`) for RSVP/upload metadata. Photos on disk.
- **Email**: Go `net/smtp` (STARTTLS) to notify the couple on new RSVP / photo upload.

## Repo layout
```
zoe-shaldon/
├── ui/             SvelteKit frontend
│   ├── tailwind.config.ts
│   ├── assets/photos/    curated couple photos (checked in), source for the background slideshow
│   └── src/
│       ├── app.css              Tailwind + Skeleton custom theme (wedding-theme)
│       ├── lib/components/PhotoBackground.svelte   ambient slide-in/out photo background
│       └── routes/
│           ├── +page.svelte        landing: hero, countdown, photo background, nav
│           ├── rsvp/+page.svelte   RSVP form
│           └── photos/+page.svelte upload form (gated) + status message before open date
├── server/         Go backend
│   ├── main.go
│   ├── internal/api/     HTTP handlers
│   ├── internal/store/   SQLite access
│   ├── internal/mail/    SMTP sending
│   └── web/              embedded static build (go:embed)
├── data/           runtime data, gitignored
│   ├── rsvp.db
│   └── photos/<upload-id>/<file>
├── concept/        source assets (save-the-date image)
├── config.example.yaml   template, checked in
├── config.yaml           real config, gitignored
├── Dockerfile
└── docker-compose.yml
```

## Data model (SQLite)
```
rsvps(id, message, created_at)
rsvp_guests(id, rsvp_id FK, name)
photo_uploads(id, name, email, phone, message, created_at)
photo_files(id, upload_id FK, filename, path, url, created_at)
```
RSVP is a party: one or more guest names attached to a single RSVP + optional shared message — no email/phone captured for RSVP (contact for RSVP questions is the phone number on the invite instead). Photo upload keeps its own name/email/phone/message, unchanged.

## API
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | `/api/rsvp` | `{names: string[], message?}` | at least one non-blank name required; insert party + guests, email couple |
| GET | `/api/config` | — | `{weddingDate, ceremonyTime, rsvpByDate, venueName, venueAddress, rsvpPhone}` (all RFC3339 where dates) — single source of truth for countdowns, gate, and displayed details |
| POST | `/api/photos/upload` | multipart: `name, message?, email?, phone?, files[]` | rejected with 403 if `now < weddingDate`, **enforced server-side** regardless of client clock |
| GET | `/healthz` | — | liveness |

## Countdowns
- Global: a compact countdown to `weddingDate` sits below the nav bar on every page (`+layout.svelte`). Once passed, it reads "Already married! 🎉" instead of digits.
- Landing page: hero shows the day/date (e.g. "Saturday, 7 November 2026"), `ceremonyTime` ("16:00 for 16:30"), and venue name/address — all from `GET /api/config`. RSVP button is hidden once the guest has RSVP'd (tracked client-side via `localStorage`, see below); replaced with a thank-you note.
- RSVP page: its own live countdown to `rsvpByDate` ("RSVP by <date>"). Once `rsvpByDate` passes, the countdown and form are replaced by an "RSVP missed" notice pointing to `rsvpPhone`.
- Photos page: reuses `weddingDate` — before that time shows "uploads open on the wedding day" instead of the upload form (no separate countdown here, the global one covers it).
- One date per purpose, no duplicated "opens at" settings to keep in sync.

## RSVP-submitted tracking
No accounts/auth — `localStorage` flag (`zoe-shaldon-rsvp-submitted`) set after a successful `POST /api/rsvp`, read by the landing page to decide whether to show the RSVP button or a confirmation. Per-browser only; a guest RSVPing from a different device won't see the flag, which is an acceptable prototype limitation.

## Photo background (landing page)
Curated couple photos already checked in at `ui/assets/photos` (33 images) — separate from `data/photos/` (runtime, guest-uploaded, gitignored). These are the source for an ambient background behind the hero content.

- `PhotoBackground.svelte`: full-bleed layer behind the hero text, cycling through a shuffled list of the photos on an interval (~6s). Each photo slides in from one edge and slides out the opposite edge (CSS `transform: translateX` + `opacity` transition, one photo at a time, next one queued behind).
- Photo list built at compile time via Vite's `import.meta.glob('/assets/photos/*.{jpg,jpeg,png}', { eager: true })` — drop a file in the folder, it's picked up on next build, no code change.
- A semi-transparent overlay in the theme's `primary` blue sits between the photos and the text so the hero copy (white/yellow) stays legible over any photo.
- Respect `prefers-reduced-motion: reduce` — fall back to a plain crossfade (opacity only, no slide) or a static single image.
- **`IMG_E8488.HEIC` in `ui/assets/photos` needs converting to JPG before build** — browsers don't render HEIC natively and Vite won't transform it; either convert now or add a build-step check that fails loudly if a `.heic` file is present.

## Photo upload gating
- Gate value is `wedding.date` from the config file (see Config below) — no separate open-date field.
- Client: countdown shown before that time; upload form hidden until then.
- Server: hard check on every upload request — client-side gating is UX only, not security.

## Upload validation
- Allowed types: jpg/png/webp/heic, sniffed via `http.DetectContentType` (not trusted from filename/extension).
- Max file size and max files per submission — values TBD, suggest 15MB/file, 10 files/submission.
- Stored at `data/photos/<upload-id>/<uuid>_<original-filename>`.

## Email
SMTP settings come from the config file (see below).
- On RSVP: send guest names + message.
- On photo upload: send name/message/contact + direct links to each stored photo.

## Config
Single YAML file, path resolved from `CONFIG_PATH` env var (default `./config.yaml`) — this is the *only* env var; everything else lives in the file so it can be edited/mounted without touching Docker env blocks.

```yaml
server:
  port: 8080

wedding:
  date: "2026-11-07T16:00:00+02:00"    # RFC3339, guest arrival time; drives countdown + upload gate
  ceremony_time: "16:00 for 16:30"     # display text
  rsvp_by: "2026-09-01T00:00:00+02:00" # RFC3339, RSVP deadline; drives the RSVP countdown
  venue_name: "Rivier Plaas"
  venue_address: "Langenhoven Rd, Sherman Park AH, Meyerton, 1961"
  rsvp_phone: "076 925 1718"

storage:
  data_dir: "./data"                  # SQLite db + uploaded photos

email:
  smtp_host: ""
  smtp_port: 587
  smtp_user: ""
  smtp_pass: ""
  from: ""
  to: ""                              # couple's inbox

upload:
  max_size_mb: 15
  max_files: 10
```
`config.example.yaml` is checked in as a template; `config.yaml` is gitignored (holds real SMTP credentials). Venue/RSVP details sourced from `concept/IMG-20260811-WA0020.jpg` (the formal invitation).

## Deployment
Host: **`zoeshaldon.warky.info`**, Docker Compose, behind an **existing Apache reverse proxy on the host** (Apache owns TLS/certs — no Caddy/in-container TLS needed). The container publishes only to `127.0.0.1:8080`, and Apache proxies to it.

### Docker
- `Dockerfile`: multi-stage — (1) `node:alpine` builds the SvelteKit static output, (2) `golang:alpine` compiles the Go binary with the built frontend copied in for `go:embed`, (3) minimal `alpine` runtime stage with just the binary.
- `docker-compose.yml`: one service, `wedding`, builds from the Dockerfile, mounts `./config.yaml:/app/config.yaml:ro` and `./data:/app/data`, published as `127.0.0.1:8080:8080` (loopback only — Apache is the only thing that should reach it).

### Apache config (host side, not part of this repo)
```
<VirtualHost *:443>
    ServerName zoeshaldon.warky.info

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/

    # existing SSL cert config for warky.info
</VirtualHost>
```
Needs `mod_proxy` and `mod_proxy_http` enabled. Multipart photo uploads: raise `LimitRequestBody` (or leave unset/0) on this vhost so Apache doesn't reject large uploads ahead of the app's own `upload.max_size_mb` check.

## QR code
Generate pointing at `https://zoeshaldon.warky.info` once the container is deployed behind Apache.

## Colour palette
Skeleton custom theme (`wedding-theme`), generated with the [Skeleton theme generator](https://skeleton.dev/docs/design/themes) from two seed colours and applied via `data-theme="wedding-theme"` in `app.html`. Light/dark mode is Skeleton's built-in mode switch (`data-mode`) — it derives both from the same theme, so there's no separate light/dark table to hand-maintain.

| Slot | Seed colour | Source |
|---|---|---|
| `primary` (blue) | `#1E4E79` | groom's shirt / dusk sky in `concept/IMG-20260811-WA0019.jpg` |
| `secondary` (yellow) | `#D9A441` | warm gold title text in the same image |

Skeleton generates the full 50–900 scale and light/dark contrast pairs from these two seeds. Tailwind utility classes (`bg-primary-500`, `text-secondary-600`, etc.) and Skeleton components consume the theme directly — no custom CSS variables needed on top.

## Design — invitation framing
Landing and RSVP pages borrow the framed-card look from the formal invitation (`concept/IMG-20260811-WA0020.jpg`): a thin border around the core content block, small-caps tracked micro-copy above the couple's names ("Save the date" / "Please join us to celebrate the union of"), day+date formatted as "Saturday, 7 November 2026" (`Intl.DateTimeFormat('en-GB', {weekday:'long', day:'numeric', month:'long', year:'numeric'})`), and the venue name/address displayed beneath.

## Open decisions (need input before/at implementation)
1. ~~Domain / hosting target~~ — resolved: `zoeshaldon.warky.info`, Docker Compose (see Deployment).
2. SMTP provider/credentials to send from (e.g. existing Gmail/SES/other).
3. Timezone for the 7 Nov 2026 cutoff — assumed SAST (+02:00).
4. Max photo size / count per submission — defaults proposed above, confirm.
5. ~~Colour palette~~ — resolved, see above. Fonts still open (image uses a script face for names/heading + serif for the date — confirm if we should source similar web fonts).

## Prototype status
Working end-to-end: `ui/` (landing with countdown + sliding photo background, invitation-framed RSVP form with party-of-guests entry + its own countdown, gated photo upload form) builds and is served by `server/` (Go, SQLite via `modernc.org/sqlite`, SMTP notifications, server-side upload gate). Verified locally: page rendering, multi-guest RSVP submit (add/remove guest rows, blank-name filtering), photo upload with content-type sniffing (rejects non-images, detects HEIC), and both the wedding-date and RSVP-deadline gates/countdowns.

Known gaps before this is real:
- SMTP not configured (open decision #2) — emails currently log "skipped" instead of sending.
- Photos in `ui/assets/photos` are unoptimized (several multi-MB originals) — fine for the background slideshow (one loads at a time), but worth compressing before going live given the site is reached over mobile data via QR code.
- No automated tests yet.
- `Dockerfile`/`docker-compose.yml` not yet run end-to-end (only `go build`/`npm run build` and the local binary were verified).
