Files
tina-barcelona-picker/README.md
T
robertoandClaude Sonnet 5 ddd2db2a52 Initial commit: Tina Barcelona trip picker
Static Cloudflare Pages site + KV-backed Pages Function for jointly
planning Tina's Barcelona visit (7 Jul - 18 Aug 2026). List and calendar
views share one client state; dated evening events are radio groups to
prevent double-booking, multi-night shows live once in TRIP_DATA.spanning
instead of being duplicated per day, and undated collections get
optionally date-tagged when picked from the calendar view.

Includes a client-side-only password gate and an instructions page.
Live at https://summeroffun.hortal.me.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:58:15 +02:00

8.2 KiB
Raw Blame History

Tina Barcelona Picker

A tiny picker page for planning Tina's Barcelona visit (7 July – 18 August 2026). Lists concerts, festival dates, exhibitions, restaurants, and holiday-leg activities; she taps what she likes and it's saved centrally so both of you can see the shortlist.

Static frontend + one Cloudflare Pages Function + one KV namespace. Runs entirely on Cloudflare's free tier.

Live at: https://summeroffun.hortal.me (also reachable at the underlying https://tina-barcelona-picker.pages.dev). Password-gated — see "Known limitations" below for what that does and doesn't protect against.

A companion page at /instructions.html explains list vs. calendar view, the one-pick-per-evening rule, and how "anytime" items get date-tagged — linked from the "How this page works →" link in the header.

Two views, one shared state

  • List view: every day as its own card, in date order, with the undated "anytime" collections (culture, dinner, evening, daytime) shown once each below.
  • Calendar view: one day at a time, navigated with Prev/Next or by tapping a cell in a clickable month grid. Each date's dot shows whether anything's been planned for it yet.

Both views read and write the exact same state, so a pick made in one shows up correctly in the other — there's no separate "calendar data" to keep in sync.

How it avoids double-booking (and repeated entries)

Each calendar date with named events (Jamboree, Les Nits Occident, Grec, Palau de la Música, Jazz at La Pedrera...) is one radio-button group — only one pick per evening is possible by construction, not by a warning after the fact.

Shows that run across more than one night (e.g. a two-night play) are not duplicated in the data — each lives once in TRIP_DATA.spanning with a list of valid dates, and is rendered as a live choice on whichever of those dates it isn't picked for yet, and as a disabled "already picked — [date]" status row on the others.

Undated categories (exhibitions, dinner, daytime activities) are plain checkboxes with no collision risk — but in the calendar view, if one has been tied to a specific date (by ticking it while that day was on screen), it shows as "already picked — [date]" on every other day instead of offering a second, contradictory tick box.

Project layout

public/
  index.html        - page shell: password gate, title/subtitle, view-toggle mount point, app + shortlist containers
  instructions.html - standalone "how this page works" guide, linked from the header
  style.css         - styling (light/dark aware) — includes calendar grid, gate, and status-row styles
  data.js           - all the trip data: TRIP_DATA.days, TRIP_DATA.spanning, TRIP_DATA.collections;
                      plus TRIP_START/TRIP_END constants that bound the calendar view
  app.js            - renders both views from data.js, talks to the API, keeps the shortlist in sync,
                      and gates the #content div behind the password prompt
functions/
  api/selections.js - Pages Function: GET returns saved picks, POST saves one change
wrangler.toml       - Cloudflare Pages config + KV binding (id already filled in — see below)

Editing the trip data

