mirror of
https://github.com/tiennm99/loto.git
synced 2026-09-07 18:17:15 +00:00
refactor: single-page app — restore master tracking grid, drop /master route
Previous commit misread "remove master board" — restore the 11x9
ones-digit-aligned tracking grid (with circular tokens + draw-order
overlay) inside MasterPanel; remove the host's own player card
("Bảng của quản trò") instead. The host can use the player board
above it; no need for a duplicate card.
Delete /master route entirely (single-page app now). Cloudflare
Pages redirect added via static/_redirects: any unknown path
301-redirects to /, so old /master bookmarks land on the homepage.
Static assets are served first so the rule only fires on misses.
Storage keys loto_master_card_grid / loto_master_card_crossed are
no longer written but old saved data stays in users' localStorage
(harmless leftover).
This commit is contained in:
@@ -6,15 +6,14 @@
|
||||
| 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 (`/`). Header + SettingsButton, PlayerBoard, conditional MasterPanel (when `settings.masterMode`), PageFooter. Indigo→purple gradient branding. |
|
||||
| `src/routes/master/+page.svelte` | Host page (`/master`). Thin shell: header + back link + MasterPanel + PageFooter. All host logic lives in `MasterPanel.svelte`. |
|
||||
| `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:5 on mobile; wider on sm+) cells with condensed bold black numbers (`tan-tan-num` font stack), white number cells, purple empty cells by default. Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. Empty cells use `--empty-cell-bg` CSS var from settings store. |
|
||||
| `src/lib/SettingsButton.svelte` | Gear icon + modal. 4 fieldsets: Giao diện (theme auto/light/dark), Chế độ quản trò (master mode toggle), Tự động xổ (auto-call + speed 1–10s), Màu ô trống (10 Excel color swatches). Reset-to-default button. Mounted on both `/` and `/master` headers. |
|
||||
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, "Số vừa xổ" hero token, "Thứ tự đã xổ" history list, host's own player card. No 11×9 tracking board (player card is enough). "Xổ số" / "Bắt đầu / Dừng" button bound to auto-call. Mounted conditionally on `/` when `settings.masterMode === true`, or directly on `/master`. |
|
||||
| `src/lib/MasterPanel.svelte` | Host controls. New game / draw, "Số vừa xổ" hero token, "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`. |
|
||||
| `src/lib/PageFooter.svelte` | Footer with tagline ("Made by miti99 with ❤️ SVG icon") + link. Mounted on `/` and `/master`. |
|
||||
|
||||
### Game Logic
|
||||
@@ -44,6 +43,7 @@
|
||||
| `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. |
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
@@ -58,22 +58,18 @@
|
||||
| `loto_grid` | Player's card numbers. |
|
||||
| `loto_crossed` | Player's marked cells. |
|
||||
| `loto_master` | Host's drawn/remaining numbers. |
|
||||
| `loto_master_card_grid` | Host's player card numbers. |
|
||||
| `loto_master_card_crossed` | Host's marked cells. |
|
||||
| `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 (/)
|
||||
│ ├── PlayerBoard (storagePrefix="loto")
|
||||
│ ├── [if settings.masterMode]
|
||||
│ │ └── MasterPanel (state, controls, draw history)
|
||||
│ └── PageFooter
|
||||
└── MasterPage (/master)
|
||||
├── MasterPanel (state, controls, draw history)
|
||||
├── Back link
|
||||
└── HomePage (/) ← single page; any other URL redirects to /
|
||||
├── PlayerBoard (storagePrefix="loto")
|
||||
├── [if settings.masterMode]
|
||||
│ └── MasterPanel (controls, history, 11×9 tracking grid)
|
||||
└── PageFooter
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
<script module>
|
||||
const STORAGE_KEY = "loto_master";
|
||||
|
||||
/**
|
||||
* 11x9 board, aligned by ones-digit. Row = ones digit, col = tens digit.
|
||||
* Col 0 holds 1..9, col 8 holds 80..90 (90 sits alone in row 10 col 8).
|
||||
* Empty slots = 0.
|
||||
* @returns {number[][]}
|
||||
*/
|
||||
function buildBoard() {
|
||||
/** @type {number[][]} */
|
||||
const board = [];
|
||||
for (let row = 0; row < 11; row++) {
|
||||
/** @type {number[]} */
|
||||
const cells = [];
|
||||
for (let col = 0; col < 9; col++) {
|
||||
let num = 0;
|
||||
if (row === 10) {
|
||||
if (col === 8) num = 90;
|
||||
} else if (row === 0) {
|
||||
if (col > 0) num = col * 10;
|
||||
} else {
|
||||
num = col === 0 ? row : col * 10 + row;
|
||||
}
|
||||
cells.push(num);
|
||||
}
|
||||
board.push(cells);
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
/** @returns {{ called: number[], remaining: number[] }} */
|
||||
function createFreshState() {
|
||||
const all = Array.from({ length: 90 }, (_, i) => i + 1);
|
||||
@@ -29,10 +57,12 @@
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const BOARD = Object.freeze(buildBoard().map((row) => Object.freeze(row)));
|
||||
const BOARD_FLAT = Object.freeze(BOARD.flatMap((r) => r));
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import PlayerBoard from "$lib/PlayerBoard.svelte";
|
||||
import { settings } from "$lib/settings-store.svelte.js";
|
||||
|
||||
let state = $state(
|
||||
@@ -53,6 +83,11 @@
|
||||
if (state) saveState(state);
|
||||
});
|
||||
|
||||
// Map number -> 1-based draw order for fast Kinh! verification.
|
||||
const callOrder = $derived(
|
||||
new Map((state?.called ?? []).map((n, i) => [n, i + 1])),
|
||||
);
|
||||
|
||||
// Auto-call interval. Single $effect that depends on autoRunning,
|
||||
// settings.autoCallSpeed, AND settings.autoCallEnabled — Svelte tears
|
||||
// down + re-arms cleanly whenever any of them change. Includes the
|
||||
@@ -203,21 +238,60 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !state}
|
||||
<!-- 11x9 master tracking board -->
|
||||
{#if state}
|
||||
<div
|
||||
aria-label="Bảng theo dõi số đã xổ"
|
||||
class="rounded-2xl overflow-hidden shadow-xl shadow-slate-200/50 dark:shadow-black/30 border border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<div class="master-grid">
|
||||
{#each BOARD_FLAT as num, idx (idx)}
|
||||
{@const hasNumber = num > 0}
|
||||
{@const order = hasNumber ? callOrder.get(num) : undefined}
|
||||
{@const isCalled = order !== undefined}
|
||||
{@const isLast = isCalled && num === lastCalled}
|
||||
{@const isLow = hasNumber && num <= 49}
|
||||
<div
|
||||
style:background-color={hasNumber ? null : "var(--empty-cell-bg)"}
|
||||
class="relative flex items-center justify-center
|
||||
aspect-square
|
||||
border-r border-b border-slate-200/80 dark:border-slate-700/60
|
||||
bg-white dark:bg-slate-800 select-none
|
||||
{isLast ? 'z-10' : ''}"
|
||||
>
|
||||
{#if hasNumber}
|
||||
<!-- Token: cream inner when called (pink ≤49 / green ≥50 ring),
|
||||
gray-ringed and dim when uncalled. -->
|
||||
<div
|
||||
class="flex items-center justify-center
|
||||
w-[82%] h-[82%] rounded-full
|
||||
text-lg sm:text-xl font-black tabular-nums
|
||||
border-[3px] transition-all
|
||||
{!isCalled
|
||||
? 'border-slate-300 dark:border-slate-600 bg-slate-50/40 dark:bg-slate-700/30 text-slate-400 dark:text-slate-500 opacity-70'
|
||||
: isLow
|
||||
? 'border-pink-500 dark:border-pink-400 bg-amber-50 dark:bg-amber-100 text-pink-500 dark:text-pink-400'
|
||||
: 'border-emerald-500 dark:border-emerald-400 bg-amber-50 dark:bg-amber-100 text-emerald-500 dark:text-emerald-400'}
|
||||
{isLast
|
||||
? 'ring-2 ring-red-500 dark:ring-red-400 ring-offset-1 ring-offset-white dark:ring-offset-slate-800 scale-110 shadow-md'
|
||||
: ''}"
|
||||
>
|
||||
{num}
|
||||
</div>
|
||||
{#if isCalled}
|
||||
<span
|
||||
class="absolute top-0.5 right-0.5 text-[9px] sm:text-[10px] font-semibold leading-none text-slate-500 dark:text-slate-400 tabular-nums"
|
||||
>
|
||||
{order}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-slate-400 dark:text-slate-500 py-10 text-sm">
|
||||
Nhấn "Ván mới" để bắt đầu
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Master's own playing card -->
|
||||
<div class="mt-10">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="text-lg font-bold text-slate-700 dark:text-slate-200">
|
||||
Bảng của quản trò
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500">
|
||||
Quản trò cũng có thể chơi cùng
|
||||
</p>
|
||||
</div>
|
||||
<PlayerBoard storagePrefix="loto_master_card" />
|
||||
</div>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<script>
|
||||
import { base } from "$app/paths";
|
||||
import MasterPanel from "$lib/MasterPanel.svelte";
|
||||
import PageFooter from "$lib/PageFooter.svelte";
|
||||
import SettingsButton from "$lib/SettingsButton.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col flex-1 items-center px-2 py-4 sm:px-3 sm:py-12">
|
||||
<div class="w-full max-w-2xl">
|
||||
<header class="relative text-center mb-6">
|
||||
<div class="absolute right-0 top-0">
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl font-extrabold tracking-tight bg-gradient-to-r from-orange-500 to-red-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Quản trò
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
Xổ số và theo dõi bảng lô tô
|
||||
</p>
|
||||
<a
|
||||
href="{base}/"
|
||||
class="mt-2 inline-block text-xs text-indigo-500 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
← Về trang người chơi
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<MasterPanel />
|
||||
|
||||
<PageFooter />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
# Cloudflare Pages: any unknown path → 301 redirect to homepage.
|
||||
# Static asset files are served first, so this only fires on misses.
|
||||
# Keeps old /master bookmarks (and any future stray URL) on the app.
|
||||
/* / 301
|
||||
Reference in New Issue
Block a user