Skip to content

vltr/arraia-do-otto

Repository files navigation

ArraiΓ‘ do Otto πŸŽͺ🌽πŸ”₯

A one-page festa-junina (Brazilian June festival) themed RSVP website for Otto's 1st birthday. Mobile-first, all copy in Brazilian Portuguese (caipirΓͺs), short-lived (it lives for a few weeks around the event).

🌐 Live: ottok.com.br (also www.ottok.com.br)

Event: Saturday 2026-06-27, 12:00–18:00, Arena Bombinhas β€” Rua AraΓ§Γ‘, 551, SertΓ£ozinho.

Built end-to-end with Claude Code as a platform β€” project memory + skill-invocation conventions in CLAUDE.md; .claude/settings.json wires the frontend-design / cloudflare / playwright plugins; visual self-check via scripts/shots.mjs (Playwright + system Chromium) so visual changes get verified during the build loop.


What it is

A single, full-screen, scroll-driven invitation that travels through a day: the sky animates from noon blue β†’ golden hour β†’ dusk β†’ bonfire night as you scroll, with an illustrated Otto in every scene, varied festa separators, and a group RSVP form that writes to a database β€” all on Cloudflare's free tier ($0/month).

Highlights

  • Dynamic sky (Sky.jsx) β€” dayβ†’night gradient, a rotating warm sunburst that sets, drifting Simpsons-style clouds (random per session), a full moon + twinkling stars at the bonfire end, and a WebGL ember particle field (Embers.jsx, Three.js/R3F, lazy-loaded and code-split).
  • Full-screen scenes with gentle scroll-snap; 2-column on desktop, compact on mobile.
  • Group RSVP β€” a responsΓ‘vel confirms for the whole household (add/remove acompanhantes); one row per person sharing a group_id. Honeypot anti-spam (Turnstile wired but disabled). Confetti on success.
  • Rustic touches β€” wood-textured plaques/frames, garland separators (lanterns, balloons, corn, candy apples) built from single generated elements tiled/hung via CSS.
  • Browser chrome follows the sky β€” theme-color + body background animate with scroll (nice on Android Chrome).
  • Social card β€” a generated Open Graph image (public/og.jpg) with Otto + balloons + wood frame.
  • Countdown, add-to-calendar (.ics + Google), Google Maps + Waze, all respecting prefers-reduced-motion.

Tech stack

  • Frontend: Vite + React 19 + Tailwind CSS v4, motion (idle/reveal helpers), three + @react-three/fiber (embers), canvas-confetti.
  • Hosting: Cloudflare Pages.
  • Backend: Cloudflare Pages Function (POST /api/rsvp).
  • Database: Cloudflare D1 (SQLite), table rsvps.
  • Anti-spam: hidden honeypot. (Cloudflare Turnstile is wired but currently disabled.)
  • Package manager: pnpm.

Directory structure

.
β”œβ”€β”€ index.html               # Vite entry; <head> OG/Twitter meta, fonts, theme-color
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ App.jsx              # composes Sky + the full-screen scenes + FloatingCTA
β”‚   β”œβ”€β”€ index.css            # Tailwind v4 @theme tokens + CSS keyframes (sky, garlands, reveal)
β”‚   β”œβ”€β”€ components/          # Sky, Sun, CloudField, Embers, Scene, Hero, Detalhes, Programacao,
β”‚   β”‚                        #   Mapa, Rsvp, Footer, Divider, WoodSign, WoodFrame, Bunting,
β”‚   β”‚                        #   Countdown, Button, OttoImage, FloatingCTA, Turnstile, ErrorBoundary
β”‚   β”œβ”€β”€ data/
β”‚   β”‚   β”œβ”€β”€ event.js         # event facts + caipirΓͺs copy (single source of truth)
β”‚   β”‚   └── clouds.js        # Simpsons-style cloud SVG silhouettes
β”‚   └── lib/
β”‚       β”œβ”€β”€ calendar.js      # .ics + Google Calendar links
β”‚       β”œβ”€β”€ links.js         # Google Maps / Waze links
β”‚       └── confetti.js      # festa-colored confetti burst
β”œβ”€β”€ functions/
β”‚   └── api/rsvp.js          # Pages Function: validate β†’ honeypot β†’ Turnstile β†’ D1 batch insert
β”œβ”€β”€ public/
β”‚   └── img/                 # generated WebP assets (Otto scenes, wood, separators) + og.jpg
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ make-og.mjs          # builds public/og.jpg            (pnpm og)
β”‚   β”œβ”€β”€ rsvps.mjs            # prints the RSVP list from D1     (pnpm rsvps)
β”‚   └── shots.mjs            # Playwright screenshots for visual self-checks
β”œβ”€β”€ migrations/
β”‚   └── 0001_add_group_id.sql
β”œβ”€β”€ docs/                    # SPEC.md, RSVP-HANDOFF.md, IMAGES.md, IMAGE-PROMPTS.md, DEPLOY.md
β”œβ”€β”€ schema.sql               # D1 table definition
β”œβ”€β”€ wrangler.jsonc           # Pages config + D1 binding
β”œβ”€β”€ CLAUDE.md                # project memory / build decisions (read this!)
└── package.json

Getting started

Prerequisites: Node 22+ and pnpm.

pnpm install

Frontend only (fast UI loop)

pnpm dev          # http://localhost:5173

You can see and exercise the whole UI here, but submitting the RSVP errors β€” the /api/rsvp Function and the database don't run under Vite.

Full stack (Functions + local D1)

# one-time: create the local table
pnpm exec wrangler d1 execute rsvp-db --local --file ./schema.sql

