# Antigravity Implementation Guide — Luyện nghe Gamification Map

> Route: `https://foxhsk.com/luyen-tap?skill=nghe`  
> Phạm vi: chỉ màn nhánh **Luyện nghe**. Không thay đổi màn chọn 4 kỹ năng và các nhánh Nói/Đọc/Viết.

## 1. Kết quả cần đạt

Thay lưới 5 card hiện tại bằng bản đồ luyện nghe dạng gamification:

- PC: 5 trạm chạy theo tuyến chữ S trên nền sông núi.
- Mobile: 5 trạm chạy dọc, xen kẽ trái/phải.
- Không có cờ ở bất kỳ vị trí nào.
- Tên trạm, badge, tiêu đề và trạng thái đều là HTML/Tailwind; tuyệt đối không nhúng chữ vào ảnh.
- Mỗi trạm chỉ có **một pill tên duy nhất** và pill này chính là `Link`; không render nút `Vào học` thứ hai.
- Giữ nguyên URL, logic chọn HSK, chuẩn 2.0/3.0, streak, XP và xu hiện có.
- Giữ nguyên top bar của `PracticeArena` và bottom navigation toàn site; không dựng thêm bản sao.

Mockup đối chiếu:

- PC: `references/luyen-nghe-pc.png`
- Mobile: `references/luyen-nghe-mobile.png`

Mockup là định hướng thị giác. Khi khác với quy chuẩn bên dưới, quy chuẩn code trong file này được ưu tiên.

## 2. Assets bắt buộc sử dụng

### Background

| Breakpoint | File | Kích thước |
| --- | --- | ---: |
| `< 1024px` | `backgrounds/listening-bg-mobile.webp` | 887×1774 |
| `>= 1024px` | `backgrounds/listening-bg-desktop.webp` | 1586×992 |

### Node và nhân vật

| ID dữ liệu | File |
| --- | --- |
| `nghe-bat-chu` | `nodes/listen-catch-words.webp` |
| `thanh-dieu` | `nodes/listen-tones.webp` |
| `hoi-thoai` | `nodes/listen-dialogue.webp` |
| `phong-nghe` | `nodes/listen-exam-room.webp` |
| `kho-audio` | `nodes/listen-audio-library.webp` |
| Foxi | `characters/foxi-listening.webp` |

Các node và Foxi là WebP 1254×1254 có alpha. Dùng ảnh trực tiếp, không crop lại, không thêm chữ/cờ vào bitmap.

Public URL bắt đầu bằng:

```text
/assets/practice-island/listening/
```

## 3. File code được phép sửa

| File | Công việc |
| --- | --- |
| `src/components/practice/PracticeArena.tsx` | Resolve map config và render component generic; giữ nguyên top bar và fallback grid |
| `src/components/practice/PracticeSkillMap.tsx` | File mới, component bản đồ dùng chung cho Nghe/Nói/Đọc/Viết |
| `src/components/practice/practiceMapConfigs.ts` | File mới, type + asset path + tọa độ + feature flags |

Không cần sửa `practiceArenaData.ts`: tên, phụ đề, URL và trạng thái `isSuggested` đã có sẵn. Component mới phải đọc `branch.title` để tên trạm luôn sửa được từ dữ liệu.

## 4. Cấu trúc component

Tạo component generic mới:

```tsx
interface PracticeSkillMapProps {
  skill: PracticeSkillId;
  config: PracticeMapConfig;
  branches: PracticeBranchItem[];
  level: number;
  standard: '2.0' | '3.0';
}
```

Trong component:

1. Đọc ảnh/tọa độ từ `practiceMapConfigs.ts`, không hardcode riêng Nghe trong JSX.
2. Ghép node config với branch bằng `branchId` và resolve `branch.href` bằng `level`/`standard`.
3. Render một canvas PC và một canvas mobile từ cùng mảng dữ liệu; không nhân đôi nội dung thủ công.
4. Chỉ render đúng một H1 lấy từ `config.hero.title`.
5. Nếu flag/config/node thiếu, `PracticeArena` phải dùng fallback grid hiện tại.

