Files
loto/docs/code-standards.md
tiennm99 574c22ddc1 refactor: rewrite from Next.js + React to SvelteKit + Svelte 5
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.
2026-04-26 21:03:41 +07:00

5.8 KiB
Raw Permalink Blame History

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

/**
 * @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:1928).

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

  1. SvelteKit imports ($app/paths, $lib/...)
  2. Third-party imports
  3. 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 via BUILD_PROFILE=gh).

Last reviewed: 2026-04-26