https://github.com/noahpro99/gnwedding2026
https://github.com/noahpro99/gnwedding2026
Last synced: 25 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/noahpro99/gnwedding2026
- Owner: noahpro99
- Created: 2026-05-31T01:23:36.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-08T07:26:02.000Z (26 days ago)
- Last Synced: 2026-06-08T09:04:45.179Z (26 days ago)
- Language: TypeScript
- Size: 20.9 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Noah & G — Wedding 2026
A wedding website. Server-rendered React via **TanStack Start**, **Bun**
runtime, **Tailwind CSS v4**, and a tiny **SQLite** database for RSVPs.
---
## Tech stack
| Concern | Choice |
| -------------- | --------------------------------------- |
| Runtime | Bun (`bun` v1.x) |
| SSR framework | TanStack Start (React 19) |
| Routing | TanStack Router (file-based) |
| Styling | Tailwind CSS v4 (`@tailwindcss/vite`) |
| Database | SQLite via `bun:sqlite` (no native deps)|
| Build | Vite 6 |
| Deploy target | Docker container on `noahpi` (Linux/ARM)|
Server functions (`createServerFn`) handle form submissions, so there's no
separate REST/API layer to maintain. Everything that touches the DB lives in
`src/server/`.
---
## Project layout
```
src/
├── routes/ file-based routes (TanStack Router)
│ ├── __root.tsx shared shell: , nav, footer
│ ├── index.tsx homepage (hero + quick links + album CTA)
│ ├── save-the-date.tsx card with PNG frame + RSVP CTA
│ ├── invite.$id.tsx invite page; "Accept/Decline" reveals RSVP form
│ ├── rsvp.tsx standalone RSVP page
│ ├── our-story.tsx timeline
│ ├── travel.tsx hotels + shuttle request form
│ ├── itinerary.tsx Google Calendar embed
│ ├── wedding-party.tsx bridal party grid
│ ├── registry.tsx outbound registry links
│ └── faq.tsx accordion
├── components/
│ ├── SiteNav.tsx sticky top nav (responsive)
│ ├── SiteFooter.tsx
│ ├── SectionHeader.tsx
│ ├── CardFrame.tsx PNG-decoration card wrapper (transparent center)
│ └── RsvpForm.tsx shared RSVP form (yes/no + party + dietary…)
├── server/
│ ├── db.ts bun:sqlite connection + schema bootstrap
│ ├── db-init.ts seed script: inserts demo invite
│ └── rsvp.ts createServerFn endpoints (RSVP, transport, invite lookup)
├── router.tsx router instance
├── routeTree.gen.ts AUTO-GENERATED by TanStack plugin
└── styles.css Tailwind import + theme tokens
public/
├── hero.jpg homepage hero (REPLACE)
└── card-frame.png invite card decoration with transparent middle (REPLACE)
data/ SQLite file lives here (gitignored, Docker volume)
```
---
## Local development
```bash
# one-time
bun install
# run dev server (SSR + HMR)
bun run dev
# → http://localhost:3000
# optional: seed a demo invite at /invite/demo
bun run db:init
```
### Things to fill in before you ship
Search the codebase for `REPLACE` to find every placeholder. Highlights:
- `src/routes/index.tsx` — wedding date, location, photo album URL
- `src/routes/itinerary.tsx` — Google Calendar embed `src`
- `src/routes/travel.tsx` — hotel names, addresses, block codes, booking URLs
- `src/routes/wedding-party.tsx` — names, roles, photos
- `src/routes/registry.tsx` — registry URLs
- `src/routes/our-story.tsx` — story milestones
- `public/hero.jpg` — main homepage photo
- `public/card-frame.png` — the PNG card decoration (transparent center)
---
## How the invite-only flow works
There are **two** invite-style pages:
1. `/save-the-date` — generic, share-with-anyone. Card art, date, RSVP/Travel links.
2. `/invite/$id` — formal invitation **personalized** to a guest. The id maps
to a row in the `invites` table (guest names, max party size). Clicking
"Accept / Decline" reveals the RSVP form pre-filled with their name and
party-size cap.
You generate one invite row per household. The link you send to that household
is `https://yoursite/invite/`. RSVPs from that page are linked back to
the invite via `rsvps.invite_id`.
### Creating invites
For now, do it from a Bun shell:
```bash
bun --print "
const { db } = await import('./src/server/db.ts');
db.run('INSERT INTO invites (id, guest_names, party_size_max) VALUES (?, ?, ?)',
['smith-family', 'The Smith Family', 4]);
console.log('done');
"
```
(A small admin page can be added later if needed.)
---
## Database
SQLite file path is controlled by `DB_PATH` (defaults to `./data/wedding.sqlite`).
Schema is created on first connection (`src/server/db.ts`). Tables:
- **invites** — `id`, `guest_names`, `party_size_max`
- **rsvps** — `invite_id?`, `primary_name`, `email`, `attending`, `party_size`,
`dietary`, `notes`, `needs_transport`, `created_at`
- **transport_requests** — separate shuttle requests from `/travel`
To inspect:
```bash
sqlite3 data/wedding.sqlite '.dump'
sqlite3 data/wedding.sqlite 'SELECT * FROM rsvps;'
```
---
## Deployment — `noahpi`
The repo is wired for CI deploy via `.github/workflows/deploy.yml`. On every
push to `main`, the workflow SSHes into `noahpi`, pulls the repo, rebuilds
the Docker image, and tells the central `~/docker-compose.yml` to come up.
### Required GitHub secrets
- `SSH_HOST` — Pi hostname or IP
- `SSH_USERNAME` — login user
- `SSH_PRIVATE_KEY` — private key authorized on the Pi
### Image
`noahpro/gnwedding2026:latest`. Built from the `Dockerfile` in this repo —
multi-stage with `oven/bun:1` for both build and runtime. The runtime stage
serves both SSR (TanStack Start) and the static `dist/client/` via a thin
`serve.ts` Bun wrapper. Container listens on `:3000`.
### docker-compose entry on the Pi
Lives in `~/docker-compose.yml` alongside the other services. Traefik picks
up routing via labels — no `ports:` mapping needed.
```yaml
gnwedding2026:
image: noahpro/gnwedding2026:latest
restart: unless-stopped
volumes:
- ./gnwedding-data:/app/data # SQLite persists across rebuilds
labels:
- traefik.enable=true
- traefik.http.routers.gnwedding2026.rule=Host(`gnwedding2026.com`)
- traefik.http.routers.gnwedding2026.entrypoints=https
- traefik.http.routers.gnwedding2026.tls=true
- traefik.http.routers.gnwedding2026.tls.certresolver=${CERT_RESOLVER}
- traefik.http.services.gnwedding2026.loadbalancer.server.port=3000
```
Public host: **gnwedding2026.com** (apex only — there's no `www.` DNS
record, and adding `www.` to the Host rule made Let's Encrypt 429 us out
for an hour. Add a `www.` A record then extend the rule with
`|| Host(\`www.gnwedding2026.com\`)` if you want both.)
SQLite file lives at `~/gnwedding-data/wedding.sqlite` and survives
image rebuilds.
### Backups
Add a host-side cron on `noahpi` to snapshot the DB:
```cron
0 3 * * * sqlite3 /home//gnwedding-data/wedding.sqlite ".backup '/home//backups/wedding-$(date +\%F).sqlite'"
```
---
## Scripts reference
| Command | What it does |
| ------------------ | ------------------------------------------------ |
| `bun run dev` | Vite dev server with SSR + HMR on `:3000` |
| `bun run build` | Production build into `dist/` |
| `bun run start` | Run the built server (`bun serve.ts`) on `:3000` |
| `bun run typecheck`| `tsc --noEmit` |
| `bun run db:init` | Create tables and insert a demo invite |