A companion web app for the DSTA CODE_EXP / BrainHack 2026 hackathon "Secret Challenge — Bingo". Replaces the original sticker/paper bingo card with a digital experience: teams play a 4×4 bingo card across the event, complete squares by scanning each other's QR codes / uploading photos / asking mentors for stamps, and earn lucky-draw entries for completed lines. A live scoreboard + photo wall runs on the venue TV; an in-app spin animation handles the draw.
npm install
cp .env.example .env
# First-time only: bootstrap a Convex deployment. Opens a browser for login, then
# generates convex/_generated/, deploys schema + functions, and prints the URL.
npx convex dev
# In another terminal:
npm run devOpen http://localhost:5173. The splash page redirects to a team's bingo card if a magic-link token is in localStorage; otherwise it shows a "got a magic link?" message. To get a token, sign in to the admin panel (/admin) and create teams.
npx convex dev writes VITE_CONVEX_URL into .env.local automatically. Edit .env to set VITE_ADMIN_PASSCODE and VITE_ORGANISER_NAMES. Mirror those server-side too:
npx convex env set ADMIN_PASSCODE 'change-me'
npx convex env set ORGANISER_NAMES 'YJ,Marcus'
# Seed the 16 bingo squares + create the singleton gameState row (idempotent).
npx convex run seed:seedAll '{ "passcode": "change-me" }'See docs/CONVEX_BOOTSTRAP.md for fuller setup notes.
| Var | Purpose |
|---|---|
VITE_CONVEX_URL |
Convex deployment URL (printed by npx convex dev) |
VITE_ADMIN_PASSCODE |
Shared mentor passcode (also ships in client bundle — fine for the trust model) |
VITE_ORGANISER_NAMES |
Comma-separated names allowed to flip game state and run the lucky draw (e.g. YJ,Marcus) |
Server-side equivalents ADMIN_PASSCODE and ORGANISER_NAMES must match — set them with npx convex env set.
src/
├── App.tsx router with code-split routes
├── main.tsx Vite entry — wires <ConvexProvider>
├── index.css Tailwind directives + theme utilities
├── lib/ pure logic + small client helpers
│ ├── types.ts re-exports Convex Doc<> types under app-friendly names
│ ├── token.ts localStorage helpers (team token, admin creds)
│ ├── lines.ts bingo line counting (rows/cols/diagonals)
│ ├── qr.ts QR magic-link encode/parse
│ ├── storage.ts uploadToConvex helper (POST to upload URL)
│ ├── standings.ts compute lines + entries per team
│ ├── admin.ts client-side passcode/organiser checks
│ └── project.ts ZIP inspection (server runs the GitHub check)
├── hooks/
│ └── useTeam.ts stacks Convex useQuery() calls for a team's view
├── components/ reusable UI
│ ├── BingoGrid.tsx, SquareCell.tsx, TeamHeader.tsx
│ ├── QRScanner.tsx, PhotoCapture.tsx
│ ├── AdminLayout.tsx
│ └── Leaderboard.tsx, PhotoWall.tsx
└── pages/ one component per route
├── Splash.tsx, TeamHome.tsx, SquareDetail.tsx, TeamQR.tsx
├── ProjectSubmit.tsx, BoothDeepfake.tsx, Scoreboard.tsx
└── admin/
├── AdminLogin.tsx, ApprovalQueue.tsx
├── TeamsManage.tsx, GameControls.tsx, DrawSpin.tsx
convex/ Convex backend
├── schema.ts table definitions + shared validators
├── teams.ts list, getByToken, create, regenerateToken
├── squares.ts list (16 squares, sorted by position)
├── completions.ts submit* mutations + listForTeam + listPending (hydrated)
├── photos.ts recent (with hydrated public URLs)
├── codeSubmissions.ts getForTeam, listAll, save (upsert)
├── gameState.ts get, setOpen
├── draw.ts run (weighted random server-side), clearWinners
├── scoreboard.ts bundle (one query for the whole TV view), stats
├── upload.ts generateUploadUrl mutation for storage
├── githubCheck.ts action (server-side fetch to GitHub REST)
├── admin.ts assertAdmin, assertOrganiser, approve/reject mutations
├── mentorActions.ts logMentorAction helper used across mutations
├── seed.ts idempotent seedAll mutation for the 16 squares
└── _generated/ auto-generated by `npx convex dev` — never edit by hand
docs/
├── CONVEX_BOOTSTRAP.md first-time setup walkthrough
├── TESTING.md manual test checklist
└── HANDOVER.md intern follow-ups + open questions
/— splash, redirects to team home if a token is stored./t/:token— team home (the bingo card)./t/:token/square/:position— square detail with the right verification UI./t/:token/qr— big QR for other teams to scan./t/:token/project— GitHub URL + ZIP submission./booth/deepfake— auto-completes the booth square (scan-target for the printed booth poster)./scoreboard— public TV view, designed for 1920×1080.
/admin— sign-in./admin/queue— pending photo / IG approvals./admin/teams— manage 40 teams + magic links./admin/game— open/close the game + live stats./admin/draw— organiser-only lucky draw with spin animation.
- Each team has an opaque token. The "magic link" is
/t/<token>. Anyone with that URL acts as that team. - Mentors share a single passcode. The frontend gates the
/adminUI; every Convex mutation also re-checks the passcode server-side viaassertAdmin(andassertOrganiserfor game-open/close + draw). They identify themselves with a free-text name on every admin login; that name is stamped on every approve/reject action (mentorActionstable = audit trail). - Organisers are the subset of mentor names listed in
VITE_ORGANISER_NAMES(client) andORGANISER_NAMES(Convex env). Both must match. - The blue colour-distinct rule and game-open lock are enforced server-side in
convex/completions.ts— bypassing the UI gets you a server error.
npm run dev # Vite dev server with HMR (run `npx convex dev` separately)
npm run build # tsc -b && vite build → dist/
npm run lint # ESLint
npm run preview # serve dist/ for local production smoke testSee docs/HANDOVER.md for known follow-ups and open questions, and docs/TESTING.md for the manual test checklist.
See docs/TESTING.md for an end-to-end test checklist. Run through it on two browser profiles (or two phones on the same Wi-Fi) to verify all the team-to-team interactions work.