From ce42eb181126149b1a8f2fae6be234fb2d7a3763 Mon Sep 17 00:00:00 2001 From: lila Date: Fri, 20 Mar 2026 18:37:38 +0100 Subject: [PATCH] chore: configure prettier with ignore rules and format scripts + running format --- .prettierignore | 19 +++++ .prettierrc | 13 +++ apps/api/tsconfig.json | 9 +- apps/web/tsconfig.json | 2 +- documentation/roadmap.md | 9 +- documentation/spec.md | 156 ++++++++++++++++++---------------- package.json | 5 +- packages/db/tsconfig.json | 4 +- packages/shared/tsconfig.json | 4 +- pnpm-lock.yaml | 10 +++ tsconfig.base.json | 4 +- tsconfig.json | 4 +- 12 files changed, 150 insertions(+), 89 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e0ef8ed --- /dev/null +++ b/.prettierignore @@ -0,0 +1,19 @@ +.tmp/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Dependencies +node_modules/ + +# Environment files +.env* + +# Logs (if you create them) +logs/ + +# Coverage reports (when you add testing) +coverage/ + +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6490d6a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "objectWrap": "collapse", + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 71f4e55..cd5bb81 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.base.json", "references": [ { "path": "../../packages/shared" }, - { "path": "../../packages/db" }, + { "path": "../../packages/db" } ], - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - }, - "include": ["src"], + "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" }, + "include": ["src"] } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 93675b1..d5e549e 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.base.json", - "references": [{ "path": "../../packages/shared" }], + "references": [{ "path": "../../packages/shared" }] } diff --git a/documentation/roadmap.md b/documentation/roadmap.md index 07b6feb..7286cee 100644 --- a/documentation/roadmap.md +++ b/documentation/roadmap.md @@ -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. diff --git a/documentation/spec.md b/documentation/spec.md index 5960a63..5fe4984 100644 --- a/documentation/spec.md +++ b/documentation/spec.md @@ -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 diff --git a/package.json b/package.json index b28e8fd..e0280f8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "concurrently \"pnpm --filter @glossa/web run dev\" \"pnpm --filter @glossa/api run dev\"", "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint ." + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "packageManager": "pnpm@10.32.1", "devDependencies": { @@ -14,6 +16,7 @@ "concurrently": "^9.2.1", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.1" } diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index ce5d26d..7155ba4 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", - "outDir": "./dist", + "outDir": "./dist" }, - "include": ["src"], + "include": ["src"] } diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index ce5d26d..7155ba4 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", - "outDir": "./dist", + "outDir": "./dist" }, - "include": ["src"], + "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f3cc5b..0ca6702 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@10.0.3) + prettier: + specifier: ^3.8.1 + version: 3.8.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -425,6 +428,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -917,6 +925,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.8.1: {} + punycode@2.3.1: {} require-directory@2.1.1: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index e125a65..2fd91dc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,6 +24,6 @@ "sourceMap": true, "strict": true, "target": "es2022", - "verbatimModuleSyntax": true, - }, + "verbatimModuleSyntax": true + } } diff --git a/tsconfig.json b/tsconfig.json index 0d5a0ff..75aa817 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ { "path": "./packages/shared" }, { "path": "./packages/db" }, { "path": "./apps/web" }, - { "path": "./apps/api" }, + { "path": "./apps/api" } ], - "files": [], + "files": [] }