Files
phy-vr360/docs/superpowers/plans/2026-06-23-vr360-nhu-quynh.md
T
2026-06-23 10:50:08 +07:00

48 KiB
Raw Blame History

Cổng số hóa di sản VR360 Xã Như Quỳnh — Kế hoạch triển khai

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Dựng cổng thông tin số hóa di sản cho Xã Như Quỳnh (Hưng Yên) trong phy-vr360, port engine + design system từ pdc-vr360, hoàn thiện tour VR360 Chùa Thái Lạc hoạt động đầy đủ.

Architecture: phy-vr360 (Next.js 16, App Router, src/app/) tái sử dụng nguyên design system (globals.css), các component landing và engine VirtualTour.tsx của pdc-vr360. Một file content.ts là nguồn dữ liệu trung tâm (15 di tích, điều hướng, tour). Engine được tổng quát hóa để chạy bằng config tour động; thêm config Chùa Thái Lạc từ 26 ảnh panorama sẵn có.

Tech Stack: Next.js 16, React 19, TypeScript, Tailwind v4, Three.js, framer-motion, lucide-react, @paper-design/shaders-react.

Quy ước kiểm thử (theo pattern codebase — không có unit test): mỗi task verify bằng npm run lintnpm run build (build chạy type-check). Các task UI/tour thêm bước kiểm tra trực quan bằng npm run dev rồi mở URL. "PASS" = build + lint không lỗi và quan sát đúng mô tả.

Nguồn tham chiếu: các file gốc nằm tại ../pdc-vr360/app/... (cùng workspace d:\Code\vr360). Khi task nói "copy từ pdc", lấy nguyên nội dung file gốc rồi áp các chỉnh sửa được liệt kê.

⚠️ Next.js 16: pdc-vr360/AGENTS.md cảnh báo bản Next.js này có breaking changes. Trước khi sửa code routing/layout khác kiến thức cũ, đọc guide trong node_modules/next/dist/docs/.

