chore: configure prettier with ignore rules and format scripts + running format
This commit is contained in:
parent
22bb8a1e4c
commit
ce42eb1811
12 changed files with 150 additions and 89 deletions
|
|
@ -5,6 +5,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 0 — Foundation
|
||||
|
||||
**Goal**: Empty repo that builds, lints, and runs end-to-end.
|
||||
**Done when**: `pnpm dev` starts both apps; `GET /api/health` returns 200; React renders a hello page.
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 1 — Vocabulary Data
|
||||
|
||||
**Goal**: Word data lives in the DB and can be queried via the API.
|
||||
**Done when**: `GET /api/terms?pair=en-it&limit=10` returns 10 terms, each with 3 distractors attached.
|
||||
|
||||
|
|
@ -39,6 +41,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 2 — Auth
|
||||
|
||||
**Goal**: Users can log in via Google or GitHub and stay logged in.
|
||||
**Done when**: JWT from OpenAuth is validated by the API; protected routes redirect unauthenticated users; user row is created on first login.
|
||||
|
||||
|
|
@ -57,6 +60,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 3 — Single-player Mode
|
||||
|
||||
**Goal**: A logged-in user can complete a full solo quiz session.
|
||||
**Done when**: User sees 10 questions, picks answers, sees their final score.
|
||||
|
||||
|
|
@ -71,6 +75,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 4 — Multiplayer Rooms (Lobby)
|
||||
|
||||
**Goal**: Players can create and join rooms; the host sees all joined players in real time.
|
||||
**Done when**: Two browser tabs can join the same room and see each other's display names update live via WebSocket.
|
||||
|
||||
|
|
@ -92,6 +97,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 5 — Multiplayer Game
|
||||
|
||||
**Goal**: Host starts a game; all players answer simultaneously in real time; a winner is declared.
|
||||
**Done when**: 2–4 players complete a 10-round game with correct live scores and a winner screen.
|
||||
|
||||
|
|
@ -110,6 +116,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 6 — Production Deployment
|
||||
|
||||
**Goal**: App is live on Hetzner, accessible via HTTPS on all subdomains.
|
||||
**Done when**: `https://app.yourdomain.com` loads; `wss://api.yourdomain.com` connects; auth flow works end-to-end.
|
||||
|
||||
|
|
@ -122,7 +129,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
|
||||
---
|
||||
|
||||
## Phase 7 — Polish & Hardening *(post-MVP)*
|
||||
## Phase 7 — Polish & Hardening _(post-MVP)_
|
||||
|
||||
Not required to ship, but address before real users arrive.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,33 +14,36 @@ A multiplayer English–Italian vocabulary trainer with a Duolingo-style quiz in
|
|||
|
||||
## 2. Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Monorepo | pnpm workspaces |
|
||||
| Frontend | React 18, Vite, TypeScript |
|
||||
| Routing | TanStack Router |
|
||||
| Server state | TanStack Query |
|
||||
| Client state | Zustand |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| Backend | Node.js, Express, TypeScript |
|
||||
| Realtime | WebSockets (`ws` library) |
|
||||
| Database | PostgreSQL 16 |
|
||||
| ORM | Drizzle ORM |
|
||||
| Cache / Queue | Valkey 8 |
|
||||
| Auth | OpenAuth (Google + GitHub) |
|
||||
| Validation | Zod (shared schemas) |
|
||||
| Testing | Vitest, React Testing Library |
|
||||
| Linting / Formatting | ESLint, Prettier |
|
||||
| Containerisation | Docker, Docker Compose |
|
||||
| Hosting | Hetzner VPS |
|
||||
| Layer | Technology |
|
||||
| -------------------- | ----------------------------- |
|
||||
| Monorepo | pnpm workspaces |
|
||||
| Frontend | React 18, Vite, TypeScript |
|
||||
| Routing | TanStack Router |
|
||||
| Server state | TanStack Query |
|
||||
| Client state | Zustand |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| Backend | Node.js, Express, TypeScript |
|
||||
| Realtime | WebSockets (`ws` library) |
|
||||
| Database | PostgreSQL 16 |
|
||||
| ORM | Drizzle ORM |
|
||||
| Cache / Queue | Valkey 8 |
|
||||
| Auth | OpenAuth (Google + GitHub) |
|
||||
| Validation | Zod (shared schemas) |
|
||||
| Testing | Vitest, React Testing Library |
|
||||
| Linting / Formatting | ESLint, Prettier |
|
||||
| Containerisation | Docker, Docker Compose |
|
||||
| Hosting | Hetzner VPS |
|
||||
|
||||
### Why `ws` over Socket.io
|
||||
|
||||
`ws` is the raw WebSocket library. For rooms of 2–4 players there is no need for Socket.io's transport fallbacks or room-management abstractions. The protocol is defined explicitly in `packages/shared`, which gives the same guarantees without the overhead.
|
||||
|
||||
### Why Valkey
|
||||
|
||||
Valkey stores ephemeral room state that does not need to survive a server restart. It keeps the PostgreSQL schema clean and makes room lookups O(1).
|
||||
|
||||
### Why pnpm workspaces without Turborepo
|
||||
|
||||
Turborepo adds parallel task running and build caching on top of pnpm workspaces. For a two-app monorepo of this size, the plain pnpm workspace commands (`pnpm -r run build`, `pnpm --filter`) are sufficient and there is one less tool to configure and maintain.
|
||||
|
||||
---
|
||||
|
|
@ -80,6 +83,7 @@ vocab-trainer/
|
|||
### pnpm workspace config
|
||||
|
||||
`pnpm-workspace.yaml` declares:
|
||||
|
||||
```
|
||||
packages:
|
||||
- 'apps/*'
|
||||
|
|
@ -89,6 +93,7 @@ packages:
|
|||
### Root scripts
|
||||
|
||||
The root `package.json` defines convenience scripts that delegate to workspaces:
|
||||
|
||||
- `dev` — starts `api` and `web` in parallel
|
||||
- `build` — builds all packages in dependency order
|
||||
- `test` — runs Vitest across all workspaces
|
||||
|
|
@ -123,23 +128,23 @@ Each layer only communicates with the layer directly below it. Business logic li
|
|||
|
||||
### Domain structure
|
||||
|
||||
| Subdomain | Service |
|
||||
|---|---|
|
||||
| `app.yourdomain.com` | React frontend |
|
||||
| `api.yourdomain.com` | Express API + WebSocket |
|
||||
| `auth.yourdomain.com` | OpenAuth service |
|
||||
| Subdomain | Service |
|
||||
| --------------------- | ----------------------- |
|
||||
| `app.yourdomain.com` | React frontend |
|
||||
| `api.yourdomain.com` | Express API + WebSocket |
|
||||
| `auth.yourdomain.com` | OpenAuth service |
|
||||
|
||||
### Docker Compose services (production)
|
||||
|
||||
| Container | Role |
|
||||
|---|---|
|
||||
| `postgres` | PostgreSQL 16, named volume |
|
||||
| `valkey` | Valkey 8, ephemeral (no persistence needed) |
|
||||
| `openauth` | OpenAuth service |
|
||||
| `api` | Express + WS server |
|
||||
| `web` | Nginx serving the Vite build |
|
||||
| `nginx-proxy` | Automatic reverse proxy |
|
||||
| `acme-companion` | Let's Encrypt certificate automation |
|
||||
| Container | Role |
|
||||
| ---------------- | ------------------------------------------- |
|
||||
| `postgres` | PostgreSQL 16, named volume |
|
||||
| `valkey` | Valkey 8, ephemeral (no persistence needed) |
|
||||
| `openauth` | OpenAuth service |
|
||||
| `api` | Express + WS server |
|
||||
| `web` | Nginx serving the Vite build |
|
||||
| `nginx-proxy` | Automatic reverse proxy |
|
||||
| `acme-companion` | Let's Encrypt certificate automation |
|
||||
|
||||
```
|
||||
nginx-proxy (:80/:443)
|
||||
|
|
@ -155,6 +160,7 @@ SSL is fully automatic via `nginx-proxy` + `acme-companion`. No manual Certbot n
|
|||
## 6. Data Model
|
||||
|
||||
### Design principle
|
||||
|
||||
Words are modelled as language-neutral **terms** with one or more **translations** per language. Adding a new language pair (e.g. English–French) requires **no schema changes** — only new rows in `translations` and `language_pairs`. The flat `english/italian` column pattern is explicitly avoided.
|
||||
|
||||
### Core tables
|
||||
|
|
@ -222,10 +228,12 @@ CREATE INDEX ON room_players (user_id);
|
|||
## 7. Vocabulary Data — WordNet + OMW
|
||||
|
||||
### Source
|
||||
|
||||
- **Princeton WordNet** — English words + synset IDs
|
||||
- **Open Multilingual Wordnet (OMW)** — Italian translations keyed by synset ID
|
||||
|
||||
### Extraction process
|
||||
|
||||
1. Run `scripts/extract_omw.py` once locally using NLTK
|
||||
2. Filter to the 1 000 most common nouns (by WordNet frequency data)
|
||||
3. Output: `packages/db/src/seed.json` — committed to the repo
|
||||
|
|
@ -243,9 +251,9 @@ The API validates the JWT from OpenAuth on every protected request. User rows ar
|
|||
|
||||
**Auth endpoint on the API:**
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/auth/me` | Validate token, return user |
|
||||
| Method | Path | Description |
|
||||
| ------ | -------------- | --------------------------- |
|
||||
| GET | `/api/auth/me` | Validate token, return user |
|
||||
|
||||
All other auth flows (login, callback, token refresh) are handled entirely by OpenAuth — the frontend redirects to `auth.yourdomain.com` and receives a JWT back.
|
||||
|
||||
|
|
@ -257,25 +265,25 @@ All endpoints prefixed `/api`. Request and response bodies validated with Zod on
|
|||
|
||||
### Vocabulary
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/language-pairs` | List active language pairs |
|
||||
| GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors |
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------------------- | --------------------------------- |
|
||||
| GET | `/language-pairs` | List active language pairs |
|
||||
| GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors |
|
||||
|
||||
### Rooms
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | `/rooms` | Create a room → returns room + code |
|
||||
| GET | `/rooms/:code` | Get current room state |
|
||||
| POST | `/rooms/:code/join` | Join a room |
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------------- | ----------------------------------- |
|
||||
| POST | `/rooms` | Create a room → returns room + code |
|
||||
| GET | `/rooms/:code` | Get current room state |
|
||||
| POST | `/rooms/:code/join` | Join a room |
|
||||
|
||||
### Users
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/users/me` | Current user profile |
|
||||
| GET | `/users/me/stats` | Games played, win rate |
|
||||
| Method | Path | Description |
|
||||
| ------ | ----------------- | ---------------------- |
|
||||
| GET | `/users/me` | Current user profile |
|
||||
| GET | `/users/me/stats` | Games played, win rate |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -287,22 +295,22 @@ All messages are JSON: `{ type: string, payload: unknown }`. The full set of typ
|
|||
|
||||
### Client → Server
|
||||
|
||||
| type | payload | Description |
|
||||
|---|---|---|
|
||||
| `room:join` | `{ code }` | Subscribe to a room's WS channel |
|
||||
| `room:leave` | — | Unsubscribe |
|
||||
| `room:start` | — | Host starts the game |
|
||||
| `game:answer` | `{ questionId, answerId }` | Player submits an answer |
|
||||
| type | payload | Description |
|
||||
| ------------- | -------------------------- | -------------------------------- |
|
||||
| `room:join` | `{ code }` | Subscribe to a room's WS channel |
|
||||
| `room:leave` | — | Unsubscribe |
|
||||
| `room:start` | — | Host starts the game |
|
||||
| `game:answer` | `{ questionId, answerId }` | Player submits an answer |
|
||||
|
||||
### Server → Client
|
||||
|
||||
| type | payload | Description |
|
||||
|---|---|---|
|
||||
| `room:state` | Full room snapshot | Sent on join and on any player join/leave |
|
||||
| `game:question` | `{ id, prompt, options[], timeLimit }` | New question broadcast to all players |
|
||||
| `game:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout |
|
||||
| `game:finished` | `{ scores[], winner }` | End of game summary |
|
||||
| `error` | `{ message }` | Protocol or validation error |
|
||||
| type | payload | Description |
|
||||
| -------------------- | -------------------------------------------------- | ----------------------------------------- |
|
||||
| `room:state` | Full room snapshot | Sent on join and on any player join/leave |
|
||||
| `game:question` | `{ id, prompt, options[], timeLimit }` | New question broadcast to all players |
|
||||
| `game:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout |
|
||||
| `game:finished` | `{ scores[], winner }` | End of game summary |
|
||||
| `error` | `{ message }` | Protocol or validation error |
|
||||
|
||||
### Multiplayer game mechanic — simultaneous answers
|
||||
|
||||
|
|
@ -382,16 +390,17 @@ TanStack Query handles all server data fetching. Zustand handles ephemeral UI an
|
|||
|
||||
## 13. Testing Strategy
|
||||
|
||||
| Type | Tool | Scope |
|
||||
|---|---|---|
|
||||
| Unit | Vitest | Services, QuizService distractor logic, Zod schemas |
|
||||
| Component | Vitest + RTL | QuestionCard, OptionButton, auth forms |
|
||||
| Integration | Vitest | API route handlers against a test DB |
|
||||
| E2E | Out of scope for MVP | — |
|
||||
| Type | Tool | Scope |
|
||||
| ----------- | -------------------- | --------------------------------------------------- |
|
||||
| Unit | Vitest | Services, QuizService distractor logic, Zod schemas |
|
||||
| Component | Vitest + RTL | QuestionCard, OptionButton, auth forms |
|
||||
| Integration | Vitest | API route handlers against a test DB |
|
||||
| E2E | Out of scope for MVP | — |
|
||||
|
||||
Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
||||
|
||||
**Critical paths to cover:**
|
||||
|
||||
- Distractor generation (correct POS, no duplicates, never includes answer)
|
||||
- Answer validation (server-side, correct scoring)
|
||||
- Game session lifecycle (create → play → complete)
|
||||
|
|
@ -402,6 +411,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
|||
## 14. Definition of Done
|
||||
|
||||
### Functional
|
||||
|
||||
- [ ] User can log in via Google or GitHub (OpenAuth)
|
||||
- [ ] User can play singleplayer: 10 rounds, score, result screen
|
||||
- [ ] User can create a room and share a code
|
||||
|
|
@ -410,6 +420,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
|||
- [ ] 1 000 English–Italian words seeded from WordNet + OMW
|
||||
|
||||
### Technical
|
||||
|
||||
- [ ] Deployed to Hetzner with HTTPS on all three subdomains
|
||||
- [ ] Docker Compose running all services
|
||||
- [ ] Drizzle migrations applied on container start
|
||||
|
|
@ -417,6 +428,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
|||
- [ ] pnpm workspace build pipeline green
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] `SPEC.md` complete
|
||||
- [ ] `.env.example` files for all apps
|
||||
- [ ] `README.md` with local dev setup instructions
|
||||
|
|
@ -425,12 +437,12 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
|||
|
||||
## 15. Out of Scope (MVP)
|
||||
|
||||
- Difficulty levels *(`frequency_rank` column exists, ready to use)*
|
||||
- Additional language pairs *(schema already supports it — just add rows)*
|
||||
- Leaderboards *(`games_played`, `games_won` columns exist)*
|
||||
- Difficulty levels _(`frequency_rank` column exists, ready to use)_
|
||||
- Additional language pairs _(schema already supports it — just add rows)_
|
||||
- Leaderboards _(`games_played`, `games_won` columns exist)_
|
||||
- Streaks / daily challenges
|
||||
- Friends / private invites
|
||||
- Audio pronunciation
|
||||
- CI/CD pipeline (manual deploy for now)
|
||||
- Rate limiting *(add before going public)*
|
||||
- Rate limiting _(add before going public)_
|
||||
- Admin panel for vocabulary management
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue