mirror of
https://github.com/tiennm99/loto.git
synced 2026-05-20 22:24:04 +00:00
d77d4a5652
Six small UI/UX phases shipped together: 1. Mobile legibility — cell aspect 3:4 on mobile (was 1:1), text-lg number, active:scale-90 press, navigator.vibrate(10) on tap, 200ms cross-draw clip-path animation on cell mark. 2. Brand — H1 italic rose-amber gradient with drop-shadow + "Hội chợ Tân Tân" subtitle. First-run state shows a faded preview card + warm welcome line instead of a gray placeholder. 3. Master focal point — "Số vừa xổ" hero scaled w-40/56 with text-7xl/8xl, role=status aria-live=assertive, scrollIntoView on each user-driven draw (gated by scrollOnNextDraw flag so reload doesn't yank the page). Master section uses transition:slide. 4. Settings polish — modal max-w-md on sm+; switchRow snippet replaces 4 boolean buttons with role=switch UI (focus ring, keyboard space/enter); empty cells get dark:[filter:brightness(0.85)_saturate(0.9)] so user's chosen purple isn't neon in dark mode. 5. Celebration tiering — 3rd+ bingo per card triggers a 12-emoji CSS confetti rain at z-[60] above the popup. 6. Docs sync — codebase-summary + pdr. Reviewer fixes applied: confetti z-index above popup; scroll-on-load guarded by interaction flag; switch focus ring. Tests 98/98 pass; build clean.
8.3 KiB
8.3 KiB
Codebase Summary
File Organization
Routing & Layout
| File | Purpose |
|---|---|
src/routes/+layout.svelte |
Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. |
src/routes/+page.svelte |
Player page (/) — the ONLY page. Header + SettingsButton, PlayerBoard, conditional MasterPanel (when settings.masterMode), PageFooter. Indigo→purple gradient branding. |
Shared Components
| File | Purpose |
|---|---|
src/lib/PlayerBoard.svelte |
Reusable player card (9×9 grid rendered as 3 stacked 3×9 mini-cards: Tân Tân / An khang thịnh vượng / Tân Tân tốt nhất). Tall (3:4 on mobile; 3:5 on sm+) cells with condensed bold black numbers (tan-tan-num font stack), white number cells, purple empty cells (dark mode dims via filter:brightness(0.85)). Handles crossed state, animated cross-out (200 ms cross-draw keyframe), active:scale-90 press, 10 ms haptic on tap. Two header actions: "Tạo bảng mới" / "Xoá đánh dấu". First-run state shows a faded preview card. Bingo popup tiers: row 1 = standard celebration; row 3+ = falling-emoji confetti rain via CSS confetti-fall. Toast "Chờ N" + audio. Accepts storagePrefix prop for multi-card isolation. |
src/lib/SettingsButton.svelte |
Gear icon + modal (responsive max-w-sm sm:max-w-md). 5 fieldsets: Giao diện (theme pills), Chế độ quản trò (switch row), Tự động xổ (switch + speed slider), Âm thanh (two switches + voice picker), Màu ô trống (10 Excel color swatches + custom picker). Boolean toggles use a shared switchRow snippet (role="switch" + keyboard support). Reset-to-default button. Mounted on /. |
src/lib/MasterPanel.svelte |
Host controls. New game / draw, large "Số vừa xổ" hero token (160 px mobile, 224 px sm+) with aria-live="assertive" + auto scrollIntoView on each new draw, "Thứ tự đã xổ" history list, 11×9 last-digit-aligned tracking grid (with circular tokens + draw-order overlay). No host's own player card (the player already has one above). "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on / when settings.masterMode === true; the wrapping section uses transition:slide for smooth toggle-in. |
src/lib/PageFooter.svelte |
Footer with tagline ("Made by miti99 with ❤️ SVG icon") + link. Mounted on /. |
Game Logic
| File | Purpose |
|---|---|
src/lib/game-logic.js |
Stateless utilities: generateGrid (constraint-aware picker — exact 5 per row & per col, ascending-sorted columns, soft "no 3 consecutive filled cols per row" via rejection sampling), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
src/lib/vietnamese-number.js |
numberToVietnamese(n) — pure utility mapping 0..90 to spoken Vietnamese, with tonal exceptions (15 → "mười lăm", 21 → "hai mươi mốt", 25 → "hai mươi lăm"). Out-of-range falls back to String(n). |
src/lib/voice.js |
Bundled-MP3 playback. Exports playNumber(n), playWaiting(n) (sequences cho + N), playBingo(), cancelPlayback(). Lazy <audio> cache, cancel-then-play, token-based cancel ensures stale promises can't resume after a new event. Reads active voice from settings.voice; URLs go through import { base } from "$app/paths" for basePath safety. |
src/lib/audio-manifest.js |
Re-exports static/audio/manifest.json as VOICES (array) + VOICE_IDS (Set) + DEFAULT_VOICE. Manifest is generated by scripts/generate-audio.py. |
src/lib/settings-store.svelte.js |
Reactive global UI settings via Svelte 5 runes. Stores 8 keys: theme (enum: "auto" / "light" / "dark"), masterMode (bool), autoCallEnabled (bool), autoCallSpeed (1–10), emptyCellColor (hex), voiceEnabledMaster (bool), voiceEnabledPlayer (bool), voice (string id matching audio manifest). Persisted to localStorage loto_settings. Pushes values to CSS vars and <html class="dark"> on :root. Per-key validators preserve old data. |
Styling
| File | Purpose |
|---|---|
src/app.css |
Root styles: Tailwind @import, CSS variables (light/dark), Tailwind v4 @variant dark (.dark *) for explicit dark-mode class selector, .loto-grid & .master-grid (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast, cross-draw 200 ms cell mark, confetti-fall 1.6 s), .cell-crossed diagonal, .confetti falling-emoji helper. |
Tests
| File | Purpose |
|---|---|
src/lib/game-logic.test.js |
27 unit tests: generateGrid shape (9×9, 5 per row/col, no duplicates), column ranges & ascending sort, no-3-consecutive soft constraint, row completion, waiting number detection, persistence (saveGrid/loadGrid/saveCrossedState/loadCrossedState with validators). |
src/lib/settings-store.test.js |
31 unit tests: defaults (incl. voice keys), loadSettings (restore 8 keys, apply CSS vars, toggle dark class, handle empty/corrupt), saveSettings, resetSettings, theme toggle (auto → OS pref detection), master mode, auto-call + speed, color validation, voice round-trip + invalid-id fallback. |
src/lib/vietnamese-number.test.js |
40 unit tests: ones (0–9), teens (10–19 incl. mười lăm), 20–90 incl. mốt and lăm exceptions, out-of-range fall-through. |
Configuration
| File | Purpose |
|---|---|
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, test, test:watch. |
eslint.config.mjs |
ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). Declares Svelte 5 rune globals (lines 16–22). |
jsconfig.json |
Path alias $lib, no checkJs. |
.gitignore |
Excludes node_modules, build, .env.local, etc. |
.env.example |
codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). |
static/_redirects |
Cloudflare Pages: /* / 301 — every unknown path 301-redirects to homepage. Static assets are served first so this only fires on misses. |
static/audio/{voiceId}/*.mp3 |
Pre-generated Vietnamese voice clips (1–90 + cho + kinh per voice). Generated once by scripts/generate-audio.py (edge-tts). Runtime never calls TTS. |
static/audio/manifest.json |
Voice list { id, edgeName, label, gender }[]. Written by the generator, imported by audio-manifest.js. |
scripts/generate-audio.py |
One-shot Python script: discovers every vi-* edge-tts voice, generates 92 clips per voice, writes manifest. Run after voice/wording changes. |
Key Data Structures
Grid: 9×9 2D array of numbers (1–90). Empty cells are 0.
Crossed: 9×9 2D array of booleans indicating marked cells.
Master State: { called: number[], remaining: number[] } — drawn and undrawn numbers.
Storage Keys (localStorage)
| Key | Use Case |
|---|---|
loto_grid |
Player's card numbers. |
loto_crossed |
Player's marked cells. |
loto_master |
Host's drawn/remaining numbers. |
loto_settings |
Global UI settings: { theme, masterMode, autoCallEnabled, autoCallSpeed, emptyCellColor }. |
loto_master_card_* keys are no longer written (host's own player card removed) but old saved data is left untouched.
Component Hierarchy
RootLayout
└── HomePage (/) ← single page; any other URL redirects to /
├── PlayerBoard (storagePrefix="loto")
├── [if settings.masterMode]
│ └── MasterPanel (controls, history, 11×9 tracking grid)
└── PageFooter
Key Functions
| Function | Location | Effect |
|---|---|---|
pickFilledCols() |
game-logic.js | Per-row column selection that guarantees exact 5 per col (forces any col whose remaining quota equals rowsLeft, random-fills the rest). |
generateGrid() |
game-logic.js | Builds 9×9; ascending-sorted numbers per column. |
isRowComplete() |
game-logic.js | Boolean: all non-zero cells in row crossed? |
getWaitingNumber() |
game-logic.js | Returns the single uncrossed number in row, or null. |
handleCellClick() |
PlayerBoard.svelte | Toggle crossed[row][col]. |
saveGrid() / loadGrid() |
game-logic.js | localStorage with prefix-based keys. |
Last reviewed: 2026-04-27 Last synced: 2026-04-27 (6-phase refactor)