mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 20:15:13 +00:00
docs: initialize project documentation
Adds the standard ./docs/ structure (overview, codebase summary, architecture, code standards, design guidelines, deployment guide, roadmap) and the code-review report under ./plans/reports/. README now points at the docs and covers the codeserver dev profile.
This commit is contained in:
1 parent
fd2d144428
commit
dd64aa6ed3
10 files changed
+1048
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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**: `<strong>` 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
|
||||
<!-- Primary Action (Indigo/Purple) -->
|
||||
<button class="px-8 py-3 rounded-full font-semibold text-white
|
||||
bg-gradient-to-r from-indigo-500 to-purple-500
|
||||
hover:from-indigo-600 hover:to-purple-600
|
||||
active:scale-95 transition-all shadow-lg shadow-indigo-500/25">
|
||||
Tạo bảng mới
|
||||
</button>
|
||||
|
||||
<!-- Host Action (Orange/Red) -->
|
||||
<button class="px-8 py-4 rounded-full font-semibold text-white text-lg
|
||||
bg-gradient-to-r from-orange-500 to-red-500
|
||||
hover:from-orange-600 hover:to-red-600
|
||||
active:scale-95 transition-all shadow-lg shadow-orange-500/25">
|
||||
Ván mới
|
||||
</button>
|
||||
|
||||
<!-- Secondary (Emerald, Draw Action) -->
|
||||
<button class="px-10 py-4 rounded-full font-semibold text-white text-lg
|
||||
bg-gradient-to-r from-emerald-500 to-teal-500
|
||||
hover:from-emerald-600 hover:to-teal-600
|
||||
active:scale-95 transition-all shadow-lg shadow-emerald-500/25">
|
||||
Xổ số
|
||||
</button>
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -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 `<audio>` tag. **Status**: Idea (no demand yet)
|
||||
|
||||
### Undo Last Cell
|
||||
Allow player to undo the most recent cross/uncross action. Requires change history or state snapshot. **Status**: Idea (low priority)
|
||||
|
||||
### Theme Switcher
|
||||
Explicit light/dark toggle button instead of relying on OS preference. **Status**: Idea (Tailwind already supports OS toggle)
|
||||
|
||||
### PWA Install
|
||||
Add service worker and manifest for "Install App" prompt on Android/iOS. **Status**: Idea (would require server-side components)
|
||||
|
||||
## Considered Phase
|
||||
|
||||
### Multiplayer Sync (Real-time)
|
||||
Host and players connect via WebSocket to sync called numbers and state. Requires backend server. Would enable:
|
||||
- Decoupled devices (players' phones, host's laptop on big screen)
|
||||
- Remote tournaments
|
||||
- Master board auto-updates all players in real-time
|
||||
|
||||
**Consideration**: Out of scope for static export. Requires major refactor (Next.js API routes + WebSocket server). Deferred indefinitely.
|
||||
|
||||
### i18n Beyond Vietnamese
|
||||
Internationalization (English, Chinese, etc.). Requires extraction of all Vietnamese strings and i18n library (next-intl). **Status**: Considered (low demand for non-Vietnamese users)
|
||||
|
||||
### Undo/Redo System
|
||||
Full undo/redo stack with history navigation. Adds complexity to state management. **Status**: Considered (YAGNI for now)
|
||||
|
||||
## Testing (Planned but Unstarted)
|
||||
|
||||
### Unit Tests
|
||||
- Game logic: `generateGrid()`, `isRowComplete()`, `getWaitingNumber()`
|
||||
- localStorage helpers: `saveGrid()`, `loadGrid()`, etc.
|
||||
|
||||
**Tech**: Jest + @testing-library/react
|
||||
|
||||
### Component Tests
|
||||
- PlayerBoard with mocked localStorage
|
||||
- Master page with different game states
|
||||
|
||||
### E2E Tests
|
||||
- Player flow: generate card → click cells → verify bingo popup
|
||||
- Host flow: new game → draw numbers → verify board state
|
||||
|
||||
**Tech**: Playwright or Cypress
|
||||
|
||||
## Future Enhancements (Speculative)
|
||||
|
||||
### Export/Import Card
|
||||
Allow player to export their grid as image or JSON, import someone else's card. Use Canvas API or print-friendly CSS.
|
||||
|
||||
### Leaderboard / Stats
|
||||
Track games won, time per bingo, etc. Requires server-side persistence. Out of scope for static export.
|
||||
|
||||
### Accessibility Improvements
|
||||
- ARIA labels for grid cells
|
||||
- Keyboard navigation (arrow keys to move, Enter to toggle)
|
||||
- Screen reader support
|
||||
|
||||
### Custom Number Range
|
||||
Host selects range (e.g., 1–75 for American bingo) instead of hardcoded 1–90. Requires config UI and refactor of game logic constants.
|
||||
|
||||
### Different Grid Sizes
|
||||
Support 8×8 or 10×10 grids. Major refactor (NUM_ROWS, NUM_COLS constants, weighted selection algorithm).
|
||||
|
||||
---
|
||||
|
||||
## Decision Rationale
|
||||
|
||||
All decisions follow **YAGNI** (You Aren't Gonna Need It):
|
||||
- No multiplayer sync → adds server dependency, breaks static export model
|
||||
- No i18n → Vietnamese-only community, localizing adds complexity without demand
|
||||
- No testing → small codebase, manual testing covers critical paths; add tests when code grows or bugs surface
|
||||
- No undo/redo → simple game, mistakes are part of play experience
|
||||
|
||||
Future work gates on **real user demand**, not speculation.
|
||||
|
||||
Last reviewed: 2026-04-26
|
||||
@@ -0,0 +1,67 @@
|
||||
# Lô Tô — Project Overview & PDR
|
||||
|
||||
## What is Lô Tô?
|
||||
|
||||
Lô tô is a traditional Vietnamese bingo game. The app replicates the game digitally for players to generate their own 9×9 number cards and mark cells as a host calls numbers from 1–90. First player to complete an entire row wins and shouts "Kinh!" (the game's victory cheer).
|
||||
|
||||
The inspiration comes from TN1 class reunions (2014–2017) where players often ran out of physical bingo cards.
|
||||
|
||||
## Core Mechanics
|
||||
|
||||
- **Players**: Generate a randomized 9×9 card with 45 numbers (5 per row, weighted distribution across columns 1–90). Click cells to mark them as numbers are called.
|
||||
- **Host**: Draws numbers randomly from a shuffled 1–90 deck, displays the current number on a large board, and tracks which numbers have been called.
|
||||
- **Bingo**: When a row is complete, the player's card triggers a celebration popup showing "Kinh!" with confetti emojis. Before bingo, toast notifications prompt "Chờ X" (waiting for X) when only one number remains in a row.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 16.2.2 (App Router, SSG via `output: "export"`)
|
||||
- **Runtime**: React 19.2.4 (Hooks: useState, useEffect, useCallback, useRef)
|
||||
- **Styling**: Tailwind CSS 4 (utility-first, animations)
|
||||
- **Persistence**: localStorage (no backend)
|
||||
- **Dev Profile**: code-server compatible via `/absproxy/{port}` basePath + HMR proxy config
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Two public pages:
|
||||
1. **`/`** — Player page. Generate a card, click cells to mark them, see bingo popup and waiting toasts.
|
||||
2. **`/master`** — Host page. Control number drawing, view 9×10 master board (tracking called vs uncalled), and host's own player card.
|
||||
|
||||
State is entirely client-side. Each page/card instance uses a unique localStorage prefix (e.g., `"loto"` for player, `"loto_master"` for host's state, `"loto_master_card"` for host's player card).
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Production**: GitHub Pages auto-deploys from `master` branch via `.github/workflows/deploy.yml`. App is served at `/loto` basePath.
|
||||
- **Development**: `npm run dev` (local), `npm run dev:codeserver` (code-server via proxy).
|
||||
- **Build**: `npm run build` generates static export to `out/` directory.
|
||||
|
||||
## Key Acceptance Criteria
|
||||
|
||||
- [x] Player can generate a new 9×9 card with valid number distribution.
|
||||
- [x] Player can click cells to toggle crossed state.
|
||||
- [x] Bingo popup triggers when row is complete, shows row number and "Kinh!" message.
|
||||
- [x] Toast notifications show "Chờ X" before bingo (one number remaining).
|
||||
- [x] Host can draw numbers and see them on the 9×10 master board.
|
||||
- [x] Host has their own player card (isolated by localStorage prefix).
|
||||
- [x] Offline persistence via localStorage (grid and crossed state).
|
||||
- [x] Dark mode support (Tailwind dark classes).
|
||||
- [x] Mobile-responsive (base + sm breakpoints).
|
||||
- [x] HMR works on code-server via proxy.
|
||||
|
||||
## Visual Language
|
||||
|
||||
- **Player gradient**: indigo → purple (primary brand, positive action).
|
||||
- **Host gradient**: orange → red (higher-stakes, control action).
|
||||
- **Completed rows**: emerald (success indicator).
|
||||
- **Waiting toast**: amber (attention, ephemeral).
|
||||
- **Emojis**: 🎉 ✨ 🎊 🥳 ❤️ (celebration, joy).
|
||||
|
||||
## Future Considerations (Not Committed)
|
||||
|
||||
- Undo last crossed cell
|
||||
- Sound effects on bingo
|
||||
- Theme switcher
|
||||
- PWA install
|
||||
- Multiplayer sync (real-time via WebSocket)
|
||||
- i18n beyond Vietnamese
|
||||
|
||||
Last reviewed: 2026-04-26
|
||||
@@ -0,0 +1,133 @@
|
||||
# System Architecture
|
||||
|
||||
## Page Flow
|
||||
|
||||
```
|
||||
Entry
|
||||
├─ / (Player Page)
|
||||
│ ├─ Load loto_grid, loto_crossed from localStorage
|
||||
│ ├─ Display 9×9 grid
|
||||
│ ├─ Generate new grid on button click
|
||||
│ ├─ Mark/unmark cells on click
|
||||
│ └─ Show bingo popup + "Chờ X" toasts
|
||||
│
|
||||
└─ /master (Host Page)
|
||||
├─ Load loto_master (called/remaining)
|
||||
├─ Display 9×10 master board (numbers 1–90)
|
||||
├─ Draw button shows next called number
|
||||
├─ Display host's own card (loto_master_card prefix)
|
||||
└─ New Game button resets called/remaining
|
||||
```
|
||||
|
||||
## State Model
|
||||
|
||||
### Player Card (`storagePrefix="loto"`)
|
||||
```
|
||||
grid: number[][] // 9×9 numbers (0 = empty)
|
||||
crossed: boolean[][] // 9×9 marked state
|
||||
```
|
||||
|
||||
Each row has exactly 5 non-zero numbers (distributed across columns via weighted random).
|
||||
|
||||
### Host State (`storagePrefix="loto_master"`)
|
||||
```
|
||||
called: number[] // [5, 23, 67, ...] — drawn in order
|
||||
remaining: number[] // [1, 2, 3, ...] minus called — shuffled initially
|
||||
```
|
||||
|
||||
### Host's Card (`storagePrefix="loto_master_card"`)
|
||||
Same as player card; isolated by prefix to allow host to play.
|
||||
|
||||
## localStorage Keys
|
||||
|
||||
| Prefix | Grid Key | Crossed Key |
|
||||
|--------|----------|-------------|
|
||||
| `"loto"` | `loto_grid` | `loto_crossed` |
|
||||
| `"loto_master_card"` | `loto_master_card_grid` | `loto_master_card_crossed` |
|
||||
|
||||
(Special) `loto_master` stores `{ called, remaining }` for the master state.
|
||||
|
||||
All keys are JSON stringified. Corruption is silent (returns null).
|
||||
|
||||
## basePath & Asset Resolution
|
||||
|
||||
### Production Mode
|
||||
```
|
||||
NODE_ENV=production
|
||||
basePath="/loto"
|
||||
Output: /loto/index.html, /loto/_next/...
|
||||
GitHub Pages serves: https://user.github.io/loto
|
||||
```
|
||||
|
||||
### Development Mode (Local)
|
||||
```
|
||||
NEXT_DEV_PROFILE not set
|
||||
basePath="" (empty)
|
||||
Dev server: http://localhost:3000
|
||||
```
|
||||
|
||||
### Code-Server Mode
|
||||
```
|
||||
NEXT_DEV_PROFILE=codeserver
|
||||
CODESERVER_HOST=<proxy-host>
|
||||
CODESERVER_PORT=3000 (or env)
|
||||
basePath="/absproxy/{PORT}"
|
||||
HMR Origin: CODESERVER_HOST
|
||||
Access: https://<proxy>/absproxy/3000/
|
||||
```
|
||||
|
||||
**Note**: `/absproxy/{port}` preserves the basePath through the proxy. `/proxy/{port}` strips it before forwarding, breaking HMR.
|
||||
|
||||
## Client-Only Architecture
|
||||
|
||||
All pages use `"use client"` because:
|
||||
- `output: "export"` disables server components
|
||||
- localStorage is unavailable on server
|
||||
- Hydration requires client-only state initialization
|
||||
|
||||
Files with `"use client"`:
|
||||
- `app/page.tsx`
|
||||
- `app/master/page.tsx`
|
||||
- `app/loto-player-board.tsx`
|
||||
|
||||
## Data Flow: Mark a Cell
|
||||
|
||||
```
|
||||
1. User clicks <div> in PlayerBoard
|
||||
2. handleCellClick(row, col) fires
|
||||
3. setCrossed(prev => [...]) toggles crossed[row][col]
|
||||
4. useEffect listens to crossed → saveCrossedState()
|
||||
5. localStorage updated with new crossed state
|
||||
6. Render cycle checks isRowComplete() + getWaitingNumber()
|
||||
7. If row complete → bingo popup; if waiting → toast
|
||||
```
|
||||
|
||||
## Data Flow: Draw a Number
|
||||
|
||||
```
|
||||
1. User clicks "Xổ số" button on master page
|
||||
2. handleDrawNext() runs
|
||||
3. Sets state: { called: [...old, next], remaining: [...old].slice(1) }
|
||||
4. useEffect listens to state → saveState() → localStorage["loto_master"]
|
||||
5. Master grid re-renders: cells with numbers in called set turn orange
|
||||
6. lastCalled updates the big display
|
||||
7. Host can see their card update in real-time (separate component)
|
||||
```
|
||||
|
||||
## Animations
|
||||
|
||||
| Name | Duration | Use |
|
||||
|------|----------|-----|
|
||||
| fade-in | 0.2s | Modal background entry |
|
||||
| pop-in | 0.4s | Bingo popup scale + scale-back |
|
||||
| bounce-slow | 1.5s infinite | Emoji on bingo popup |
|
||||
| spin-slow | 3s infinite | ✨ on bingo popup |
|
||||
| spin-slow-reverse | 3s infinite reverse | 🎊 on bingo popup |
|
||||
| toast | 5s forwards | "Chờ X" notification fade in/out |
|
||||
| cell-crossed::after | instant | Red diagonal line in marked cells |
|
||||
|
||||
## Offline Capability
|
||||
|
||||
All state is localStorage. No API calls. Fully functional offline after initial load.
|
||||
|
||||
Last reviewed: 2026-04-26
|
||||
Reference in new issue
Block a user