mirror of
https://github.com/tiennm99/loto.git
synced 2026-05-15 00:58:36 +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.
181 lines
5.8 KiB
Markdown
181 lines
5.8 KiB
Markdown
# 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 `let` or `const` for 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.json` has `checkJs: 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 T` for utility functions.
|
||
|
||
### Client-Only Architecture
|
||
- SvelteKit's `ssr: false` in `+layout.js` disables 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 via `animate-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
|
||
```js
|
||
/**
|
||
* @param {number[][]} grid
|
||
* @param {string} [prefix]
|
||
*/
|
||
function saveGrid(grid, prefix = "loto") {
|
||
localStorage.setItem(`${prefix}_grid`, JSON.stringify(grid));
|
||
}
|
||
```
|
||
|
||
### Loading
|
||
```js
|
||
/**
|
||
* @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
|
||
```js
|
||
/**
|
||
* 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
|
||
|
||
1. SvelteKit imports (`$app/paths`, `$lib/...`)
|
||
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";
|
||
```
|
||
|
||
## Component Patterns
|
||
|
||
### Props Pattern
|
||
```svelte
|
||
<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:
|
||
```svelte
|
||
<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 via `BUILD_PROFILE=gh`).
|
||
|
||
Last reviewed: 2026-04-26
|