Copy cấu trúc type và config sẵn từ `practiceMapConfigs.example.ts`. Quy trình nhân bản Nói/Đọc/Viết nằm trong `PRACTICE-MAP-PIPELINE.md`.

Gợi ý cấu trúc DOM của một trạm:

```tsx
<article className="absolute flex flex-col items-center">
  <Image src={art.src} alt="" width={1254} height={1254} sizes={art.sizes} />

  {branch.isSuggested && (
    <span className="rounded-full bg-[#FFE36A] px-2.5 py-1 text-xs font-black text-slate-900">
      Gợi ý
    </span>
  )}

  <Link
    href={resolvedHref}
    aria-label={`${branch.title}. Mở bài luyện tập`}
    className="inline-flex min-h-[44px] items-center rounded-full border border-[#E5EAF0] bg-white px-5 font-black text-[#DF002A] shadow-sm hover:border-[#FF0036] hover:bg-red-50 whitespace-nowrap"
  >
    {branch.title}
  </Link>
</article>
```

## 5. Bố cục PC

Canvas nằm trong `max-w-[1240px] mx-auto`, tỷ lệ gần `1240 / 760`, tối thiểu cao 720px. Dùng desktop background `cover center`.

| Trạm | Left | Top | Width gợi ý |
| --- | ---: | ---: | ---: |
| Nghe bắt chữ | 2% | 36% | 21% |
| Thanh điệu | 21% | 51% | 21% |
| Hội thoại | 42% | 33% | 21% |
| Phòng nghe | 63% | 51% | 21% |
| Kho audio | 80% | 36% | 19% |

- Hero nằm giữa phía trên: eyebrow `LUYỆN NGHE`, H1 và câu `Mỗi ngày nghe rõ hơn một chút`.
- Foxi đặt bên trái hero, không che trạm đầu.
- Tên trạm là pill trắng nằm ngoài ảnh và chính là link duy nhất của trạm.
- Node hover: `-translate-y-2 scale-[1.02]`; pill hover đổi border đỏ/nền đỏ rất nhạt.
- Không đưa các bảng gỗ/chữ viết tay của mockup vào code; chúng chỉ là trang trí trong concept và không có asset bắt buộc.

## 6. Bố cục mobile

Áp dụng dưới `lg`:

- Canvas rộng 100%, `max-w-[440px]`, cao khoảng 1720–1800px.
- Background mobile dùng `cover top center`.
- Hero cao gọn ở đầu canvas, không vượt quá 150px.
- Foxi khoảng 120–150px, đặt gần hero nhưng không chặn trạm đầu.
- Các trạm rộng khoảng 58–64% canvas, xen kẽ trái/phải.

| Trạm | Căn | Top gợi ý |
| --- | --- | ---: |
| Nghe bắt chữ | trái | 220px |
| Thanh điệu | phải | 500px |
| Hội thoại | trái | 790px |
| Phòng nghe | phải | 1080px |
| Kho audio | trái | 1370px |

Phải chừa khoảng đáy cho bottom navigation đang có của toàn site. Không tạo bottom nav mới từ mockup.

## 7. Typography và màu bắt buộc

- Brand CTA: `#FF0036`; hover `#D9002C`.
- Hero red: `#DF002A`.
- Gold: `#FFE36A`.
- Border: `#E5EAF0`.
- Text chính: `text-slate-900`; text phụ: `text-slate-600`.
- Chữ trên nền đỏ luôn trắng; nhấn trên đỏ chỉ dùng vàng gold.
- Tất cả nút/pill: `rounded-full`, `whitespace-nowrap`, mobile `min-h-[44px]`.
- Không dùng màu cam cũ `#F97316` hoặc `#D84300` cho CTA/active.

## 8. Responsive và accessibility