Everything you see on the page comes from public/data.js. To add/remove/change an option, just edit that file — no other file needs to change. Structure:

  • days: array of { date, label, locked?, note?, options: [{ id, title, meta, url?, default? }] }. locked renders a non-editable "already confirmed" card above the picks (e.g. Jazz at La Pedrera) — omit options (or leave it empty) if there's nothing else to offer that night. note renders a small warning line (e.g. "you're travelling this day"). default: true on an option pre-selects it when nothing's been explicitly chosen yet (used for the free Clàssica a la Platja concert).
  • spanning: array of { id, title, meta, url?, dates: [...] } — a single show valid on more than one date. Don't copy-paste it into each day's options array — list it once here and it'll appear correctly (with already-picked-elsewhere handling) on every date in dates.
  • collections: array of { id, title, options: [...], dateRange? } — checkbox groups. A dateRange: [start, end] (used for the two holiday legs) makes the collection render inline in the day-by-day timeline instead of the "anytime" section at the bottom, and restricts which calendar days show it in the per-day "also available today" panel.

IDs must stay unique across the whole file (they're also the KV storage keys), but otherwise there's no schema to run — just valid JS.

Local development

npm install -g wrangler   # if not already installed
wrangler pages dev public

This runs the site with a local, file-backed KV store (no Cloudflare account needed for local dev). Visit the printed localhost URL.

Deploying (free tier)

The project is already deployed (KV namespace created, wrangler.toml filled in, custom domain live — see "Live at" above). This section is for re-deploying after content changes, or for standing it up again from scratch on a different account.

Re-deploying after an edit (the common case):

wrangler pages deploy public --project-name=tina-barcelona-picker

That's it — it's a static site plus one function, so deploys take seconds. No need to touch KV or the custom domain again; both stay attached to the project.

Setting up from scratch (only needed once, or on a new account):

  1. Log in (opens a browser): wrangler login
  2. Create the Pages project: wrangler pages project create tina-barcelona-picker --production-branch=main
  3. Create the KV namespace: wrangler kv namespace create PICKS — copy the returned id into wrangler.toml under [[kv_namespaces]] id = "...".
  4. Deploy: wrangler pages deploy public --project-name=tina-barcelona-picker --commit-dirty=true (the --commit-dirty flag is only needed if this directory has uncommitted git changes at deploy time). Wrangler will print the live *.pages.dev URL, and the KV binding comes along automatically from wrangler.toml — no separate dashboard step needed.
  5. Add a custom domain (optional). Wrangler's CLI has no pages domain command, and the OAuth token from wrangler login is scoped zone:read only — it cannot create DNS records. Two options:
    • Add it via the Cloudflare dashboard: Pages project → Custom domains → add the domain, then add the CNAME record it asks for in the zone's DNS settings.
    • Or, with the same OAuth token (has pages:write), register the domain on the Pages project via the API: POST /accounts/{account_id}/pages/projects/{project}/domains {"name": "..."}. That alone doesn't create the DNS record — you (or a token with Zone:DNS:Edit) still need to add a proxied CNAME from the subdomain to <project>.pages.dev. SSL cert validation can take several minutes to a couple of hours after the CNAME appears before the domain's status flips to "active" — check with GET /accounts/{account_id}/pages/projects/{project}/domains/{domain}.

Free tier notes

  • Pages: free, unlimited requests for static assets.
  • Pages Functions: free tier is 100,000 requests/day — this project will use a tiny fraction of that.
  • KV: free tier is 100,000 reads/day and 1,000 writes/day, one namespace. This app does one read on page load and one write per tap — comfortably inside the limit even with heavy use.
  • No cost, no card required for any of the above at this scale.

Known limitations (by design, not oversights)

  • No real auth — a fake password gate only. The page shows a "🔒 Private plan" prompt and only reveals the content if you type the password (currently Tina, set in GATE_PASSWORD in public/app.js). It's a client-side speed bump, not security: the password is sitting in plain sight in the JS source, and anyone who opens dev tools can bypass it in seconds. Anyone with the URL can still view/edit picks once past the gate; don't post the URL publicly.
  • No per-user attribution. It's one shared shortlist, not "Tina's picks" vs "Roberto's picks" — simplest thing that works for a joint plan.
  • Last write wins. If both of you tap at the exact same moment, one save can overwrite the other. Not worth solving for two people picking restaurants.