# Amic Invisible (Qui em toca?) — llms-full.txt > A full, machine-readable overview of the **Amic Invisible** ("Qui em toca?") project, intended for LLMs and automated agents. ## What this project is A tiny web app to organise a Secret Santa ("amic invisible") draw with the least possible friction. Create a group, invite people by link or add them manually, optionally vote on a budget, define exclusions, run the draw and let each participant reveal their own result privately. The product is Catalan-first and fully localised into Spanish and English. It is served at `https://quiemtoca.cat`. ## Product principles - **One job, done well.** No feature that distracts from the draw. - **Private by default.** Assignments are encrypted at rest and only revealed to each participant via a private link. - **No mandatory accounts.** A draw works with zero sign-up. - **Share however you like.** Public link, or a private per-person link (email optional). - **Fast and accessible.** Little JavaScript, respectful of reduced motion, keyboard-friendly. - **SEO without clutter.** Useful content lives in dedicated pages, never keyword-stuffed into the homepage. ## Tech stack | Area | Technology | | --- | --- | | Frontend | [Astro](https://astro.build/) (v7, static output) | | Styles | SCSS | | Animation | GSAP | | Hosting | Netlify | | Backend | Netlify Functions (`.mjs` under `netlify/functions`) | | Persistence | Netlify Blobs | | Transactional email | Resend | ## Repository layout ``` src/ ├── i18n/ # Translation system (index.ts, strings.ts) ├── data/ # seo.ts, structured-data.ts, legal.ts ├── components/ │ ├── pages/ # HomePage, ParticiparPage, GestionarPage, ResultatPage │ └── ... # SeoHead, LangSwitcher, CookieNotice, BugReport, SupportLink, SocialLinks, LegalPage ├── pages/ # Thin locale wrappers │ ├── index.astro # ca (default, root URL) │ ├── participar.astro │ ├── gestionar.astro │ ├── resultat.astro │ ├── privacitat.astro │ ├── cookies.astro │ ├── es/*.astro # /es/… Spanish pages │ └── en/*.astro # /en/… English pages ├── dev/ # Local-only API stubs (/api/draws, /api/feedback) └── styles/global.scss netlify/ ├── functions/ # draws.mjs, feedback.mjs, support-checkout.mjs └── lib/ ├── draw-logic.mjs # Assignment generation + crypto helpers ├── draw-store.mjs # Persistence (Netlify Blobs / local JSON in dev) ├── i18n.mjs # Server-side strings (errors, budget labels, email copy) └── email/ # Resend integration + templates ``` ## Internationalization (i18n) The site supports three locales: **ca** (Catalan, default), **es** (Spanish), **en** (English). - **Routing:** Astro's built-in i18n with `prefixDefaultLocale: false`. Catalan lives at the root (`/`), Spanish at `/es/`, English at `/en/`. - **Strings:** All UI copy lives in `src/i18n/strings.ts` as a flat `key -> text` map per locale. The `ca` object is the source of truth; `es` and `en` are typed against it (`const es: typeof ca`), so a missing key is a compile error. - **Helpers:** `getLocaleFromUrl()`, `translate(locale, key, vars)` with `{placeholder}` interpolation, `formatNumber()`. - **Client scripts:** Each page component receives the current locale dictionary via Astro's `define:vars` and uses a small `t()` helper. - **Server strings:** `netlify/lib/i18n.mjs` holds error messages, budget labels and email copy. `resolveLocale()` falls back to `ca`. - **Browser detection:** An inline script on the homepage redirects first-time visitors from `/` to `/es/` or `/en/` based on `navigator.languages`, unless the visitor has an explicit preference stored in `localStorage` (`amic-invisible:lang`). - **Language switcher:** `src/components/LangSwitcher.astro` renders subtle `CAT · ES · EN` links that persist the choice and navigate to the same page in the chosen locale. ## Pages and routes | Route | Purpose | Indexed | | --- | --- | --- | | `/` (ca), `/es/`, `/en/` | Group creation wizard (homepage) | yes | | `/privacitat/` (+ `/es/`, `/en/`) | Privacy policy | yes | | `/cookies/` (+ `/es/`, `/en/`) | Cookie policy | yes | | `/participar/` (+ `/es/`, `/en/`) | Public join page | no (noindex) | | `/gestionar/` (+ `/es/`, `/en/`) | Private creator panel | no (noindex) | | `/resultat/` (+ `/es/`, `/en/`) | Private participant result | no (noindex) | Local `astro dev` only injects `/api/draws` and `/api/feedback` stubs. There is no `/roadmap` or `/emails` surface; project status lives on the private martifenosa.com Quiemtoca board. ## Creation wizard (homepage) A four-step, Typeform-style flow: 1. Group name (suggested default applied if left empty). 2. Budget — fixed (15/20/30 €), custom, anonymous poll, or no limit. 3. How to add people — share a link, or add them manually. 4. Creator email (optional) — used to send the private management access. Then a summary, group creation (`POST /api/draws { action: "create" }`), and a share screen with the private creator link and the public join link. ## Data model (draw record) Stored in Netlify Blobs, one JSON document per group. Key fields: - `id`, `name`, `locale`, `status` (`open` | `closed` | `drawn`) - `joinMethod` (`link` | `manual`) - `budget` (`{ mode: "fixed", amount, label }` or `{ mode: "poll", label }`) - `organizerEmail`, `adminTokenHash` - `participants[]` — each with `id`, `name`, `email`, `addedBy`, `accessTokenHash`, `accessTokenEncrypted`, `budgetVote`, `assignment` (encrypted after draw) - `exclusions[]` (`{ giverId, receiverId }`), `finalBudget`, `drawnAt` - `emailDelivery` — per-channel counters (`attempted`, `sent`, `failed`, `skipped`) for `organizerAccess`, `participantAccess`, `drawResult` Security properties: - Admin and participant tokens are random and stored as SHA-256 hashes. - Participant access tokens and assignments are encrypted with AES-256-GCM (`DATA_ENCRYPTION_KEY`). - `publicView()` never leaks participants, tokens, or assignments. ## API (`POST /api/draws`) A single endpoint dispatching on `action`: - `create` — create a group (returns `joinUrl`, `adminUrl`). - `public` — public group summary. - `join` — add a participant (self or by creator when `adminToken` is present). - `participant` — load a private result (reveals `assignment` only when `status === "drawn"`). - `vote` — submit an anonymous budget vote. - `admin` — full creator view. - `close` / `reopen` / `draw` — lifecycle actions. - `remove-participant` / `reset-participant-access` — participant management. Errors are returned as `{ error: "…" }` with a proper HTTP status, localised to the draw's locale. ## Assignment generation (`draw-logic.mjs`) `generateAssignments(participants, exclusions)` produces a random valid matching (no self-gifts, exclusions respected) using an augmenting-path maximum matching. Returns `null` when impossible. Allowed up to 100 participants. ## Email flow (`netlify/lib/email`) Three transactional templates, all localised via `netlify/lib/i18n.mjs`: 1. `organizer-access` — sent on group creation when an organizer email is given. 2. `participant-access` — sent on join (or access regeneration). 3. `draw-result` — sent after the draw, in batches of up to 100. Properties: HTML + plain text, user content escaped, idempotency keys to prevent duplicates, retries with backoff, and per-channel delivery counters. Email failures never roll back the draw; the private link is always returned to the UI. The draw result email never reveals the assignment or the budget. ## SEO - Per-locale `