diff --git a/web/.gitignore b/web/.gitignore index 17e8407..ca8968d 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -5,3 +5,4 @@ out/ .env.local .env*.local tsconfig.tsbuildinfo +repomix-output.xml diff --git a/web/README.md b/web/README.md index ebf8259..2b8bbcb 100644 --- a/web/README.md +++ b/web/README.md @@ -2,14 +2,32 @@ Bàn số của trò chơi "Lô tô" — Next.js app. +Two pages: `/` for players, `/master` for the host (quản trò) — calls numbers, shows a tracking board, and has its own player card to play along. + +See `docs/` for project overview, architecture, code standards, and deployment. + ## Development ```bash npm run dev ``` +### Inside code-server (reverse proxy) + +```bash +cp .env.example .env.local +# edit .env.local: set CODESERVER_HOST and CODESERVER_PORT +npm run dev:codeserver +``` + +Open `https:///absproxy//`. + +Use `/absproxy/{port}/`, **not** `/proxy/{port}/` — the latter strips the path prefix and breaks Next's `basePath`. HMR may not survive the proxy; refresh manually if it disconnects. + ## Build ```bash npm run build ``` + +Static export to `out/`. Deployed to GitHub Pages from `master` via `.github/workflows/deploy.yml`. diff --git a/web/docs/code-standards.md b/web/docs/code-standards.md new file mode 100644 index 0000000..ab7c11c --- /dev/null +++ b/web/docs/code-standards.md @@ -0,0 +1,138 @@ +# Code Standards + +## File Naming & Structure + +- **Kebab-case** for all files: `loto-player-board.tsx`, `loto-game-logic.ts`. +- **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. + +## React & TypeScript Conventions + +### Hooks +- **useState**: For local UI state (grid, crossed, form inputs). +- **useEffect**: For side effects (load from localStorage, save to localStorage, detect changes). +- **useCallback**: For event handlers to stabilize function identity across renders. +- **useRef**: For mutable values that don't trigger renders (timers, celebratedRows set). + +### Typing +- Explicit `export interface Props { ... }` for component props. +- Use `Readonly<{ children: React.ReactNode }>` for layout children. +- Avoid `any`; use precise types (e.g., `number[][]` for grid). + +### Client-Only Constraint +- All interactive pages must have `"use client"` at the top. +- No server-side data fetching; use localStorage instead. +- No async Server Components. + +## 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 +```typescript +function saveGrid(grid: number[][], prefix = "loto"): void { + localStorage.setItem(`${prefix}_grid`, JSON.stringify(grid)); +} +``` + +### Loading +```typescript +function loadGrid(prefix = "loto"): number[][] | null { + 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 useEffect 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 | `loto-player-board.tsx` | 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 `loto-game-logic.ts:19–28`). + +### Example +```typescript +/** Weighted random selection of a column index */ +function randomANumberInRow(weights: number[]): number { + // 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]; + } + // ... +} +``` + +## Import Organization + +1. React/Next imports +2. Third-party imports +3. Local component/utility imports + +```typescript +import { useCallback, useState } from "react"; +import Link from "next/link"; +import PlayerBoard from "./loto-player-board"; +import { generateGrid } from "./loto-game-logic"; +``` + +## Testing (Not Currently Implemented) + +Future tests should follow: +- Unit: test game logic (generateGrid, isRowComplete, getWaitingNumber) in isolation. +- Component: mock localStorage, render PlayerBoard with different props. +- E2E: player flow (generate → click → bingo). + +## Configuration + +### Environment Variables +- **NEXT_DEV_PROFILE**: "codeserver" triggers proxy config. +- **CODESERVER_HOST**: Hostname for HMR (required if NEXT_DEV_PROFILE=codeserver). +- **CODESERVER_PORT**: Port (defaults to 3000). + +Set in `.env.local` (not committed). + +### Build Targets +- **output: "export"**: Static HTML export (no Node.js server needed). +- **basePath**: Configurable per deployment environment. + +Last reviewed: 2026-04-26 diff --git a/web/docs/codebase-summary.md b/web/docs/codebase-summary.md new file mode 100644 index 0000000..9cd4a2e --- /dev/null +++ b/web/docs/codebase-summary.md @@ -0,0 +1,75 @@ +# Codebase Summary + +## File Organization + +### Routing & Layout +| File | Purpose | +|------|---------| +| `app/layout.tsx` | Root HTML layout. Sets Vietnamese lang, imports Geist font, applies global flex layout. | +| `app/page.tsx` | Player page (`/`). Instructions toggle, PlayerBoard component, indigo gradient branding. | +| `app/master/page.tsx` | Host page (`/master`). Controls (new game, draw number), 9×10 master board, host's player card. | + +### Shared Components +| File | Purpose | +|------|---------| +| `app/loto-player-board.tsx` | Reusable player card (9×9 grid). Handles crossed state, bingo popup, "Chờ X" toast. Accepts `storagePrefix` prop for multi-card isolation. | + +### Game Logic +| File | Purpose | +|------|---------| +| `app/loto-game-logic.ts` | Stateless utilities: generateGrid (weighted column selection), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. | + +### Styling +| File | Purpose | +|------|---------| +| `app/globals.css` | Root styles: Tailwind @import, CSS variables (light/dark), `.loto-grid` & `.master-grid` (9-col), animations (fade-in, pop-in, bounce-slow, spin-slow, toast), `.cell-crossed` diagonal. | + +### Configuration +| File | Purpose | +|------|---------| +| `next.config.ts` | Dual basePath: prod `/loto`, codeserver `/absproxy/{PORT}`. Exports static HTML. HMR-aware. | +| `package.json` | Next 16.2.2, React 19.2.4, Tailwind 4. Scripts: dev, dev:codeserver, build, start, lint. | +| `eslint.config.mjs` | ESLint 9 config (Next.js preset). | +| `.gitignore` | Excludes node_modules, .next, .env.local, etc. | +| `.env.example` | Template for env vars (currently none required for runtime; codeserver profile reads CODESERVER_HOST/PORT). | + +## Key Data Structures + +**Grid**: 9×9 2D array of numbers (1–90). Empty cells are 0. +**Crossed**: 9×9 2D array of booleans indicating marked cells. +**Master State**: `{ called: number[], remaining: number[] }` — drawn and undrawn numbers. + +## Storage Keys (localStorage) + +| Key | Use Case | +|-----|----------| +| `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. | + +## Component Hierarchy + +``` +RootLayout +├── HomePage (/) +│ ├── Instructions toggle +│ └── PlayerBoard (storagePrefix="loto") +└── MasterPage (/master) + ├── Controls (new game, draw) + ├── Master board (9×10) + └── PlayerBoard (storagePrefix="loto_master_card") +``` + +## Key Functions + +| Function | Location | Effect | +|----------|----------|--------| +| `generateGrid()` | loto-game-logic.ts:52 | Creates 9×9 with weighted column selection (5 nums/row). | +| `isRowComplete()` | loto-game-logic.ts:108 | Boolean: all non-zero cells in row crossed? | +| `getWaitingNumber()` | loto-game-logic.ts:120 | Returns the single uncrossed number in row, or null. | +| `handleCellClick()` | loto-player-board.tsx:112 | Toggle crossed[row][col]. | +| `handleDrawNext()` | master/page.tsx:88 | Pop first number from remaining, add to called. | + +Last reviewed: 2026-04-26 diff --git a/web/docs/deployment-guide.md b/web/docs/deployment-guide.md new file mode 100644 index 0000000..818ef9e --- /dev/null +++ b/web/docs/deployment-guide.md @@ -0,0 +1,151 @@ +# Deployment Guide + +## Production Deployment (GitHub Pages) + +### Automatic Deploy +The app auto-deploys from the `master` branch via `.github/workflows/deploy.yml`. + +**Workflow**: +1. Push to `master` +2. GitHub Actions runs `npm run build` +3. Static export written to `out/` directory +4. Pages deployed to `https://{user}.github.io/loto` + +**Note**: basePath is set to `/loto` in production (`next.config.ts:23`). + +### Manual Deploy (if needed) +```bash +npm run build +# out/ directory ready for upload +``` + +Then deploy `out/` folder to GitHub Pages or any static host. + +## Development Environment + +### Local Dev +```bash +npm install +npm run dev +``` + +Access at `http://localhost:3000` (no basePath). + +HMR works automatically. + +### Code-Server Dev + +For browser-based development (VS Code in browser): + +**1. Start code-server** with Node.js environment: +```bash +code-server --no-auth +``` + +**2. Create `.env.local`** in project root: +``` +CODESERVER_HOST=your-machine.example.com +CODESERVER_PORT=3000 +``` + +Replace `your-machine.example.com` with your actual hostname/IP (must match the proxy URL you'll access). + +**3. Run dev server**: +```bash +npm run dev:codeserver +``` + +This sets `NEXT_DEV_PROFILE=codeserver`, triggering the code-server config path in `next.config.ts`. + +**4. Access via browser**: +Navigate to: +``` +https://your-codeserver-host/absproxy/3000/ +``` + +**Key Points**: +- `/absproxy/{port}` (NOT `/proxy/{port}`) preserves basePath through the proxy. +- HMR socket connects to `CODESERVER_HOST` for live reload. +- If HMR fails, manually refresh the page (CSS/JS changes still apply server-side). + +### Manual Refresh Workaround +If HMR over proxy is unreliable: +1. Make code changes +2. Manually refresh browser (F5) +3. Dev server has already compiled the changes + +This is normal in proxy environments. + +## Build & Output + +### Build Command +```bash +npm run build +``` + +Generates: +- `out/` — Complete static HTML export +- `.next/` — Build cache (not needed for deployment) + +### Export Settings +- `output: "export"` in `next.config.ts` +- No server-side rendering +- All pages pre-rendered to HTML + JS bundles + +### Asset Hosting +- `assetPrefix` matches `basePath` (prod: `/loto`, dev/codeserver: empty or `/absproxy/{port}`) +- CSS, JS, fonts all prefixed correctly +- GitHub Pages serves from repository root, so `/loto` paths resolve correctly + +## Environment Variables + +### Required (code-server only) +- `CODESERVER_HOST` — hostname for HMR proxy +- `CODESERVER_PORT` — port (default 3000) + +### Optional +- `NEXT_DEV_PROFILE` — set to "codeserver" to enable proxy mode (usually set by `npm run dev:codeserver`) + +### Not Used at Runtime +- No database URL, API keys, or secrets (all client-side, localStorage) +- `.env.local` is `.gitignore`d and safe for local config + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| 404 on subpages after deploy | basePath mismatch | Verify `basePath="/loto"` in prod; local dev should be empty | +| HMR not connecting (code-server) | CODESERVER_HOST not set | Add `CODESERVER_HOST=...` to `.env.local` | +| Assets 404 (code-server) | Wrong proxy URL | Use `/absproxy/{port}`, not `/proxy/{port}` | +| Page blank after refresh | State not persisted | Check browser localStorage is enabled | +| Stale CSS (code-server) | HMR failed | Manually refresh page (F5) | + +## CI/CD Pipeline + +`.github/workflows/deploy.yml`: +- Triggers on `push` to `master` +- Installs dependencies (`npm install`) +- Builds app (`npm run build`) +- Deploys `out/` folder to GitHub Pages + +No manual steps required; push to master and GitHub Pages updates automatically. + +## Performance Checklist + +- [x] Static export (no server overhead) +- [x] Tailwind purged for production size +- [x] localStorage reduces bundle—no API calls +- [x] Images minimal (mostly CSS gradients) +- [x] Fonts: Geist via Google Fonts CDN + +Bundle analysis: Run `npm run build && ls -lh out/` to inspect file sizes. + +## Security Considerations + +- No sensitive data in code (no API keys, secrets) +- `.env.local` is local-only, not committed +- localStorage scoped to origin +- No external API calls (offline-capable) +- GitHub Pages HTTPS by default + +Last reviewed: 2026-04-26 diff --git a/web/docs/design-guidelines.md b/web/docs/design-guidelines.md new file mode 100644 index 0000000..9d8875f --- /dev/null +++ b/web/docs/design-guidelines.md @@ -0,0 +1,153 @@ +# Design Guidelines + +## Visual Identity + +### Color Palette + +**Primary (Player)** +- Gradient: indigo-500 → purple-500 +- Use for: Main headings, generate button, hover states +- Semantic: Calm, welcoming, player-friendly + +**Secondary (Host)** +- Gradient: orange-500 → red-500 +- Use for: Host headings, new game button, high-stakes actions +- Semantic: Energetic, commanding, host authority + +**Success (Completed Rows)** +- Background: emerald-100 (light) / emerald-900/40 (dark) +- Text: emerald-500 (light) / emerald-400 (dark) +- Use for: Crossed cells in completed rows + +**Attention (Waiting Toast)** +- Background: amber-500/90 +- Text: white +- Use for: "Chờ X" toast notifications + +**Neutral (Grid & Text)** +- Borders: slate-200 (light) / slate-700 (dark) +- Background: white (light) / slate-800 (dark) +- Text: slate-600 (light) / slate-400 (dark) + +### Dark Mode +All colors have corresponding `dark:` variants. Use Tailwind's `prefers-color-scheme: dark` media query (set in `globals.css`). + +## Typography + +- **Font**: Geist Sans (Google Fonts), fallback to Arial/Helvetica. +- **Headings**: Extrabold (font-extrabold) for main title, bold (font-bold) for secondary. +- **Body**: Regular weight, slate-600 (light) / slate-400 (dark). +- **Emphasis**: `` for important copy (e.g., button labels in instructions). + +### Sizing +- **Page title**: `text-4xl sm:text-5xl` (responsive) +- **Subheading**: `text-3xl sm:text-4xl` +- **Body**: `text-base sm:text-lg` +- **Small**: `text-xs text-slate-400` + +## Layout & Spacing + +### Breakpoints +- **Mobile first**: Base styles for mobile, then `sm:` (640px+), `md:` (768px+), `lg:` (1024px+). +- **Padding**: `px-3 py-8 sm:py-12` (horizontal, vertical responsive). +- **Gaps**: `gap-3`, `gap-6`, `gap-1.5` (consistent spacing scale). + +### Grid Layout +- **Player/Host grid**: `grid-template-columns: repeat(9, 1fr)` (equal cells, no gap). +- **Master board**: Same 9-column layout (9×10 = 90 cells). +- **Cell sizing**: `aspect-square` for perfect squares. +- **Borders**: `border-r border-b` (right & bottom edges only, for clean lines). + +### Containers +- **Max width**: `max-w-lg` (player), `max-w-2xl` (host) — centered with `mx-auto`. +- **Flex wrap**: Use `flex flex-wrap gap-1.5` for number history chips. + +## Component Patterns + +### Button Styles +```html + + + + + + + + +``` + +### Card Styling +- Border: `border border-slate-200 dark:border-slate-700` +- Rounded: `rounded-2xl` +- Shadow: `shadow-xl shadow-slate-200/50 dark:shadow-black/30` +- Overflow: `overflow-hidden` (clip content to rounded corners) + +### Text Links +- Color: `text-indigo-500 dark:text-indigo-400` (player page) +- Color: `text-orange-500 dark:text-orange-400` (host page) +- Hover: `hover:underline` + +## Animations + +### Entrance +- **fade-in** (0.2s): Modal background, instant attention +- **pop-in** (0.4s): Bingo popup, celebratory scale bounce + +### Continuous +- **bounce-slow** (1.5s): Emoji on bingo popup (translateY oscillation) +- **spin-slow** (3s): ✨ emoji (360° rotation) +- **spin-slow-reverse** (3s): 🎊 emoji (counter-rotation) + +### Ephemeral +- **toast** (5s): "Chờ X" notification (scale 0.8→1.05→1, fade out) + +### Interactive +- **active:scale-95**: Button press response +- **transition-all**: Smooth color/shadow changes on hover + +## Emoji Usage + +Intentional emojis (matching traditional bingo celebration): +- 🎉 ✨ 🎊 🥳 ❤️ (bingo popup) +- ❤️ (footer) + +Keep emojis rare; use for celebration only. + +## Vietnamese Copy + +- **Player instructions**: Simple, directive tone. "Nhấn" (press), "để" (to). +- **Toast messages**: Short. "Chờ X" (waiting for X). +- **Popups**: Celebratory. "Kinh!" (victory cry), "Tuyệt vời!" (awesome!). +- **Links**: Clear CTA. "Trang quản trò →" (host page), "← Về trang người chơi" (back to player). + +## Mobile Responsiveness + +- Stack vertically on mobile (`flex flex-col`). +- Reduce padding on small screens (`py-8` → `sm:py-12`). +- Enlarge text for readability on mobile (`text-base sm:text-lg`). +- Buttons stay full-width or flex-wrap on mobile. +- Images/grids use `aspect-square` to maintain proportion. + +## Accessibility + +- Alt text: Not critical (no images), but grid cells have semantic ARIA if needed. +- Focus states: Implicit via Tailwind (`focus:ring-2`), add if extending components. +- Contrast: Tailwind slate/indigo/emerald combos meet WCAG AA. +- Dark mode: Respects `prefers-color-scheme`, not forced. + +Last reviewed: 2026-04-26 diff --git a/web/docs/development-roadmap.md b/web/docs/development-roadmap.md new file mode 100644 index 0000000..aa1c005 --- /dev/null +++ b/web/docs/development-roadmap.md @@ -0,0 +1,99 @@ +# Development Roadmap + +This document tracks **future work only**. Completed features live in git commit history, not here. + +## Currently Implemented Features + +The app is fully functional for core gameplay: +- 9×9 player card generation with weighted number distribution +- Cell marking (toggle crossed state) +- Bingo detection and celebration popup +- "Chờ X" waiting notifications +- Host number drawing from 1–90 deck +- 9×10 master board tracking called numbers +- Host's own player card (isolated instance) +- localStorage persistence +- Dark mode +- Mobile responsive +- Offline capable + +## Idea Phase + +### Sound Effects on Bingo +Play celebratory chime or "Kinh!" voice snippet when row completes. Could use Web Audio API or `