OpenClaw + ClawTeam + Superpowers Git Initialization
This commit is contained in:
@@ -1,580 +0,0 @@
|
||||
# Production-Ready Lô Tô Game — Full Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a full-stack multiplayer Lô Tô game (Node.js backend + React frontend) that is production-ready, deployable to Zalo Mini Apps, with robust testing, authentication, real-time gameplay, and full economy/social features.
|
||||
|
||||
**Architecture:** Server-authoritative WebSocket game server (Socket.io) with React SPA frontend, using PostgreSQL for persistence and Redis for real-time state/rate limiting. All game logic validated server-side; client uses optimistic UI.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Node.js, Express, Socket.io, JSON Web Tokens (JWT), PostgreSQL (Supabase/Neon), Redis (Upstash)
|
||||
- Frontend: React 18, Vite, Socket.io-client, Framer Motion, Zustand (state), Tailwind CSS (styling)
|
||||
- Testing: Jest (unit), Cypress (E2E multi-player)
|
||||
- Deployment: Docker, Vercel (frontend), Railway/Render (backend), GitHub Actions (CI)
|
||||
- Auth: JWT, Zalo OAuth (Phase 2), anonymous temp users (Phase 1)
|
||||
|
||||
---
|
||||
|
||||
## Pre-Implementation Setup
|
||||
|
||||
Before any tasks, ensure environment:
|
||||
|
||||
- Create directory structure for frontend (`client/`) and backend (`server/`)
|
||||
- Initialize Git repository (if not already)
|
||||
- Create `.gitignore` (Node, React, VSCode,OS, env files)
|
||||
- Set up root `docker-compose.yml` for local DB+Redis
|
||||
- Create `.env.example` with all required env vars: DB URLs, JWT secret, Redis URL, etc.
|
||||
- Set up root `package.json` workspaces or separate package.json server/client
|
||||
- Configure ESLint + Prettier for consistency
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation (Backend Core) — 2 weeks
|
||||
|
||||
### Task 1: Initialize Backend Project & Docker Compose
|
||||
|
||||
**Files:**
|
||||
- Create: `server/package.json`
|
||||
- Create: `server/.env.example`
|
||||
- Create: `server/.eslintrc.js`
|
||||
- Create: `server/README.md`
|
||||
- Create: `docker-compose.yml` at repo root
|
||||
- Create: `.gitignore`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — not applicable (setup task)
|
||||
- [ ] **Step 2: Create server/package.json** with dependencies: `express`, `socket.io`, `pg`, `ioredis`, `jsonwebtoken`, `uuid`, `dotenv`, `winston`, `cors`, `helmet`
|
||||
- [ ] **Step 3: Create docker-compose.yml** with services: `postgres` (image: postgres:15), `redis` (image: redis:7)
|
||||
- [ ] **Step 4: Create .env.example** listing all env vars (DATABASE_URL, REDIS_URL, JWT_SECRET, PORT=3001, NODE_ENV=development)
|
||||
- [ ] **Step 5: Create server README** with setup instructions
|
||||
- [ ] **Step 6: Commit** with message "feat: initialize backend project and docker compose"
|
||||
|
||||
### Task 2: Database Schema Migration
|
||||
|
||||
**Files:**
|
||||
- Create: `server/db/migrations/001_initial_schema.sql`
|
||||
- Create: `server/db/index.js` ( connects to PostgreSQL using `pg` or `supabase` client)
|
||||
- Create: `server/db/redisClient.js` (connects to Redis)
|
||||
|
||||
- [ ] **Step 1: Write failing test** — create `server/db/schema.test.js` that imports the schema file and checks that all tables exist using raw SQL introspection (skip in CI if DB not up)
|
||||
- [ ] **Step 2: Write SQL migration** with all tables and indexes exactly as spec (users, rooms, room_players, coin_transactions, game_history)
|
||||
- [ ] **Step 3: Write db/index.js** that exports pool with `DATABASE_URL` connection; include `query` helper that logs slow queries (>100ms)
|
||||
- [ ] **Step 4: Write db/redisClient.js** that creates Redis client from `REDIS_URL`, exports with `get`/`set`/`sadd`/`zadd` wrappers
|
||||
- [ ] **Step 5: Run test** — ensure migration SQL can be applied (docker compose up -d; psql -f migration)
|
||||
- [ ] **Step 6: Commit** "feat: database schema and clients"
|
||||
|
||||
### Task 3: JWT Auth Service (Anonymous)
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/auth.js`
|
||||
- Create: `server/middleware/auth.js`
|
||||
- Create: `server/routes/auth.js`
|
||||
- Modify: `server/index.js` (or `server/app.js`) to register auth routes
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `server/services/auth.test.js`:
|
||||
- test `createAnonymousUser()` returns a JWT and creates user record with temp_id
|
||||
- test `authenticateJWT()` validates token and returns user payload
|
||||
- [ ] **Step 2: Implement auth.service**:
|
||||
- `generateTempUserId()` — UUID v4
|
||||
- `createAnonymousUser()` — INSERT into users (temp_id, coins=1000) RETURNING id, temp_id, coins; sign JWT with payload `{ userId, tempId, type: 'anonymous' }`
|
||||
- `verifyToken(token)` — verify JWT secret, return payload
|
||||
- [ ] **Step 3: Implement auth.middleware** that extracts Bearer token, calls verifyToken, attaches `req.user`
|
||||
- [ ] **Step 4: Implement auth.route**:
|
||||
- POST `/api/auth/anonymous` → creates anonymous account, returns `{ token, user: { id, coins } }`
|
||||
- GET `/api/auth/me` → returns user from token
|
||||
- [ ] **Step 5: Wire up server** to use routes and middleware
|
||||
- [ ] **Step 6: Run tests** — pass
|
||||
- [ ] **Step 7: Commit** "feat: anonymous JWT auth"
|
||||
|
||||
### Task 4: Socket.io Server Setup
|
||||
|
||||
**Files:**
|
||||
- Create: `server/socket/socketServer.js`
|
||||
- Create: `server/socket/handlers/roomHandlers.js`
|
||||
- Create: `server/socket/handlers/gameHandlers.js`
|
||||
- Modify: `server/index.js` to initialize Socket.io and attach handlers
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `server/socket/socketServer.test.js` using `socket.io-client` to connect, emit `create_room`, expect `room_created` event (mock DB)
|
||||
- [ ] **Step 2: Implement socketServer.js**:
|
||||
- `setupSocket(server)` attaches Socket.io to HTTP server
|
||||
- uses `socket.use(authMiddleware)` to authenticate JWT
|
||||
- registers namespaces (none, default)
|
||||
- [ ] **Step 3: Implement roomHandlers.js**:
|
||||
- `onCreateRoom(socket, data)` validates betAmount, maxPlayers, highlightSeconds; generates room code; creates room record; joins socket to room; emits `room_created`
|
||||
- `onJoinRoom(socket, {roomCode})` validates code; adds player to room_players; joins socket room; emits `room_joined` with state
|
||||
- `onReady(socket)` marks player ready; if all ready, transitions to `playing` after countdown
|
||||
- [ ] **Step 4: Implement gameHandlers.js** (stubs):
|
||||
- `onPlaceGrain` — validations (server-authoritative) will be added later
|
||||
- `onCallKinh` — stub: push to Redis claims set
|
||||
- `onChat` — broadcast to room
|
||||
- [ ] **Step 5: Wire in socketServer to index.js**
|
||||
- [ ] **Step 6: Run test** — pass
|
||||
- [ ] **Step 7: Commit** "feat: socket.io server and room/game handlers"
|
||||
|
||||
### Task 5: Rate Limiting Middleware
|
||||
|
||||
**Files:**
|
||||
- Create: `server/middleware/rateLimit.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `server/middleware/rateLimit.test.js`:
|
||||
- Simulate 10 requests from same user within second; first N allowed, rest blocked with 429
|
||||
- TTL expiration allows after 1s
|
||||
- [ ] **Step 2: Implement rateLimit middleware**:
|
||||
- Uses Redis key `rate_limit:{userId}:{action}` with INCR and EXPIRE 1s
|
||||
- Allows max 10 requests per second per action type (grain_placement, call_kinh, chat)
|
||||
- Returns 429 with JSON `{ error: 'rate_limited' }` if exceeded
|
||||
- Attachable to socket.io via `socket.use(...)`
|
||||
- [ ] **Step 3: Apply rate limiting** to `place_grain`, `call_kinh`, `chat` events in socket handlers
|
||||
- [ ] **Step 4: Run tests** — pass
|
||||
- [ ] **Step 5: Commit** "feat: rate limiting per user action"
|
||||
|
||||
### Task 6: Balance Reservation at Join
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/socket/handlers/roomHandlers.js` (join_room)
|
||||
- Create: `server/services/coinService.js`
|
||||
- Create: `server/services/coinService.test.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — test `reserveBet(userId, roomBet)` deducts from users.coins and logs coin_transaction type=BET_PLACED with negative amount; test failure when insufficient coins
|
||||
- [ ] **Step 2: Implement coinService**:
|
||||
- `reserveBet(userId, amount)` — atomic: UPDATE users SET coins = coins - amount WHERE id=$1 AND coins >= amount; if row count 0 throw insufficient; then INSERT coin_transaction (type=BET_PLACED, amount=-amount, balance_after = (SELECT coins FROM users WHERE id=$1))
|
||||
- `refundBet(userId, amount)` — opposite (used on leave before start)
|
||||
- `applyWin(userId, amount, roomId)` — add coins, log KINH_WON
|
||||
- `applyPenalty(userId, amount, roomId)` — subtract coins, log KINH_PENALTY
|
||||
- [ ] **Step 3: Wire join_room** to call `reserveBet` when player joins; if fails, emit error and reject join
|
||||
- [ ] **Step 4: Run tests** — pass
|
||||
- [ ] **Step 5: Commit** "feat: coin reservation on room join"
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Gameplay Engine — 2 weeks
|
||||
|
||||
### Task 7: Number Draw Engine
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/drawEngine.js`
|
||||
- Create: `server/services/drawEngine.test.js`
|
||||
- Modify: `server/socket/handlers/gameHandlers.js` (add `startGame`)
|
||||
|
||||
- [ ] **Step 1: Write failing test** — test `shuffleNumbers()` returns array of 90 unique numbers; test `drawNext()` returns number not previously drawn
|
||||
- [ ] **Step 2: Implement drawEngine**:
|
||||
- `createDeck()` returns shuffled numbers 1..90 using Fisher-Yates
|
||||
- `DrawSession` class: holds remaining deck, drawn array, current index. Method `drawNext()` returns next number, stores in drawn array with timestamp
|
||||
- [ ] **Step 3: Implement `startGame(roomId)` handler**:
|
||||
- Fetch room players; verify all ready or owner forced start
|
||||
- Create DrawSession for room (store in Redis: `room:{roomId}:draw_state` = JSON with deck)
|
||||
- Set room status = 'playing'; broadcast `game_start` with empty drawnNumbers
|
||||
- Begin async loop: for each number, `setTimeout` highlightSeconds*1000, then emit `number_drawn` and `highlight_end`
|
||||
- [ ] **Step 4: Run tests** — pass
|
||||
- [ ] **Step 5: Commit** "feat: number draw engine with broadcast"
|
||||
|
||||
### Task 8: Rice Grain Placement Validation
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/boardService.js`
|
||||
- Create: `server/services/boardService.test.js`
|
||||
- Modify: `server/socket/handlers/gameHandlers.js` (onPlaceGrain)
|
||||
|
||||
- [ ] **Step 1: Write failing test** — test `canPlaceGrain(userId, row, col, drawnNumbers)` returns false if column's number not in drawnNumbers; true if drawn; also check max 5 grains per row
|
||||
- [ ] **Step 2: Implement boardService**:
|
||||
- `getNumberForCell(row, col)` returns number (1–90) based on standard Lô Tô card layout (3 rows × 9 cols, 5 numbers per row distributed by decades)
|
||||
- `isNumberDrawn(num, drawnNumbers)` — includes in array check
|
||||
- `canPlaceGrain(userId, row, col, drawnNumbers, roomPlayers)` — ensures user has not exceeded 5 grains per row; cell not already occupied by that user; number drawn; bet reserved sufficient
|
||||
- `placeGrain(userId, row, col)` — records grain placement (in Redis set `room:{roomId}:player:{userId}:grains` serialized) and broadcast to room
|
||||
- [ ] **Step 3: in onPlaceGrain**, validate with boardService; if valid, update Redis and broadcast `grain_placed` event with position; if invalid, emit error
|
||||
- [ ] **Step 4: Run tests** — pass
|
||||
- [ ] **Step 5: Commit** "feat: rice grain placement validation"
|
||||
|
||||
### Task 9: Concurrent Kinh Handling
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/socket/handlers/gameHandlers.js` (onCallKinh)
|
||||
- Create: `server/services/kinhService.js`
|
||||
- Create: `server/services/kinhService.test.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — test `validateKinh(userId, roomId)` returns true if all rows that have grains fully match drawnNumbers; false otherwise
|
||||
- [ ] **Step 2: Implement `validateKinh`**:
|
||||
- Get user's grains from Redis; compute rows where they have exactly 5 grains; check every grain's number is in drawnNumbers
|
||||
- Returns object `{ valid: boolean, rowsCompleted: number[] }`
|
||||
- [ ] **Step 3: Implement onCallKinh**:
|
||||
- When received, add to Redis sorted set `room:{roomId}:kinh_claims` with score=Date.now()
|
||||
- Broadcast `kinh_claim` to room so UI shows pending badge
|
||||
- Do not respond immediately; wait for freeze window end
|
||||
- [ ] **Step 4: Modify draw engine**:
|
||||
- After `highlight_end` emitted, set a short freeze timeout (500ms)
|
||||
- After freeze, collect all userIds from Redis sorted set (score within window); clear set; process all claims via `processKinhClaims(roomId, claims)`
|
||||
- [ ] **Step 5: Implement processKinhClaims**:
|
||||
- For each claimant, run `validateKinh`
|
||||
- Separate into validWinners and invalidClaimants
|
||||
- Compute totalPot = sum of all bets (roomBet * numPlayers)
|
||||
- Compute reward per valid = totalPot / validCount (integer math; any remainder left in pot? Could add to next round or house. We'll do integer division, remainder stays with house for simplicity)
|
||||
- For each invalid, penalty = betAmount to *every other player* in room (including other invalids). Implement loop: for each invalid `p`, for each other player `q` in room_players, transfer `betAmount` from p to q using coinService
|
||||
- For each valid, reward = their share (already computed) added to balance via coinService
|
||||
- Update room status to 'settling'; then 'settled' after DB updates
|
||||
- Broadcast `game_settled` with full results (including pre/post balances)
|
||||
- [ ] **Step 6: Run tests** — pass (use unit tests to validate math; integration will test end-to-end)
|
||||
- [ ] **Step 7: Commit** "feat: concurrent Kinh verification and settlement"
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Frontend Core — 2 weeks
|
||||
|
||||
### Task 10: Initialize React Frontend with Vite
|
||||
|
||||
**Files:**
|
||||
- Create: `client/package.json`
|
||||
- Create: `client/vite.config.js`
|
||||
- Create: `client/index.html`
|
||||
- Create: `client/src/main.jsx`
|
||||
- Create: `client/src/App.jsx`
|
||||
- Create: `client/tailwind.config.js` (or use plain CSS per retro theme)
|
||||
- Create: `client/.env.example`
|
||||
|
||||
- [ ] **Step 1: Create client/package.json** with deps: `react`, `react-dom`, `vite`, `socket.io-client`, `framer-motion`, `zustand`, `axios`, `tailwindcss`
|
||||
- [ ] **Step 2: Create vite.config.js** for React, set base for Zalo Mini App later (`base: './'`), proxy API to backend for dev (`server.proxy`)
|
||||
- [ ] **Step 3: Create index.html** simple root div
|
||||
- [ ] **Step 4: Create main.jsx** ReactDOM.render into #root
|
||||
- [ ] **Step 5: Create App.jsx** with basic routing (Lobby vs Game) using React state for now
|
||||
- [ ] **Step 6: Create .env.example** with VITE_API_URL, VITE_WS_URL
|
||||
- [ ] **Step 7: Commit** "feat: initialize React frontend with Vite"
|
||||
|
||||
### Task 11: Authentication Context & Login UI
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/contexts/AuthContext.jsx`
|
||||
- Create: `client/src/hooks/useAuth.js`
|
||||
- Create: `client/src/components/Auth/AuthModal.jsx`
|
||||
- Create: `client/src/components/Auth/LoginButton.jsx`
|
||||
- Modify: `client/src/App.jsx` to use AuthContext and show login UI
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `client/src/contexts/AuthContext.test.jsx` with Jest Testing Library: test that after login, context has token and user; test localStorage persistence
|
||||
- [ ] **Step 2: Implement AuthContext**:
|
||||
- State: `user` (null or {id, coins}), `token` (string), `loading`
|
||||
- Effect on mount: check localStorage for token; if present, call `/api/auth/me` to validate; else clear
|
||||
- `loginAnonymous()` calls POST `/api/auth/anonymous`, saves token+user to localStorage, updates state
|
||||
- `logout()` clears state and localStorage
|
||||
- [ ] **Step 3: Implement LoginButton** that triggers `loginAnonymous`; also placeholder Zalo button (disabled behind flag)
|
||||
- [ ] **Step 4: Implement AuthModal** that shows login options, closes after login
|
||||
- [ ] **Step 5: Wire App.jsx**: if not authenticated, show login modal; else show main app (lobby placeholder)
|
||||
- [ ] **Step 6: Run tests** — pass
|
||||
- [ ] **Step 7: Commit** "feat: anonymous auth context and UI"
|
||||
|
||||
### Task 12: WebSocket Hook & Socket Context
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/hooks/useWebSocket.js`
|
||||
- Create: `client/src/contexts/GameContext.jsx`
|
||||
- Create: `client/src/services/socketService.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `client/src/hooks/useWebSocket.test.js`: mock socket.io-client; test that on connection, state is connected; on event, handler called
|
||||
- [ ] **Step 2: Implement socketService.js**:
|
||||
- Singleton that creates `socket` instance from `io()` with auth token
|
||||
- Methods: `on(event, handler)`, `emit(event, payload)`, `disconnect()`
|
||||
- Auto-reconnect logic
|
||||
- [ ] **Step 3: Implement useWebSocket** hook that provides `socket` object via context
|
||||
- [ ] **Step 4: Implement GameContext**:
|
||||
- State: `room`, `players`, `gameState` ('lobby','playing','frozen','settled'), `currentNumber`, `myGrains`, etc.
|
||||
- Socket event listeners: `room_joined`, `player_joined`, `number_drawn`, `highlight_end`, `kinh_claim`, `game_settled`, `balance_update`, `error`
|
||||
- Methods: `createRoom`, `joinRoom`, `ready`, `placeGrain`, `callKinh`, `sendChat`, `leave`
|
||||
- [ ] **Step 5: Run tests** — pass
|
||||
- [ ] **Step 6: Commit** "feat: WebSocket hook and game context"
|
||||
|
||||
### Task 13: Lobby & Room Management UI
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/components/Lobby/LobbyList.jsx`
|
||||
- Create: `client/src/components/Lobby/RoomCard.jsx`
|
||||
- Create: `client/src/components/Lobby/CreateRoomModal.jsx`
|
||||
- Modify: `client/src/App.jsx` to show lobby when authenticated
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `client/src/components/Lobby/LobbyList.test.jsx`: test that room cards display bet amount, player count; test join button calls joinRoom
|
||||
- [ ] **Step 2: Implement CreateRoomModal** with form fields: bet amount (input), max players (select 2-8), highlight seconds (select 1-5). Submits to `createRoom` from GameContext.
|
||||
- [ ] **Step 3: Implement RoomCard** to show room code, bet, players count, status; join button disabled if full or started
|
||||
- [ ] **Step 4: Implement LobbyList** that fetches public rooms from `/api/rooms/public` (backend stub needed later) or via socket event `public_rooms` (we'll add that to backend later). For now, mock data; later will wire to backend.
|
||||
- [ ] **Step 5: Wire App.jsx** to show LobbyList after login; show CreateRoomModal button
|
||||
- [ ] **Step 6: Run tests** — pass
|
||||
- [ ] **Step 7: Commit** "feat: lobby UI and room creation"
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Core Gameplay Frontend — 2 weeks
|
||||
|
||||
### Task 14: Lotto Card & Board
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/components/Game/LottoCard.jsx`
|
||||
- Create: `client/src/utils/cardPositions.js`
|
||||
- Modify: `client/src/components/Game/GameBoard.jsx` (to be created)
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `client/src/components/Game/LottoCard.test.jsx`: test that grid has 3 rows, 9 cols; test clicking cell triggers `placeGrain`; test grains display as placed
|
||||
- [ ] **Step 2: Implement cardPositions.js** with `getNumberForCell(row, col)` (0-indexed rows 0-2, cols 0-8). Rules: Column 0 = numbers 1-9; column 1 = 10-19; ... column 8 = 81-90? Actually Lô Tô: 9 columns represent tens: 1-9,10-19,...,80-90. Each column has 3 rows, but only 5 numbers per row across columns. Standard layout: Each row contains 5 numbers placed in specific column positions based on decade. We need precise mapping. Quick reference: traditional Lô Tô card:
|
||||
- Columns: 1 (1-9), 2 (10-19), 3 (20-29), 4 (30-39), 5 (40-49), 6 (50-59), 7 (60-69), 8 (70-79), 9 (80-90)
|
||||
- Rows: top, middle, bottom. Each row has 5 numbers; the distribution across columns follows rules: each row includes exactly 5 numbers from 5 different columns. There are standard patterns but we can simplify: randomly assign 5 numbers (from the column's range) to each row, ensuring each column used at least once across rows? For simplicity, we can predefine a fixed pattern for all cards: each card identical layout. Let's define a static mapping array `CELL_NUMBERS[3][9]` where each cell has a number (1-90) or null. Only 15 cells are numbers; others empty visual. We'll create a deterministic layout (same for all players) to avoid needing dynamic generation. We'll hardcode a valid Lô Tô card pattern: e.g.,
|
||||
Row1: 1,11,21,31,41 at columns 1,2,3,4,5; others empty.
|
||||
Row2: 51,61,71,81,91? Wait 91 doesn't exist. Let's design properly: We need 5 numbers per row across 9 columns; each column appears at most once per row. Use this pattern (just an example):
|
||||
Row1: col0=1, col1=11, col2=25, col3=33, col4=44, others null
|
||||
Row2: col0=5, col1=17, col2=28, col4=49, col6=62
|
||||
Row3: col1=20, col3=39, col5=58, col7=79, col8=90
|
||||
We'll make a specific valid layout and hardcode it. It's not random; all players have same card pattern. That's fine for MVP.
|
||||
Implement `getNumberForCell(row, col)` returns that hardcoded number or null.
|
||||
- [ ] **Step 3: Implement LottoCard component**:
|
||||
- Renders a 3x9 grid using CSS grid.
|
||||
- For each cell: if number exists, clickable area; if null, empty placeholder with retro styling.
|
||||
- Shows rice grain overlay if cell is in `grains` set (from GameContext)
|
||||
- If cell's number is in `drawnNumbers`, highlight with CSS class
|
||||
- onClick calls `placeGrain(row, col)`
|
||||
- [ ] **Step 4: Implement GameBoard**:
|
||||
- Contains LottoCard, NumberBag animation, current number display, Kinh button, players panel
|
||||
- Uses Framer Motion for NumberBag (shake) when number being drawn
|
||||
- [ ] **Step 5: Run tests** — pass
|
||||
- [ ] **Step 6: Commit** "feat: Lotto card and game board UI"
|
||||
|
||||
### Task 15: Framer Motion Animations
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/components/Game/NumberBag.jsx` (new file)
|
||||
- Modify: `client/src/components/Game/RiceGrain.jsx` (new file)
|
||||
- Modify: `client/src/styles/retroTheme.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** (visual, manual) — skip unit; will verify visually
|
||||
- [ ] **Step 2: Create NumberBag component** with animation using Framer Motion: while `isShaking` true, rotate and scale; while `drawing` emits number ball with motion
|
||||
- [ ] **Step 3: Create RiceGrain component** — when placed, animate drop (y: -100 to 0 with bounce) using `motion.div`
|
||||
- [ ] **Step 4: Create retroTheme.js** with color palette: nâu gỗ, vàng ố, đỏ đất, xanh lá cũ; fonts: handwriting retro; textures optional (use CSS patterns)
|
||||
- [ ] **Step 5: Apply retro theme to components** (buttons, cards)
|
||||
- [ ] **Step 6: Manual QA**: run app, verify animations smooth (≥30fps)
|
||||
- [ ] **Step 7: Commit** "feat: animations and retro styling"
|
||||
|
||||
### Task 16: In-Game HUD & Controls
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/components/Game/PlayersPanel.jsx`
|
||||
- Create: `client/src/components/Game/KinhButton.jsx`
|
||||
- Create: `client/src/components/Game/ChatPanel.jsx`
|
||||
- Modify: `client/src/components/Game/GameBoard.jsx` to include these
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `client/src/components/Game/KinhButton.test.jsx`: test disabled when not playing; click calls `callKinh`
|
||||
- [ ] **Step 2: Implement PlayersPanel**:
|
||||
- Shows list: avatar, name, ready status, balance (pre-game), grains count
|
||||
- Updates via GameContext
|
||||
- [ ] **Step 3: Implement KinhButton**:
|
||||
- Visible only during playing and after user has completed at least one row (check row completeness from grains)
|
||||
- Clicking emits `callKinh`; button disabled after click to prevent spam
|
||||
- Shows countdown timer if freeze window active? Could show "Verifying..." message
|
||||
- [ ] **Step 4: Implement ChatPanel**:
|
||||
- Simple input + send button; messages list scrolling; emits `chat` event; receives `chat_message`
|
||||
- No persistence beyond current room
|
||||
- [ ] **Step 5: Run tests** — pass
|
||||
- [ ] **Step 6: Commit** "feat: in-game HUD, players panel, Kinh button, chat"
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 (cont'd): Economy, Social & Polish — 2 weeks
|
||||
|
||||
### Task 17: Coin Economy & Daily Bonus
|
||||
|
||||
**Files:**
|
||||
- Create: `server/routes/economy.js`
|
||||
- Create: `server/services/coinService.js` (extend with dailyBonus)
|
||||
- Create: `client/src/components/Profile/CoinWallet.jsx`
|
||||
- Create: `client/src/components/Profile/DailyBonusButton.jsx`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `server/services/coinService.test.js`: test `claimDailyBonus(userId)` gives coins only if last claim >24h ago; updates `last_claimed_at` in users (add column); test failure if already claimed
|
||||
- [ ] **Step 2: Add DB column** `last_daily_bonus_at TIMESTAMP` to users (migration `002_add_daily_bonus.sql`)
|
||||
- [ ] **Step 3: Implement `claimDailyBonus` in coinService** with 24h check; inserts coin_transaction type=DAILY_BONUS
|
||||
- [ ] **Step 4: Create route `POST /api/economy/daily-bonus`** protected by auth; calls service; returns new balance
|
||||
- [ ] **Step 5: Frontend: DailyBonusButton** that calls endpoint, shows next available time; update AuthContext user balance after success
|
||||
- [ ] **Step 6: Run tests** — pass
|
||||
- [ ] **Step 7: Commit** "feat: daily coin bonus system"
|
||||
|
||||
### Task 18: Achievements System
|
||||
|
||||
**Files:**
|
||||
- Create: `server/routes/achievements.js`
|
||||
- Create: `server/services/achievementService.js`
|
||||
- Create: `server/db/migrations/003_achievements.sql` (tables: achievements, user_achievements)
|
||||
- Create: `client/src/components/Profile/AchievementsList.jsx`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — define achievement logic: e.g., FIRST_WIN (first Kinh win), CONCURRENT_KINH_3 (call Kinh with >=2 others simultaneously). Test that when game_settled event emitted, service checks and grants achievements; test duplicate prevention
|
||||
- [ ] **Step 2: Create DB tables**:
|
||||
- `achievements` (id, code unique, name, description, coin_reward)
|
||||
- `user_achievements` (user_id, achievement_id, earned_at, UNIQUE(user_id,achievement_id))
|
||||
- Insert seed data for several achievements
|
||||
- [ ] **Step 3: Implement achievementService**:
|
||||
- `checkAndGrantAchievements(userId, event, payload)` — switch on event (e.g., 'game_settled'); query user stats; if not earned, insert into user_achievements and add coin_reward via coinService
|
||||
- `getUserAchievements(userId)` for UI
|
||||
- [ ] **Step 4: Wire achievement check** into `processKinhClaims` after settlement: emit event to achievementService for each winner (and possibly others)
|
||||
- [ ] **Step 5: Route `GET /api/achievements`** returns list of all achievements with earned flag
|
||||
- [ ] **Step 6: Frontend: AchievementsList** component displays badges, earned status, coin rewards
|
||||
- [ ] **Step 7: Run tests** — pass
|
||||
- [ ] **Step 8: Commit** "feat: achievements system with coin rewards"
|
||||
|
||||
### Task 19: Spin Wheel Mini-Game
|
||||
|
||||
**Files:**
|
||||
- Create: `server/routes/spin-wheel.js`
|
||||
- Create: `server/services/spinWheelService.js`
|
||||
- Create: `client/src/components/Profile/SpinWheel.jsx`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — test `spinWheel(userId)` returns random prize segment; test cooldown (24h); test probability distribution
|
||||
- [ ] **Step 2: Add DB columns** to users: `last_spun_at TIMESTAMP`
|
||||
- [ ] **Step 3: Define wheel segments** array: `[{label: '100 coins', reward: 100, weight: 50}, {label: '500 coins', reward: 500, weight: 10}, {label: 'Try again', reward: 0, weight: 40}]` etc.
|
||||
- [ ] **Step 4: Implement spinWheelService**:
|
||||
- Weighted random selection based on segments
|
||||
- Check cooldown; if available, grant coins via coinService, set `last_spun_at`, return prize
|
||||
- [ ] **Step 5: Route `POST /api/spin`** returns prize result and new balance
|
||||
- [ ] **Step 6: Frontend: SpinWheel component** with animated wheel (Framer Motion spin animation), button to spin (disabled if on cooldown), display result
|
||||
- [ ] **Step 7: Run tests** — pass
|
||||
- [ ] **Step 8: Commit** "feat: daily spin wheel mini-game"
|
||||
|
||||
### Task 20: Chat System
|
||||
|
||||
**Files:**
|
||||
- Modify backend: `server/socket/handlers/gameHandlers.js` to store chat in Redis list `room:{roomId}:chat` (max 100) and broadcast `chat_message`
|
||||
- Modify frontend: `client/src/components/Game/ChatPanel.jsx` already created; ensure displays messages with sender name and timestamp
|
||||
|
||||
- [ ] **Step 1: Write failing test** — integration test: two sockets in same room; one sends chat; other receives event with correct sender and message
|
||||
- [ ] **Step 2: Implement server** chat handler:
|
||||
- `onChat(socket, { message })`: validate non-empty, length < 200; record in Redis list (LTRIM to keep last 100); broadcast `chat_message` with userId, name (from room_players), message, timestamp
|
||||
- [ ] **Step 3: Implement client** ChatPanel: maintain local messages array; on `chat_message` event, append; scroll to bottom
|
||||
- [ ] **Step 4: (Optional) Profanity filter** — simple blacklist array; filter message before broadcast; if blocked, emit error to sender only
|
||||
- [ ] **Step 5: Run tests** — pass
|
||||
- [ ] **Step 6: Commit** "feat: in-room chat"
|
||||
|
||||
### Task 21: Profile & Stats
|
||||
|
||||
**Files:**
|
||||
- Create: `server/routes/profile.js`
|
||||
- Create: `client/src/components/Profile/ProfilePage.jsx`
|
||||
- Modify: `client/src/App.jsx` add route for profile
|
||||
|
||||
- [ ] **Step 1: Write failing test** — `server/routes/profile.test.js`: test GET returns user stats: total_wins, total_losses, recent history (last 10 coin_transactions)
|
||||
- [ ] **Step 2: Implement route**:
|
||||
- `GET /api/profile` → selects from users, left join coin_transactions, returns summary and recent history
|
||||
- [ ] **Step 3: Frontend ProfilePage** displays: avatar (from user record if Zalo, else placeholder), name, coins, win/loss counts, recent transactions in table, achievements section (link to AchievementsList), daily bonus button, spin wheel section
|
||||
- [ ] **Step 4: Navigation** to profile from lobby
|
||||
- [ ] **Step 5: Run tests** — pass
|
||||
- [ ] **Step 6: Commit** "feat: user profile and stats page"
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing, Docker & Deployment — 1 week
|
||||
|
||||
### Task 22: Jest Unit Tests (Comprehensive)
|
||||
|
||||
**Files:**
|
||||
- Create: `server/jest.config.js`
|
||||
- Create: `client/jest.config.js` (if using Jest for React)
|
||||
- Fill: multiple unit tests for all services: auth, coinService, boardService, drawEngine, kinhService
|
||||
|
||||
- [ ] **Step 1: Write tests** for every service function (see earlier tasks). Aim 80%+ coverage.
|
||||
- [ ] **Step 2: Configure Jest** for server (babel or ts if using TS; we're using plain JS so default)
|
||||
- [ ] **Step 3: Add test script** to server/package.json: `"test": "jest"`
|
||||
- [ ] **Step 4: Run coverage** and fix any gaps
|
||||
- [ ] **Step 5: Commit** "test: comprehensive unit test suite"
|
||||
|
||||
### Task 23: Cypress E2E Multi-Player Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `client/cypress.config.js`
|
||||
- Create: `client/cypress/e2e/create-room.cy.js`
|
||||
- Create: `client/cypress/e2e/gameplay.cy.js`
|
||||
- Create: `client/cypress/e2e/concurrent-kinh.cy.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test** (setup): test that Cypress can visit app and create a room
|
||||
- [ ] **Step 2: Implement `create-room.cy.js`**:
|
||||
- Visit app, login anonymous, create room, verify room created, join second browser instance (using `cy.origin` or two windows simulation)
|
||||
- [ ] **Step 3: Implement `gameplay.cy.js`**:
|
||||
- 2 players join room, ready, game starts; simulate placing grains; call Kinh; verify settlement and balances
|
||||
- [ ] **Step 4: Implement `concurrent-kinh.cy.js`**:
|
||||
- 4 players; simulate near-simultaneous Kinh calls (some valid, some invalid); verify final balances match expected penalty distribution
|
||||
- [ ] **Step 5: Add Cypress to client package.json**; script `"cypress:open"` and `"cypress:run"`
|
||||
- [ ] **Step 6: Run E2E suite** to ensure passes
|
||||
- [ ] **Step 7: Commit** "test: Cypress E2E multi-player scenarios"
|
||||
|
||||
### Task 24: Docker & Docker Compose for Production-Like Local
|
||||
|
||||
**Files:**
|
||||
- Create: `Dockerfile` (for backend)
|
||||
- Create: `client/Dockerfile` (for frontend static build)
|
||||
- Modify: `docker-compose.yml` to include services: `frontend`, `backend`, `postgres`, `redis`
|
||||
- Create: `.dockerignore`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — none
|
||||
- [ ] **Step 2: Write backend Dockerfile**:
|
||||
- Node 20 alpine
|
||||
- Copy package.json, install, copy server/, expose 3001, CMD `node server/index.js`
|
||||
- [ ] **Step 3: Write frontend Dockerfile**:
|
||||
- Node 20 alpine; copy client package.json; install; copy client/; `npm run build`; output static files; serve with nginx: `nginx:alpine` and copy build to `/usr/share/nginx/html`
|
||||
- [ ] **Step 4: Update docker-compose** to build and link services; set env vars; frontend depends on backend; network them
|
||||
- [ ] **Step 5: Test local docker compose up** — app accessible at http://localhost (frontend) and backend ws at ws://localhost:3001
|
||||
- [ ] **Step 6: Commit** "feat: Docker and docker-compose for full stack"
|
||||
|
||||
### Task 25: GitHub Actions CI
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/ci.yml`
|
||||
|
||||
- [ ] **Step 1: Write failing test** — none
|
||||
- [ ] **Step 2: Create CI workflow**:
|
||||
- Triggers on push to main, PRs
|
||||
- Jobs:
|
||||
- `backend-tests`: setup Node, run `npm ci`, `npm test` in server/, also lint
|
||||
- `frontend-tests`: setup Node, run `npm ci`, `npm test` in client/ (if unit tests exist), also `npm run build` to ensure build succeeds
|
||||
- `e2e-tests`: uses Cypress Docker image; run `npm run cy:run` in client/; needs backend service running (docker compose up -d inside workflow? Use Cypress included services? Simpler: spin up backend via docker compose in before_script)
|
||||
- Upload artifacts (screenshots, videos) on failure
|
||||
- [ ] **Step 3: Commit** "ci: GitHub Actions pipeline"
|
||||
|
||||
### Task 26: Free Tier Deployment Configuration
|
||||
|
||||
**Files:**
|
||||
- Create: `client/vercel.json` (or rely on defaults)
|
||||
- Create: `railway.json` for backend (or use Dockerfile)
|
||||
- Update: `client/.env.production` with VITE_API_URL and VITE_WS_URL pointing to Railway backend URL (which will be set as env at deploy time)
|
||||
- Create: `server/.env.production.example` with required vars (Railway provides DATABASE_URL etc.)
|
||||
|
||||
- [ ] **Step 1: Write failing test** — none
|
||||
- [ ] **Step 2: Create Vercel config** (optional) for SPA routing redirects
|
||||
- [ ] **Step 3: Prepare Railway deployment**: ensure Dockerfile present; add `railway.json` if needed (just specify Docker build)
|
||||
- [ ] **Step 4: Document environment variables** required for production in README
|
||||
- [ ] **Step 5: Commit** "deploy: free tier configs for Vercel + Railway"
|
||||
|
||||
### Task 27: Zalo Mini App Build Script
|
||||
|
||||
**Files:**
|
||||
- Create: `client/package.json` scripts: `"build:zalo": "vite build --config vite.zalo.config.js"`
|
||||
- Create: `client/vite.zalo.config.js` with appropriate base and output dir `dist/zalo`
|
||||
- Create: `client/zalo.config.json` (Zalo Mini App manifest) (placeholder)
|
||||
- Create: `client/src/platforms/zalo/adapter.js` to wrap Zalo SDK calls
|
||||
|
||||
- [ ] **Step 1: Write failing test** — none
|
||||
- [ ] **Step 2: Create Zalo-specific Vite config** that sets base to `./`, outputs to `dist/zalo`, builds as library? (Zalo Mini App expects a single JS file and assets). We'll produce static assets.
|
||||
- [ ] **Step 3: Create zalo.config.json** with appID, name, version, orientation, etc. (placeholders)
|
||||
- [ ] **Step 4: Create Zalo adapter** that provides methods: `login()`, `share()`, `getUserInfo()`. Use real SDK when in Zalo environment; fallback to mock in web.
|
||||
- [ ] **Step 5: Update Auth flow** to use ZaloLoginButton that calls adapter.login; on success, hit backend Zalo OAuth flow
|
||||
- [ ] **Step 6: Build test**: run `npm run build:zalo` and verify output in `dist/zalo/`
|
||||
- [ ] **Step 7: Commit** "feat: Zalo Mini App build configuration"
|
||||
|
||||
---
|
||||
|
||||
## Post-Implementation: Local Development & QA
|
||||
|
||||
- Ensure `docker-compose up` brings up everything
|
||||
- Backend runs on :3001, frontend on :5173 (Vite default)
|
||||
- Provide instructions in README for running locally and for deployment
|
||||
|
||||
---
|
||||
|
||||
## Notes for Subagent Workers
|
||||
|
||||
- **TDD:** ALWAYS write test FIRST, then code to make it pass. No exceptions.
|
||||
- **Commits:** Small, logical commits after each step with clear messages.
|
||||
- **Self-review:** After completing your assigned tasks, run the tests, lint, and check that your code follows the architecture in the spec.
|
||||
- **Cross-review:** After your task is complete and self-reviewed, you will be asked to review another worker's code. Use the 3-tier review criteria: Plan Alignment, Code Quality, Architecture, Documentation, Issues.
|
||||
- **Blockers:** If you encounter a dependency on another task that is not yet done, ask the orchestrator to reorder tasks or create a minimal stub.
|
||||
|
||||
---
|
||||
|
||||
**Plan complete and saved to `docs/superpowers/plans/2026-03-31-lotto-game-full-stack.md`.**
|
||||
|
||||
**Execution approach:** Subagent-Driven (you will dispatch fresh subagents per task or small task group). I will now begin spawning subagents for Phase 1 tasks sequentially, ensuring each task is fully implemented, reviewed, and committed before moving to the next.
|
||||
|
||||
Let's start with **Task 1: Initialize Backend Project & Docker Compose**.
|
||||
-1815
File diff suppressed because it is too large
Load Diff
@@ -1,269 +0,0 @@
|
||||
# Portfolio Tết Theme - Design Spec
|
||||
|
||||
**Date:** 2026-03-31
|
||||
**Author:** Sagi (Orchestrator AI)
|
||||
**Status:** Draft → Approved by user
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Build a personal portfolio website with traditional Tết (Vietnamese Lunar New Year) theme: red/gold colors, falling petals, subtle fireworks, lion dance motifs. Multi-page with clear routing. Responsive and smooth animations.
|
||||
|
||||
**Tech Stack:**
|
||||
- Next.js 14+ (latest) with App Router
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Framer Motion
|
||||
- Deploy target: Vercel (recommended)
|
||||
|
||||
---
|
||||
|
||||
## 2. Project Structure
|
||||
|
||||
```
|
||||
clawteam-portfolio/
|
||||
├── app/
|
||||
│ ├── layout.tsx
|
||||
│ ├── page.tsx (Home)
|
||||
│ ├── projects/
|
||||
│ │ └── page.tsx
|
||||
│ ├── my-corner/
|
||||
│ │ ├── page.tsx (Blog list)
|
||||
│ │ └── [slug]/
|
||||
│ │ └── page.tsx (Single post)
|
||||
│ └── globals.css
|
||||
├── components/
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── Hero.tsx
|
||||
│ ├── About.tsx
|
||||
│ ├── Experience.tsx
|
||||
│ ├── Contact.tsx
|
||||
│ ├── ProjectCard.tsx
|
||||
│ ├── BlogCard.tsx
|
||||
│ ├── TetEffects.tsx (falling petals, fireworks)
|
||||
│ └── ui/ (buttons, inputs, etc.)
|
||||
├── data/
|
||||
│ ├── profile.ts (personal info from CV)
|
||||
│ ├── experience.ts
|
||||
│ ├── projects.ts
|
||||
│ └── blogPosts.ts
|
||||
├── lib/
|
||||
│ └── utils.ts
|
||||
├── public/
|
||||
│ ├── avatar.jpeg
|
||||
│ └── images/...
|
||||
├── styles/
|
||||
│ └── tailwind.css (if using @tailwind directive)
|
||||
├── next.config.js
|
||||
├── tsconfig.json
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Pages & Routes
|
||||
|
||||
### `/` (Home) - Single page with sections
|
||||
- Hero (avatar + intro)
|
||||
- About
|
||||
- Experience (timeline)
|
||||
- Contact (form + info)
|
||||
|
||||
### `/projects` (Projects)
|
||||
- Grid of project cards
|
||||
|
||||
### `/my-corner` (Blog list)
|
||||
- List of personal posts
|
||||
|
||||
### `/my-corner/[slug]` (Single post)
|
||||
- Full content page
|
||||
|
||||
---
|
||||
|
||||
## 4. Content Mapping (from CV & avatar)
|
||||
|
||||
### Profile Data (`data/profile.ts`)
|
||||
```ts
|
||||
{
|
||||
name: "Nguyễn Ngọc Trí Vĩ",
|
||||
title: "AI Engineer",
|
||||
avatar: "/avatar.jpeg",
|
||||
email: "nntrivi2001@gmail.com",
|
||||
phone: "090 321 5095",
|
||||
linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi",
|
||||
portfolio: "https://nguyenngoctrivi.vercel.app",
|
||||
languages: ["Vietnamese (native)", "English (intermediate)"],
|
||||
bio: `Short bio...`
|
||||
}
|
||||
```
|
||||
|
||||
### Experience Data (`data/experience.ts`)
|
||||
Array of 4 positions with dates, company, role, description bullets.
|
||||
|
||||
### Projects Data (`data/projects.ts`)
|
||||
From CV "Portfolio & Chứng chỉ" section. Initially minimal: title, description, tech stack, links (demo/GitHub). Can be expanded later.
|
||||
|
||||
### Blog Data (`data/blogPosts.ts`)
|
||||
Initial placeholder posts with personal content (photos, memories, experiences). Slug-based.
|
||||
|
||||
---
|
||||
|
||||
## 5. Components Detail
|
||||
|
||||
#### Header
|
||||
- Fixed top, transparent background with blur
|
||||
- Logo (text "Trí Vĩ") left
|
||||
- Nav links: Home, Projects, My Corner
|
||||
- Mobile hamburger menu
|
||||
- Active section highlight (Home only)
|
||||
|
||||
#### Footer
|
||||
- Copyright: © 2025 - 2026 Nguyễn Ngọc Trí Vĩ
|
||||
- Social icons (LinkedIn, GitHub, etc.)
|
||||
|
||||
#### Hero
|
||||
- Large avatar (circular or rounded square)
|
||||
- Name + title
|
||||
- Short intro line
|
||||
- Call-to-action buttons: "View Projects", "Contact"
|
||||
- Background: gradient red/yellow
|
||||
- TetEffects overlay (falling petals)
|
||||
|
||||
#### About
|
||||
- Two-column: image (avatar or personal photo) + text
|
||||
- Text: bio, languages, contact summary
|
||||
|
||||
#### Experience (Timeline)
|
||||
- Vertical line with nodes
|
||||
- Each node: date, company, role, bullet points
|
||||
- Alternating left/right or single column left-aligned
|
||||
- Framer Motion: slide in on scroll
|
||||
|
||||
#### Contact
|
||||
- Simple form: name, email, message
|
||||
- Submit: show success message + confetti effect (Framer Motion)
|
||||
- Also display email, phone, LinkedIn as text/links
|
||||
|
||||
#### ProjectCard
|
||||
- Image/thumbnail
|
||||
- Title, description
|
||||
- Tech badges
|
||||
- Links: Demo, GitHub
|
||||
- Hover: lift + shadow
|
||||
|
||||
#### BlogCard
|
||||
- Optional image
|
||||
- Title, date, excerpt
|
||||
- "Read more" link
|
||||
|
||||
#### TetEffects
|
||||
- Canvas-based falling petals (pink/red, slow drift)
|
||||
- Subtle random fireworks (sparkles) on scroll or idle
|
||||
- Decorative lion/dragon silhouettes in header/footer (SVG)
|
||||
|
||||
---
|
||||
|
||||
## 6. Styling & Theme
|
||||
|
||||
**Colors (Tailwind config):**
|
||||
```js
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#D32F2F', // Red
|
||||
50: '#FFEBEE',
|
||||
100: '#FFCDD2',
|
||||
// ...
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: '#FFC107', // Gold/Yellow
|
||||
// ...
|
||||
},
|
||||
// Neutrals...
|
||||
}
|
||||
```
|
||||
|
||||
**Fonts:**
|
||||
- Headings: `Playfair Display` (or `Noto Serif Vietnamese`)
|
||||
- Body: `Inter` (or `Noto Sans Vietnamese`)
|
||||
- Load via Google Fonts in `layout.tsx`
|
||||
|
||||
**Responsive breakpoints:**
|
||||
- Mobile: < 768px (stack, hamburger)
|
||||
- Tablet: 768px - 1024px (2-col grid for projects)
|
||||
- Desktop: > 1024px (3-col grid, full layout)
|
||||
|
||||
---
|
||||
|
||||
## 7. Animations (Framer Motion)
|
||||
|
||||
- **Page transitions:** fade in/out between routes
|
||||
- **Scroll reveal:** sections slide up + fade when entering viewport (use `IntersectionObserver` or `framer-motion`'s `whileInView`)
|
||||
- **Hero:** avatar pulse or float
|
||||
- **Timeline items:** stagger entrance
|
||||
- **Project cards:** hover scale + shadow
|
||||
- **Contact form:** input focus transitions
|
||||
- **TetEffects:** canvas animations; optional subtle CSS keyframes for decorative elements
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Management
|
||||
|
||||
All content stored in `data/` as TypeScript modules. Easy to edit without touching components.
|
||||
|
||||
Example `data/profile.ts`:
|
||||
```ts
|
||||
export const profile = {
|
||||
name: "Nguyễn Ngọc Trí Vĩ",
|
||||
title: "AI Engineer",
|
||||
avatar: "/avatar.jpeg",
|
||||
email: "nntrivi2001@gmail.com",
|
||||
phone: "090 321 5095",
|
||||
linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi",
|
||||
portfolio: "https://nguyenngoctrivi.vercel.app",
|
||||
languages: ["Vietnamese (native)", "English (intermediate)"],
|
||||
bio: `A passionate AI Engineer...`,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Deployment
|
||||
|
||||
- `next build` → `out/` or Vercel automatic
|
||||
- Environment: Node.js 18+
|
||||
- Vercel: import repo, set build command `next build`, output `next start`
|
||||
- Custom domain: optional
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Enhancements (Out of Scope for v1)
|
||||
|
||||
- Blog CMS (Markdown files or headless CMS)
|
||||
- Dark mode
|
||||
- i18n (Vietnamese/English toggle)
|
||||
- More advanced Tet animations (lion dance interactive)
|
||||
- Testimonials section
|
||||
- Resume PDF download
|
||||
|
||||
---
|
||||
|
||||
## 11. Acceptance Criteria
|
||||
|
||||
- [ ] Next.js project initializes and runs (`npm run dev`)
|
||||
- [ ] Tailwind configured and functional
|
||||
- [ ] All 3 routes render correctly
|
||||
- [ ] Content from CV is accurately represented
|
||||
- [ ] Responsive on mobile/tablet/desktop
|
||||
- [ ] Tet theme applied (colors, fonts, decorations)
|
||||
- [ ] Animations smooth (60fps)
|
||||
- [ ] Avatar image displays
|
||||
- [ ] Contact form shows success on submit (no backend needed)
|
||||
- [ ] No console errors
|
||||
- [ ] Ready to deploy to Vercel
|
||||
|
||||
---
|
||||
|
||||
**Spec approved by user on 2026-03-31. Proceed to implementation planning.**
|
||||
Reference in New Issue
Block a user