refactor: move shared modules out of app/

`app/` should hold route segments only. Game logic and the player-card
component are imported by both routes, so they belong outside:

  app/loto-game-logic.ts   -> lib/game-logic.ts
  app/loto-player-board.tsx -> components/player-board.tsx

Imports use the existing @/* alias. Drops the redundant `loto-` prefix.
Also fixes the package name from the scaffold default.
This commit is contained in:
2026-04-26 19:34:37 +07:00
parent dd64aa6ed3
commit 254a6dc39a
8 changed files with 16 additions and 16 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import PlayerBoard from "../loto-player-board";
import PlayerBoard from "@/components/player-board";
const STORAGE_KEY = "loto_master";
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useState } from "react";
import PlayerBoard from "./loto-player-board";
import PlayerBoard from "@/components/player-board";
export default function Home() {
const [showInstructions, setShowInstructions] = useState(false);
@@ -9,7 +9,7 @@ import {
loadGrid,
saveCrossedState,
saveGrid,
} from "./loto-game-logic";
} from "@/lib/game-logic";
interface PlayerBoardProps {
/** localStorage key prefix; allows multiple independent boards (e.g. user vs master) */
+5 -5
View File
@@ -2,7 +2,7 @@
## File Naming & Structure
- **Kebab-case** for all files: `loto-player-board.tsx`, `loto-game-logic.ts`.
- **Kebab-case** for all files: `player-board.tsx`, `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.
@@ -81,13 +81,13 @@ function loadGrid(prefix = "loto"): number[][] | null {
| 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 |
| kebab-case | `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:1928`).
- Inline comments for complex logic (e.g., weighted random selection in `lib/game-logic.ts:1928`).
### Example
```typescript
@@ -111,8 +111,8 @@ function randomANumberInRow(weights: number[]): number {
```typescript
import { useCallback, useState } from "react";
import Link from "next/link";
import PlayerBoard from "./loto-player-board";
import { generateGrid } from "./loto-game-logic";
import PlayerBoard from "@/components/player-board";
import { generateGrid } from "@/lib/game-logic";
```
## Testing (Not Currently Implemented)
+6 -6
View File
@@ -12,12 +12,12 @@
### 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. |
| `components/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. |
| `lib/game-logic.ts` | Stateless utilities: generateGrid (weighted column selection), saveGrid, loadGrid, saveCrossedState, loadCrossedState, isRowComplete, getWaitingNumber. |
### Styling
| File | Purpose |
@@ -66,10 +66,10 @@ RootLayout
| 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]. |
| `generateGrid()` | game-logic.ts:52 | Creates 9×9 with weighted column selection (5 nums/row). |
| `isRowComplete()` | game-logic.ts:108 | Boolean: all non-zero cells in row crossed? |
| `getWaitingNumber()` | game-logic.ts:120 | Returns the single uncrossed number in row, or null. |
| `handleCellClick()` | 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
+1 -1
View File
@@ -88,7 +88,7 @@ All pages use `"use client"` because:
Files with `"use client"`:
- `app/page.tsx`
- `app/master/page.tsx`
- `app/loto-player-board.tsx`
- `components/player-board.tsx`
## Data Flow: Mark a Cell
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "nextjs-temp",
"name": "loto",
"version": "0.1.0",
"private": true,
"scripts": {