mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-15 02:17:11 +00:00
build(web): upgrade to SvelteKit 3 and adapter-static 4, move the PWA to vite-plugin-pwa
This commit is contained in:
1 parent
28825b78e4
commit
124d65f800
22 files changed
+310
-217
No files matched your search
@@ -127,19 +127,19 @@ function randomANumberInRow(weights) {
|
||||
|
||||
## Internal Navigation
|
||||
|
||||
- Use `import { base } from '$app/paths'` for internal links/asset URLs to preserve basePath across deployments.
|
||||
- Example: `<img src="{base}/icon.svg">` resolves on both root (`""`) and subpath (`/loto`) builds.
|
||||
- Use `asset()` (static files) and `resolve()` (routes) from `$app/paths` to preserve basePath across deployments. Their arguments don't start with `/` (route IDs passed to `resolve()` are the exception).
|
||||
- Example: `<img src={asset("icons/icon-192.png")}>` resolves on both root (`""`) and subpath (`/loto`) builds.
|
||||
|
||||
## Import Organization
|
||||
|
||||
1. SvelteKit imports (`$app/paths`, `$lib/...`)
|
||||
1. SvelteKit and app-library imports (`$app/paths`, `#lib/...`, always with the file extension)
|
||||
2. Third-party imports
|
||||
3. Local component/utility imports
|
||||
|
||||
```js
|
||||
import { base } from "$app/paths";
|
||||
import { generateGrid, isRowComplete } from "$lib/game-logic.js";
|
||||
import PlayerBoard from "$lib/PlayerBoard.svelte";
|
||||
import { asset } from "$app/paths";
|
||||
import { generateGrid, isRowComplete } from "#lib/game-logic.js";
|
||||
import PlayerBoard from "#lib/PlayerBoard.svelte";
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| `src/lib/active-tab.svelte.js` | Single-active-tab coordinator via `BroadcastChannel`. New tab broadcasts `claim`; old tab sets `activeTab.inactive = true`. `+layout.svelte` mounts `watchActiveTab()` on boot and renders a fullscreen overlay banner ("Phiên Lô tô đang chạy ở tab khác. Nhấn để tiếp tục tại đây.") when inactive — nhấn calls `claimActiveTab()` which broadcasts back, inactivating the other tab in turn. Soft coordination only (cooperating tabs); no-op in browsers without `BroadcastChannel` (legacy iOS Safari ≤15.4). Prevents double auto-call intervals, double localStorage writers, and overlapping audio across tabs. |
|
||||
| `src/lib/player-auto-cross.js` | Pure `applyMasterCalls({grid, crossed, called, lastHandledIndex, manualUnticks, mode})`. Cursor-by-index dedup (vs the retired `at`-timestamp model), so any caller can pass `lastHandledIndex: 0` to replay master's full history (used by player regen + "Xoá đánh dấu" in both mode). Manual unticks suppress re-cross on replay. |
|
||||
| `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/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 `asset()` 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 9 keys: `theme` (enum: "auto" / "light" / "dark"), `mode` (enum: "player" / "master" / "both"; replaces legacy `masterMode`), `autoCallEnabled` (bool), `autoCallSpeed` (1–10), `emptyCellColor` (hex), `voiceEnabledMaster` (bool), `voiceEnabledPlayer` (bool), `voiceWaitingNumber` (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. One-shot migration: `masterMode: true` → `mode: "both"`. |
|
||||
|
||||
@@ -47,21 +47,20 @@
|
||||
### Configuration & PWA
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `svelte.config.js` | adapter-static (HTML export), dual basePath via BUILD_PROFILE env, SvelteKit PWA plugin config. |
|
||||
| `vite.config.js` | Tailwind + SvelteKit + PWA plugins. codeserver HMR config (port, allowedHosts, hmr). |
|
||||
| `package.json` | SvelteKit 2, Svelte 5 (runes), Tailwind 4, Vite, @vite-pwa/sveltekit. Scripts: dev, dev:codeserver, build, build:gh, lint, test, test:watch. |
|
||||
| `vite.config.js` | Tailwind + SvelteKit + PWA plugins. SvelteKit options (adapter-static, dual basePath via `base-path.js`) go to the `sveltekit()` plugin; there is no `svelte.config.js`. vite-plugin-pwa generates `sw.js` from a `buildApp` hook. codeserver HMR config (port, allowedHosts, hmr). |
|
||||
| `package.json` | SvelteKit 3, Svelte 5 (runes), Tailwind 4, Vite, vite-plugin-pwa. `imports` maps `#lib/*` to `src/lib/*`. Scripts: dev, dev:codeserver, build, build:gh, lint, check, verify:pwa, test, test:watch. |
|
||||
| `.github/workflows/ci.yml` | Repo-root pipeline for push/PR. `test` (`npm test`) gates a two-profile `build` matrix; the artifacts feed `deploy-pages` (canonical, `https://tiennm99.github.io/loto/`), `deploy-firebase`, `preview-firebase`, and `android-debug`. |
|
||||
| `.github/actions/setup-web/` | Composite action: Node 24 + `npm ci` in `web/`. Shared by `ci.yml` and `android-release.yml`. |
|
||||
| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). Declares Svelte 5 rune globals. |
|
||||
| `jsconfig.json` | Path alias `$lib`, no checkJs. |
|
||||
| `jsconfig.json` | Extends `$app/tsconfig`; checkJs on for `src/` and `vite.config.js`. |
|
||||
| `.gitignore` | Excludes node_modules, build, .env.local, etc. |
|
||||
| `.env.example` | codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). |
|
||||
| `static/manifest.webmanifest` | PWA manifest: name, short_name, icons (192/512px), theme colors, display: standalone, scope, start_url. |
|
||||
| `static/icons/{192,512}.png` | PWA icons (auto-generated by @vite-pwa/sveltekit from source). |
|
||||
| `static/icons/{192,512}.png` | PWA icons, rendered from `static/icons/source*.svg` (see README). |
|
||||
| `static/audio/{voiceId}/*.mp3` | Pre-generated Vietnamese voice clips (1–90 + cho + kinh per voice). Generated by `scripts/generate-audio.py`. SW caches at runtime (CacheFirst). |
|
||||
| `static/audio/manifest.json` | Voice list `{ id, edgeName, label, gender }[]`. |
|
||||
| `scripts/generate-audio.py` | One-shot Python script: generates 92 clips per voice, writes manifest. |
|
||||
| Service Worker (auto) | @vite-pwa/sveltekit generates `/pwa-manifest.json`, `/sw.js`. App shell precache ~353 KB. Auto-update mode with reload toast. |
|
||||
| Service Worker (auto) | vite-plugin-pwa generates `/sw.js` (the manifest is the hand-written `static/manifest.webmanifest`). Precaches the app shell and the default voice. Prompt mode with an update banner. |
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
| `npm run build` | `""` (root) | Local preview / generic static host |
|
||||
| `npm run build:gh` | `/loto` | GitHub Pages → `https://tiennm99.github.io/loto` (canonical) |
|
||||
|
||||
Implementation: `svelte.config.js` reads `BUILD_PROFILE` env. Default is empty
|
||||
Implementation: `base-path.js` reads `BUILD_PROFILE` env and `vite.config.js`
|
||||
passes the result to the `sveltekit()` plugin's `paths.base`. 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.
|
||||
Static-file URLs go through `asset()` and route links through `resolve()`
|
||||
from `$app/paths`, so they survive either profile without code changes.
|
||||
|
||||
## Production Deployment — GitHub Pages
|
||||
|
||||
@@ -98,7 +99,7 @@ Generates:
|
||||
- `.svelte-kit/` — Build cache (not needed for deployment)
|
||||
|
||||
### Export Settings
|
||||
- `adapter-static` in `svelte.config.js`
|
||||
- `adapter-static`, passed to the `sveltekit()` plugin in `vite.config.js`
|
||||
- No server-side rendering (SSR disabled via `ssr: false`)
|
||||
- All pages pre-rendered to HTML + JS bundles
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ That format is intentionally out of scope.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: SvelteKit 2 with Svelte 5 (runes mode)
|
||||
- **Framework**: SvelteKit 3 with Svelte 5 (runes mode)
|
||||
- **Runtime**: Svelte 5 runes ($state, $derived, $effect, $props)
|
||||
- **Styling**: Tailwind CSS 4 (utility-first, animations)
|
||||
- **Persistence**: localStorage (no backend)
|
||||
|
||||
@@ -174,7 +174,7 @@ Each draw creates a new object (even repeat numbers) to ensure reactive re-fire.
|
||||
|
||||
**Static Storage**: All game state in localStorage. No API calls. Fully functional offline.
|
||||
|
||||
**Service Worker**: @vite-pwa/sveltekit auto-generates `/sw.js` with:
|
||||
**Service Worker**: vite-plugin-pwa generates `/sw.js` (wired into the SvelteKit build in `vite.config.js`) with:
|
||||
- App shell precache (~353 KB): HTML, CSS, JS bundles for instant load
|
||||
- Audio runtime caching: Voice clips cached on-demand (CacheFirst strategy)
|
||||
- Navigation: NetworkFirst (try network, fallback to cache)
|
||||
|
||||
Reference in new issue
Block a user