{"id":51018555,"url":"https://github.com/gtrabanco/bingo-ev","last_synced_at":"2026-06-21T14:01:06.511Z","repository":{"id":363831690,"uuid":"1264685924","full_name":"gtrabanco/bingo-ev","owner":"gtrabanco","description":"Bingo de la resignación de los conductores de VE 🚘⚡️","archived":false,"fork":false,"pushed_at":"2026-06-18T14:03:53.000Z","size":592,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-18T14:05:54.323Z","etag":null,"topics":["bingo","coches","electric-vehicles","electricos","espanol","game","parodia","parody","spanish"],"latest_commit_sha":null,"homepage":"https://bingo.gruxon.com","language":"Astro","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gtrabanco.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-10T05:12:17.000Z","updated_at":"2026-06-18T12:39:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gtrabanco/bingo-ev","commit_stats":null,"previous_names":["gtrabanco/bingo-ev"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gtrabanco/bingo-ev","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gtrabanco%2Fbingo-ev","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gtrabanco%2Fbingo-ev/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gtrabanco%2Fbingo-ev/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gtrabanco%2Fbingo-ev/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gtrabanco","download_url":"https://codeload.github.com/gtrabanco/bingo-ev/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gtrabanco%2Fbingo-ev/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34610832,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bingo","coches","electric-vehicles","electricos","espanol","game","parodia","parody","spanish"],"created_at":"2026-06-21T14:01:05.035Z","updated_at":"2026-06-21T14:01:06.496Z","avatar_url":"https://github.com/gtrabanco.png","language":"Astro","funding_links":[],"categories":[],"sub_categories":[],"readme":"# El Bingo del Cargador — bingo.gruxon.com\n\nSingle-page parody bingo of typical public EV-charger problems in Spain. Each visitor gets a\nrandom card (\"cartón\") modeled after a real Spanish bingo card: a 3x4 grid where each row\nholds 2 situations and 2 blanks (6 situations total). Completing a row sings \"línea\";\ncompleting all 6 within one month earns a downloadable mock certificate. Spanish UI with a\ndry-sarcastic tone; no brand names anywhere.\n\nGame state lives in `localStorage` (no auth, no accounts). A minimal Cloudflare D1 registry\nmakes completed cards **verifiable**: every diploma links to `/v/\u003ccardId\u003e`, a public page\nthat confirms the bingo was sung within the one-month window — per the server clock.\n\n## Stack\n\n- [Astro](https://astro.build) + [Tailwind CSS 4](https://tailwindcss.com)\n- [@astrojs/cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/)\n  adapter: static homepage + a tiny server side (API + verification page) on\n  Cloudflare Workers with a D1 database.\n- [uqr](https://github.com/unjs/uqr) (~3 KB, zero deps) to draw the verification QR on the\n  diploma — the only other runtime dependency.\n\n## Project structure\n\n```text\n/\n├── migrations/\n│   └── 0001_init.sql              # D1 schema: the single `cards` table\n├── public/\n│   └── favicon.svg\n├── src/\n│   ├── components/\n│   │   ├── BingoCard.astro        # Cartón frame (header strip, grid, footer strip)\n│   │   └── CertificateModal.astro # \u003cdialog\u003e: canvas preview, PNG download, verify link\n│   ├── data/\n│   │   └── situations.json        # Single source of truth: the situation pool (Spanish)\n│   ├── layouts/\n│   │   └── Layout.astro           # HTML shell, meta, global styles\n│   ├── lib/\n│   │   ├── api.ts                 # Client for the registry API (degrades to offline play)\n│   │   ├── card.ts                # Cartón generation + validation + expiry rule\n│   │   ├── certificate.ts         # Certificate stub: canvas render + PNG download\n│   │   ├── storage.ts             # localStorage persistence (guarded, in-memory fallback)\n│   │   └── wins.ts                # Win detection: línea per row, bingo = all situations\n│   ├── pages/\n│   │   ├── api/cards/             # POST issue · POST [id]/complete · DELETE [id]\n│   │   ├── v/[id].astro           # Public verification page (Verificado/En juego/No consta)\n│   │   └── index.astro            # The whole game: markup + client-side wiring\n│   └── styles/\n│       └── global.css             # Tailwind import, theme tokens, cell/dauber styles\n├── astro.config.ts\n├── wrangler.jsonc                 # Worker config + D1 binding\n└── package.json\n```\n\n## Commands\n\n| Command           | Action                                  |\n| :---------------- | :-------------------------------------- |\n| `npm install`     | Install dependencies                    |\n| `npm run dev`     | Dev server at `http://localhost:4321`   |\n| `npm run build`   | Production build to `./dist/`           |\n| `npm run preview` | Preview the production build locally    |\n\n## How it works\n\n- **Card**: 6 situations are drawn at random from `src/data/situations.json` (40 entries)\n  and placed 2 per row on a 3x4 grid; the other cells are blanks, like a real cartón. The\n  card gets a short serial id; the chosen layout is stored with it, so a saved card keeps\n  rendering correctly even if the pool changes later.\n- **Persistence**: `localStorage` keys — `evbingo.currentCardId` (pointer),\n  `evbingo.card.\u003ccardId\u003e` (card + marks + completion), `evbingo.nick` (certificate name).\n  All access is wrapped in try/catch so private-browsing modes degrade to in-memory play.\n- **Wins**: real-cartón rules. Completing the situations of a row triggers a \"¡Línea!\"\n  toast (blanks don't count); marking all 6 opens the certificate dialog (\"¡Bingo!\"). Once\n  sung, a bingo stays sung even if cells are unmarked later.\n- **Dual marks**: each cell can be marked as *suffered* (kind 1, red dab) or *caused by\n  you* (kind 2, orange dab). A 3-way toggle sets the input mode: Sinvergüenza (taps mark\n  as caused), Mixto (tap = suffered, double tap = caused) or Resignado EV (taps mark as\n  suffered). Both kinds count towards línea/bingo.\n- **Honorifics**: one certificate design for everyone; the title depends on behavior —\n  «Resignado Sufridor» (0 caused, gets thanked for exemplary conduct), «Granujilla»\n  (some caused, less than half) or «Sinvergüenza» (half or more caused).\n- **Groups** (`/g/\u003cid\u003e`): create a group with a **unique name**, choose who can join\n  (anyone with the link, or only with a **password**) and whether the board is **public**\n  (standings + aliases visible to anyone) or **members-only**. Joining always requires an\n  **alias**. Only the FIRST completion wins (atomic claim server-side); later bingos still\n  get their diploma, and completed cards can't join. The group page has share buttons\n  (WhatsApp · X · copy/native) whose text spells out the join policy and board visibility.\n  Passwords are stored salted-hashed (`groupId:password`, SHA-256), never plaintext;\n  members-only standings are fetched client-side and only returned for a card that proves\n  membership with its owner secret.\n- **Expiry**: a card must be completed within one calendar month of its creation. Expired\n  incomplete cards are frozen under a \"CADUCADO\" stamp and must be regenerated. Enforced\n  client-side for the UI and server-side (server clock) for verification.\n- **Certificate**: a placeholder canvas render (1200×900) with \"¡Bingo!\", the nick, the\n  completion date, the card serial, the verification URL and a QR pointing to it,\n  downloadable as PNG. Final design pending.\n- **Sharing**: dialog buttons for WhatsApp (wa.me intent), X and Bluesky (prefilled compose\n  intents citing @gruxon), Instagram (Web Share sheet with the PNG on mobile; caption-copy +\n  profile fallback elsewhere) and YouTube (caption-copy + channel). A \"Compartir cómo voy\"\n  button shares the live read-only card view (`/c/\u003cid\u003e`).\n- **Live card view** (`GET /c/\u003cid\u003e`): public, read-only render of a card and its marks for\n  spectators. Mutations require the owner secret, which never leaves the owner's browser.\n- **Recover by email** (optional, no auth): a player can link an email to a card and get a\n  recovery email with owner links (id + secret), optionally opting into the newsletter.\n- **Privacy** (`/privacidad`): GDPR notice — controller, purposes, lawful basis, retention,\n  rights — linked from the email form disclaimer and the footer.\n\n## Verification registry (Cloudflare D1)\n\nOne tiny table (`migrations/0001_init.sql`): `cards (id, created_at, completed_at, nick)`.\n\nLifecycle — designed so the table stays small:\n\n- **Issue** (`POST /api/cards`): the server generates the card id and creation timestamp.\n  Each issue also sweeps expired, never-completed rows (opportunistic GC).\n- **Complete** (`POST /api/cards/:id/complete`): accepted only within one month of\n  creation, per the server clock. Late attempts get `410` and the row is deleted.\n  Re-posting on a completed card only updates the diploma nick.\n- **Discard** (`DELETE /api/cards/:id`): called when a card is regenerated or found\n  expired. Completed cards are immune — their record is what makes diplomas verifiable.\n- **Verify** (`GET /v/\u003cid\u003e`): public page with three outcomes — Verificado (completed in\n  time, shows nick + date), En juego (registered, pending), No consta (unknown or expired).\n\nIf the API is unreachable, the game plays on with locally generated cards — they just\nwon't be verifiable. Astro's built-in CSRF check (`checkOrigin`) protects the endpoints\nfrom cross-site form posts.\n\nColumns added over time: `secret` (owner token), `cells`/`marks` (for the live `/c/\u003cid\u003e`\nview), `email`/`newsletter` (optional recovery + opt-in), `alias` (group display name).\nThe `groups` table carries `name` (unique), `join_policy` (`open`/`password`),\n`password_hash` and `public_board`. See `migrations/`.\n\n## Newsletter \u0026 privacy\n\n- **Newsletter sign-ups** are stored in this site's own D1 `newsletter` table\n  (`email`, `source`, `consented_at`) — *not* pushed to Brevo. `source` is the origin\n  hostname so the list can later be consolidated across Gabriel's sites. The opt-in is an\n  explicit, unticked checkbox (GDPR consent); the timestamp is recorded. **No confirmation\n  email is sent** — the form is the confirmation. Once a visitor opts in, a local flag\n  (`evbingo.newsletter`) stops the checkbox being offered again on that device.\n- **GDPR**: no analytics or advertising cookies; game state is localStorage only (strictly\n  necessary, no banner needed). Email is collected only with consent, with a disclaimer at\n  the point of collection and a full policy at `/privacidad`. Update the contact address and\n  controller details there if they change.\n\n## Email sending (Brevo) — optional\n\nOnly the **recovery email** uses [Brevo](https://www.brevo.com), and it's best-effort: with\nno config the game runs fine and recovery just no-ops. Config:\n\n| Name                 | Where        | What                                                        |\n| :------------------- | :----------- | :---------------------------------------------------------- |\n| `BREVO_API_KEY`      | **secret**   | API key (`xkeysib-…`) from Brevo → SMTP \u0026 API → API Keys    |\n| `BREVO_SENDER_EMAIL` | var          | A verified sender address (Senders, Domains \u0026 Dedicated IPs)|\n| `BREVO_SENDER_NAME`  | var          | Display name for the sender                                 |\n\n```sh\nnpx wrangler secret put BREVO_API_KEY      # paste the xkeysib-... key when prompted\n# set the vars in wrangler.jsonc, or in the dashboard\n```\n\nFor local dev, fill `.dev.vars` (gitignored) with the same names.\n\n## Local development\n\n```sh\nnpm install\nnpx wrangler d1 migrations apply ev-bingo --local   # once, creates the local DB\nnpm run dev                                         # game + API + /v on :4321\n```\n\n## Deploy (Cloudflare Workers)\n\n### Git-connected (Workers Builds) — recommended\n\nConnect the repository to a Worker in the Cloudflare dashboard and set:\n\n- **Build command**: `npm run build`\n- **Deploy command**: `npm run deploy`\n\n`npm run deploy` applies pending D1 migrations (`wrangler d1 migrations apply ev-bingo\n--remote`, idempotent — already-applied ones are skipped) and then runs `wrangler deploy`,\nso schema changes ship automatically with every push. If the build token ever lacks D1\npermissions, run `npm run db:migrate` once locally and check the token scopes.\n\n### Manual\n\n```sh\nnpx wrangler login          # one time\nnpm run deploy              # migrations + deploy\n```\n\nThe build emits the final Worker config to `dist/server/wrangler.json` (wrangler picks it\nup automatically via `.wrangler/deploy/config.json`). Bindings without ids (e.g. the\nadapter's `SESSION` KV) are auto-provisioned by wrangler on deploy.\n\n## Out of scope for now\n\nAccounts, final certificate design, photo upload/collage, analytics, and a public gallery.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgtrabanco%2Fbingo-ev","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgtrabanco%2Fbingo-ev","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgtrabanco%2Fbingo-ev/lists"}