Bar trivia PWA with WebRTC multiplayer, Reveal.js slides, and buzzer gameplay. No backend — runs entirely in the browser.
Live: https://morbeo.github.io/viktorani/
API docs:
- Tech stack
- Getting started
- Documentation
- Project structure
- Multiplayer
- Data storage
- CI/CD
- Favicon & icons
- Contributing
| Layer | Library |
|---|---|
| UI | React 19 + TypeScript |
| Styling | Tailwind CSS v4 |
| Build | Vite 8 |
| PWA | vite-plugin-pwa |
| Presentation | Reveal.js |
| Storage | Dexie.js (IndexedDB) |
| Multiplayer A | PeerJS (WebRTC) |
| Multiplayer B | Gun.js + SEA encryption |
| Routing | React Router v7 (HashRouter) |
git clone https://github.com/morbeo/viktorani.git
cd viktorani
npm install
npm run dev| Script | What it does |
|---|---|
npm run dev |
Start dev server at localhost:5173 |
npm run build |
Type-check + production build → dist/ |
npm run docs |
Generate API docs → docs/api/ (gitignored) |
npm run lint |
ESLint across all *.ts / *.tsx files |
npm run typecheck |
Type-check app and test files (both tsconfigs) |
npm run test |
Run unit tests once via Vitest |
npm run test:watch |
Run tests in watch mode |
npm run test:coverage |
Run tests with V8 coverage report |
npm run preview |
Serve the production build locally |
npm run pack |
Build release tarball via scripts/pack.sh |
npm run release:dry |
Preview release — no changes made |
npm run release |
Interactive release: choose version, tag, push |
npm run release:patch |
Non-interactive patch bump |
npm run release:minor |
Non-interactive minor bump |
| Document | Description |
|---|---|
| Host guide | Setting up and running a trivia night |
| Player guide | Joining a game and buzzing in |
| API docs | Generated TypeDoc — transport, DB, hooks |
| Architecture decisions | ADRs for all major technical decisions |
To generate API docs locally:
npm run docs
# Output: docs/api/ (gitignored — generated at build time)src/
├── db/
│ ├── index.ts # Dexie schema — all collections
│ └── snapshot.ts # JSON export / import
├── transport/
│ ├── types.ts # GameEvent / PlayerEvent interfaces
│ ├── PeerJSTransport.ts
│ ├── GunTransport.ts # SEA-encrypted Gun.js relay
│ └── index.ts # TransportManager — auto-detect + manual override
├── hooks/
│ ├── useTransport.ts
│ ├── useBuzzer.ts
│ ├── useGameVisibility.ts
│ ├── useScoreboard.ts
│ └── useTimer.ts
├── components/
│ ├── AdminLayout.tsx
│ ├── host/ # HostQuestionPanel and sub-components
│ ├── timer/ # TimerPanel, TimerCard, CreateTimerModal
│ └── ui/index.tsx # Button, Card, Input, Modal, Badge…
├── pages/
│ ├── admin/ # Dashboard, Questions, Games, GameMaster, Layouts, Notes, Settings
│ └── player/ # Join, Play
├── test/
│ ├── setup.ts # jsdom polyfills (fake-indexeddb, URL mocks)
│ ├── transport.test.ts
│ ├── db.test.ts
│ ├── db-migration.test.ts
│ ├── ui.test.tsx
│ ├── routing.test.tsx
│ ├── questions-search.test.ts
│ ├── settings.test.tsx
│ ├── host/ # HostQuestionPanel component tests
│ ├── buzzer.test.ts
│ └── timer/ # Timer hook and component tests
└── App.tsx # HashRouter + all routes
docs/
├── adr/ # Architecture Decision Records
└── user-guide/ # Host and player guides
public/
├── favicon.svg # SVG favicon (source of truth)
├── icon-192.png # PWA icon — generate from favicon.svg (see Favicon section)
└── icon-512.png # PWA icon — generate from favicon.svg
No backend server. Two transport options, selectable per game:
| Mode | Mechanism | Internet required |
|---|---|---|
| PeerJS | WebRTC via PeerJS signaling | Initial handshake only |
| Gun.js | Decentralised relay + SEA encryption | Initial handshake only |
| Auto | Tries PeerJS (8s timeout) → falls back to Gun.js | Initial handshake only |
The host generates a Room ID and (for Gun.js) a 4-word passphrase. Both are embedded in the QR code that players scan. After the initial connection, all game state flows browser-to-browser with no server involvement.
Data on public Gun.js relays is encrypted using SEA (Security, Encryption, Authorization) with a shared secret derived from the passphrase + Room ID. The passphrase is displayed large on the Game Master screen so the host can read it aloud if QR scanning fails.
All data lives in IndexedDB (via Dexie.js) on the host device. Nothing is sent to any server.
Collections: difficulties, tags, questions, rounds, games, teams, players, buzzEvents, layouts, widgets, notes, timers, gameQuestions.
Backup: export a full JSON snapshot from the Dashboard. Import it on any device to restore or share your question bank.
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
PRs to master |
PR title lint + type-check, lint, test, build — both required to merge |
deploy.yml |
push to master + manual |
Type-check → lint → test → build → deploy to GitHub Pages |
docs.yml |
push to master (src/ changes) |
Generate TypeDoc → publish to gh-pages under /api/ |
release.yml |
push of v* tags |
Build tarball → generate release notes → publish GitHub Release |
automerge.yml |
CI workflow completes |
Auto-merge Dependabot patch/minor PRs when CI passes |
All workflows set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true to opt into the Node 24 runner ahead of the June 2026 forced migration.
deploy.yml only triggers on pushes that touch src/, public/, index.html, vite.config.ts, or package*.json, and ignores tag pushes — preventing a collision with release.yml when release-it bumps the version.
master requires two status checks before merge:
| Check | What it gates |
|---|---|
CI / lint-title |
PR title follows conventional commits |
CI / ci |
Type-check, lint, test, build all pass |
Force pushes and branch deletion are blocked. To apply or re-apply protection:
bash scripts/protect-master.shNote:
scripts/is gitignored. Add scripts explicitly withgit add --force scripts/
Dependencies are updated weekly (Monday 08:00 Sofia time). Patch and minor updates are grouped into a single PR per ecosystem and auto-merged once CI passes. Major bumps require manual review.
Labels used: dependencies (npm + Actions), ci (Actions only). Create them once:
gh label create dependencies --color 0075ca --description "Dependency updates"
gh label create ci --color e4e669 --description "CI/CD changes"Conventional commits drive automatic versioning via release-it + git-cliff:
| Commit prefix | Version bump |
|---|---|
feat: |
minor (0.x.0) |
fix:, perf: |
patch (0.0.x) |
feat!: / BREAKING CHANGE: |
major (x.0.0) |
To cut a release locally:
npm run release:dry # preview — no changes made
npm run release # interactive: choose version, tag, push, publish
npm run release:patch # non-interactive patch bump
npm run release:minor # non-interactive minor bumprelease-it will:
- Run lint + tests
- Bump the version in
package.json - Build the release tarball via
scripts/pack.sh - Commit, tag, and push
- Update
CHANGELOG.md
Pushing the tag triggers release.yml, which builds a fresh tarball, generates per-release notes with git-cliff, and publishes the GitHub Release. GITHUB_TOKEN is sufficient — no extra PAT required.
The source of truth is public/favicon.svg. From it, generate the PNG icons needed
by the PWA manifest and Apple devices:
# Using sharp-cli (install once)
npm install -g sharp-cli
sharp -i public/favicon.svg -o public/icon-192.png resize 192
sharp -i public/favicon.svg -o public/icon-512.png resize 512Or use Squoosh / RealFaviconGenerator
to generate them manually and drop them into public/.
The index.html uses %BASE_URL% so paths resolve correctly under /viktorani/
on GitHub Pages and at / in dev.
Tests are organised by feature area under src/test/. Run them with:
npm run test # run once
npm run test:watch # watch mode during development
npm run test:coverage # coverage report → coverage/lcov.infoTests run on every commit locally (pre-commit hook) and on every push to master (deploy workflow gate).
Two tsconfigs keep app and test types separate:
tsconfig.app.json— excludessrc/test/, no vitest globalstsconfig.test.json— includes onlysrc/test/, addsvitest/globalsand@testing-library/jest-dom
- Fork → branch off
master - Follow conventional commits (
feat(scope): description) - Pre-commit hooks (husky) run lint-staged (ESLint + Prettier on staged files), typecheck, and the full test suite automatically
- PR title must pass conventional commit lint (checked by
pr-title.yml) - Open PR against
master
feat · fix · refactor · perf · test · docs · ci · chore · build · epic
epicis used as the type on PR-level commits that close a multi-subtask issue.
admin · player · gamemaster · transport · db · ui · routing · pwa · build · deps · release · test · lint · github