pnpm build
pnpm pages:dev    # http://localhost:8788  (serves dist/ + functions/ + a local D1)

Read the local rows:

pnpm rsvps --local

Scripts

Command What it does
pnpm dev Vite dev server (frontend only).
pnpm build Production build to dist/.
pnpm preview Preview the built dist/.
pnpm pages:dev wrangler pages dev β€” full stack (Functions + local D1). Build first.
pnpm og Regenerate the social card public/og.jpg (resvg + sharp).
pnpm rsvps Pretty-print the RSVP list grouped by household (--local for the dev DB).

node scripts/shots.mjs [url] screenshots the running dev/preview at several scroll fractions using the system Chromium via Playwright β€” used to verify visual changes.


RSVP backend & database

POST /api/rsvp accepts JSON:

{
  "name": "JoΓ£o",
  "attending": 1,                 // 1 = coming, 0 = not
  "dietary": "sem glΓΊten",
  "companions": [{ "name": "Maria", "dietary": "" }],  // only used when attending = 1
  "fax": "",                      // honeypot β€” must stay empty
  "turnstileToken": "..."         // verified only if TURNSTILE_SECRET_KEY is set
}

It validates, drops honeypot hits, then writes one row per person (companions are attending = 1), all sharing a group_id (crypto.randomUUID(), NULL when solo), via DB.batch. Returns { ok, count }. The old single-person payload still works.

CREATE TABLE rsvps (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  name        TEXT NOT NULL,
  attending   INTEGER NOT NULL,   -- 1 = vem, 0 = nΓ£o vem
  dietary     TEXT,
  group_id    TEXT,               -- shared per household submission; NULL = solo
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

Images

Otto's scenes are generated externally on the Picsart web app (the gen-ai API rejects real-child face references; the web app allows them) from the prompts in docs/IMAGES.md, using Otto's real photo as the face reference. Hero/footer are transparent cutouts composited over the live sky; the 4 programaΓ§Γ£o scenes are framed cards. Decorative assets (wood texture, separator elements) were generated via the Picsart gen-ai CLI and keyed/optimized with sharp. Everything is committed as optimized WebP under public/img/.


Working from another machine (Cloudflare auth)

wrangler.jsonc already points at the production resources (Pages project arraia-do-otto, D1 rsvp-db with its database_id), so on a fresh machine you only need to authenticate wrangler with the Cloudflare account that owns them:

pnpm install
pnpm exec wrangler login      # opens the browser β†’ authorize the account (rkuesters@gmail.com)
pnpm exec wrangler whoami     # confirm the email + account id

After that, deploy and the reports work as usual:

pnpm build && pnpm exec wrangler pages deploy dist --project-name arraia-do-otto
pnpm rsvps                    # read production RSVPs

Notes:

  • Secrets are not in git. Nothing is secret while Turnstile is disabled. If you re-enable it, put the site key in .env (.env.example β†’ .env, baked into the build) and the secret key in .dev.vars for local dev / a Pages secret (wrangler pages secret put TURNSTILE_SECRET_KEY) for prod β€” and remember both gates must move together (a secret with no widget 403s every submission).
  • The local D1 lives under .wrangler/ (gitignored), per machine. For local full-stack dev, recreate it: pnpm exec wrangler d1 execute rsvp-db --local --file ./schema.sql (then any migrations/*.sql).
  • For CI/headless (no browser for OAuth), set a CLOUDFLARE_API_TOKEN env var with Pages (edit) + D1 (edit) permissions instead of wrangler login.

Deployment

Cloudflare Pages, project arraia-do-otto, served on the custom domain ottok.com.br.

pnpm build
pnpm exec wrangler pages deploy dist --project-name arraia-do-otto

Full walkthrough (D1 create, remote schema, Turnstile, custom domain): docs/DEPLOY.md. Read RSVPs in production with pnpm rsvps.


Built with Claude Code

This site was designed and built collaboratively in Claude Code. Skills/tools that shaped it:

  • frontend-design β€” the distinctive, non-generic visual direction.
  • cloudflare / wrangler / cloudflare-email-service β€” Pages + D1 + Functions, and exploring email notifications.
  • gen-ai-use (Picsart gen-ai CLI) β€” generated the wood texture and the separator elements.
  • humanizer β€” available for the guest-facing caipirΓͺs copy (kept manual by the owner's call).
  • Playwright + system Chromium β€” screenshot-driven visual verification (scripts/shots.mjs).

See CLAUDE.md for build decisions and a couple of gotchas (notably: the scroll-linked sky is driven by a plain rAF state, not motion MotionValues, because scroll-bound opacity didn't flush to the DOM in this motion v12 + React 19 setup).


TODO / ideas

  • CSV export of the guest list for the caterer (headcount) β€” closer to the event.
  • Turnstile anti-spam β€” wired but disabled (2026-06-03): the managed widget blocked legit guests on Safari / Samsung Internet. Honeypot stays active. Revisit only with a non-blocking config (e.g. invisible mode) if spam becomes a problem.
  • Email-on-RSVP notification β€” deferred; Pages Functions can't use the send_email binding, so it'd need a companion Worker or a transactional API.
  • Optional www β†’ apex redirect for a canonical URL.
  • Refresh the OG art / swap in newer Otto renders if desired.

Feito com muuuuito amô pela mamãe e pelo papai do Otto ❀️

About

My son's 1st-birthday RSVP site: a festa-junina invitation shipped end-to-end with Claude Code as a platform. Cloudflare Pages + D1 + Pages Functions + R3F WebGL, $0/mo on the free tier. CLAUDE.md captures the project memory and skill-invocation conventions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors