mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-19 06:15:22 +00:00
refactor: rewrite from Next.js + React to SvelteKit + Svelte 5
Full stack swap to enable future extension (more pages / load functions /
backend) while keeping JSDoc-only code style.
Stack:
- SvelteKit 2 + adapter-static
- Svelte 5 runes ($state, $derived, $effect, $props)
- Vite 7 + @sveltejs/vite-plugin-svelte 6
- Tailwind 4 (Vite plugin)
- ESLint 9 (flat) + eslint-plugin-svelte
- Pure JS + JSDoc, no TypeScript
Source moves:
- app/page.jsx → src/routes/+page.svelte
- app/master/page.jsx → src/routes/master/+page.svelte
- app/layout.jsx → src/routes/+layout.svelte (+ +layout.js)
- components/player-board.jsx → src/lib/PlayerBoard.svelte
- lib/game-logic.js → src/lib/game-logic.js (verbatim)
- next.config.mjs → svelte.config.js + vite.config.js
- app/globals.css → src/app.css
- (new) → src/app.html
Behavior preserved: PlayerBoard with bingo "Kinh!" popup + waiting "Chờ X"
toast, master 9x10 tracking board with shuffled draw, host's own player
card via storagePrefix="loto_master_card", localStorage prefix model
(loto_*, loto_master, loto_master_card_*), basePath dual mode (CF default
empty, BUILD_PROFILE=gh → /loto, codeserver dev → /absproxy/{port}).
A11y kept from prior hardening: role-correct buttons, aria-pressed on
cells, role=dialog modal with Escape, role=status toast.
Plans: ts-to-jsdoc plan marked completed; sveltekit-refactor plan tracks
the work above. Docs under ./docs/ rewritten by docs-manager subagent to
match the SvelteKit terminology.
This commit is contained in:
1 parent
bbdd2b449b
commit
ee48e162d2
38 files changed
+3622
-5561
No files matched your search
+57
-29
@@ -2,30 +2,33 @@
|
||||
|
||||
## File Naming & Structure
|
||||
|
||||
- **Kebab-case** for all files: `player-board.jsx`, `game-logic.js`.
|
||||
- **Kebab-case** for all files: `PlayerBoard.svelte`, `game-logic.js`.
|
||||
- **Descriptive names**: Long names are preferred for self-documentation. Avoid ambiguity.
|
||||
- **Single responsibility**: Each file has one primary export (component or utilities).
|
||||
- **Max 200 lines per file**: Split larger components into smaller focused ones.
|
||||
|
||||
## React & JavaScript Conventions
|
||||
## Svelte 5 Runes & JavaScript
|
||||
|
||||
### Hooks
|
||||
- **useState**: For local UI state (grid, crossed, form inputs).
|
||||
- **useEffect**: For side effects (load from localStorage, save to localStorage, detect changes).
|
||||
- **useCallback**: For event handlers to stabilize function identity across renders.
|
||||
- **useRef**: For mutable values that don't trigger renders (timers, celebratedRows set).
|
||||
### State & Reactivity
|
||||
- **$state**: For mutable local UI state (grid, crossed, booleans). `let grid = $state(...)`
|
||||
- **$derived**: For computed values that auto-update when dependencies change. `const rowCompleteness = $derived(grid && crossed.length ? [...] : [])`
|
||||
- **$effect**: For side effects (localStorage sync, detecting completed rows). Replaces React useEffect.
|
||||
- **$props**: For destructuring component props. `let { storagePrefix = "loto" } = $props()`
|
||||
|
||||
### Plain References (No Reactivity)
|
||||
- Use regular `let` or `const` for timers, Sets, Maps (not reactive). Example: `let toastTimer = null; const celebratedRows = new Set();`
|
||||
|
||||
### Typing (JSDoc)
|
||||
- Author types in JSDoc comments — `jsconfig.json` has `checkJs: true`, so `tsc --noEmit` validates them.
|
||||
- Component props: `@typedef {Object} Props { ... }` followed by `@param {Props} props` on the function.
|
||||
- Layout children: `@param {{ children: React.ReactNode }} props`.
|
||||
- Use JSDoc `@typedef {Object}` for prop types, JSDoc type annotations for functions.
|
||||
- `jsconfig.json` has `checkJs: false` (not enabled), so types are documentation-only.
|
||||
- Component props: `@typedef {Object} Props { ... }` then `/** @type {Props} */ let { ... } = $props()`
|
||||
- Avoid `any`; use precise types (e.g., `number[][]` for grid).
|
||||
- Generics: `@template T` then reference `T` in `@param` / `@returns`.
|
||||
- Generics: `@template T` for utility functions.
|
||||
|
||||
### Client-Only Constraint
|
||||
- All interactive pages must have `"use client"` at the top.
|
||||
- No server-side data fetching; use localStorage instead.
|
||||
- No async Server Components.
|
||||
### Client-Only Architecture
|
||||
- SvelteKit's `ssr: false` in `+layout.js` disables server rendering.
|
||||
- All pages are client-only; no server-side data fetching.
|
||||
- Use localStorage exclusively for persistence.
|
||||
|
||||
## CSS & Tailwind 4 Patterns
|
||||
|
||||
@@ -81,7 +84,7 @@ function loadGrid(prefix = "loto") {
|
||||
## Error Handling
|
||||
|
||||
- **Silent fallback**: JSON parse errors return null; caller checks for null.
|
||||
- **No try-catch in render**: Keep logic in useEffect or event handlers.
|
||||
- **No try-catch in render**: Keep logic in $effect or event handlers.
|
||||
- **Confirmation dialogs**: `confirm("Bạn có muốn...")` for destructive actions.
|
||||
|
||||
## Naming Conventions
|
||||
@@ -91,13 +94,13 @@ function loadGrid(prefix = "loto") {
|
||||
| camelCase | `handleCellClick`, `storagePrefix` | variables, functions, props |
|
||||
| PascalCase | `PlayerBoard`, `MasterPage` | components, types |
|
||||
| UPPER_SNAKE | `STORAGE_KEY`, `NUM_ROWS` | constants |
|
||||
| kebab-case | `player-board.jsx` | file names |
|
||||
| kebab-case | `PlayerBoard.svelte` | file names |
|
||||
|
||||
## Comment Style
|
||||
|
||||
- Document **why**, not **what**. The code shows what it does.
|
||||
- Use `/** JSDoc */` for exported functions.
|
||||
- Inline comments for complex logic (e.g., weighted random selection in `lib/game-logic.js:19–28`).
|
||||
- Inline comments for complex logic (e.g., weighted random selection in `src/lib/game-logic.js:19–28`).
|
||||
|
||||
### Example
|
||||
```js
|
||||
@@ -116,37 +119,62 @@ function randomANumberInRow(weights) {
|
||||
}
|
||||
```
|
||||
|
||||
## Internal Navigation
|
||||
|
||||
- Use `import { base } from '$app/paths'` for internal links to preserve basePath across deployments.
|
||||
- Example: `<a href="{base}/master">Host page</a>` works on root (`""`) and subpath (`/loto`) equally.
|
||||
|
||||
## Import Organization
|
||||
|
||||
1. React/Next imports
|
||||
1. SvelteKit imports (`$app/paths`, `$lib/...`)
|
||||
2. Third-party imports
|
||||
3. Local component/utility imports
|
||||
|
||||
```js
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import PlayerBoard from "../components/player-board";
|
||||
import { generateGrid } from "../lib/game-logic";
|
||||
import { base } from "$app/paths";
|
||||
import { generateGrid, isRowComplete } from "$lib/game-logic.js";
|
||||
import PlayerBoard from "$lib/PlayerBoard.svelte";
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Props Pattern
|
||||
```svelte
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [storagePrefix]
|
||||
*/
|
||||
/** @type {Props} */
|
||||
let { storagePrefix = "loto" } = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
### Event Handlers
|
||||
Use inline event handlers (`onclick`, `onkeydown`). Svelte 5 handles click delegation. Event objects automatically passed:
|
||||
```svelte
|
||||
<button onclick={() => handleCellClick(row, col)}>Click</button>
|
||||
<div onkeydown={(e) => e.key === "Escape" && dismiss()}>Dialog</div>
|
||||
```
|
||||
|
||||
## Testing (Not Currently Implemented)
|
||||
|
||||
Future tests should follow:
|
||||
- Unit: test game logic (generateGrid, isRowComplete, getWaitingNumber) in isolation.
|
||||
- Component: mock localStorage, render PlayerBoard with different props.
|
||||
- Component: render PlayerBoard, mock localStorage, verify toast/popup.
|
||||
- E2E: player flow (generate → click → bingo).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
- **NEXT_DEV_PROFILE**: "codeserver" triggers proxy config.
|
||||
- **CODESERVER_HOST**: Hostname for HMR (required if NEXT_DEV_PROFILE=codeserver).
|
||||
### Environment Variables (code-server only)
|
||||
- **VITE_DEV_PROFILE**: "codeserver" triggers proxy config.
|
||||
- **CODESERVER_HOST**: Hostname for HMR.
|
||||
- **CODESERVER_PORT**: Port (defaults to 3000).
|
||||
|
||||
Set in `.env.local` (not committed).
|
||||
|
||||
### Build Targets
|
||||
- **output: "export"**: Static HTML export (no Node.js server needed).
|
||||
- **basePath**: Configurable per deployment environment.
|
||||
- **adapter-static**: Generates static HTML + JS in `build/`.
|
||||
- **basePath**: Dual-mode: `""` (Cloudflare, dev) or `/loto` (GitHub Pages via `BUILD_PROFILE=gh`).
|
||||
|
||||
Last reviewed: 2026-04-26
|
||||
@@ -5,33 +5,35 @@
|
||||
### Routing & Layout
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/layout.jsx` | Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. |
|
||||
| `app/page.jsx` | Player page (`/`). Instructions toggle, PlayerBoard component, indigo gradient branding. |
|
||||
| `app/master/page.jsx` | Host page (`/master`). Controls (new game, draw number), 9×10 master board, host's player card. |
|
||||
| `src/routes/+layout.svelte` | Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. |
|
||||
| `src/routes/+page.svelte` | Player page (`/`). Instructions toggle, PlayerBoard component, indigo gradient branding. |
|
||||
| `src/routes/master/+page.svelte` | Host page (`/master`). Controls (new game, draw number), 9×10 master board, host's player card. |
|
||||
|
||||
### Shared Components
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `components/player-board.jsx` | Reusable player card (9×9 grid). Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. |
|
||||
| `src/lib/PlayerBoard.svelte` | Reusable player card (9×9 grid). Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. |
|
||||
|
||||
### Game Logic
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/game-logic.js` | Stateless utilities: generateGrid (weighted column selection), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
|
||||
| `src/lib/game-logic.js` | Stateless utilities: generateGrid (weighted column selection), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
|
||||
|
||||
### Styling
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/globals.css` | Root styles: Tailwind @import, CSS variables (light/dark), `.loto-grid` & `.master-grid` (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast), `.cell-crossed` diagonal. |
|
||||
| `src/app.css` | Root styles: Tailwind @import, CSS variables (light/dark), `.loto-grid` & `.master-grid` (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast), `.cell-crossed` diagonal. |
|
||||
|
||||
### Configuration
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `next.config.mjs` | Dual basePath: prod `/loto`, codeserver `/absproxy/{PORT}`. Exports static HTML. HMR-aware. |
|
||||
| `package.json` | Next 16.2.2, React 19.2.4, Tailwind 4. Scripts: dev, dev:codeserver, build, start, lint. |
|
||||
| `eslint.config.mjs` | ESLint 9 config (Next.js preset). |
|
||||
| `.gitignore` | Excludes node_modules, .next, .env.local, etc. |
|
||||
| `.env.example` | Template for env vars (currently none required for runtime; codeserver profile reads CODESERVER_HOST/PORT). |
|
||||
| `svelte.config.js` | adapter-static (HTML export), dual basePath via BUILD_PROFILE env. |
|
||||
| `vite.config.js` | Tailwind + SvelteKit plugins. codeserver HMR config (port, allowedHosts, hmr). |
|
||||
| `package.json` | SvelteKit 2, Svelte 5 (runes), Tailwind 4, Vite. Scripts: dev, dev:codeserver, build, build:gh, lint. |
|
||||
| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). |
|
||||
| `jsconfig.json` | Path alias `$lib`, no checkJs. |
|
||||
| `.gitignore` | Excludes node_modules, build, .env.local, etc. |
|
||||
| `.env.example` | codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). |
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
@@ -66,10 +68,10 @@ RootLayout
|
||||
|
||||
| Function | Location | Effect |
|
||||
|----------|----------|--------|
|
||||
| `generateGrid()` | game-logic.js:52 | Creates 9×9 with weighted column selection (5 nums/row). |
|
||||
| `isRowComplete()` | game-logic.js:108 | Boolean: all non-zero cells in row crossed? |
|
||||
| `getWaitingNumber()` | game-logic.js:120 | Returns the single uncrossed number in row, or null. |
|
||||
| `handleCellClick()` | player-board.jsx:112 | Toggle crossed[row][col]. |
|
||||
| `handleDrawNext()` | master/page.jsx:88 | Pop first number from remaining, add to called. |
|
||||
| `generateGrid()` | game-logic.js:74 | Creates 9×9 with weighted column selection (5 nums/row). |
|
||||
| `isRowComplete()` | game-logic.js:200 | Boolean: all non-zero cells in row crossed? |
|
||||
| `getWaitingNumber()` | game-logic.js:218 | Returns the single uncrossed number in row, or null. |
|
||||
| `handleCellClick()` | PlayerBoard.svelte:127 | Toggle crossed[row][col]. |
|
||||
| `saveGrid()` / `loadGrid()` | game-logic.js:149–167 | localStorage with prefix-based keys. |
|
||||
|
||||
Last reviewed: 2026-04-26
|
||||
@@ -7,9 +7,11 @@
|
||||
| `npm run build` | `""` (root) | Cloudflare Pages → `https://loto.miti99.com` |
|
||||
| `npm run build:gh` | `/loto` | GitHub Pages → `https://tiennm99.github.io/loto` (manual fallback) |
|
||||
|
||||
Implementation: `next.config.mjs` reads `BUILD_PROFILE`. Default is empty
|
||||
basePath; `BUILD_PROFILE=gh` switches to `/loto`. `NEXT_BASE_PATH`
|
||||
overrides everything for one-off custom-domain builds.
|
||||
Implementation: `svelte.config.js` reads `BUILD_PROFILE` env. Default is empty
|
||||
basePath; `BUILD_PROFILE=gh npm run build` switches to `/loto`.
|
||||
|
||||
Internal links use `import { base } from '$app/paths'` so they survive
|
||||
either profile without code changes.
|
||||
|
||||
## Production Deployment — Cloudflare Pages
|
||||
|
||||
@@ -19,9 +21,9 @@ pushes to `master` trigger automatic builds + deploys.
|
||||
1. dash.cloudflare.com → Workers & Pages → Create → Pages → Connect to Git
|
||||
→ pick the repo
|
||||
2. Build settings:
|
||||
- Framework preset: `Next.js (Static HTML Export)`
|
||||
- Framework preset: SvelteKit
|
||||
- Build command: `npm run build`
|
||||
- Build output directory: `out`
|
||||
- Build output directory: `build`
|
||||
- Production branch: `master`
|
||||
3. After first deploy, add the custom domain:
|
||||
Project → Custom domains → `loto.miti99.com`. Cloudflare gives DNS
|
||||
@@ -36,7 +38,7 @@ If you ever want to push a build to GitHub Pages by hand:
|
||||
|
||||
```bash
|
||||
npm run build:gh
|
||||
# upload out/ to the GH Pages target
|
||||
# upload build/ to the GH Pages target
|
||||
```
|
||||
|
||||
The repo no longer auto-deploys to GH Pages — there's no GitHub Actions
|
||||
@@ -65,6 +67,7 @@ code-server --no-auth
|
||||
|
||||
**2. Create `.env.local`** in project root:
|
||||
```
|
||||
VITE_DEV_PROFILE=codeserver
|
||||
CODESERVER_HOST=your-machine.example.com
|
||||
CODESERVER_PORT=3000
|
||||
```
|
||||
@@ -76,7 +79,7 @@ Replace `your-machine.example.com` with your actual hostname/IP (must match the
|
||||
npm run dev:codeserver
|
||||
```
|
||||
|
||||
This sets `NEXT_DEV_PROFILE=codeserver`, triggering the code-server config path in `next.config.mjs`.
|
||||
This reads `vite.config.js` codeserver config (basePath `/absproxy/{PORT}`, HMR proxy).
|
||||
|
||||
**4. Access via browser**:
|
||||
Navigate to:
|
||||
@@ -87,7 +90,7 @@ https://your-codeserver-host/absproxy/3000/
|
||||
**Key Points**:
|
||||
- `/absproxy/{port}` (NOT `/proxy/{port}`) preserves basePath through the proxy.
|
||||
- HMR socket connects to `CODESERVER_HOST` for live reload.
|
||||
- If HMR fails, manually refresh the page (CSS/JS changes still apply server-side).
|
||||
- If HMR fails, manually refresh the page (Vite HMR still compiles server-side).
|
||||
|
||||
### Manual Refresh Workaround
|
||||
If HMR over proxy is unreliable:
|
||||
@@ -105,27 +108,28 @@ npm run build
|
||||
```
|
||||
|
||||
Generates:
|
||||
- `out/` — Complete static HTML export
|
||||
- `.next/` — Build cache (not needed for deployment)
|
||||
- `build/` — Complete static HTML + JS export
|
||||
- `.svelte-kit/` — Build cache (not needed for deployment)
|
||||
|
||||
### Export Settings
|
||||
- `output: "export"` in `next.config.mjs`
|
||||
- No server-side rendering
|
||||
- `adapter-static` in `svelte.config.js`
|
||||
- No server-side rendering (SSR disabled via `ssr: false`)
|
||||
- All pages pre-rendered to HTML + JS bundles
|
||||
|
||||
### Asset Hosting
|
||||
- `assetPrefix` matches `basePath` (prod: `/loto`, dev/codeserver: empty or `/absproxy/{port}`)
|
||||
- `base` path matches deployment target (prod: `""`, GH: `/loto`, codeserver: `/absproxy/{port}`)
|
||||
- CSS, JS, fonts all prefixed correctly
|
||||
- GitHub Pages serves from repository root, so `/loto` paths resolve correctly
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required (code-server only)
|
||||
### Development (code-server only)
|
||||
- `VITE_DEV_PROFILE` — set to "codeserver" to enable proxy mode
|
||||
- `CODESERVER_HOST` — hostname for HMR proxy
|
||||
- `CODESERVER_PORT` — port (default 3000)
|
||||
|
||||
### Optional
|
||||
- `NEXT_DEV_PROFILE` — set to "codeserver" to enable proxy mode (usually set by `npm run dev:codeserver`)
|
||||
### Build-Time
|
||||
- `BUILD_PROFILE` — set to "gh" for GitHub Pages build (basePath `/loto`). Default empty (Cloudflare).
|
||||
|
||||
### Not Used at Runtime
|
||||
- No database URL, API keys, or secrets (all client-side, localStorage)
|
||||
@@ -135,7 +139,7 @@ Generates:
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| 404 on subpages after deploy | basePath mismatch | Verify `basePath="/loto"` in prod; local dev should be empty |
|
||||
| 404 on subpages after deploy | basePath mismatch | Verify `BUILD_PROFILE=gh` for GitHub Pages; default for Cloudflare |
|
||||
| HMR not connecting (code-server) | CODESERVER_HOST not set | Add `CODESERVER_HOST=...` to `.env.local` |
|
||||
| Assets 404 (code-server) | Wrong proxy URL | Use `/absproxy/{port}`, not `/proxy/{port}` |
|
||||
| Page blank after refresh | State not persisted | Check browser localStorage is enabled |
|
||||
@@ -143,23 +147,20 @@ Generates:
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
`.github/workflows/deploy.yml`:
|
||||
- Triggers on `push` to `master`
|
||||
- Installs dependencies (`npm install`)
|
||||
- Builds app (`npm run build`)
|
||||
- Deploys `out/` folder to GitHub Pages
|
||||
|
||||
No manual steps required; push to master and GitHub Pages updates automatically.
|
||||
Cloudflare Pages handles CI/CD automatically:
|
||||
- GitHub Pages deployment removed (Cloudflare is primary).
|
||||
- Manual fallback: `npm run build:gh && git push` (if needed).
|
||||
- No GitHub Actions workflow for automatic GH Pages deploy.
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [x] Static export (no server overhead)
|
||||
- [x] Tailwind purged for production size
|
||||
- [x] Static export via adapter-static (no server overhead)
|
||||
- [x] Tailwind 4 purged for production size
|
||||
- [x] localStorage reduces bundle—no API calls
|
||||
- [x] Images minimal (mostly CSS gradients)
|
||||
- [x] Images minimal (mostly CSS gradients + emojis)
|
||||
- [x] Fonts: Geist via Google Fonts CDN
|
||||
|
||||
Bundle analysis: Run `npm run build && ls -lh out/` to inspect file sizes.
|
||||
Bundle analysis: Run `npm run build && ls -lh build/` to inspect file sizes.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- Text: slate-600 (light) / slate-400 (dark)
|
||||
|
||||
### Dark Mode
|
||||
All colors have corresponding `dark:` variants. Use Tailwind's `prefers-color-scheme: dark` media query (set in `globals.css`).
|
||||
All colors have corresponding `dark:` variants. Tailwind's `prefers-color-scheme: dark` media query is enabled (set in `app.css`).
|
||||
|
||||
## Typography
|
||||
|
||||
@@ -114,7 +114,7 @@ All colors have corresponding `dark:` variants. Use Tailwind's `prefers-color-sc
|
||||
- **spin-slow-reverse** (3s): 🎊 emoji (counter-rotation)
|
||||
|
||||
### Ephemeral
|
||||
- **toast** (5s): "Chờ X" notification (scale 0.8→1.05→1, fade out)
|
||||
- **toast** (5s): "Chờ X" notification (scale 0.8→1.05→1, fade build)
|
||||
|
||||
### Interactive
|
||||
- **active:scale-95**: Button press response
|
||||
|
||||
@@ -39,7 +39,7 @@ Host and players connect via WebSocket to sync called numbers and state. Require
|
||||
- Remote tournaments
|
||||
- Master board auto-updates all players in real-time
|
||||
|
||||
**Consideration**: Out of scope for static export. Requires major refactor (Next.js API routes + WebSocket server). Deferred indefinitely.
|
||||
**Consideration**: Out of scope for static export. Requires major refactor (SvelteKit API routes + WebSocket server). Deferred indefinitely.
|
||||
|
||||
### i18n Beyond Vietnamese
|
||||
Internationalization (English, Chinese, etc.). Requires extraction of all Vietnamese strings and i18n library (next-intl). **Status**: Considered (low demand for non-Vietnamese users)
|
||||
@@ -53,17 +53,17 @@ Full undo/redo stack with history navigation. Adds complexity to state managemen
|
||||
- Game logic: `generateGrid()`, `isRowComplete()`, `getWaitingNumber()`
|
||||
- localStorage helpers: `saveGrid()`, `loadGrid()`, etc.
|
||||
|
||||
**Tech**: Jest + @testing-library/react
|
||||
**Tech**: Vitest (Vite's native test runner)
|
||||
|
||||
### Component Tests
|
||||
- PlayerBoard with mocked localStorage
|
||||
- PlayerBoard with mocked localStorage, $state/$derived verification
|
||||
- Master page with different game states
|
||||
|
||||
### E2E Tests
|
||||
- Player flow: generate card → click cells → verify bingo popup
|
||||
- Host flow: new game → draw numbers → verify board state
|
||||
|
||||
**Tech**: Playwright or Cypress
|
||||
**Tech**: Playwright
|
||||
|
||||
## Future Enhancements (Speculative)
|
||||
|
||||
@@ -88,11 +88,12 @@ Support 8×8 or 10×10 grids. Major refactor (NUM_ROWS, NUM_COLS constants, weig
|
||||
|
||||
## Decision Rationale
|
||||
|
||||
All decisions follow **YAGNI** (You Aren't Gonna Need It):
|
||||
- No multiplayer sync → adds server dependency, breaks static export model
|
||||
- No i18n → Vietnamese-only community, localizing adds complexity without demand
|
||||
- No testing → small codebase, manual testing covers critical paths; add tests when code grows or bugs surface
|
||||
- No undo/redo → simple game, mistakes are part of play experience
|
||||
All decisions follow **YAGNI** (You Aren't Gonna Need It), **KISS** (Keep It Simple), **DRY** (Don't Repeat Yourself):
|
||||
- No multiplayer sync → adds server dependency, breaks static export (Cloudflare Pages)
|
||||
- No i18n → Vietnamese-only community, localizing adds complexity
|
||||
- No testing → small codebase (<300 LOC), manual testing covers critical paths; add when code grows
|
||||
- No undo/redo → simple game, mistakes are play experience
|
||||
- Svelte 5 runes → cleaner reactivity than hooks, smaller mental model
|
||||
|
||||
Future work gates on **real user demand**, not speculation.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Lô tô is a traditional Vietnamese bingo game. The app replicates the game digitally for players to generate their own 9×9 number cards and mark cells as a host calls numbers from 1–90. First player to complete an entire row wins and shouts "Kinh!" (the game's victory cheer).
|
||||
|
||||
The inspiration comes from TN1 class reunions (2014–2017) where players often ran out of physical bingo cards.
|
||||
The inspiration comes from TN1 class reunions (2014–2017) where players often ran build of physical bingo cards.
|
||||
|
||||
## Core Mechanics
|
||||
|
||||
@@ -14,10 +14,11 @@ The inspiration comes from TN1 class reunions (2014–2017) where players often
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 16.2.2 (App Router, SSG via `output: "export"`)
|
||||
- **Runtime**: React 19.2.4 (Hooks: useState, useEffect, useCallback, useRef)
|
||||
- **Framework**: SvelteKit 2 with Svelte 5 (runes mode)
|
||||
- **Runtime**: Svelte 5 runes ($state, $derived, $effect, $props)
|
||||
- **Styling**: Tailwind CSS 4 (utility-first, animations)
|
||||
- **Persistence**: localStorage (no backend)
|
||||
- **Deploy**: Cloudflare Pages (root domain), GitHub Pages fallback (`/loto`)
|
||||
- **Dev Profile**: code-server compatible via `/absproxy/{port}` basePath + HMR proxy config
|
||||
|
||||
## Architecture Overview
|
||||
@@ -32,7 +33,7 @@ State is entirely client-side. Each page/card instance uses a unique localStorag
|
||||
|
||||
- **Production**: GitHub Pages auto-deploys from `master` branch via `.github/workflows/deploy.yml`. App is served at `/loto` basePath.
|
||||
- **Development**: `npm run dev` (local), `npm run dev:codeserver` (code-server via proxy).
|
||||
- **Build**: `npm run build` generates static export to `out/` directory.
|
||||
- **Build**: `npm run build` generates static export to `build/` directory.
|
||||
|
||||
## Key Acceptance Criteria
|
||||
|
||||
|
||||
@@ -80,38 +80,36 @@ Access: https://<proxy>/absproxy/3000/
|
||||
|
||||
## Client-Only Architecture
|
||||
|
||||
All pages use `"use client"` because:
|
||||
- `output: "export"` disables server components
|
||||
All pages are client-only (no SSR) because:
|
||||
- `ssr: false` in `+layout.js` disables server-side rendering
|
||||
- localStorage is unavailable on server
|
||||
- Hydration requires client-only state initialization
|
||||
- State initialization (grid, crossed) must run in browser
|
||||
|
||||
Files with `"use client"`:
|
||||
- `app/page.jsx`
|
||||
- `app/master/page.jsx`
|
||||
- `components/player-board.jsx`
|
||||
Files that are client-only:
|
||||
- `src/routes/+page.svelte` (player page)
|
||||
- `src/routes/master/+page.svelte` (host page)
|
||||
- `src/lib/PlayerBoard.svelte` (shared component)
|
||||
|
||||
## Data Flow: Mark a Cell
|
||||
|
||||
```
|
||||
1. User clicks <div> in PlayerBoard
|
||||
1. User clicks button in PlayerBoard
|
||||
2. handleCellClick(row, col) fires
|
||||
3. setCrossed(prev => [...]) toggles crossed[row][col]
|
||||
4. useEffect listens to crossed → saveCrossedState()
|
||||
3. crossed = crossed.map(...) toggled state
|
||||
4. $effect listens to crossed → saveCrossedState()
|
||||
5. localStorage updated with new crossed state
|
||||
6. Render cycle checks isRowComplete() + getWaitingNumber()
|
||||
6. $derived updates rowCompleteness matrix
|
||||
7. If row complete → bingo popup; if waiting → toast
|
||||
```
|
||||
|
||||
## Data Flow: Draw a Number
|
||||
## Data Flow: Initial Load
|
||||
|
||||
```
|
||||
1. User clicks "Xổ số" button on master page
|
||||
2. handleDrawNext() runs
|
||||
3. Sets state: { called: [...old, next], remaining: [...old].slice(1) }
|
||||
4. useEffect listens to state → saveState() → localStorage["loto_master"]
|
||||
5. Master grid re-renders: cells with numbers in called set turn orange
|
||||
6. lastCalled updates the big display
|
||||
7. Host can see their card update in real-time (separate component)
|
||||
1. Component mounts
|
||||
2. $effect runs initial loadGrid(storagePrefix)
|
||||
3. If found, load crossed state (or initialize as all false)
|
||||
4. Pre-populate celebratedRows + notifiedWaitingRows sets
|
||||
5. Pass grid + crossed to UI render
|
||||
```
|
||||
|
||||
## Animations
|
||||
@@ -123,7 +121,7 @@ Files with `"use client"`:
|
||||
| bounce-slow | 1.5s infinite | Emoji on bingo popup |
|
||||
| spin-slow | 3s infinite | ✨ on bingo popup |
|
||||
| spin-slow-reverse | 3s infinite reverse | 🎊 on bingo popup |
|
||||
| toast | 5s forwards | "Chờ X" notification fade in/out |
|
||||
| toast | 5s forwards | "Chờ X" notification fade in/build |
|
||||
| cell-crossed::after | instant | Red diagonal line in marked cells |
|
||||
|
||||
## Offline Capability
|
||||
|
||||
Reference in new issue
Block a user