Files
zoe-shaldon/PLAN.md
warkanum 39f5b8d5cc feat(ui): add wedding theme styles and layout components
* Introduced wedding-themed CSS variables for styling.
* Created layout component for the wedding site with navigation and countdown.
* Added RSVP and photo upload pages with form handling.
* Implemented QR code page for wedding site access.
* Configured TypeScript and Vite for the project.
* Added robots.txt for search engine crawling.
2026-08-11 23:31:35 +02:00

12 KiB
Raw Permalink Blame History

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 (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 "). 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.

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 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 50900 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).