https://github.com/gtrabanco/bingo-ev
Bingo de la resignación de los conductores de VE 🚘⚡️
https://github.com/gtrabanco/bingo-ev
bingo coches electric-vehicles electricos espanol game parodia parody spanish
Last synced: 23 days ago
JSON representation
Bingo de la resignación de los conductores de VE 🚘⚡️
- Host: GitHub
- URL: https://github.com/gtrabanco/bingo-ev
- Owner: gtrabanco
- Created: 2026-06-10T05:12:17.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-18T14:03:53.000Z (26 days ago)
- Last Synced: 2026-06-18T14:05:54.323Z (26 days ago)
- Topics: bingo, coches, electric-vehicles, electricos, espanol, game, parodia, parody, spanish
- Language: Astro
- Homepage: https://bingo.gruxon.com
- Size: 578 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# El Bingo del Cargador — bingo.gruxon.com
Single-page parody bingo of typical public EV-charger problems in Spain. Each visitor gets a
random card ("cartón") modeled after a real Spanish bingo card: a 3x4 grid where each row
holds 2 situations and 2 blanks (6 situations total). Completing a row sings "línea";
completing all 6 within one month earns a downloadable mock certificate. Spanish UI with a
dry-sarcastic tone; no brand names anywhere.
Game state lives in `localStorage` (no auth, no accounts). A minimal Cloudflare D1 registry
makes completed cards **verifiable**: every diploma links to `/v/`, a public page
that confirms the bingo was sung within the one-month window — per the server clock.
## Stack
- [Astro](https://astro.build) + [Tailwind CSS 4](https://tailwindcss.com)
- [@astrojs/cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/)
adapter: static homepage + a tiny server side (API + verification page) on
Cloudflare Workers with a D1 database.
- [uqr](https://github.com/unjs/uqr) (~3 KB, zero deps) to draw the verification QR on the
diploma — the only other runtime dependency.
## Project structure
```text
/
├── migrations/
│ └── 0001_init.sql # D1 schema: the single `cards` table
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ ├── BingoCard.astro # Cartón frame (header strip, grid, footer strip)
│ │ └── CertificateModal.astro # : canvas preview, PNG download, verify link
│ ├── data/
│ │ └── situations.json # Single source of truth: the situation pool (Spanish)
│ ├── layouts/
│ │ └── Layout.astro # HTML shell, meta, global styles
│ ├── lib/
│ │ ├── api.ts # Client for the registry API (degrades to offline play)
│ │ ├── card.ts # Cartón generation + validation + expiry rule
│ │ ├── certificate.ts # Certificate stub: canvas render + PNG download
│ │ ├── storage.ts # localStorage persistence (guarded, in-memory fallback)
│ │ └── wins.ts # Win detection: línea per row, bingo = all situations
│ ├── pages/
│ │ ├── api/cards/ # POST issue · POST [id]/complete · DELETE [id]
│ │ ├── v/[id].astro # Public verification page (Verificado/En juego/No consta)
│ │ └── index.astro # The whole game: markup + client-side wiring
│ └── styles/
│ └── global.css # Tailwind import, theme tokens, cell/dauber styles
├── astro.config.ts
├── wrangler.jsonc # Worker config + D1 binding
└── package.json
```
## Commands
| Command | Action |
| :---------------- | :-------------------------------------- |
| `npm install` | Install dependencies |
| `npm run dev` | Dev server at `http://localhost:4321` |
| `npm run build` | Production build to `./dist/` |
| `npm run preview` | Preview the production build locally |
## How it works
- **Card**: 6 situations are drawn at random from `src/data/situations.json` (40 entries)
and placed 2 per row on a 3x4 grid; the other cells are blanks, like a real cartón. The
card gets a short serial id; the chosen layout is stored with it, so a saved card keeps
rendering correctly even if the pool changes later.
- **Persistence**: `localStorage` keys — `evbingo.currentCardId` (pointer),
`evbingo.card.` (card + marks + completion), `evbingo.nick` (certificate name).
All access is wrapped in try/catch so private-browsing modes degrade to in-memory play.
- **Wins**: real-cartón rules. Completing the situations of a row triggers a "¡Línea!"
toast (blanks don't count); marking all 6 opens the certificate dialog ("¡Bingo!"). Once
sung, a bingo stays sung even if cells are unmarked later.
- **Dual marks**: each cell can be marked as *suffered* (kind 1, red dab) or *caused by
you* (kind 2, orange dab). A 3-way toggle sets the input mode: Sinvergüenza (taps mark
as caused), Mixto (tap = suffered, double tap = caused) or Resignado EV (taps mark as
suffered). Both kinds count towards línea/bingo.
- **Honorifics**: one certificate design for everyone; the title depends on behavior —
«Resignado Sufridor» (0 caused, gets thanked for exemplary conduct), «Granujilla»
(some caused, less than half) or «Sinvergüenza» (half or more caused).
- **Groups** (`/g/`): create a group with a **unique name**, choose who can join
(anyone with the link, or only with a **password**) and whether the board is **public**
(standings + aliases visible to anyone) or **members-only**. Joining always requires an
**alias**. Only the FIRST completion wins (atomic claim server-side); later bingos still
get their diploma, and completed cards can't join. The group page has share buttons
(WhatsApp · X · copy/native) whose text spells out the join policy and board visibility.
Passwords are stored salted-hashed (`groupId:password`, SHA-256), never plaintext;
members-only standings are fetched client-side and only returned for a card that proves
membership with its owner secret.
- **Expiry**: a card must be completed within one calendar month of its creation. Expired
incomplete cards are frozen under a "CADUCADO" stamp and must be regenerated. Enforced
client-side for the UI and server-side (server clock) for verification.
- **Certificate**: a placeholder canvas render (1200×900) with "¡Bingo!", the nick, the
completion date, the card serial, the verification URL and a QR pointing to it,
downloadable as PNG. Final design pending.
- **Sharing**: dialog buttons for WhatsApp (wa.me intent), X and Bluesky (prefilled compose
intents citing @gruxon), Instagram (Web Share sheet with the PNG on mobile; caption-copy +
profile fallback elsewhere) and YouTube (caption-copy + channel). A "Compartir cómo voy"
button shares the live read-only card view (`/c/`).
- **Live card view** (`GET /c/`): public, read-only render of a card and its marks for
spectators. Mutations require the owner secret, which never leaves the owner's browser.
- **Recover by email** (optional, no auth): a player can link an email to a card and get a
recovery email with owner links (id + secret), optionally opting into the newsletter.
- **Privacy** (`/privacidad`): GDPR notice — controller, purposes, lawful basis, retention,
rights — linked from the email form disclaimer and the footer.
## Verification registry (Cloudflare D1)
One tiny table (`migrations/0001_init.sql`): `cards (id, created_at, completed_at, nick)`.
Lifecycle — designed so the table stays small:
- **Issue** (`POST /api/cards`): the server generates the card id and creation timestamp.
Each issue also sweeps expired, never-completed rows (opportunistic GC).
- **Complete** (`POST /api/cards/:id/complete`): accepted only within one month of
creation, per the server clock. Late attempts get `410` and the row is deleted.
Re-posting on a completed card only updates the diploma nick.
- **Discard** (`DELETE /api/cards/:id`): called when a card is regenerated or found
expired. Completed cards are immune — their record is what makes diplomas verifiable.
- **Verify** (`GET /v/`): public page with three outcomes — Verificado (completed in
time, shows nick + date), En juego (registered, pending), No consta (unknown or expired).
If the API is unreachable, the game plays on with locally generated cards — they just
won't be verifiable. Astro's built-in CSRF check (`checkOrigin`) protects the endpoints
from cross-site form posts.
Columns added over time: `secret` (owner token), `cells`/`marks` (for the live `/c/`
view), `email`/`newsletter` (optional recovery + opt-in), `alias` (group display name).
The `groups` table carries `name` (unique), `join_policy` (`open`/`password`),
`password_hash` and `public_board`. See `migrations/`.
## Newsletter & privacy
- **Newsletter sign-ups** are stored in this site's own D1 `newsletter` table
(`email`, `source`, `consented_at`) — *not* pushed to Brevo. `source` is the origin
hostname so the list can later be consolidated across Gabriel's sites. The opt-in is an
explicit, unticked checkbox (GDPR consent); the timestamp is recorded. **No confirmation
email is sent** — the form is the confirmation. Once a visitor opts in, a local flag
(`evbingo.newsletter`) stops the checkbox being offered again on that device.
- **GDPR**: no analytics or advertising cookies; game state is localStorage only (strictly
necessary, no banner needed). Email is collected only with consent, with a disclaimer at
the point of collection and a full policy at `/privacidad`. Update the contact address and
controller details there if they change.
## Email sending (Brevo) — optional
Only the **recovery email** uses [Brevo](https://www.brevo.com), and it's best-effort: with
no config the game runs fine and recovery just no-ops. Config:
| Name | Where | What |
| :------------------- | :----------- | :---------------------------------------------------------- |
| `BREVO_API_KEY` | **secret** | API key (`xkeysib-…`) from Brevo → SMTP & API → API Keys |
| `BREVO_SENDER_EMAIL` | var | A verified sender address (Senders, Domains & Dedicated IPs)|
| `BREVO_SENDER_NAME` | var | Display name for the sender |
```sh
npx wrangler secret put BREVO_API_KEY # paste the xkeysib-... key when prompted
# set the vars in wrangler.jsonc, or in the dashboard
```
For local dev, fill `.dev.vars` (gitignored) with the same names.
## Local development
```sh
npm install
npx wrangler d1 migrations apply ev-bingo --local # once, creates the local DB
npm run dev # game + API + /v on :4321
```
## Deploy (Cloudflare Workers)
### Git-connected (Workers Builds) — recommended
Connect the repository to a Worker in the Cloudflare dashboard and set:
- **Build command**: `npm run build`
- **Deploy command**: `npm run deploy`
`npm run deploy` applies pending D1 migrations (`wrangler d1 migrations apply ev-bingo
--remote`, idempotent — already-applied ones are skipped) and then runs `wrangler deploy`,
so schema changes ship automatically with every push. If the build token ever lacks D1
permissions, run `npm run db:migrate` once locally and check the token scopes.
### Manual
```sh
npx wrangler login # one time
npm run deploy # migrations + deploy
```
The build emits the final Worker config to `dist/server/wrangler.json` (wrangler picks it
up automatically via `.wrangler/deploy/config.json`). Bindings without ids (e.g. the
adapter's `SESSION` KV) are auto-provisioned by wrangler on deploy.
## Out of scope for now
Accounts, final certificate design, photo upload/collage, analytics, and a public gallery.