Files
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

177 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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).