mirror of
https://github.com/tiennm99/loto.git
synced 2026-05-14 12:58:48 +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.
5.8 KiB
5.8 KiB
Code Standards
File Naming & Structure
- Kebab-case for all files:
PlayerBoard.svelte,game-logic.js. - Descriptive names: Long names are preferred for self-documentation. Avoid ambiguity.
- Single responsibility: Each file has one primary export (component or utilities).
- Max 200 lines per file: Split larger components into smaller focused ones.
Svelte 5 Runes & JavaScript
State & Reactivity
- $state: For mutable local UI state (grid, crossed, booleans).
let grid = $state(...) - $derived: For computed values that auto-update when dependencies change.
const rowCompleteness = $derived(grid && crossed.length ? [...] : []) - $effect: For side effects (localStorage sync, detecting completed rows). Replaces React useEffect.
- $props: For destructuring component props.
let { storagePrefix = "loto" } = $props()
Plain References (No Reactivity)
- Use regular
letorconstfor timers, Sets, Maps (not reactive). Example:let toastTimer = null; const celebratedRows = new Set();
Typing (JSDoc)
- Use JSDoc
@typedef {Object}for prop types, JSDoc type annotations for functions. jsconfig.jsonhascheckJs: false(not enabled), so types are documentation-only.- Component props:
@typedef {Object} Props { ... }then/** @type {Props} */ let { ... } = $props() - Avoid
any; use precise types (e.g.,number[][]for grid). - Generics:
@template Tfor utility functions.
Client-Only Architecture
- SvelteKit's
ssr: falsein+layout.jsdisables server rendering. - All pages are client-only; no server-side data fetching.
- Use localStorage exclusively for persistence.
CSS & Tailwind 4 Patterns
Utilities
- Utility-first:
className="px-4 py-2 rounded-lg text-white". - Responsive:
sm:,md:,lg:prefixes for breakpoints. - Dark mode:
dark:bg-slate-800,dark:text-white. - Animations: Custom keyframes in
globals.css, apply viaanimate-fade-in.
Layout
- Flexbox for alignment:
flex flex-col items-center justify-center. - Grid for game boards:
.loto-grid { grid-template-columns: repeat(9, 1fr); }. - Aspect ratio for square cells:
aspect-square.
Gradients
- Player page:
from-indigo-500 to-purple-500. - Host page:
from-orange-500 to-red-500. - Completed rows:
bg-emerald-100+text-emerald-500. - Shadows:
shadow-lg shadow-indigo-500/25.
localStorage Patterns
Saving
/**
* @param {number[][]} grid
* @param {string} [prefix]
*/
function saveGrid(grid, prefix = "loto") {
localStorage.setItem(`${prefix}_grid`, JSON.stringify(grid));
}
Loading
/**
* @param {string} [prefix]
* @returns {number[][] | null}
*/
function loadGrid(prefix = "loto") {
const data = localStorage.getItem(`${prefix}_grid`);
if (!data) return null;
try {
return JSON.parse(data);
} catch {
return null;
}
}
Key Pattern: {prefix}_{key} enables multiple independent boards per component reuse.
Error Handling
- Silent fallback: JSON parse errors return null; caller checks for null.
- No try-catch in render: Keep logic in $effect or event handlers.
- Confirmation dialogs:
confirm("Bạn có muốn...")for destructive actions.
Naming Conventions
| Pattern | Example | Usage |
|---|---|---|
| camelCase | handleCellClick, storagePrefix |
variables, functions, props |
| PascalCase | PlayerBoard, MasterPage |
components, types |
| UPPER_SNAKE | STORAGE_KEY, NUM_ROWS |
constants |
| kebab-case | PlayerBoard.svelte |
file names |
Comment Style
- Document why, not what. The code shows what it does.
- Use
/** JSDoc */for exported functions. - Inline comments for complex logic (e.g., weighted random selection in
src/lib/game-logic.js:19–28).
Example
/**
* Weighted random selection of a column index.
* @param {number[]} weights
* @returns {number}
*/
function randomANumberInRow(weights) {
// Convert weights to cumulative distribution for O(n) lookup
const tempWeight = [...weights];
for (let i = 1; i < tempWeight.length; i++) {
tempWeight[i] += tempWeight[i - 1];
}
// ...
}
Internal Navigation
- Use
import { base } from '$app/paths'for internal links to preserve basePath across deployments. - Example:
<a href="{base}/master">Host page</a>works on root ("") and subpath (/loto) equally.
Import Organization
- SvelteKit imports (
$app/paths,$lib/...) - Third-party imports
- Local component/utility imports
import { base } from "$app/paths";
import { generateGrid, isRowComplete } from "$lib/game-logic.js";
import PlayerBoard from "$lib/PlayerBoard.svelte";
Component Patterns
Props Pattern
<script>
/**
* @typedef {Object} Props
* @property {string} [storagePrefix]
*/
/** @type {Props} */
let { storagePrefix = "loto" } = $props();
</script>
Event Handlers
Use inline event handlers (onclick, onkeydown). Svelte 5 handles click delegation. Event objects automatically passed:
<button onclick={() => handleCellClick(row, col)}>Click</button>
<div onkeydown={(e) => e.key === "Escape" && dismiss()}>Dialog</div>
Testing (Not Currently Implemented)
Future tests should follow:
- Unit: test game logic (generateGrid, isRowComplete, getWaitingNumber) in isolation.
- Component: render PlayerBoard, mock localStorage, verify toast/popup.
- E2E: player flow (generate → click → bingo).
Configuration
Environment Variables (code-server only)
- VITE_DEV_PROFILE: "codeserver" triggers proxy config.
- CODESERVER_HOST: Hostname for HMR.
- CODESERVER_PORT: Port (defaults to 3000).
Set in .env.local (not committed).
Build Targets
- adapter-static: Generates static HTML + JS in
build/. - basePath: Dual-mode:
""(Cloudflare, dev) or/loto(GitHub Pages viaBUILD_PROFILE=gh).
Last reviewed: 2026-04-26