Đường dẫn alias: cả hai dự án dùng @/app/.... Ở phy-vr360, @/*./src/*, nên @/app/content = src/app/content.ts. Giữ nguyên kiểu import @/app/... khi copy.


PHASE 1 — Nền tảng (dependencies, design system, layout, assets)

Task 1: Thêm dependencies còn thiếu

Files:

  • Modify: phy-vr360/package.json

  • Step 1: Cài các package khớp pdc-vr360

Chạy tại thư mục phy-vr360:

cd d:/Code/vr360/phy-vr360
npm install three@^0.184.0 framer-motion@^12.40.0 lucide-react@^1.16.0 @paper-design/shaders-react@^0.0.76 lottie-react@^2.4.1
npm install -D @types/three@^0.184.1
  • Step 2: Verify cài đặt

Run: npm ls three framer-motion lucide-react @paper-design/shaders-react Expected: liệt kê đủ 4 package, không có UNMET DEPENDENCY.

  • Step 3: Commit
git add package.json package-lock.json
git commit -m "chore: thêm dependencies cho VR360 Như Quỳnh (three, framer-motion, lucide, shaders)"

Task 2: Port design system globals.css

Files:

  • Replace: phy-vr360/src/app/globals.css (copy từ pdc-vr360/app/globals.css, toàn bộ 1380 dòng)

  • Step 1: Copy nguyên file

cp d:/Code/vr360/pdc-vr360/app/globals.css d:/Code/vr360/phy-vr360/src/app/globals.css

Không sửa nội dung — đây là hệ token + class public-* cần giữ y hệt để đảm bảo palette xanhvàng di sản và chất lượng đã kiểm chứng.

  • Step 2: Verify build CSS

Run: npm run build Expected: build PASS (có thể còn lỗi do page.tsx mặc định vẫn dùng class cũ — chấp nhận, sẽ thay ở Task 5). Nếu lỗi chỉ liên quan globals.css (cú pháp Tailwind) thì phải sửa; lỗi khác bỏ qua ở bước này.

  • Step 3: Commit
git add src/app/globals.css
git commit -m "feat: port design system globals.css (palette di sản, class public-*)"

Task 3: Cập nhật layout gốc (font, lang, metadata)

Files:

  • Modify: phy-vr360/src/app/layout.tsx

  • Step 1: Thay toàn bộ nội dung layout

Ghi đè phy-vr360/src/app/layout.tsx bằng:

import type { Metadata, Viewport } from "next";
import { Noto_Sans } from "next/font/google";
import "./globals.css";

const notoSans = Noto_Sans({
  subsets: ["latin", "vietnamese"],
  variable: "--font-noto-sans",
  weight: ["400", "500", "600", "700", "800"],
});

export const metadata: Metadata = {
  title: "VR360 Di sản Xã Như Quỳnh",
  description:
    "Cổng số hóa di tích, không gian văn hóa và giá trị truyền thống của Xã Như Quỳnh, tỉnh Hưng Yên bằng công nghệ tham quan thực tế ảo VR360.",
};

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="vi" className={`${notoSans.variable} h-full antialiased`}>
      <body className="min-h-full flex flex-col">{children}</body>
    </html>
  );
}
  • Step 2: Verify

Run: npm run lint Expected: không lỗi ở layout.tsx.

  • Step 3: Commit
git add src/app/layout.tsx
git commit -m "feat: layout gốc dùng Noto Sans tiếng Việt, metadata Như Quỳnh"

Task 4: Copy assets (icon, modal)

Files:

  • Create: phy-vr360/public/modal/ (copy từ pdc-vr360/public/modal/)

  • Verify: phy-vr360/public/icon/ (đã có flycam.png, hotspotelement.png, _ggMapPin.png)

  • Step 1: Copy thư mục modal (nền welcome/info của tour)

cp -r "d:/Code/vr360/pdc-vr360/public/modal" "d:/Code/vr360/phy-vr360/public/modal"
  • Step 2: Xác nhận icon đã đủ

Run: ls d:/Code/vr360/phy-vr360/public/icon Expected: _ggMapPin.png flycam.png hotspotelement.png

  • Step 3: Commit
git add public/modal
git commit -m "feat: copy assets modal cho viewer VR360"

PHASE 2 — Dữ liệu & các trang landing

Task 5: Tạo content.ts — nguồn dữ liệu trung tâm

Files:

  • Create: phy-vr360/src/app/content.ts

Định nghĩa các export mà component port sẽ dùng. HeritageSite giữ các field component cũ cần (id, image, type, status, shortDescription, isAvailable, vrUrl, detailUrl) và bổ sung field mới (level, images, longDescription, location).

  • Step 1: Viết file content.ts

Tạo phy-vr360/src/app/content.ts:

const imgBase = "/images/Đình Thái Lạc"; // thư mục chứa 26 ảnh Chùa Thái Lạc
export const img = (file: string) => encodeURI(`${imgBase}/${file}`);

export const commune = {
  name: "Xã Như Quỳnh",
  province: "tỉnh Hưng Yên",
  fullName: "Xã Như Quỳnh, tỉnh Hưng Yên",
  intro:
    "Vùng đất cổ giàu truyền thống văn hiến và khoa bảng, nơi lưu giữ hệ thống di tích đình, chùa, đền, văn miếu trải dài nhiều thế kỷ.",
};

export const activeTour = {
  title: "Chùa Thái Lạc (Pháp Vân tự)",
  subtitle: "Di tích quốc gia — chạm khắc gỗ thời Trần",
  href: "/tour/chua-thai-lac",
  heroImage: img("Chính Diện Cổng Chùa.jpg"),
  coverImage: img("Chính Sảnh.jpg"),
  gateImage: img("Chính Diện Cổng Chùa.jpg"),
  shrineImage: img("Chính Điện 3.jpg"),
  sceneCount: "26",
  duration: "15-20 phút",
};

export const navigation = [
  { href: "/", label: "Trang chủ" },
  { href: "/gioi-thieu", label: "Giới thiệu" },
  { href: "/di-tich", label: "Di tích" },
  { href: "/lien-he", label: "Liên hệ" },
];

export type HeritageLevel = "Cấp quốc gia" | "Cấp tỉnh";

export type HeritageSite = {
  id: string; // = slug, dùng cho /di-tich/[slug] và anchor
  name: string;
  type: string; // "Chùa" | "Đình" | "Đền" | "Văn miếu" | "Di tích khác"
  level: HeritageLevel;
  status: "Đang mở tour" | "Đang bổ sung tư liệu";
  shortDescription: string;
  longDescription?: string[];
  image: string; // ảnh đại diện
  images?: string[];
  vrUrl?: string;
  detailUrl: string;
  isAvailable: boolean;
  location?: string;
};

export const heritageSites: HeritageSite[] = [
  {
    id: "chua-thai-lac",
    name: "Chùa Thái Lạc (Pháp Vân tự)",
    type: "Chùa",
    level: "Cấp quốc gia",
    status: "Đang mở tour",
    shortDescription:
      "Ngôi chùa cổ thờ Pháp Vân trong hệ Tứ Pháp, nổi tiếng với những mảng chạm khắc gỗ thời Trần (thế kỷ 14) thuộc hàng quý hiếm bậc nhất Việt Nam.",
    longDescription: [
      "Chùa Thái Lạc, tên chữ là Pháp Vân tự, là một trong những ngôi chùa cổ tiêu biểu của vùng Như Quỳnh, thờ Pháp Vân — vị thần Mây trong tín ngưỡng Tứ Pháp của cư dân nông nghiệp đồng bằng Bắc Bộ.",
      "Giá trị đặc biệt của di tích nằm ở hệ thống ván chạm khắc gỗ có niên đại thời Trần (thế kỷ 14), khắc họa các nhạc công, vũ nữ, hình tượng Phật giáo với kỹ thuật điêu luyện — nguồn tư liệu quý cho nghiên cứu mỹ thuật cổ Việt Nam.",
      "Không gian chùa gồm cổng, sân, tiền đường, thượng điện cùng các ban thờ và cảnh quan vườn, ao, được số hóa đầy đủ trong tour VR360.",
    ],
    image: img("Chính Diện Cổng Chùa.jpg"),
    vrUrl: "/tour/chua-thai-lac",
    detailUrl: "/di-tich/chua-thai-lac",
    isAvailable: true,
    location: "Xã Như Quỳnh, tỉnh Hưng Yên",
  },
  // --- 8 di tích quốc gia còn lại (mẫu, mô tả ngắn) ---
  nationalStub("den-ghenh", "Đền Ghênh (Đền Ỷ Lan)", "Đền",
    "Đền thờ Nguyên phi Ỷ Lan, gắn với truyền thống tôn vinh người phụ nữ có công với dân với nước."),
  nationalStub("tu-vu-ho-truong", "Từ Vũ họ Trương", "Di tích khác",
    "Từ đường dòng họ Trương, di tích phản ánh truyền thống dòng tộc và tín ngưỡng thờ tổ tiên của địa phương."),
  nationalStub("bai-da-den-tu-vu", "Bãi đá đền Từ Vũ", "Di tích khác",
    "Cụm di tích khảo cổ — kiến trúc đá gắn với đền Từ Vũ, mang giá trị lịch sử và văn hóa lâu đời."),
  nationalStub("dinh-at", "Đình Ất", "Đình",
    "Đình làng cổ, không gian sinh hoạt tín ngưỡng và cộng đồng tiêu biểu của làng quê Như Quỳnh."),
  nationalStub("chua-hanh-lac", "Chùa Hành Lạc", "Chùa",
    "Ngôi chùa làng cổ kính, trung tâm sinh hoạt Phật giáo và văn hóa tâm linh của cộng đồng."),
  nationalStub("dinh-hanh-lac", "Đình Hành Lạc", "Đình",
    "Đình làng gắn với tín ngưỡng thờ Thành hoàng và lễ hội truyền thống của địa phương."),
  nationalStub("chua-ong", "Chùa Ông", "Chùa",
    "Di tích Phật giáo cổ, lưu giữ nhiều giá trị kiến trúc và hiện vật tín ngưỡng."),
  nationalStub("chua-vinh-thai", "Chùa Vĩnh Thái", "Chùa",
    "Ngôi chùa cổ mang đậm dấu ấn kiến trúc và tín ngưỡng truyền thống vùng Như Quỳnh."),
  // --- 6 di tích cấp tỉnh (mẫu) ---
  provincialStub("dinh-le-xa", "Đình Lê Xá", "Đình",
    "Đình làng Lê Xá, không gian thờ tự và sinh hoạt cộng đồng giàu giá trị văn hóa."),
  provincialStub("dinh-ngo-xuyen", "Đình Ngô Xuyên", "Đình",
    "Đình làng Ngô Xuyên gắn với truyền thống tín ngưỡng và lễ hội địa phương."),
  provincialStub("chua-tu-kinh", "Chùa Từ Kính", "Chùa",
    "Ngôi chùa cổ, điểm sinh hoạt Phật giáo và văn hóa tâm linh của cộng đồng."),
  provincialStub("cung-chi-nguyen", "Cung Chí Nguyên", "Di tích khác",
    "Di tích gắn với nhà văn Thạch Lam — nơi khơi nguồn cảm hứng cho truyện ngắn \"Dưới bóng hoàng lan\"."),
  provincialStub("van-mieu-nhu-quynh", "Văn miếu Như Quỳnh", "Văn miếu",
    "Văn miếu tôn vinh truyền thống hiếu học và khoa bảng của vùng đất Như Quỳnh."),
  provincialStub("dau-tich-chua-la", "Dấu tích Chùa Lá", "Chùa",
    "Dấu tích ngôi chùa lớn, nổi tiếng trước Cách mạng tháng Tám, mang giá trị lịch sử sâu sắc."),
];

function nationalStub(id: string, name: string, type: string, desc: string): HeritageSite {
  return stub(id, name, type, "Cấp quốc gia", desc);
}
function provincialStub(id: string, name: string, type: string, desc: string): HeritageSite {
  return stub(id, name, type, "Cấp tỉnh", desc);
}
function stub(id: string, name: string, type: string, level: HeritageLevel, desc: string): HeritageSite {
  return {
    id,
    name,
    type,
    level,
    status: "Đang bổ sung tư liệu",
    shortDescription: desc,
    image: img("Chính Sảnh.jpg"), // ảnh tạm dùng chung cho các di tích chưa có tư liệu
    detailUrl: `/di-tich/${id}`,
    isAvailable: false,
    location: "Xã Như Quỳnh, tỉnh Hưng Yên",
  };
}

Lưu ý: function khai báo dạng hoisted nên dùng được trước khi định nghĩa trong cùng module — mảng heritageSites gọi nationalStub/provincialStub hợp lệ.

  • Step 2: Verify type-check

Run: npx tsc --noEmit Expected: không lỗi trong content.ts.

  • Step 3: Commit
git add src/app/content.ts
git commit -m "feat: content.ts — dữ liệu 15 di tích Như Quỳnh + cấu hình tour"

Task 6: Port component nền hero (shader + panorama)

Files:

  • Create: phy-vr360/src/app/components/ui/hero-shader-background.tsx

  • Create: phy-vr360/src/app/components/ui/hero-panorama-background.tsx

  • Step 1: Copy 2 file nền hero nguyên trạng

mkdir -p d:/Code/vr360/phy-vr360/src/app/components/ui
cp "d:/Code/vr360/pdc-vr360/app/components/ui/hero-shader-background.tsx" "d:/Code/vr360/phy-vr360/src/app/components/ui/hero-shader-background.tsx"
cp "d:/Code/vr360/pdc-vr360/app/components/ui/hero-panorama-background.tsx" "d:/Code/vr360/phy-vr360/src/app/components/ui/hero-panorama-background.tsx"

Hai file dùng prop imageSrc động và import @/app/components/ui/... — tương thích phy-vr360, không cần sửa.

  • Step 2: Verify type-check

Run: npx tsc --noEmit Expected: không lỗi ở 2 file này (dependency @paper-design/shaders-react đã cài ở Task 1).

  • Step 3: Commit
git add src/app/components/ui
git commit -m "feat: port component nền hero (shader + panorama)"

Task 7: Port SiteHeader (thay branding Như Quỳnh)

Files:

  • Create: phy-vr360/src/app/components/landing/SiteHeader.tsx (copy từ pdc + sửa)

  • Step 1: Copy file rồi áp các sửa đổi

mkdir -p d:/Code/vr360/phy-vr360/src/app/components/landing
cp "d:/Code/vr360/pdc-vr360/app/components/landing/SiteHeader.tsx" "d:/Code/vr360/phy-vr360/src/app/components/landing/SiteHeader.tsx"

Sửa trong file vừa copy:

  1. Bỏ logo ảnh thiếu, thay bằng icon. Xóa dòng:
const dinhCongLogo = encodeURI("/images/định công.png");

Đổi import lucide thêm Landmark:

import { Landmark, Menu, PlayCircle, X } from "lucide-react";

Bỏ import Image from "next/image"; (không còn dùng).

  1. Thay khối logo <span class="public-logo-mark">...<Image .../></span> bằng:
<span className="public-logo-mark grid h-11 w-11 shrink-0 place-items-center overflow-hidden rounded-full border border-[rgb(142_95_11_/_0.24)] bg-[linear-gradient(135deg,rgb(255_255_255_/_0.94),rgb(232_219_176_/_0.74))] shadow-[0_10px_24px_rgb(37_75_54_/_0.1),inset_0_1px_0_rgb(255_255_255_/_0.92)]">
  <Landmark className="h-5 w-5 text-[var(--primary)]" strokeWidth={1.8} />
</span>
  1. Thay text thương hiệu:
  • aria-label="VR360 Định Công"aria-label="VR360 Di sản Như Quỳnh"

  • VR360 Định CôngVR360 Như Quỳnh

  • Không gian văn hóa sốDi sản văn hóa Xã Như Quỳnh

  • Step 2: Verify

Run: npx tsc --noEmit Expected: không lỗi; không còn import Image thừa (lint sẽ báo nếu sót).

  • Step 3: Commit
git add src/app/components/landing/SiteHeader.tsx
git commit -m "feat: port SiteHeader với branding Như Quỳnh"

Files:

  • Create: phy-vr360/src/app/components/landing/SiteFooter.tsx

  • Create: phy-vr360/src/app/components/landing/BackToTopButton.tsx

  • Step 1: Copy BackToTopButton nguyên trạng

cp "d:/Code/vr360/pdc-vr360/app/components/landing/BackToTopButton.tsx" "d:/Code/vr360/phy-vr360/src/app/components/landing/BackToTopButton.tsx"

(Không chứa nội dung Định Công — giữ nguyên.)

  • Step 2: Copy SiteFooter rồi sửa nội dung
cp "d:/Code/vr360/pdc-vr360/app/components/landing/SiteFooter.tsx" "d:/Code/vr360/phy-vr360/src/app/components/landing/SiteFooter.tsx"

Sửa trong file:

  • VR360 Định CôngVR360 Như Quỳnh

  • Câu mô tả → Cổng số hóa di tích và không gian văn hóa Xã Như Quỳnh, tỉnh Hưng Yên bằng công nghệ VR360.

  • Địa chỉ Số 1 ngõ 282 Kim Giang...Xã Như Quỳnh, tỉnh Hưng Yên

  • Link http://dinhcong.hanoi.gov.vn/ + nhãn Cổng thông tin phường Định Công# tạm + nhãn Cổng thông tin Xã Như Quỳnh (kèm chú thích Đang cập nhật nếu cần)

  • Bỏ block Facebook (https://www.facebook.com/profile.php?id=...) HOẶC đổi thành # với nhãn Trang thông tin địa phương — chọn bỏ để tránh link sai.

  • © 2026 VR360 Định Công.© 2026 VR360 Di sản Xã Như Quỳnh.

  • Step 3: Verify

Run: npx tsc --noEmit && npm run lint Expected: không lỗi; không còn import thừa (nếu bỏ block Facebook làm ExternalLink không dùng → xóa khỏi import).

  • Step 4: Commit
git add src/app/components/landing/SiteFooter.tsx src/app/components/landing/BackToTopButton.tsx
git commit -m "feat: port SiteFooter (liên hệ Như Quỳnh) + BackToTopButton"

Task 9: Port + sửa CulturalSpaceExplorer (lọc theo cấp di tích)

Files:

  • Create: phy-vr360/src/app/components/landing/CulturalSpaceExplorer.tsx

Bộ lọc gốc lọc theo "Loại địa điểm" với các giá trị Định Công. Như Quỳnh cần lọc theo Cấp (quốc gia/tỉnh) + Loại phù hợp.

  • Step 1: Copy file
cp "d:/Code/vr360/pdc-vr360/app/components/landing/CulturalSpaceExplorer.tsx" "d:/Code/vr360/phy-vr360/src/app/components/landing/CulturalSpaceExplorer.tsx"
  • Step 2: Sửa bộ lọc loại + thêm bộ lọc cấp

Thay khối khai báo filter (dòng ~9-13) bằng:

const statusFilters = ["Tất cả", "Đang mở tour", "Đang bổ sung tư liệu"] as const;
const levelFilters = ["Tất cả", "Cấp quốc gia", "Cấp tỉnh"] as const;
const typeFilters = ["Tất cả", "Đình", "Chùa", "Đền", "Văn miếu", "Di tích khác"] as const;

type StatusFilter = (typeof statusFilters)[number];
type LevelFilter = (typeof levelFilters)[number];
type TypeFilter = (typeof typeFilters)[number];

Trong component, thêm state level cạnh status/type:

const [level, setLevel] = useState<LevelFilter>("Tất cả");

Thay logic matchesType trong filteredSites bằng (lọc theo cấp + loại chính xác hơn vì site.levelsite.type đã chuẩn hóa trong content.ts):

const matchesLevel = level === "Tất cả" || site.level === level;
const matchesType = type === "Tất cả" || site.type === type;
return matchesQuery && matchesStatus && matchesLevel && matchesType;

Trong JSX khối bộ lọc, thêm một <FilterGroup> cho cấp (đặt trước "Loại địa điểm"):

<FilterGroup label="Cấp xếp hạng" options={levelFilters} value={level} onChange={setLevel} />

Cập nhật nút "Xóa bộ lọc" để reset thêm setLevel("Tất cả").

Đổi grid bộ lọc lg:grid-cols-[1fr_auto_auto]lg:grid-cols-[1fr_auto_auto_auto] để chứa 3 nhóm filter.

  • Step 3: Verify

Run: npx tsc --noEmit Expected: không lỗi.

  • Step 4: Commit
git add src/app/components/landing/CulturalSpaceExplorer.tsx
git commit -m "feat: CulturalSpaceExplorer lọc theo cấp xếp hạng + loại di tích"

Task 10: Trang chủ /

Files:

  • Replace: phy-vr360/src/app/page.tsx (dựa trên pdc-vr360/app/page.tsx)

  • Step 1: Copy page.tsx của pdc rồi sửa nội dung

cp "d:/Code/vr360/pdc-vr360/app/page.tsx" "d:/Code/vr360/phy-vr360/src/app/page.tsx"

Áp các sửa đổi:

  1. metadata: title Trang chủ | VR360 Như Quỳnh; description về Xã Như Quỳnh.
  2. kicker/heading/giới thiệu đổi "Định Công" → nội dung Như Quỳnh:
    • kicker: VR360 số hóa di sản Xã Như Quỳnh
    • h1: Khám phá di sản văn hóa Xã Như Quỳnh qua trải nghiệm VR360
    • đoạn mô tả: Hành trình số hóa hệ thống di tích, không gian văn hóa và giá trị truyền thống của Xã Như Quỳnh, tỉnh Hưng Yên bằng công nghệ tham quan thực tế ảo 360 độ.
  3. Ảnh hero (src="/images-tour/Đình Làng-Đền Thờ/12...jpg") → dùng dữ liệu: thay bằng activeTour.coverImage.
  4. Khối "Giới thiệu nhanh" / "Dự án số hóa di tích phường Định Công" → "...di sản Xã Như Quỳnh"; nội dung 2 đoạn đổi sang Như Quỳnh (vùng đất khoa bảng, hệ thống đìnhchùa–đềnvăn miếu).
  5. Section "Di tích nổi bật" — heading: Không gian đang mở tour VR360. (giữ lưới availableSites).
  6. Mảng benefits giữ nguyên (nội dung tổng quát, không nhắc Định Công). Kiểm tra: không còn chữ "Định Công" nào sót — search và thay hết.
  • Step 2: Verify build + dev

Run: npm run build Expected: PASS, không còn lỗi import (đã có đủ component + content).

Run: npm run dev rồi mở http://localhost:3000/ Expected: trang chủ hiển thị hero Như Quỳnh, lưới di tích nổi bật (Chùa Thái Lạc "Đang mở tour"), các di tích "Đang bổ sung tư liệu", không lỗi console nghiêm trọng.

  • Step 3: Commit
git add src/app/page.tsx
git commit -m "feat: trang chủ Như Quỳnh"

Task 11: Trang Danh mục di tích /di-tich

Files:

  • Create: phy-vr360/src/app/di-tich/page.tsx (dựa trên pdc)

  • Step 1: Copy rồi sửa

mkdir -p d:/Code/vr360/phy-vr360/src/app/di-tich
cp "d:/Code/vr360/pdc-vr360/app/di-tich/page.tsx" "d:/Code/vr360/phy-vr360/src/app/di-tich/page.tsx"

Sửa:

  1. metadata: Di tích | VR360 Như Quỳnh; description Như Quỳnh.
  2. kicker Danh sách di tích và tour VR360 (giữ); h1 Hệ thống di tích Xã Như Quỳnh.
  3. Đoạn mô tả → Khám phá 15 di tích lịch sử — văn hóa của Xã Như Quỳnh: 9 di tích xếp hạng cấp quốc gia và 6 di tích cấp tỉnh.
  4. Ảnh hero phụ (/images-tour/Chùa Liên Hoa/1 Trước Cổng.jpg) → activeTour.coverImage.
  5. Khối CTA cuối "phường Định Công" → "Xã Như Quỳnh".
  6. CulturalSpaceExplorer sites={heritageSites} giữ nguyên.
  • Step 2: Verify build + dev

Run: npm run build Expected: PASS.

Run: mở http://localhost:3000/di-tich Expected: 15 thẻ di tích; bộ lọc "Cấp xếp hạng" (Tất cả/Quốc gia/Tỉnh), "Trạng thái", "Loại địa điểm" hoạt động; tìm kiếm hoạt động; bộ đếm "Hiển thị X / 15".

  • Step 3: Commit
git add src/app/di-tich/page.tsx
git commit -m "feat: trang danh mục 15 di tích Như Quỳnh có bộ lọc theo cấp"

Task 12: Trang chi tiết di tích /di-tich/[slug]

Files:

  • Create: phy-vr360/src/app/di-tich/[slug]/page.tsx

Trang này pdc chưa có (pdc dùng anchor) — tạo mới. Hiển thị 1 di tích từ heritageSites, dùng SiteHeader/SiteFooter, hỗ trợ generateStaticParams.

  • Step 1: Tạo file

phy-vr360/src/app/di-tich/[slug]/page.tsx:

import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, MapPin, PlayCircle, Tag } from "lucide-react";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
import { heritageSites } from "@/app/content";

export function generateStaticParams() {
  return heritageSites.map((site) => ({ slug: site.id }));
}

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const site = heritageSites.find((s) => s.id === slug);
  if (!site) return { title: "Không tìm thấy di tích | VR360 Như Quỳnh" };
  return {
    title: `${site.name} | VR360 Như Quỳnh`,
    description: site.shortDescription,
  };
}

export default async function HeritageDetailPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const site = heritageSites.find((s) => s.id === slug);
  if (!site) notFound();

  const paragraphs = site.longDescription ?? [site.shortDescription];

  return (
    <main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
      <SiteHeader />

      <section className="public-subpage-hero relative isolate overflow-hidden border-b border-[var(--surface-border)]">
        <Image src={site.image} alt={site.name} fill priority sizes="100vw" className="public-subpage-hero-image object-cover" />
        <div className="public-subpage-hero-content mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
          <Link href="/di-tich" className="inline-flex items-center gap-2 text-sm font-bold text-[var(--tour-ink)] transition hover:text-[var(--primary)]">
            <ArrowLeft className="h-4 w-4" strokeWidth={1.8} />
            Tất cả di tích
          </Link>
          <div className="public-rise mt-5 max-w-4xl">
            <div className="flex flex-wrap gap-2">
              <span className="public-chip"><Tag className="h-4 w-4" strokeWidth={1.8} />{site.level}</span>
              <span className="public-chip">{site.type}</span>
              <span className="public-chip">{site.status}</span>
            </div>
            <h1 className="public-gradient-text public-heading-safe mt-4 text-[clamp(2.4rem,5.4vw,4.6rem)] font-extrabold">
              {site.name}
            </h1>
            {site.location ? (
              <p className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[var(--foreground)]/82">
                <MapPin className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
                {site.location}
              </p>
            ) : null}
          </div>
        </div>
      </section>

      <section className="mx-auto max-w-3xl px-4 py-14 sm:px-6 lg:py-20">
        <div className="grid gap-4 text-base leading-8 text-[var(--foreground)]/82">
          {paragraphs.map((text, i) => (
            <p key={i}>{text}</p>
          ))}
        </div>

        {site.isAvailable && site.vrUrl ? (
          <Link href={site.vrUrl} className="public-cta mt-8 w-fit">
            <PlayCircle className="h-4 w-4" strokeWidth={1.8} />
            Tham quan VR360
          </Link>
        ) : (
          <p className="public-panel mt-8 rounded-[8px] p-5 text-sm leading-6 text-[var(--muted-foreground)]">
             liệu VR360 cho di tích này đang được bổ sung  sẽ sớm ra mắt.
          </p>
        )}
      </section>

      <SiteFooter />
      <BackToTopButton />
    </main>
  );
}

Lưu ý Next.js 16: params là Promise (async). Nếu guide trong node_modules/next/dist/docs/ cho biết khác, điều chỉnh theo guide.

  • Step 2: Verify build + dev

Run: npm run build Expected: PASS; build sinh được route động (15 trang qua generateStaticParams).

Run: mở http://localhost:3000/di-tich/chua-thai-lachttp://localhost:3000/di-tich/dinh-at Expected: trang Thái Lạc có nút "Tham quan VR360"; trang Đình Ất hiển thị thông báo "đang bổ sung". Slug sai → trang 404.

  • Step 3: Commit
git add "src/app/di-tich/[slug]/page.tsx"
git commit -m "feat: trang chi tiết di tích /di-tich/[slug]"

Task 13: Trang Giới thiệu /gioi-thieu

Files:

  • Create: phy-vr360/src/app/gioi-thieu/page.tsx

Tự viết (không copy pdc — nội dung khác hẳn), dùng layout chuẩn + class public-*.

  • Step 1: Tạo file

phy-vr360/src/app/gioi-thieu/page.tsx:

import type { Metadata } from "next";
import Image from "next/image";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
import { activeTour, heritageSites } from "@/app/content";

export const metadata: Metadata = {
  title: "Giới thiệu | VR360 Như Quỳnh",
  description:
    "Giới thiệu về Xã Như Quỳnh, tỉnh Hưng Yên — vùng đất văn hiến và hệ thống di tích lịch sử — văn hóa được số hóa bằng công nghệ VR360.",
};

const nationalCount = heritageSites.filter((s) => s.level === "Cấp quốc gia").length;
const provincialCount = heritageSites.filter((s) => s.level === "Cấp tỉnh").length;

const stats = [
  { value: String(nationalCount), label: "di tích xếp hạng cấp quốc gia" },
  { value: String(provincialCount), label: "di tích cấp tỉnh" },
  { value: activeTour.sceneCount, label: "điểm 360° trong tour đầu tiên" },
];

const paragraphs = [
  "Xã Như Quỳnh, tỉnh Hưng Yên là vùng đất cổ giàu truyền thống văn hiến và khoa bảng, nơi lưu giữ một hệ thống di tích đình, chùa, đền, văn miếu phong phú trải dài nhiều thế kỷ.",
  "Dự án số hóa di sản được xây dựng nhằm bảo tồn tư liệu, quảng bá giá trị văn hóa địa phương và tạo điều kiện để người dân, học sinh, du khách tham quan trực tuyến các di tích tiêu biểu bằng công nghệ thực tế ảo VR360.",
  "Mỗi không gian được giới thiệu bằng hình ảnh thật, điểm tương tác, chú thích tiếng Việt và cấu trúc nội dung dễ theo dõi, hướng tới mục tiêu chuyển đổi số di sản giai đoạn 20262030.",
];

export default function AboutPage() {
  return (
    <main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
      <SiteHeader />

      <section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
        <Image src={activeTour.gateImage} alt="" fill priority sizes="100vw" className="public-subpage-hero-image object-cover" />
        <div className="public-subpage-hero-content mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
          <div className="public-rise max-w-4xl">
            <p className="public-kicker">Giới thiệu</p>
            <h1 className="public-gradient-text public-heading-safe mt-4 text-[clamp(2.55rem,5.8vw,5rem)] font-extrabold">
              Di sản văn hóa  Như Quỳnh
            </h1>
          </div>
        </div>
      </section>

      <section className="mx-auto max-w-3xl px-4 py-14 sm:px-6 lg:py-20">
        <div className="grid gap-4 text-base leading-8 text-[var(--foreground)]/82">
          {paragraphs.map((text, i) => (
            <p key={i}>{text}</p>
          ))}
        </div>
      </section>

      <section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
        <div className="mx-auto grid max-w-7xl gap-4 px-4 py-14 sm:px-6 sm:grid-cols-3 lg:py-20">
          {stats.map((s) => (
            <div key={s.label} className="public-panel rounded-[8px] p-6 text-center">
              <p className="public-gradient-text text-4xl font-extrabold">{s.value}</p>
              <p className="mt-2 text-sm leading-6 text-[var(--muted-foreground)]">{s.label}</p>
            </div>
          ))}
        </div>
      </section>

      <SiteFooter />
      <BackToTopButton />
    </main>
  );
}
  • Step 2: Verify build + dev

Run: npm run build → PASS. Mở http://localhost:3000/gioi-thieu → hiển thị nội dung + 3 thẻ thống kê (9 / 6 / 26).

  • Step 3: Commit
git add src/app/gioi-thieu/page.tsx
git commit -m "feat: trang giới thiệu Xã Như Quỳnh"

Task 14: Trang Liên hệ /lien-he

Files:

  • Create: phy-vr360/src/app/lien-he/page.tsx

  • Step 1: Tạo file

phy-vr360/src/app/lien-he/page.tsx:

import type { Metadata } from "next";
import { MapPin, Mail, Phone } from "lucide-react";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";

export const metadata: Metadata = {
  title: "Liên hệ | VR360 Như Quỳnh",
  description: "Thông tin liên hệ và đầu mối cung cấp tư liệu cho dự án số hóa di sản Xã Như Quỳnh.",
};

const contacts = [
  { icon: MapPin, label: "Địa chỉ", value: "Xã Như Quỳnh, tỉnh Hưng Yên" },
  { icon: Phone, label: "Điện thoại", value: "Đang cập nhật" },
  { icon: Mail, label: "Email", value: "Đang cập nhật" },
];

export default function ContactPage() {
  return (
    <main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
      <SiteHeader />

      <section className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:py-24">
        <p className="public-kicker">Liên hệ</p>
        <h1 className="public-gradient-text public-heading-safe mt-3 text-[clamp(2.2rem,5vw,3.6rem)] font-extrabold">
          Liên hệ & đóng góp  liệu
        </h1>
        <p className="mt-5 text-base leading-8 text-[var(--foreground)]/82">
          Người dân, tổ chức   liệu, hình nh hoặc câu chuyện về các di tích của  Như Quỳnh
          vui lòng liên hệ để cùng hoàn thiện kho dữ liệu số di sản.
        </p>

        <div className="mt-10 grid gap-4">
          {contacts.map((c) => {
            const Icon = c.icon;
            return (
              <div key={c.label} className="public-panel flex items-center gap-4 rounded-[8px] p-5">
                <span className="grid h-11 w-11 shrink-0 place-items-center rounded-full bg-[rgb(200_164_106_/_0.18)]">
                  <Icon className="h-5 w-5 text-[var(--primary)]" strokeWidth={1.8} />
                </span>
                <span>
                  <span className="block text-sm font-bold text-[var(--muted-foreground)]">{c.label}</span>
                  <span className="block text-base font-bold text-[var(--tour-ink)]">{c.value}</span>
                </span>
              </div>
            );
          })}
        </div>
      </section>

      <SiteFooter />
      <BackToTopButton />
    </main>
  );
}
  • Step 2: Verify build + dev

Run: npm run build → PASS. Mở http://localhost:3000/lien-he → hiển thị 3 thẻ liên hệ.

  • Step 3: Commit
git add src/app/lien-he/page.tsx
git commit -m "feat: trang liên hệ"

PHASE 3 — Tour VR360 Chùa Thái Lạc

Task 15: Port engine VirtualTour & tổng quát hóa cho tour động

Files:

  • Create: phy-vr360/src/app/components/VirtualTour.tsx (copy từ pdc + sửa)

Mục tiêu: engine chạy bằng config động, KHÔNG kèm dữ liệu Định Công/Liên Hoa (vì phy không có ảnh đó). Bước này đưa engine vào trạng thái biên dịch được với MỘT config rỗng tạm; Task 16 mới nạp dữ liệu Thái Lạc.

  • Step 1: Copy nguyên file engine
cp "d:/Code/vr360/pdc-vr360/app/components/VirtualTour.tsx" "d:/Code/vr360/phy-vr360/src/app/components/VirtualTour.tsx"
  • Step 2: Tổng quát hóa kiểu định danh

Trong file:

  1. SceneId (dòng ~30, union các "scene-1"|...): đổi thành
type SceneId = string;
  1. TourConfig.id (dòng ~106) đổi từ union "dinh-lang-dinh-cong" | "chua-lien-hoa" thành string.
  2. Prop component (dòng ~1121): đổi mặc định
export default function VirtualTour({
  tourId = "chua-thai-lac",
}: {
  tourId?: string;
}) {
  • Step 3: Cắt bỏ dữ liệu Định Công & Liên Hoa

Xóa các phần chỉ phục vụ 2 tour cũ (các hằng từ ~dòng 124 đến phần build config ~1014), gồm: basePath, chuaBasePath, narrationBasePath, chuaNarrationBasePath, heroImage, welcomeBackgroundImage, dinhCardImage, shrineCardImage, backgroundAudio (file nhạc Định Công), dinhPracticalInfo, chuaPracticalInfo, mảng scenes của Định Công, continuousNarrationGroups/mapRoutes của Định Công, chuaScenes/chuaSceneNarration/chuaContinuousNarrationGroups/chuaMapRoutes, dinhTourConfig, chuaTourConfig.

GIỮ LẠI: các type (Hotspot, TourScene, TourConfig, MapRoute, WelcomeCardConfig, PracticalInfoItem, TourPracticalInfo, InfoMarker...), hằng cấu hình camera (PANORAMA_CAMERA_DISTANCE, DEFAULT_FOV, ...), hotspotIcon/infoModalBackground/welcomeTitleBackground, hàm createTourConfig, toàn bộ logic render/Three.js, và cấu trúc toursById/getTourConfig.

Thay khối build config bằng import từ file config (tạo ở Task 16):

import { chuaThaiLacTourConfig } from "@/app/tours/chua-thai-lac";

const toursById = {
  "chua-thai-lac": chuaThaiLacTourConfig,
} satisfies Record<string, TourConfig>;

const defaultTourConfig = chuaThaiLacTourConfig;

const getTourConfig = (tourId?: string) => toursById[tourId ?? ""] ?? defaultTourConfig;

Để createTourConfig và các type dùng được từ file config, export chúng:

export type { TourConfig, TourScene, Hotspot, MapRoute, WelcomeCardConfig, TourPracticalInfo, PracticalInfoItem };
export { createTourConfig };

(và export các hằng path nếu config cần, ví dụ không cần — config tự định nghĩa path ảnh.)

Vì khối Three.js render rất lớn (~3000 dòng) tham chiếu tourConfig.*, KHÔNG động vào phần đó. Chỉ thay phần dữ liệu + cách chọn config ở đầu/cuối file.

  • Step 4: Tạm thời chưa build được — chuyển ngay sang Task 16 để có file config rồi mới verify chung. (Không commit lẻ bước này.)

Task 16: Cấu hình tour Chùa Thái Lạc (26 cảnh)

Files:

  • Create: phy-vr360/src/app/tours/chua-thai-lac.ts

  • Step 1: Khảo sát thứ tự cảnh từ ảnh

Mở 26 ảnh trong phy-vr360/public/images/Đình Thái Lạc/ để xác định tuyến đi hợp lý và hướng hotspot. Danh sách file (giữ đúng tên, có dấu/space — dùng encodeURI): 4.jpg, Ao Cá Koi.jpg, Chính Diện Cổng Chùa.jpg, Chính Sảnh.jpg, Chính Điện 3.jpg, Chính.jpg, Chùa 1 Giữa.jpg, Chùa 1 Hữu.jpg, Chùa 1 Tả.jpg, Chùa 1 Trong - Giữa.jpg, Chùa 1 Trong Tả.jpg, Cổng Bên Trái.jpg, Cổng Hữu.jpg, Duong Tả Vòm.jpg, Giuawx.jpg, Gốc Ngoã Hữu.jpg, Gốc Ngoã Tả.jpg, Hữu 2.jpg, Hữu 3.jpg, La Hán Tả.jpg, Lối Vào Been Trái.jpg, Nhà Phật Hữu.jpg, Sau Chùa.jpg, Tượng Quan Âm.jpg, Đường Hữu.jpg, Đường Tả Qua Vòm.jpg

  • Step 2: Viết file config

Tạo phy-vr360/src/app/tours/chua-thai-lac.ts. Mẫu (điền đủ 26 cảnh; mapPosition rải đều trên sơ đồ 0100; hotspots nối cảnh liền kề; yaw/pitch chỉnh ở Step 4):

import {
  createTourConfig,
  type TourConfig,
  type TourScene,
} from "@/app/components/VirtualTour";

const base = "/images/Đình Thái Lạc";
const p = (file: string) => encodeURI(`${base}/${file}`);

const scenes: TourScene[] = [
  {
    id: "scene-1",
    order: "01",
    title: "Chính diện cổng chùa",
    location: "Khu cổng",
    image: p("Chính Diện Cổng Chùa.jpg"),
    initialYaw: 0,
    mapPosition: { x: 50, y: 92 },
    hotspots: [
      { targetId: "scene-2", label: "Vào sân", yaw: 0, pitch: -12 },
    ],
  },
  // ... định nghĩa đủ scene-2 → scene-26 theo tuyến:
  // Cổng (Chính Diện Cổng Chùa, Cổng Hữu, Cổng Bên Trái, Lối Vào Been Trái)
  // → Đường/vòm (Đường Hữu, Đường Tả Qua Vòm, Duong Tả Vòm)
  // → Chính Sảnh → Chính → Chính Điện 3 → Giuawx → 4
  // → Ban thờ (Chùa 1 Tả/Giữa/Hữu, Chùa 1 Trong - Giữa, Chùa 1 Trong Tả, Hữu 2, Hữu 3)
  // → Tượng (Tượng Quan Âm, La Hán Tả, Nhà Phật Hữu)
  // → Ngoại cảnh (Ao Cá Koi, Gốc Ngoã Tả, Gốc Ngoã Hữu, Sau Chùa)
  // Mỗi scene có ÍT NHẤT 1 hotspot tiến + (nếu hợp lý) 1 hotspot lùi về cảnh trước.
];

export const chuaThaiLacTourConfig: TourConfig = createTourConfig({
  id: "chua-thai-lac",
  title: "Chùa Thái Lạc (Pháp Vân tự)",
  subtitle: "Di tích quốc gia — chạm khắc gỗ thời Trần",
  welcomeTitle: "Chùa Thái Lạc",
  welcomeAccent: "Pháp Vân tự",
  welcomeSubtitle:
    "Tham quan 360° ngôi chùa cổ thờ Pháp Vân, nổi tiếng với mảng chạm khắc gỗ thời Trần.",
  welcomeBackgroundImage: p("Chính Sảnh.jpg"),
  welcomeCards: [
    { image: p("Chính Diện Cổng Chùa.jpg"), label: "Cổng chùa", rotate: "-rotate-6", sceneId: "scene-1" },
    { image: p("Tượng Quan Âm.jpg"), label: "Đài Quan Âm", rotate: "rotate-5", sceneId: "scene-1" },
  ],
  backgroundAudio: "", // chưa có nhạc nền; engine xử lý chuỗi rỗng = không phát
  practicalInfo: {
    intro: "Thông tin phục vụ tham quan và lễ chùa tại Chùa Thái Lạc (Pháp Vân tự).",
    items: [
      { label: "Địa chỉ", value: "Xã Như Quỳnh, tỉnh Hưng Yên." },
      { label: "Người phụ trách", value: "Đang cập nhật." },
      { label: "Số điện thoại", value: "Đang cập nhật." },
      { label: "Giờ mở cửa", value: "Đón khách theo giờ hành chính và lịch sinh hoạt của nhà chùa." },
    ],
    notes: [
      "Giữ trang phục lịch sự, đi nhẹ nói khẽ, không tự ý di chuyển đồ thờ.",
      "Thông tin người phụ trách và liên hệ sẽ được cập nhật khi có dữ liệu xác thực.",
    ],
  },
  scenes,
  sceneNarration: new Map(), // chưa có file mp3 thuyết minh
  continuousNarrationGroups: [],
  mapRoutes: scenes
    .map((s, i) => (i < scenes.length - 1 ? { from: s.id, to: scenes[i + 1].id } : null))
    .filter((r): r is { from: string; to: string } => r !== null),
});

⚠️ Xác minh chữ ký createTourConfig thực tế trong VirtualTour.tsx (Omit<TourConfig,"sceneById"|"locationGroups">) và các field bắt buộc của TourConfig/TourScene đã export ở Task 15. Điền cho khớp 100% — KHÔNG để field thiếu. Nếu sceneNarrationMap<SceneId,string[]>, new Map() hợp lệ. Nếu backgroundAudio rỗng gây lỗi phát audio, kiểm tra logic audio trong engine và bọc điều kiện if (backgroundAudio) ở chỗ tạo Audio() (sửa tối thiểu trong VirtualTour.tsx, có ghi chú).

  • Step 3: Verify biên dịch (gộp Task 15+16)

Run: npx tsc --noEmit Expected: không lỗi type ở VirtualTour.tsxchua-thai-lac.ts. Sửa cho đến khi sạch (các field config khớp type).

Run: npm run build Expected: PASS.

  • Step 4: Commit (engine + config)
git add src/app/components/VirtualTour.tsx src/app/tours/chua-thai-lac.ts
git commit -m "feat: port engine VirtualTour + cấu hình tour Chùa Thái Lạc"

Task 17: Route tour /tour/chua-thai-lac + tinh chỉnh trải nghiệm

Files:

  • Create: phy-vr360/src/app/tour/chua-thai-lac/page.tsx

  • Step 1: Tạo route

phy-vr360/src/app/tour/chua-thai-lac/page.tsx:

import type { Metadata } from "next";
import VirtualTour from "@/app/components/VirtualTour";

export const metadata: Metadata = {
  title: "VR360 Chùa Thái Lạc (Pháp Vân tự)",
  description:
    "Trải nghiệm tham quan thực tế ảo 360° Chùa Thái Lạc (Pháp Vân tự) — di tích quốc gia tại Xã Như Quỳnh, tỉnh Hưng Yên.",
};

export default function ChuaThaiLacTourPage() {
  return <VirtualTour tourId="chua-thai-lac" />;
}
  • Step 2: Verify dev (trải nghiệm tour)

Run: npm run dev → mở http://localhost:3000/tour/chua-thai-lac Kiểm tra:

  • Màn hình welcome hiển thị tên Chùa Thái Lạc + thẻ vào cảnh.

  • Vào tour: panorama 360° kéo/zoom được, ảnh tải đúng.

  • Hotspot chuyển cảnh hoạt động, đi hết được 26 cảnh.

  • Bảng "danh sách cảnh" (locationGroups), bản đồ tuyến, modal thông tin hiển thị.

  • Không có lỗi 404 ảnh trong Network, không lỗi console nghiêm trọng.

  • Step 3: Tinh chỉnh hotspot/yaw

Quan sát trực tiếp, chỉnh initialYaw, yaw/pitch/rotation của từng hotspot trong chua-thai-lac.ts cho mũi tên trỏ đúng hướng đi. Lặp lại đến khi tuyến tham quan mạch lạc.

  • Step 4: Verify build + commit

Run: npm run build → PASS.

git add "src/app/tour/chua-thai-lac/page.tsx" src/app/tours/chua-thai-lac.ts
git commit -m "feat: route tour /tour/chua-thai-lac + tinh chỉnh hotspot"

PHASE 4 — Hoàn thiện

Task 18: Dọn scaffold mặc định & favicon/metadata

Files:

  • Modify/verify: phy-vr360/public/ (xóa next.svg, vercel.svg, window.svg, globe.svg, file.svg nếu không dùng)

  • Verify: không còn tham chiếu nội dung Next.js mặc định

  • Step 1: Xóa asset scaffold không dùng

cd d:/Code/vr360/phy-vr360/public
rm -f next.svg vercel.svg window.svg globe.svg file.svg

(Giữ favicon.ico trong src/app/.)

  • Step 2: Quét sót "Định Công" / "create next app"

Run: grep -rni "định công\|create next app\|dinhcong" d:/Code/vr360/phy-vr360/src Expected: KHÔNG còn kết quả. Nếu còn, sửa.

  • Step 3: Verify build

Run: npm run build → PASS, không cảnh báo ảnh thiếu.

  • Step 4: Commit
git add -A
git commit -m "chore: dọn scaffold mặc định, quét sạch nội dung Định Công"

Task 19: Rà soát responsive, hiệu năng & nghiệm thu cuối

Files: không tạo mới — kiểm thử toàn site.

  • Step 1: Lint + build sạch

Run: npm run lint && npm run build Expected: cả hai PASS, không warning đáng kể.

  • Step 2: Kiểm tra responsive từng trang

npm run dev, mở DevTools, kiểm tra ở 3 khổ (375px / 768px / 1280px) các trang: /, /gioi-thieu, /di-tich, /di-tich/chua-thai-lac, /lien-he, /tour/chua-thai-lac. Kiểm: header/menu mobile mở-đóng đúng; lưới di tích không vỡ; không cuộn ngang; chữ không tràn.

  • Step 3: Kiểm điều hướng

Click toàn bộ link điều hướng + nút "Tham quan VR360" + breadcrumb "Tất cả di tích" + "Back to top". Đảm bảo không link chết.

  • Step 4: Đối chiếu spec

Mở docs/superpowers/specs/2026-06-23-vr360-nhu-quynh-design.md, xác nhận: 6 nhóm trang đủ; 15 di tích đúng phân cấp (9/6); tên "Chùa Thái Lạc (Pháp Vân tự)" đúng; tour hoạt động ngang dinh-lang-dinh-cong; pdc-vr360 không bị đụng tới.

  • Step 5: Commit (nếu có sửa)
git add -A
git commit -m "fix: hoàn thiện responsive & nghiệm thu cổng di sản Như Quỳnh"

Tự rà soát kế hoạch (đã thực hiện khi viết)

  • Phủ spec: §2 cấu trúc trang → Task 1014, 17; §3 danh mục 15 di tích → Task 5, 9, 11; §4 mô hình dữ liệu → Task 5; §5 tour Thái Lạc → Task 1517; §6 design system → Task 2, 69; §1 dependencies/port → Task 14. Đủ.
  • Không placeholder: mọi task có code thật hoặc lệnh copy file gốc cụ thể + danh sách sửa đổi rõ ràng. Phần điền 26 cảnh (Task 16) là công việc dữ liệu thực, kèm mẫu đầy đủ + thứ tự tuyến + yêu cầu khớp type.
  • Nhất quán type: HeritageSite (Task 5) khớp field mà CulturalSpaceExplorer (Task 9), trang [slug] (Task 12), trang chủ/di-tich dùng. TourConfig/createTourConfig export ở Task 15 được Task 16 dùng đúng tên.
  • Điểm rủi ro đã gắn cờ: API Next.js 16 (params Promise, đọc guide), chữ ký createTourConfig thực tế, xử lý backgroundAudio rỗng — đều có ghi chú kiểm chứng trong task tương ứng.