mirror of
https://github.com/tiennm99/loto.git
synced 2026-09-01 04:20:00 +00:00
Settings expansion: 4 new keys (theme auto/light/dark, masterMode, autoCallEnabled, autoCallSpeed 1-10) with per-key validators that preserve old saved data. Default empty-cell color flipped from Tân Tân blue to Excel Standard Purple #7030A0; preset palette swapped to Office's 10 standard colors (5x2 grid). 15 new tests, 53 total. Theme system: Tailwind v4 @variant dark (.dark *); applyTheme() toggles <html class="dark"> based on settings.theme; auto mode mirrors prefers-color-scheme via matchMedia listener (cleanly torn down when switching modes). Existing dark-mode CSS converted from @media to :where(.dark) selectors. Mobile fit: PlayerBoard cells aspect-square on mobile, sm:aspect-[3/5] desktop. Number text scales text-base sm:text-2xl md:text-3xl. Page padding tightened (px-2 py-4 sm:px-3 sm:py-12). Container bumped max-w-lg to max-w-2xl. Header / footer: removed instructions toggle and "Trang quản trò" link from player page. New PageFooter.svelte (tagline + Made by miti99 with [SVG heart] link); also duplicated in PlayerBoard's closing section-label band per request. Heart is inline SVG (red), not emoji. Master mode: extracted everything from /master route into reusable MasterPanel.svelte. /master route slimmed to header + MasterPanel + footer. / mounts MasterPanel conditionally when settings.masterMode. Storage prefixes unchanged. Master tracking grid removed: per request, the 11x9 ones-digit master board is gone. Host still gets controls, "Số vừa xổ" hero, draw history list, and their own player card. Player card is enough. Auto-call: single $effect lifecycle keyed on (autoRunning, settings.autoCallSpeed, settings.autoCallEnabled). Setup / clear setInterval cleanly across speed changes, master-mode toggle off, component unmount, and "user disabled auto" mid-run. Button toggles "Xổ số" -> "Bắt đầu / Dừng". Speed slider in Settings (only visible when master mode is on). aria-label / aria-valuetext on slider. Code-review nice-to-fixes applied: folded the two MasterPanel $effects into one, removed muddled onkeydown on the SettingsButton modal backdrop, added slider a11y attrs. Docs: PDR / codebase-summary / system-architecture / development-roadmap / code-standards all synced to the new state.
4.6 KiB
4.6 KiB
System Architecture
Page Flow
Entry (+layout.svelte)
├─ onMount: loadSettings() — restore all 5 settings keys from loto_settings,
│ apply CSS vars, toggle <html class="dark"> on theme/OS pref, setup auto-call effect
│
├─ / (Player Page)
│ ├─ Load loto_grid, loto_crossed from localStorage
│ ├─ Display 9×9 PlayerBoard (empty cells use --empty-cell-bg from settings)
│ ├─ Generate new grid on button click
│ ├─ Mark/unmark cells on click
│ ├─ Show bingo popup + "Chờ X" toasts
│ ├─ [if settings.masterMode === true]
│ │ └─ Mount MasterPanel (host controls + draw history)
│ └─ PageFooter (tagline + miti99 link)
│
└─ /master (Host Page)
├─ MasterPanel (state, controls, draw history; same as above)
├─ Back link to /
└─ PageFooter
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 AND each column has exactly 5 (constraint-aware picker — no slack). Numbers within a column are sorted ascending top-to-bottom (lô tô hội chợ Tân Tân convention).
Settings (loto_settings)
theme: "auto" | "light" | "dark" // Display mode
masterMode: boolean // Show MasterPanel on /
autoCallEnabled: boolean // Enable auto-call timer
autoCallSpeed: number (1–10) // Speed in seconds
emptyCellColor: "#rrggbb" // Hex color (default #7030A0 Excel Purple)
Host State (storagePrefix="loto_master")
called: number[] // [5, 23, 67, ...] — drawn in order
remaining: number[] // [1, 2, 3, ...] minus called — shuffled initially
autoRunning: boolean // Auto-call timer active
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-27 Last synced: 2026-04-27 (6-phase refactor)