- Test bắt buộc: 320×740, 360×740, 390×844, 412×915, 768×1024, 1240×800.
- Không có horizontal scroll: `document.documentElement.scrollWidth <= window.innerWidth`.
- Mỗi link phải có focus ring rõ: `focus-visible:ring-4 focus-visible:ring-[#FF0036]/30`.
- Vì node nằm trong link có tên bên dưới, ảnh node dùng `alt=""`; link dùng tên trạm làm accessible name.
- Không dùng text nhỏ hơn 12px cho nội dung có nghĩa.
- Reuse animation `islandBob` hiện có trong `src/app/globals.css`; animation lệch thời gian giữa các node.
- Khi `prefers-reduced-motion: reduce`, animation phải dừng hoặc không ảnh hưởng khả năng sử dụng.

## 9. Hiệu năng

- Dùng `next/image` cho 5 node và Foxi; khai báo đúng `width={1254}` và `height={1254}`.
- `sizes` PC gần `250px`, mobile gần `260px`; không dùng `sizes="100vw"` cho node.
- Chỉ asset nằm đầu viewport mới được `priority`; các node phía dưới mobile để lazy-load mặc định.
- Không import ảnh thành base64 và không import JSON lớn vào Client Component.
- Không cài thêm dependency.

## 10. Logic URL phải giữ nguyên

| Trạm | URL |
| --- | --- |
| Nghe bắt chữ | `/quiz?mode=listening&level={level}&standard={2.0|3.0}&count=10` |
| Thanh điệu | `/pinyin` |
| Hội thoại | `/lessons` |
| Phòng nghe | `/mock-test` |
| Kho audio | `/past-papers` |

Thay đổi HSK hoặc 2.0/3.0 trên top bar phải cập nhật URL của `Nghe bắt chữ` mà không làm mất lựa chọn hiện tại.

## 11. Những phần tuyệt đối không làm

- Không sửa hoặc tái xuất assets trong folder này.
- Không thêm cờ.
- Không đặt tên trạm vào ảnh và không render nút `Vào học` riêng.
- Không làm lại top bar, level picker hoặc bottom navigation.
- Không thay route của 5 trạm.
- Không sửa giao diện Nói/Đọc/Viết.
- Không bump version, deploy, restart bot hoặc dừng tiến trình dev đang chạy.
- Không sửa `package.json`, `src/config/version.ts`, `.env*`, `node_modules/`, `.next/`.

## 12. Acceptance checklist

- [ ] `/luyen-tap?skill=nghe` hiển thị bản đồ gamification thay cho lưới card.
- [ ] PC và mobile bám mockup, dùng đúng background theo breakpoint.
- [ ] Đủ 5 node, đúng thứ tự, đúng asset và đúng URL.
- [ ] Không có cờ; không có chữ raster trong node/background.
- [ ] Tên trạm lấy từ `branch.title` và hiển thị bằng pill HTML.
- [ ] Mỗi trạm chỉ có một link pill tên; không có nút `Vào học` thứ hai.
- [ ] Top bar/HSK/2.0–3.0/streak/XP/xu hoạt động như trước.
- [ ] Màn Nói/Đọc/Viết không thay đổi.
- [ ] Chỉ có một H1.
- [ ] Không tràn ngang ở mọi viewport bắt buộc.
- [ ] Không phát sinh lỗi TypeScript/ESLint/budget.

## 13. Verify và walkthrough

Chụp screenshot tại:

```text
https://foxhsk.com/luyen-tap?skill=nghe
```

Chạy và dán output thật vào `.agents/handoff/2026-09-23-luyen-nghe-gamification-map.walkthrough.md`:

```bash
npm run typecheck
npm run lint
npm run audit:budget
npm run build
```

Nếu `npm run dev` đang giữ `.next`, không chạy build song song và không tự dừng tiến trình của người khác. Ghi rõ build bị hoãn trong walkthrough để Auditor chạy sau.

Antigravity chỉ được sửa file trong mục 3. Nếu cần file ngoài allowlist, phải dừng và ghi lý do, không tự mở rộng phạm vi.
