mirror of
https://github.com/tiennm99/loto.git
synced 2026-05-16 06:59:01 +00:00
574c22ddc1
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.
3.7 KiB
3.7 KiB
System Architecture
Page Flow
Entry
├─ / (Player Page)
│ ├─ Load loto_grid, loto_crossed from localStorage
│ ├─ Display 9×9 grid
│ ├─ Generate new grid on button click
│ ├─ Mark/unmark cells on click
│ └─ Show bingo popup + "Chờ X" toasts
│
└─ /master (Host Page)
├─ Load loto_master (called/remaining)
├─ Display 9×10 master board (numbers 1–90)
├─ Draw button shows next called number
├─ Display host's own card (loto_master_card prefix)
└─ New Game button resets called/remaining
State Model
Player Card (storagePrefix="loto")
grid: number[][] // 9×9 numbers (0 = empty)
crossed: boolean[][] // 9×9 marked state
Each row has exactly 5 non-zero numbers (distributed across columns via weighted random).
Host State (storagePrefix="loto_master")
called: number[] // [5, 23, 67, ...] — drawn in order
remaining: number[] // [1, 2, 3, ...] minus called — shuffled initially
Host's Card (storagePrefix="loto_master_card")
Same as player card; isolated by prefix to allow host to play.
localStorage Keys
| Prefix | Grid Key | Crossed Key |
|---|---|---|
"loto" |
loto_grid |
loto_crossed |
"loto_master_card" |
loto_master_card_grid |
loto_master_card_crossed |
(Special) loto_master stores { called, remaining } for the master state.
All keys are JSON stringified. Corruption is silent (returns null).
basePath & Asset Resolution
Production Mode
NODE_ENV=production
basePath="/loto"
Output: /loto/index.html, /loto/_next/...
GitHub Pages serves: https://user.github.io/loto
Development Mode (Local)
NEXT_DEV_PROFILE not set
basePath="" (empty)
Dev server: http://localhost:3000
Code-Server Mode
NEXT_DEV_PROFILE=codeserver
CODESERVER_HOST=<proxy-host>
CODESERVER_PORT=3000 (or env)
basePath="/absproxy/{PORT}"
HMR Origin: CODESERVER_HOST
Access: https://<proxy>/absproxy/3000/
Note: /absproxy/{port} preserves the basePath through the proxy. /proxy/{port} strips it before forwarding, breaking HMR.
Client-Only Architecture
All pages are client-only (no SSR) because:
ssr: falsein+layout.jsdisables server-side rendering- localStorage is unavailable on server
- State initialization (grid, crossed) must run in browser
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 button in PlayerBoard
2. handleCellClick(row, col) fires
3. crossed = crossed.map(...) toggled state
4. $effect listens to crossed → saveCrossedState()
5. localStorage updated with new crossed state
6. $derived updates rowCompleteness matrix
7. If row complete → bingo popup; if waiting → toast
Data Flow: Initial Load
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
| Name | Duration | Use |
|---|---|---|
| fade-in | 0.2s | Modal background entry |
| pop-in | 0.4s | Bingo popup scale + scale-back |
| 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/build |
| cell-crossed::after | instant | Red diagonal line in marked cells |
Offline Capability
All state is localStorage. No API calls. Fully functional offline after initial load.
Last reviewed: 2026-04-26