678977ab23
- Tablet-first responsive scoreboard: whole player card is tap target - Per-step wizard backgrounds and live summary panel - Larger touch targets, safe-area handling, reduced-motion support - German UI error strings in App.tsx - Updated SOUL.md and README.md for current architecture - Cleanup: removed legacy components, .cursor/, UI_IMPROVEMENTS.md - Renamed package to bscscore, fixed TOTAL_WIZARD_STEPS
224 lines
7.8 KiB
Markdown
224 lines
7.8 KiB
Markdown
# BSC Score - Pool Scoring Application
|
|
|
|
A tablet-first PWA for live pool/billiards scoring at the table. Built with **Astro 5** (islands) and **Preact**, with reusable logic under `src/lib`.
|
|
|
|
## Features
|
|
|
|
- **Game types**: 8-Ball, 9-Ball, 10-Ball; Race-to or 14/1 Endlos
|
|
- **Live scoring**: Tap a player's score card for +1; undo for the last point
|
|
- **New-game wizard**: 7 steps with running summary (`WizardSummary`) and per-step colored backgrounds
|
|
- **Break tracking**: Winnerbreak / Wechselbreak, break-order selection, live break indicator on scoreboard
|
|
- **Player history**: Quick-pick from previously used names
|
|
- **Game list**: Filter (Alle / Aktiv / Abgeschlossen), tap card to open, delete with confirmation
|
|
- **Rematch**: Start a new game with the same players after completion
|
|
- **PWA**: Offline via service worker + IndexedDB (`BSCScoreDB`), fullscreen manifest, German UI
|
|
- **TypeScript**: Strict mode throughout
|
|
|
|
**Device priority:** landscape tablet at the table, then mobile portrait, then desktop. Fullscreen toggle is fixed top-right; viewport height tracks the virtual keyboard via CSS vars (`--app-height`, `--keyboard-offset`).
|
|
|
|
## Architecture
|
|
|
|
Reusable code lives under `src/lib` so the core can be embedded outside the Astro shell.
|
|
|
|
- **`@lib/domain`** — Types, constants, validation, pure helpers
|
|
- **`@lib/data`** — IndexedDB adapters and `GameService`
|
|
- **`@lib/state`** — Hooks (`useGameState`, `useNavigation`, `useNewGameWizard`, modals)
|
|
- **`@lib/ui`** — Stateless primitives (Button, Card, Modal, Layout) with CSS modules
|
|
- **`@lib/features/*`** — Feature bundles (game-list, game-detail, game-lifecycle, new-game wizard)
|
|
|
|
`src/components` is a thin Astro host: `App.tsx` orchestrates screens and islands.
|
|
|
|
Module docs: `src/lib/docs/architecture.md` and per-package READMEs.
|
|
|
|
## Getting Started
|
|
|
|
### Prerequisites
|
|
|
|
- Node.js 18+
|
|
- npm
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
git clone <repository-url>
|
|
cd bscscore
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Dev server: `http://localhost:3000`
|
|
|
|
### Scripts
|
|
|
|
```bash
|
|
npm run dev # Dev server (--host)
|
|
npm run build # Production build → dist/
|
|
npm run preview # Preview production build
|
|
npm run test:record # Playwright codegen (record flows)
|
|
npm run test:e2e # Run Playwright recordings
|
|
```
|
|
|
|
### Docker build
|
|
|
|
```bash
|
|
docker run -it -v $(pwd):/app -w /app --rm node:latest npx astro build
|
|
```
|
|
|
|
## Testing
|
|
|
|
Playwright recordings under `tests/recordings/` cover core game flows. See `tests/recordings/README.md`.
|
|
|
|
```bash
|
|
# Terminal 1
|
|
npm run dev
|
|
|
|
# Terminal 2 — record
|
|
npm run test:record
|
|
|
|
# Replay all
|
|
npm run test:e2e
|
|
```
|
|
|
|
### Seed test matches (browser console)
|
|
|
|
Paste while the app is open. Inserts 10 sample matches into IndexedDB and reloads.
|
|
|
|
```js
|
|
await (async () => {
|
|
const DB_NAME = 'BSCScoreDB';
|
|
const DB_VERSION = 1;
|
|
const GAMES_STORE = 'games';
|
|
|
|
const openDb = () =>
|
|
new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
const now = Date.now();
|
|
const minute = 60 * 1000;
|
|
|
|
const seedGames = [
|
|
{ players: ['Alex', 'Ben'], gameType: '8-Ball', raceTo: 5, scores: [5, 2], status: 'completed', minutesAgo: 190 },
|
|
{ players: ['Chris', 'Dana', 'Eli'], gameType: '8-Ball', raceTo: 6, scores: [4, 6, 2], status: 'completed', minutesAgo: 175 },
|
|
{ players: ['Faye', 'Gus'], gameType: '9-Ball', raceTo: 7, scores: [7, 5], status: 'completed', minutesAgo: 160 },
|
|
{ players: ['Hugo', 'Iris', 'Jade'], gameType: '9-Ball', raceTo: 4, scores: [2, 3, 1], status: 'active', minutesAgo: 145 },
|
|
{ players: ['Kai', 'Lena'], gameType: '10-Ball', raceTo: 8, scores: [8, 6], status: 'completed', minutesAgo: 130 },
|
|
{ players: ['Mona', 'Nico', 'Omar'], gameType: '10-Ball', raceTo: 5, scores: [5, 1, 4], status: 'completed', minutesAgo: 115 },
|
|
{ players: ['Pia', 'Quin'], gameType: '8-Ball', raceTo: 4, scores: [1, 1], status: 'active', minutesAgo: 100 },
|
|
{ players: ['Rex', 'Sara', 'Timo'], gameType: '9-Ball', raceTo: 3, scores: [3, 2, 0], status: 'completed', minutesAgo: 85 },
|
|
{ players: ['Uma', 'Vik'], gameType: '10-Ball', raceTo: 6, scores: [2, 4], status: 'active', minutesAgo: 70 },
|
|
{ players: ['Will', 'Xena', 'Yara'], gameType: '8-Ball', raceTo: 7, scores: [6, 4, 7], status: 'completed', minutesAgo: 55 },
|
|
];
|
|
|
|
const gameRecords = seedGames.map((entry, index) => {
|
|
const createdAtMs = now - entry.minutesAgo * minute;
|
|
const updatedAtMs = createdAtMs + 15 * 1000;
|
|
const id = now + index + 1;
|
|
const isThreePlayer = entry.players.length === 3;
|
|
const winnerIndex = entry.scores.indexOf(Math.max(...entry.scores));
|
|
|
|
const game = {
|
|
id,
|
|
gameType: entry.gameType,
|
|
raceTo: entry.raceTo,
|
|
status: entry.status,
|
|
createdAt: new Date(createdAtMs).toISOString(),
|
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
log: [
|
|
{
|
|
type: 'score_change',
|
|
timestamp: new Date(createdAtMs + 5 * 1000).toISOString(),
|
|
description: 'Seed data: score progression',
|
|
},
|
|
...(entry.status === 'completed'
|
|
? [
|
|
{
|
|
type: 'game_complete',
|
|
timestamp: new Date(updatedAtMs).toISOString(),
|
|
description: `Winner: ${entry.players[winnerIndex]}`,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
undoStack: [],
|
|
player1: entry.players[0],
|
|
player2: entry.players[1],
|
|
...(isThreePlayer ? { player3: entry.players[2] } : {}),
|
|
score1: entry.scores[0],
|
|
score2: entry.scores[1],
|
|
...(isThreePlayer ? { score3: entry.scores[2] } : {}),
|
|
breakRule: 'winnerbreak',
|
|
breakOrder: isThreePlayer ? [1, 2, 3] : [1, 2],
|
|
currentBreakerIdx: index % entry.players.length,
|
|
};
|
|
|
|
return {
|
|
id,
|
|
game,
|
|
lastModified: updatedAtMs,
|
|
syncStatus: 'local',
|
|
version: 1,
|
|
createdAt: createdAtMs,
|
|
};
|
|
});
|
|
|
|
const db = await openDb();
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const tx = db.transaction([GAMES_STORE], 'readwrite');
|
|
const store = tx.objectStore(GAMES_STORE);
|
|
|
|
for (const record of gameRecords) {
|
|
store.put(record);
|
|
}
|
|
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error);
|
|
tx.onabort = () => reject(tx.error);
|
|
});
|
|
|
|
console.log(`Inserted ${gameRecords.length} test matches into IndexedDB.`);
|
|
window.location.reload();
|
|
})();
|
|
```
|
|
|
|
## Project structure
|
|
|
|
```
|
|
src/
|
|
├── components/ # Astro shell (App.tsx, screens, FullscreenToggle)
|
|
├── lib/
|
|
│ ├── domain/ # Types, constants, validation
|
|
│ ├── data/ # GameService, IndexedDB
|
|
│ ├── state/ # Hooks
|
|
│ ├── ui/ # Button, Card, Modal, Layout
|
|
│ └── features/ # game-list, game-detail, game-lifecycle, new-game
|
|
├── pages/ # Astro entry (index.astro)
|
|
└── styles/ # Global tokens (index.css)
|
|
```
|
|
|
|
## Design system
|
|
|
|
Tokens in `src/styles/index.css`:
|
|
|
|
- **Theme**: Dark only — background `#1a1a1a`, surfaces `#222`, accent `#ff9800`
|
|
- **Typography**: System font stack (no webfonts)
|
|
- **Touch targets**: 48px min, 56px comfortable, 64px large
|
|
- **Breakpoints**: 480px / 768px / 1024px / 1200px (`@lib/domain/constants`)
|
|
- **Scoreboard cards**: Per-player gradient colors (`--color-player-1` … `3`); entire card is the tap target
|
|
- **Components**: Button variants `primary` | `secondary` | `danger`; Card `default` | `elevated` | `outlined`
|
|
|
|
UI copy and validation messages: `@lib/domain/constants` (`VALIDATION_MESSAGES`, `ARIA_LABELS`). Product tone: `SOUL.md`.
|
|
|
|
## Stack
|
|
|
|
- [Astro](https://astro.build/) 5 + [Preact](https://preactjs.com/)
|
|
- TypeScript strict, CSS Modules, Vite
|
|
- Playwright for recorded E2E flows
|
|
|
|
## Deploy
|
|
|
|
`npm run build` → `dist/` → nginx via `compose.yml` on `score.bsc-gp.de`.
|