Compare commits
13 Commits
59563db3b4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4465d174fe | |||
| e081f54bd4 | |||
| 6bc57fe144 | |||
| 5945f30773 | |||
| 95a0836c11 | |||
| 9519e395e0 | |||
| ea402e4422 | |||
| 5cf7726372 | |||
| 1fc3431ace | |||
| abc8924eee | |||
| 6fa8a6016a | |||
| 8ad9c7113b | |||
| be682ac95f |
@@ -0,0 +1,95 @@
|
|||||||
|
# Lồng tiếng thuyết minh + nhạc nền — Tour Chùa Thái Lạc
|
||||||
|
|
||||||
|
Ngày: 2026-06-25
|
||||||
|
|
||||||
|
## Mục tiêu
|
||||||
|
|
||||||
|
Gắn âm thanh thuyết minh (lồng tiếng) cho từng cảnh trong tour VR360 Chùa Thái Lạc,
|
||||||
|
và thêm nhạc nền chạy liên tục trên toàn bộ tour với âm lượng nhỏ nhẹ để không lấn lời thuyết minh.
|
||||||
|
|
||||||
|
## Bối cảnh kỹ thuật
|
||||||
|
|
||||||
|
Engine `src/app/components/VirtualTour.tsx` đã hỗ trợ đầy đủ, **không cần sửa logic**:
|
||||||
|
|
||||||
|
- `backgroundAudio: string` — một track nhạc nền phát lặp (`loop`). Âm lượng nền
|
||||||
|
`AUDIO_TARGET_VOLUME = 0.16`, tự động hạ xuống `BACKGROUND_DUCK_VOLUME = 0.045`
|
||||||
|
khi đang phát thuyết minh (ducking). Chuỗi rỗng `""` → engine bỏ qua, không render audio.
|
||||||
|
- `sceneNarration: Map<SceneId, string[]>` — mỗi cảnh có một MẢNG nguồn âm thanh,
|
||||||
|
phát tuần tự (hàng đợi). Âm lượng thuyết minh `NARRATION_TARGET_VOLUME = 0.7`.
|
||||||
|
- `continuousNarrationGroups: SceneId[][]` — các cảnh trong cùng nhóm dùng chung file
|
||||||
|
sẽ phát LIỀN MẠCH (không tua lại) khi khách di chuyển giữa chúng.
|
||||||
|
|
||||||
|
Đây là việc cấu hình DỮ LIỆU trong `src/app/tours/chua-thai-lac.ts` + đổi tên file âm thanh.
|
||||||
|
|
||||||
|
## Yêu cầu
|
||||||
|
|
||||||
|
### 1. Đổi tên 13 file âm thanh sang ASCII
|
||||||
|
|
||||||
|
Theo convention dự án (tránh lỗi URL-encode + phân biệt hoa/thường khi deploy lên Linux/Vercel).
|
||||||
|
Hai file `.MP3` (chữ hoa) được chuẩn hóa thành `.mp3`.
|
||||||
|
|
||||||
|
| File hiện tại | Tên mới |
|
||||||
|
|---|---|
|
||||||
|
| `public/media/chua-thai-lac/Lời chào.mp3` | `loi-chao.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Tam quan.mp3` | `tam-quan.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Sân Tịnh An.mp3` | `san-tinh-an.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Hồ Di Lặc.MP3` | `ho-di-lac.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Tiền sảnh.mp3` | `tien-sanh.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Tôn Đài Đức Phật Thích Ca.mp3` | `ton-dai-duc-phat.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Vườn tháp cổ.mp3` | `vuon-thap-co.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Khách xá.mp3` | `khach-xa.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Khách đường.mp3` | `khach-duong.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Hữu Vu La Hán- Tả Vu La Hán.MP3` | `huu-vu-ta-vu-la-han.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Nhà Tổ.mp3` | `nha-to.mp3` |
|
||||||
|
| `public/media/chua-thai-lac/Tiền Đường.mp3` | `tien-duong.mp3` |
|
||||||
|
| `public/media/nhac-nen/Nhạc Phật Giáo sâu lắng.mp3` | `nhac-phat-giao.mp3` |
|
||||||
|
|
||||||
|
### 2. Cấu hình `src/app/tours/chua-thai-lac.ts`
|
||||||
|
|
||||||
|
**a) Nhạc nền:** `backgroundAudio: ""` → `"/media/nhac-nen/nhac-phat-giao.mp3"`.
|
||||||
|
Giữ nguyên âm lượng mặc định của engine (nền 0.16, ducking 0.045 khi thuyết minh).
|
||||||
|
|
||||||
|
**b) Bảng thuyết minh `narrationGroups`** — mở rộng kiểu trường `audio` thành `string | string[]`
|
||||||
|
để cảnh 1-3 phát nối tiếp hai file (lời chào rồi Tam quan):
|
||||||
|
|
||||||
|
```
|
||||||
|
[loi-chao, tam-quan] → cảnh 1, 2, 3
|
||||||
|
san-tinh-an → cảnh 4, 5
|
||||||
|
ho-di-lac → cảnh 6
|
||||||
|
tien-sanh → cảnh 7, 8 (MỚI)
|
||||||
|
ton-dai-duc-phat → cảnh 9 (MỚI)
|
||||||
|
vuon-thap-co → cảnh 10 (MỚI)
|
||||||
|
(không thuyết minh) → cảnh 11
|
||||||
|
khach-xa → cảnh 12, 13, 14
|
||||||
|
khach-duong → cảnh 15
|
||||||
|
huu-vu-ta-vu-la-han → cảnh 16, 18
|
||||||
|
nha-to → cảnh 17
|
||||||
|
tien-duong → cảnh 19, 20, 21
|
||||||
|
```
|
||||||
|
|
||||||
|
Vòng lặp build `sceneNarration` phải chuẩn hóa `audio` (string hoặc string[]) thành mảng nguồn
|
||||||
|
qua `audioForName` cho mỗi cảnh.
|
||||||
|
|
||||||
|
**c) `continuousNarrationGroups`** — thêm `["scene-7", "scene-8"]` (hai cảnh kề nhau dùng chung
|
||||||
|
`tien-sanh`). Các nhóm hiện có giữ nguyên. Cảnh 16 & 18 vẫn KHÔNG gom (cách nhau bởi cảnh 17)
|
||||||
|
nên sẽ phát lại — đúng hành vi mong muốn.
|
||||||
|
|
||||||
|
## Hành vi đã thống nhất
|
||||||
|
|
||||||
|
- Cảnh 1: phát `loi-chao` rồi tự động phát `tam-quan`; di chuyển 1→2→3 không tua lại.
|
||||||
|
- Rời cảnh 1-3 rồi quay lại → "lời chào" phát lại từ đầu (hành vi mặc định, chấp nhận được).
|
||||||
|
- Nhạc nền nhỏ nhẹ và tự hạ thấp hơn nữa khi có thuyết minh.
|
||||||
|
|
||||||
|
## Kiểm chứng
|
||||||
|
|
||||||
|
1. `npm run lint` / `npm run build` không lỗi type.
|
||||||
|
2. `npm run dev`, vào tour và xác nhận:
|
||||||
|
- Nhạc nền chạy nhẹ liên tục mọi cảnh.
|
||||||
|
- Mỗi cảnh phát đúng file thuyết minh theo bảng trên.
|
||||||
|
- Cảnh 1 nghe "lời chào → tam quan" nối tiếp.
|
||||||
|
- Nhạc nền tự nhỏ lại khi đang có lời thuyết minh.
|
||||||
|
|
||||||
|
## Ngoài phạm vi
|
||||||
|
|
||||||
|
- Không sửa logic engine `VirtualTour.tsx`.
|
||||||
|
- Không thay đổi nội dung popup info markers, hướng mũi tên, hay sơ đồ mini-map.
|
||||||
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 473 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
After Width: | Height: | Size: 565 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
After Width: | Height: | Size: 527 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 431 KiB |
@@ -3,9 +3,10 @@ import Image from "next/image";
|
|||||||
import { Boxes, ScanLine, Sparkles } from "lucide-react";
|
import { Boxes, ScanLine, Sparkles } from "lucide-react";
|
||||||
import AntiquityShowcaseClient from "./AntiquityShowcaseClient";
|
import AntiquityShowcaseClient from "./AntiquityShowcaseClient";
|
||||||
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
import { activeTour, antiquities } from "@/app/content";
|
import { antiquities, subpageHero } from "@/app/content";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Cổ vật 3D | VR360 Như Quỳnh",
|
title: "Cổ vật 3D | VR360 Như Quỳnh",
|
||||||
@@ -25,14 +26,14 @@ export default function AntiquityPage() {
|
|||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
||||||
<Image src={activeTour.shrineImage} alt="" fill priority sizes="100vw" className="public-subpage-hero-image object-cover" />
|
<Image src={subpageHero.coVat} alt="" fill preload sizes="100vw" className="public-subpage-hero-image public-parallax-bg 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-subpage-hero-content mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:py-28">
|
||||||
<div className="public-rise public-hero-rule max-w-4xl">
|
<div className="public-rise public-hero-rule max-w-4xl">
|
||||||
<p className="public-kicker">Phòng trưng bày số</p>
|
<p className="public-kicker">Phòng trưng bày số</p>
|
||||||
<h1 className="public-gradient-text public-heading-safe mt-4 text-[clamp(2.55rem,5.8vw,5rem)] font-extrabold">
|
<h1 className="public-hero-display public-heading-safe mt-5 text-[clamp(2.55rem,5.8vw,5rem)] font-extrabold">
|
||||||
Cổ vật & hiện vật 3D
|
Cổ vật & <span className="public-display-accent">hiện vật 3D</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
|
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 sm:text-lg">
|
||||||
Bộ sưu tập số các cổ vật tiêu biểu thuộc hệ thống di tích Xã Như Quỳnh. Mỗi hiện vật được
|
Bộ sưu tập số các cổ vật tiêu biểu thuộc hệ thống di tích Xã Như Quỳnh. Mỗi hiện vật được
|
||||||
dựng thành mô hình ba chiều để chiêm ngưỡng trọn vẹn từ mọi góc nhìn.
|
dựng thành mô hình ba chiều để chiêm ngưỡng trọn vẹn từ mọi góc nhìn.
|
||||||
</p>
|
</p>
|
||||||
@@ -43,27 +44,39 @@ export default function AntiquityPage() {
|
|||||||
<AntiquityShowcaseClient items={antiquities} />
|
<AntiquityShowcaseClient items={antiquities} />
|
||||||
|
|
||||||
<section className="border-t border-[var(--surface-border)] bg-[var(--surface-band)]">
|
<section className="border-t 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 md:grid-cols-3 lg:py-18">
|
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-20">
|
||||||
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
|
<p className="public-kicker public-kicker--center">Vì sao số hóa 3D</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
|
Giá trị của hiện vật số
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 grid gap-x-8 gap-y-12 md:grid-cols-3">
|
||||||
{highlights.map((item) => {
|
{highlights.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
<article key={item.title} className="public-panel public-stagger-item rounded-[14px] p-6">
|
<article key={item.title} className="public-stagger-item flex flex-col items-center text-center">
|
||||||
<Icon className="h-7 w-7 text-[var(--primary)]" strokeWidth={1.7} />
|
<span className="relative grid h-16 w-16 place-items-center">
|
||||||
<h3 className="mt-4 text-lg font-bold text-[var(--tour-ink)]">{item.title}</h3>
|
<HeritageSeal className="absolute inset-0 h-full w-full text-[var(--primary)] opacity-35" />
|
||||||
<p className="mt-2 text-sm leading-6 text-[var(--muted-foreground)]">{item.detail}</p>
|
<Icon className="relative h-6 w-6 text-[var(--primary)]" strokeWidth={1.7} />
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-6 text-lg font-bold text-[var(--tour-ink)]">{item.title}</h3>
|
||||||
|
<p className="mt-2.5 max-w-[34ch] text-sm leading-6 text-[var(--muted-foreground)]">{item.detail}</p>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:py-16">
|
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
||||||
<div className="public-panel public-glow rounded-[16px] p-6 text-center sm:p-9">
|
<div className="public-panel public-glow rounded-[18px] p-8 text-center sm:p-12">
|
||||||
<p className="public-kicker justify-center">Mở rộng bộ sưu tập</p>
|
<HeritageSeal className="mx-auto h-10 w-10 text-[var(--primary)] opacity-80" />
|
||||||
<h2 className="public-display mt-3 text-2xl font-bold text-[var(--tour-ink)] sm:text-3xl">
|
<p className="public-kicker public-kicker--center mt-5">Mở rộng bộ sưu tập</p>
|
||||||
|
<h2 className="public-display public-heading-safe mx-auto mt-3 max-w-[24ch] text-2xl font-bold text-[var(--tour-ink)] sm:text-3xl">
|
||||||
Hiện vật từ các di tích khác đang được số hóa
|
Hiện vật từ các di tích khác đang được số hóa
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mx-auto mt-3 max-w-[64ch] text-sm leading-7 text-[var(--foreground)]/76 sm:text-base">
|
<p className="mx-auto mt-4 max-w-[64ch] text-sm leading-7 text-[var(--muted-foreground)] sm:text-base">
|
||||||
Phòng trưng bày sẽ tiếp tục bổ sung các cổ vật, sắc phong, bia đá và đồ thờ tự tiêu biểu
|
Phòng trưng bày sẽ tiếp tục bổ sung các cổ vật, sắc phong, bia đá và đồ thờ tự tiêu biểu
|
||||||
của toàn bộ hệ thống di tích Xã Như Quỳnh trong giai đoạn 2026–2030.
|
của toàn bộ hệ thống di tích Xã Như Quỳnh trong giai đoạn 2026–2030.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
export type TourVideoMarker = {
|
||||||
|
title: string;
|
||||||
|
eyebrow: string;
|
||||||
|
content?: string[];
|
||||||
|
video: { src: string; poster?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal phát video cho marker trong tour VR360. Mở rộng (16:9), tự phát khi mở
|
||||||
|
// (trong chuỗi cử chỉ người dùng); nếu trình duyệt chặn autoplay có tiếng thì
|
||||||
|
// controls vẫn hiển thị để người dùng tự phát. Backdrop + card dùng lại animation
|
||||||
|
// toàn cục của VirtualTour (tourModalBackdrop / tourPanelIn).
|
||||||
|
export function TourVideoModal({
|
||||||
|
marker,
|
||||||
|
isClosing,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
marker: TourVideoMarker;
|
||||||
|
isClosing: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void video.play().catch(() => {
|
||||||
|
// Autoplay có tiếng bị chặn — controls đã bật để người dùng tự phát.
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Dừng ngay khi đóng để không còn tiếng vọng trong lúc backdrop mờ dần.
|
||||||
|
video.pause();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-40 grid place-items-center bg-[rgb(10_8_6_/_0.74)] px-3 py-4 backdrop-blur-[4px] sm:px-5 sm:py-6 ${
|
||||||
|
isClosing
|
||||||
|
? "animate-[tourModalBackdropOut_240ms_ease-in_both]"
|
||||||
|
: "animate-[tourModalBackdrop_320ms_ease-out_both]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-0 cursor-default"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Đóng video"
|
||||||
|
/>
|
||||||
|
<article className="relative w-[min(94vw,1100px)] max-h-[92dvh] overflow-hidden rounded-[14px] border border-[rgb(232_207_170_/_0.18)] bg-[rgb(20_17_14_/_0.94)] shadow-[0_38px_120px_rgb(0_0_0_/_0.6)] animate-[tourPanelIn_360ms_cubic-bezier(.2,.9,.2,1)_both]">
|
||||||
|
<div className="flex items-start justify-between gap-4 px-4 pb-2.5 pt-3.5 sm:px-5 sm:pt-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[0.6rem] font-black uppercase tracking-[0.18em] text-[var(--tour-gold)] sm:text-[0.66rem]">
|
||||||
|
{marker.eyebrow}
|
||||||
|
</p>
|
||||||
|
<h2 className="font-display-vn mt-0.5 truncate text-xl font-bold text-[rgb(255,238,184)] sm:text-2xl">
|
||||||
|
{marker.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="grid h-8 w-8 shrink-0 place-items-center rounded-full border border-white/15 bg-white/10 text-white/90 backdrop-blur transition hover:bg-white/20 active:scale-95 sm:h-9 sm:w-9"
|
||||||
|
aria-label="Đóng video"
|
||||||
|
title="Đóng"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 sm:h-[1.1rem] sm:w-[1.1rem]" strokeWidth={2.2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative grid w-full place-items-center bg-black">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={marker.video.src}
|
||||||
|
poster={marker.video.poster}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
preload="auto"
|
||||||
|
className="aspect-video max-h-[64dvh] w-full object-contain"
|
||||||
|
>
|
||||||
|
Trình duyệt của bạn không hỗ trợ phát video.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{marker.content && marker.content.length > 0 ? (
|
||||||
|
<div className="max-h-[20dvh] overflow-y-auto px-4 py-3.5 sm:px-5 sm:py-4">
|
||||||
|
<div className="space-y-2.5 font-display-vn text-[0.86rem] leading-6 text-white/72 sm:text-[0.92rem] sm:leading-7">
|
||||||
|
{marker.content.map((paragraph) => (
|
||||||
|
<p key={paragraph} className="text-justify">
|
||||||
|
{paragraph}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import * as THREE from "three";
|
|||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||||
import letterAnimation from "@/letter.json";
|
import letterAnimation from "@/letter.json";
|
||||||
|
import { TourVideoModal } from "@/app/components/TourVideoModal";
|
||||||
import { chuaThaiLacTour } from "@/app/tours/chua-thai-lac";
|
import { chuaThaiLacTour } from "@/app/tours/chua-thai-lac";
|
||||||
import { lqip } from "@/app/tours/chua-thai-lac.lqip";
|
import { lqip } from "@/app/tours/chua-thai-lac.lqip";
|
||||||
|
|
||||||
@@ -49,7 +50,9 @@ type InfoMarker = {
|
|||||||
pitch: number;
|
pitch: number;
|
||||||
rotation?: number;
|
rotation?: number;
|
||||||
content: string[];
|
content: string[];
|
||||||
iconType?: "envelope" | "landmark" | "info";
|
iconType?: "envelope" | "landmark" | "info" | "video";
|
||||||
|
// Khi có `video`, marker mở modal phát video thay vì popup chữ (dùng iconType "video").
|
||||||
|
video?: { src: string; poster?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
type TourScene = {
|
type TourScene = {
|
||||||
@@ -1043,6 +1046,8 @@ function TourExperience({
|
|||||||
const narrationTokenRef = useRef(0);
|
const narrationTokenRef = useRef(0);
|
||||||
const activeNarrationSrcRef = useRef<string | null>(null);
|
const activeNarrationSrcRef = useRef<string | null>(null);
|
||||||
const lastNarrationSceneRef = useRef<SceneId | null>(null);
|
const lastNarrationSceneRef = useRef<SceneId | null>(null);
|
||||||
|
// Listener "ended" của bài đang phát — giữ lại để gỡ sạch khi dừng/đổi hàng đợi.
|
||||||
|
const narrationEndedRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const { orientation, isSupported, requestPermission, startListening } = useDeviceOrientation();
|
const { orientation, isSupported, requestPermission, startListening } = useDeviceOrientation();
|
||||||
|
|
||||||
@@ -1053,6 +1058,10 @@ function TourExperience({
|
|||||||
activeNarrationSrcRef.current = null;
|
activeNarrationSrcRef.current = null;
|
||||||
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
|
if (narrationEndedRef.current) {
|
||||||
|
audio.removeEventListener("ended", narrationEndedRef.current);
|
||||||
|
narrationEndedRef.current = null;
|
||||||
|
}
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.currentTime = 0;
|
audio.currentTime = 0;
|
||||||
audio.onended = null;
|
audio.onended = null;
|
||||||
@@ -1074,6 +1083,12 @@ function TourExperience({
|
|||||||
const token = narrationTokenRef.current;
|
const token = narrationTokenRef.current;
|
||||||
narrationQueueRef.current = [...sources];
|
narrationQueueRef.current = [...sources];
|
||||||
|
|
||||||
|
// Gỡ listener "ended" của lần phát trước (nếu còn) để không bị gọi chồng.
|
||||||
|
if (narrationEndedRef.current) {
|
||||||
|
audio.removeEventListener("ended", narrationEndedRef.current);
|
||||||
|
narrationEndedRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
const playNext = async () => {
|
const playNext = async () => {
|
||||||
if (token !== narrationTokenRef.current) {
|
if (token !== narrationTokenRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -1087,17 +1102,28 @@ function TourExperience({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio.src !== nextSource) {
|
// Đổi nguồn rồi load() để play() không bị huỷ giữa chừng khi chuyển bài
|
||||||
|
// trong hàng đợi — đảm bảo cảnh 1-3 phát "lời chào" xong NỐI TIẾP "Tam quan".
|
||||||
audio.src = nextSource;
|
audio.src = nextSource;
|
||||||
}
|
|
||||||
|
|
||||||
audio.currentTime = 0;
|
|
||||||
audio.playbackRate = NARRATION_PLAYBACK_RATE;
|
audio.playbackRate = NARRATION_PLAYBACK_RATE;
|
||||||
audio.volume = NARRATION_TARGET_VOLUME;
|
audio.volume = NARRATION_TARGET_VOLUME;
|
||||||
|
audio.load();
|
||||||
activeNarrationSrcRef.current = nextSource;
|
activeNarrationSrcRef.current = nextSource;
|
||||||
|
|
||||||
|
// Mỗi bài gắn một listener "ended" riêng (once) → khi bài này hết thì
|
||||||
|
// tự phát bài kế tiếp trong hàng đợi. Không phụ thuộc audio.onended (dễ bị null).
|
||||||
|
const handleEnded = () => {
|
||||||
|
narrationEndedRef.current = null;
|
||||||
|
void playNext();
|
||||||
|
};
|
||||||
|
narrationEndedRef.current = handleEnded;
|
||||||
|
audio.addEventListener("ended", handleEnded, { once: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await audio.play();
|
await audio.play();
|
||||||
|
if (token !== narrationTokenRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onNarrationStateChange?.(true);
|
onNarrationStateChange?.(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DOMException && error.name === "AbortError") {
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
@@ -1108,10 +1134,6 @@ function TourExperience({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
audio.onended = () => {
|
|
||||||
void playNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
void playNext();
|
void playNext();
|
||||||
},
|
},
|
||||||
[onNarrationStateChange],
|
[onNarrationStateChange],
|
||||||
@@ -1194,6 +1216,22 @@ function TourExperience({
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [activeInfoMarker, closeInfoMarker]);
|
}, [activeInfoMarker, closeInfoMarker]);
|
||||||
|
|
||||||
|
// Marker video đang mở → dừng thuyết minh và hạ nhạc nền để không chồng tiếng với
|
||||||
|
// video; khi đóng thì khôi phục âm lượng nền. Tái dùng tín hiệu duck sẵn có
|
||||||
|
// (onNarrationStateChange) nên không cần thêm prop điều khiển audio.
|
||||||
|
const isVideoMarkerOpen = Boolean(activeInfoMarker?.video) && !isInfoMarkerClosing;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVideoMarkerOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopNarration();
|
||||||
|
onNarrationStateChange?.(true);
|
||||||
|
return () => {
|
||||||
|
onNarrationStateChange?.(false);
|
||||||
|
};
|
||||||
|
}, [isVideoMarkerOpen, stopNarration, onNarrationStateChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const narrationAudio = narrationRef.current;
|
const narrationAudio = narrationRef.current;
|
||||||
|
|
||||||
@@ -1205,6 +1243,10 @@ function TourExperience({
|
|||||||
window.clearTimeout(closingInfoTimerRef.current);
|
window.clearTimeout(closingInfoTimerRef.current);
|
||||||
}
|
}
|
||||||
if (narrationAudio) {
|
if (narrationAudio) {
|
||||||
|
if (narrationEndedRef.current) {
|
||||||
|
narrationAudio.removeEventListener("ended", narrationEndedRef.current);
|
||||||
|
narrationEndedRef.current = null;
|
||||||
|
}
|
||||||
narrationAudio.onended = null;
|
narrationAudio.onended = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2199,7 +2241,9 @@ function TourExperience({
|
|||||||
setActivePanel(null);
|
setActivePanel(null);
|
||||||
setIsInfoMarkerClosing(false);
|
setIsInfoMarkerClosing(false);
|
||||||
|
|
||||||
if (isRead) {
|
// Marker video mở modal phát ngay, không qua hiệu ứng "mở thư" 520ms.
|
||||||
|
if (marker.video || isRead) {
|
||||||
|
setReadMarkers((prev) => new Set(prev).add(markerKey));
|
||||||
setActiveInfoMarker(marker);
|
setActiveInfoMarker(marker);
|
||||||
setOpeningInfoMarkerId(null);
|
setOpeningInfoMarkerId(null);
|
||||||
return;
|
return;
|
||||||
@@ -2213,7 +2257,7 @@ function TourExperience({
|
|||||||
openingInfoTimerRef.current = null;
|
openingInfoTimerRef.current = null;
|
||||||
}, 520);
|
}, 520);
|
||||||
}}
|
}}
|
||||||
aria-label={`Mở thông tin ${marker.title}`}
|
aria-label={marker.video ? `Phát video: ${marker.title}` : `Mở thông tin ${marker.title}`}
|
||||||
title={marker.title}
|
title={marker.title}
|
||||||
>
|
>
|
||||||
{/* TÌM ĐOẠN NÀY VÀ THAY TOÀN BỘ */}
|
{/* TÌM ĐOẠN NÀY VÀ THAY TOÀN BỘ */}
|
||||||
@@ -2232,6 +2276,34 @@ function TourExperience({
|
|||||||
rotation={marker.rotation ?? 0}
|
rotation={marker.rotation ?? 0}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
) : marker.iconType === "video" ? (
|
||||||
|
// Marker dạng KHUNG VIDEO thu nhỏ (tỉ lệ 16:9) — giống màn hình phát video
|
||||||
|
// mini, có nút play ở giữa; bấm để mở modal phát to.
|
||||||
|
<div
|
||||||
|
className={`relative aspect-video w-[5.5rem] overflow-hidden rounded-[10px] border backdrop-blur-md transition-all duration-300 sm:w-[6.75rem] ${isHovered
|
||||||
|
? "scale-[1.05] border-[rgba(255,234,176,1)] shadow-[0_0_0_3px_rgba(232,207,170,0.2),0_0_30px_rgba(255,210,115,0.5),0_16px_34px_rgba(0,0,0,0.44)]"
|
||||||
|
: "scale-100 border-[rgba(232,207,170,0.82)] shadow-[0_0_0_2px_rgba(232,207,170,0.14),0_0_18px_rgba(232,207,170,0.3),0_10px_24px_rgba(0,0,0,0.42)]"} ${isRead ? "opacity-95" : "opacity-100"}`}
|
||||||
|
>
|
||||||
|
{!isRead ? (
|
||||||
|
<span className="tour-temple-icon-pulse absolute inset-[-0.55rem] -z-10 rounded-[16px]" />
|
||||||
|
) : null}
|
||||||
|
{/* Nền giả lập khung phim + ánh sáng nhẹ phía trên cho ra dáng màn hình */}
|
||||||
|
<span className="absolute inset-0 bg-[linear-gradient(135deg,rgba(40,20,16,0.96),rgba(120,42,33,0.9)_52%,rgba(18,12,10,0.97))]" />
|
||||||
|
<span className="absolute inset-x-0 top-0 h-1/2 bg-[linear-gradient(180deg,rgba(255,246,214,0.16),transparent)]" />
|
||||||
|
{/* Nút play ở giữa khung */}
|
||||||
|
<span className="absolute inset-0 grid place-items-center">
|
||||||
|
<span
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded-full border bg-[rgba(8,6,5,0.46)] backdrop-blur-sm transition-all duration-300 sm:h-8 sm:w-8 ${isHovered
|
||||||
|
? "scale-110 border-[rgba(255,234,176,1)] bg-[rgba(8,6,5,0.58)]"
|
||||||
|
: "border-white/55"}`}
|
||||||
|
>
|
||||||
|
<Play
|
||||||
|
className={`h-3.5 w-3.5 translate-x-[1px] fill-current transition-colors duration-300 sm:h-4 sm:w-4 ${isHovered ? "text-white" : "text-[rgb(255,238,184)]"}`}
|
||||||
|
strokeWidth={1.4}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
@@ -2276,7 +2348,20 @@ function TourExperience({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeInfoMarker ? (
|
{activeInfoMarker?.video ? (
|
||||||
|
<TourVideoModal
|
||||||
|
marker={{
|
||||||
|
title: activeInfoMarker.title,
|
||||||
|
eyebrow: activeInfoMarker.eyebrow,
|
||||||
|
content: activeInfoMarker.content,
|
||||||
|
video: activeInfoMarker.video,
|
||||||
|
}}
|
||||||
|
isClosing={isInfoMarkerClosing}
|
||||||
|
onClose={closeInfoMarker}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeInfoMarker && !activeInfoMarker.video ? (
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-0 z-40 grid place-items-center bg-[rgb(22_18_15_/_0.44)] px-3 py-4 backdrop-blur-[3px] sm:px-5 sm:py-6 ${isInfoMarkerClosing
|
className={`fixed inset-0 z-40 grid place-items-center bg-[rgb(22_18_15_/_0.44)] px-3 py-4 backdrop-blur-[3px] sm:px-5 sm:py-6 ${isInfoMarkerClosing
|
||||||
? "animate-[tourModalBackdropOut_240ms_ease-in_both]"
|
? "animate-[tourModalBackdropOut_240ms_ease-in_both]"
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
// Số liệu đếm lên từ 0 khi cuộn tới (IntersectionObserver kích hoạt 1 lần, rAF chạy
|
||||||
|
// mượt theo khung hình). Tôn trọng prefers-reduced-motion: hiện thẳng giá trị cuối.
|
||||||
|
// SSR render sẵn giá trị cuối nên không nhảy layout / tốt cho SEO khi chưa cuộn tới.
|
||||||
|
export function CountUp({
|
||||||
|
value,
|
||||||
|
duration = 1400,
|
||||||
|
className,
|
||||||
|
suffix = "",
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
duration?: number;
|
||||||
|
className?: string;
|
||||||
|
suffix?: string;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
const [display, setDisplay] = useState(value);
|
||||||
|
const startedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
// Giảm chuyển động: giữ nguyên giá trị cuối (đã là `value` từ useState ban đầu).
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!entries[0]?.isIntersecting || startedRef.current) return;
|
||||||
|
startedRef.current = true;
|
||||||
|
observer.disconnect();
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const progress = Math.min((now - start) / duration, 1);
|
||||||
|
const eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
|
||||||
|
setDisplay(Math.round(value * eased));
|
||||||
|
if (progress < 1) requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
setDisplay(0);
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
},
|
||||||
|
{ threshold: 0.4 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [value, duration]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={ref} className={className}>
|
||||||
|
{display}
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,6 +55,16 @@ export function CulturalSpaceExplorer({ sites }: { sites: HeritageSite[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
||||||
|
<div className="mb-8 max-w-3xl">
|
||||||
|
<p className="public-kicker">Tra cứu di tích</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
|
Tìm theo loại hình & cấp xếp hạng
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)] sm:text-base">
|
||||||
|
Lọc nhanh toàn bộ hệ thống di tích để chọn không gian bạn muốn khám phá.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="public-panel rounded-[20px] p-5 sm:p-7">
|
<div className="public-panel rounded-[20px] p-5 sm:p-7">
|
||||||
{/* Hàng tìm kiếm + tóm tắt kết quả */}
|
{/* Hàng tìm kiếm + tóm tắt kết quả */}
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { festivalGallery } from "@/app/content";
|
||||||
|
|
||||||
|
// Gallery "Khoảnh khắc lễ hội" — bố cục bento: dưới lg là lưới 2 cột đều, từ lg dùng
|
||||||
|
// grid-auto-flow dense với vài ô lớn/cao để tạo nhịp điệu biên tập, tránh kiểu 3 thẻ đều.
|
||||||
|
export function FestivalGallery() {
|
||||||
|
return (
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
||||||
|
<div className="public-split-reveal max-w-3xl rounded-[6px]">
|
||||||
|
<p className="public-kicker">Khoảnh khắc lễ hội</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
|
Sắc màu hội làng bên ngôi chùa cổ
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 max-w-[64ch] text-sm leading-6 text-[var(--muted-foreground)] sm:text-base">
|
||||||
|
Những khoảnh khắc thật của lễ hội truyền thống tại Chùa Pháp Vân (Thái Lạc) — đám rước, múa rồng,
|
||||||
|
nghi thức tế lễ và đêm cầu an bên ngôi chùa cổ.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-9 grid auto-rows-[9.5rem] grid-cols-2 gap-3 sm:auto-rows-[12rem] sm:gap-4 lg:auto-rows-[12.5rem] lg:grid-cols-4 lg:[grid-auto-flow:dense]">
|
||||||
|
{festivalGallery.map((photo) => (
|
||||||
|
<figure
|
||||||
|
key={photo.src}
|
||||||
|
className={`public-stagger-item group relative overflow-hidden rounded-[14px] ring-1 ring-[var(--surface-border)] shadow-[0_18px_46px_rgb(37_75_54_/_0.14)] ${photo.span ?? ""}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={photo.src}
|
||||||
|
alt={photo.alt}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1024px) 50vw, 25vw"
|
||||||
|
className="object-cover transition-transform duration-[900ms] ease-[cubic-bezier(.2,.9,.2,1)] group-hover:scale-[1.06]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0 bg-[linear-gradient(180deg,transparent_36%,rgb(8_31_23_/_0.82))] transition-opacity duration-500 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
<figcaption className="absolute inset-x-0 bottom-0 p-4 sm:p-5">
|
||||||
|
<p className="font-display-vn text-base font-extrabold leading-tight text-white drop-shadow-sm sm:text-lg">
|
||||||
|
{photo.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[0.78rem] leading-5 text-white/82 sm:text-sm">{photo.note}</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Play } from "lucide-react";
|
||||||
|
|
||||||
|
// Trình phát video lễ hội: hiện poster + nút phát tuỳ biến cho tới khi người dùng bấm,
|
||||||
|
// sau đó mới nạp video và bật điều khiển gốc của trình duyệt.
|
||||||
|
// preload="none" để không tải file video nặng khi chưa cần — tốt cho mobile & băng thông.
|
||||||
|
export function FestivalVideo({
|
||||||
|
src,
|
||||||
|
poster,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
poster: string;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
|
||||||
|
function handlePlay() {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
setStarted(true);
|
||||||
|
void video.play().catch(() => {
|
||||||
|
// Một số trình duyệt chặn phát tự động — controls đã bật để người dùng tự phát.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group relative overflow-hidden rounded-[18px] border border-[var(--surface-border)] bg-black shadow-[0_30px_86px_rgb(37_75_54_/_0.18)]">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={src}
|
||||||
|
poster={poster}
|
||||||
|
controls={started}
|
||||||
|
preload="none"
|
||||||
|
playsInline
|
||||||
|
className="aspect-video w-full object-cover"
|
||||||
|
>
|
||||||
|
Trình duyệt của bạn không hỗ trợ phát video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
{!started && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePlay}
|
||||||
|
aria-label={`Phát video: ${title}`}
|
||||||
|
className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[linear-gradient(180deg,rgb(8_31_23_/_0.26),rgb(8_31_23_/_0.6))] transition"
|
||||||
|
>
|
||||||
|
<span className="flex h-[4.5rem] w-[4.5rem] items-center justify-center rounded-full border border-white/40 bg-[rgb(8_31_23_/_0.42)] text-white backdrop-blur-md transition duration-300 group-hover:scale-105 group-hover:border-[var(--tour-gold-light)] group-hover:bg-[rgb(8_31_23_/_0.58)]">
|
||||||
|
<Play className="h-7 w-7 translate-x-[2px] fill-current" strokeWidth={1.5} />
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-white/20 bg-[rgb(8_31_23_/_0.46)] px-4 py-1.5 text-sm font-bold text-white backdrop-blur-md">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Hệ motif "triện di sản" — dấu triện hình hoa thị (hai hình vuông lồng nhau tạo sao tám
|
||||||
|
// cánh) gợi con dấu sắc phong & hoa văn chạm khắc. Dùng làm dấu hiệu thương hiệu xuyên suốt:
|
||||||
|
// nhãn mục, đường phân cách trang trí, điểm nhấn ở hero và chân trang. Thuần SVG (server
|
||||||
|
// component), kế thừa màu qua currentColor nên đặt màu bằng class text-* ở nơi sử dụng.
|
||||||
|
|
||||||
|
export function HeritageSeal({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 48 48" fill="none" aria-hidden="true" className={className}>
|
||||||
|
<circle cx="24" cy="24" r="22.5" stroke="currentColor" strokeWidth="1" opacity="0.4" />
|
||||||
|
<circle cx="24" cy="24" r="18" stroke="currentColor" strokeWidth="1.1" />
|
||||||
|
<rect x="13" y="13" width="22" height="22" stroke="currentColor" strokeWidth="1.2" />
|
||||||
|
<rect
|
||||||
|
x="13"
|
||||||
|
y="13"
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
transform="rotate(45 24 24)"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="24" r="4.2" stroke="currentColor" strokeWidth="1.2" />
|
||||||
|
<circle cx="24" cy="24" r="1.4" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đường phân cách trang trí: dấu triện ở giữa, hai sợi chỉ vàng mảnh tan dần hai bên.
|
||||||
|
// Tĩnh, an toàn với prefers-reduced-motion; tạo nhịp nghỉ giữa các mục lớn.
|
||||||
|
export function SectionDivider({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center gap-4 ${className}`} aria-hidden="true">
|
||||||
|
<span className="h-px w-full max-w-[8.5rem] bg-[linear-gradient(90deg,transparent,rgb(142_95_11_/_0.42))]" />
|
||||||
|
<HeritageSeal className="h-[1.7rem] w-[1.7rem] shrink-0 text-[var(--primary)]" />
|
||||||
|
<span className="h-px w-full max-w-[8.5rem] bg-[linear-gradient(90deg,rgb(142_95_11_/_0.42),transparent)]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,28 +1,41 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Globe2, Mail, MapPin, Phone } from "lucide-react";
|
import { Globe2, Mail, MapPin, Phone } from "lucide-react";
|
||||||
|
import { HeritageSeal, SectionDivider } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { contact, navigation } from "@/app/content";
|
import { contact, navigation } from "@/app/content";
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer id="contact" className="px-4 pb-8 sm:px-6">
|
<footer id="contact" className="px-4 pb-10 sm:px-6">
|
||||||
<div className="mx-auto grid max-w-7xl gap-8 border-t border-[var(--surface-border)] pt-8 lg:grid-cols-[1.1fr_0.72fr_0.82fr]">
|
<SectionDivider className="mx-auto max-w-7xl pb-12 pt-2" />
|
||||||
|
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[1.15fr_0.7fr_0.85fr]">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="public-display text-xl font-bold text-[var(--tour-ink)]">VR360 Như Quỳnh</p>
|
<div className="flex items-center gap-3">
|
||||||
<p className="mt-3 max-w-[62ch] text-sm leading-6 text-[var(--muted-foreground)]">
|
<span className="grid h-11 w-11 shrink-0 place-items-center 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-[inset_0_1px_0_rgb(255_255_255_/_0.92)]">
|
||||||
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.
|
<HeritageSeal className="h-6 w-6 text-[var(--primary)]" />
|
||||||
|
</span>
|
||||||
|
<p className="public-display text-xl font-bold leading-tight text-[var(--tour-ink)]">VR360 Như Quỳnh</p>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 max-w-[58ch] text-sm leading-7 text-[var(--muted-foreground)]">
|
||||||
|
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ệ tham quan thực tế ảo VR360.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="grid gap-2 text-sm" aria-label="Liên kết nhanh">
|
<nav className="grid content-start gap-2.5 text-sm" aria-label="Liên kết nhanh">
|
||||||
<p className="font-bold text-[var(--tour-ink)]">Liên kết nhanh</p>
|
<p className="font-bold text-[var(--tour-ink)]">Liên kết nhanh</p>
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<Link key={item.href} href={item.href} className="w-fit text-[var(--muted-foreground)] transition-colors hover:text-[var(--tour-ink)]">
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="group inline-flex w-fit items-center gap-2 text-[var(--muted-foreground)] transition-colors hover:text-[var(--primary)]"
|
||||||
|
>
|
||||||
|
<span aria-hidden className="h-1 w-1 rotate-45 bg-[var(--primary)]/45 transition-transform group-hover:scale-150" />
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="grid gap-3 text-sm leading-6 text-[var(--muted-foreground)] lg:text-right">
|
<div className="grid content-start gap-3 text-sm leading-6 text-[var(--muted-foreground)] lg:text-right">
|
||||||
<p className="font-bold text-[var(--tour-ink)]">Thông tin liên hệ</p>
|
<p className="font-bold text-[var(--tour-ink)]">Thông tin liên hệ</p>
|
||||||
<p className="inline-flex items-start gap-2 lg:justify-end">
|
<p className="inline-flex items-start gap-2 lg:justify-end">
|
||||||
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-[var(--primary)]" strokeWidth={1.8} />
|
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-[var(--primary)]" strokeWidth={1.8} />
|
||||||
@@ -47,8 +60,10 @@ export function SiteFooter() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-8 max-w-7xl border-t border-[var(--surface-border)] pt-5 text-center text-sm font-semibold text-[var(--muted-foreground)]">
|
|
||||||
© 2026 VR360 Di sản Xã Như Quỳnh.
|
<div className="mx-auto mt-10 flex max-w-7xl flex-col items-center justify-between gap-2 border-t border-[var(--surface-border)] pt-5 text-sm font-semibold text-[var(--muted-foreground)] sm:flex-row">
|
||||||
|
<p>© 2026 VR360 Di sản Xã Như Quỳnh.</p>
|
||||||
|
<p className="text-[var(--muted-foreground)]/85">Chuyển đổi số di sản · giai đoạn 2026–2030</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Landmark, Menu, PlayCircle, X } from "lucide-react";
|
import { Menu, PlayCircle, X } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { activeTour, navigation } from "@/app/content";
|
import { activeTour, navigation } from "@/app/content";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
@@ -19,7 +20,7 @@ export function SiteHeader() {
|
|||||||
aria-label="VR360 Di sản Như Quỳnh"
|
aria-label="VR360 Di sản Như Quỳnh"
|
||||||
>
|
>
|
||||||
<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)]">
|
<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} />
|
<HeritageSeal className="h-6 w-6 text-[var(--primary)]" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="public-display block truncate text-base font-bold leading-tight text-[var(--ink-on-light)] sm:text-lg">
|
<span className="public-display block truncate text-base font-bold leading-tight text-[var(--ink-on-light)] sm:text-lg">
|
||||||
@@ -73,7 +74,7 @@ export function SiteHeader() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={`overflow-hidden border-t border-[var(--surface-border)] bg-[rgb(255_255_255_/_0.9)] transition-[max-height] duration-240 lg:hidden ${
|
className={`overflow-hidden border-t border-[var(--surface-border)] bg-[rgb(255_255_255_/_0.9)] transition-[max-height] duration-240 lg:hidden ${
|
||||||
mobileMenuOpen ? "max-h-80" : "max-h-0"
|
mobileMenuOpen ? "max-h-96" : "max-h-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="grid gap-2 px-4 py-4">
|
<div className="grid gap-2 px-4 py-4">
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
// Chữ tự gõ rồi xóa, lặp qua danh sách cụm từ — tạo điểm nhấn kể chuyện ở hero.
|
||||||
|
// Tôn trọng prefers-reduced-motion: hiển thị tĩnh cụm từ đầu (không gõ).
|
||||||
|
// Một effect tự lên lịch: mọi setState diễn ra TRONG callback của setTimeout nên
|
||||||
|
// không gây "cascading renders" (đúng khuyến nghị react-hooks).
|
||||||
|
export function Typewriter({
|
||||||
|
words,
|
||||||
|
typingSpeed = 95,
|
||||||
|
deletingSpeed = 45,
|
||||||
|
pause = 1600,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
words: string[];
|
||||||
|
typingSpeed?: number;
|
||||||
|
deletingSpeed?: number;
|
||||||
|
pause?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [text, setText] = useState(words[0] ?? "");
|
||||||
|
const stateRef = useRef({ wordIndex: 0, deleting: false });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (words.length <= 1) return;
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
const s = stateRef.current;
|
||||||
|
const current = words[s.wordIndex % words.length];
|
||||||
|
|
||||||
|
let delay: number;
|
||||||
|
if (!s.deleting && text === current) {
|
||||||
|
delay = pause; // gõ xong cụm hiện tại → dừng một nhịp rồi xóa
|
||||||
|
} else {
|
||||||
|
delay = s.deleting ? deletingSpeed : typingSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const word = words[s.wordIndex % words.length];
|
||||||
|
|
||||||
|
if (!s.deleting && text === word) {
|
||||||
|
s.deleting = true;
|
||||||
|
setText(word.slice(0, Math.max(word.length - 1, 0)));
|
||||||
|
} else if (s.deleting && text === "") {
|
||||||
|
s.deleting = false;
|
||||||
|
s.wordIndex = (s.wordIndex + 1) % words.length;
|
||||||
|
setText(words[s.wordIndex % words.length].slice(0, 1));
|
||||||
|
} else {
|
||||||
|
setText((prev) =>
|
||||||
|
s.deleting ? word.slice(0, prev.length - 1) : word.slice(0, prev.length + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [text, words, typingSpeed, deletingSpeed, pause]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
{text}
|
||||||
|
<span className="public-typewriter-caret" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroun
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
|
className="public-parallax-bg absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
|
||||||
style={{ backgroundImage: `url("${imageSrc}")` }}
|
style={{ backgroundImage: `url("${imageSrc}")` }}
|
||||||
/>
|
/>
|
||||||
<div aria-hidden className="absolute inset-0 bg-[rgb(243_247_240_/_0.12)]" />
|
<div aria-hidden className="absolute inset-0 bg-[rgb(243_247_240_/_0.12)]" />
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export const activeTour = {
|
|||||||
title: "Chùa Thái Lạc (Pháp Vân tự)",
|
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",
|
subtitle: "Di tích quốc gia, chạm khắc gỗ thời Trần",
|
||||||
href: "/tour/chua-thai-lac",
|
href: "/tour/chua-thai-lac",
|
||||||
heroImage: img("01.jpg"), // Chính diện cổng chùa
|
// Ảnh nền hero (CSS background) — toàn cảnh lễ hội múa rồng, ảnh thật của di tích.
|
||||||
|
heroImage: "/images/le-hoi/toan-canh-le-hoi.jpg",
|
||||||
|
// Ảnh lớn bên phải hero — nghi lễ rước kiệu (qua next/image).
|
||||||
|
heroFeatureImage: "/images/le-hoi/ruoc-kieu.jpg",
|
||||||
coverImage: img("08.jpg"), // Chính sảnh
|
coverImage: img("08.jpg"), // Chính sảnh
|
||||||
gateImage: img("01.jpg"), // Chính diện cổng chùa
|
gateImage: img("01.jpg"), // Chính diện cổng chùa
|
||||||
shrineImage: img("11.jpg"), // Chính điện
|
shrineImage: img("11.jpg"), // Chính điện
|
||||||
@@ -29,6 +32,126 @@ export const activeTour = {
|
|||||||
duration: "15-20 phút",
|
duration: "15-20 phút",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dải ảnh "không gian tiêu biểu" cho gallery cuộn ngang ở trang chủ — ảnh THẬT của
|
||||||
|
// Chùa Thái Lạc kèm chú thích đúng không gian, trình bày trong khung kính cao cấp.
|
||||||
|
export const featuredSpaces = [
|
||||||
|
{ file: "01.jpg", title: "Tam quan", note: "Cổng chùa chồng diêm hai tầng tám mái" },
|
||||||
|
{ file: "07.jpg", title: "Hồ Di Lặc", note: "Tượng Phật Di Lặc bên hồ cá Koi" },
|
||||||
|
{ file: "08.jpg", title: "Tiền sảnh", note: "Sân lát đá trước gác chuông" },
|
||||||
|
{ file: "10.jpg", title: "Tôn đài Đức Phật Thích Ca", note: "Đài lộ thiên giữa sân chùa" },
|
||||||
|
{ file: "11.jpg", title: "Vườn tháp cổ", note: "Những bảo tháp gạch trần cổ kính" },
|
||||||
|
{ file: "18.jpg", title: "Hành lang La Hán", note: "Dãy tượng La Hán dọc hành lang gỗ" },
|
||||||
|
{ file: "24.jpg", title: "Tiền đường", note: "Vì kèo chạm khắc gỗ thời Trần" },
|
||||||
|
{ file: "25.jpg", title: "Thượng điện", note: "Khám thờ gỗ và hệ cột cổ" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Video lễ hội truyền thống — tư liệu động bổ sung cho không gian di sản tĩnh.
|
||||||
|
// Gắn với tín ngưỡng Tứ Pháp (cầu mưa) của cư dân nông nghiệp Bắc Bộ, cùng hệ tín
|
||||||
|
// ngưỡng thờ Pháp Vân hiện diện ngay tại Chùa Thái Lạc của vùng Như Quỳnh.
|
||||||
|
export const festivalVideo = {
|
||||||
|
kicker: "Lễ hội & văn hóa",
|
||||||
|
title: "Lễ hội cầu mưa Lạc Hồng – Văn Lâm",
|
||||||
|
src: "/video/le-hoi-cau-mua-lac-hong-van-lam.mp4",
|
||||||
|
poster: "/images/le-hoi/san-chua-tu-tren-cao.jpg", // Ảnh thật sân chùa ngày hội
|
||||||
|
heading: "Lễ hội cầu mưa — nhịp sống tín ngưỡng Tứ Pháp",
|
||||||
|
description: [
|
||||||
|
"Lễ hội cầu mưa ở Lạc Hồng (Văn Lâm, Hưng Yên) là sinh hoạt tín ngưỡng lâu đời của cư dân nông nghiệp đồng bằng Bắc Bộ, gắn với hệ Tứ Pháp — Pháp Vân, Pháp Vũ, Pháp Lôi, Pháp Điện — cầu cho mưa thuận gió hòa, mùa màng tươi tốt.",
|
||||||
|
"Cùng dòng tín ngưỡng ấy, Chùa Thái Lạc (Pháp Vân tự) của vùng Như Quỳnh thờ vị thần Mây Pháp Vân, để lại dấu ấn đậm nét trên những mảng chạm khắc gỗ thời Trần còn lưu giữ đến nay.",
|
||||||
|
],
|
||||||
|
ctaLabel: "Khám phá VR360 Chùa Thái Lạc",
|
||||||
|
ctaHref: "/tour/chua-thai-lac",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Ảnh lễ hội thật của Chùa Pháp Vân (Thái Lạc) — Quang Trung, Lạc Hồng, Văn Lâm, Hưng Yên.
|
||||||
|
// Đặt tại public/images/le-hoi/ với tên ASCII đã tối ưu (resize + nén) để an toàn khi triển khai.
|
||||||
|
const leHoi = (file: string) => `/images/le-hoi/${file}`;
|
||||||
|
|
||||||
|
export type FestivalPhoto = {
|
||||||
|
src: string;
|
||||||
|
title: string;
|
||||||
|
note: string;
|
||||||
|
alt: string;
|
||||||
|
// Lớp span Tailwind cho bố cục bento ở breakpoint lg (dưới lg là lưới 2 cột đều).
|
||||||
|
span?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bộ ảnh "Khoảnh khắc lễ hội" — bố cục bento ở trang chủ. Ô đầu (đêm cầu an) làm ô chủ đạo.
|
||||||
|
export const festivalGallery: FestivalPhoto[] = [
|
||||||
|
{
|
||||||
|
src: leHoi("le-cau-an-ban-dem.jpg"),
|
||||||
|
title: "Lễ cầu an",
|
||||||
|
note: "Phật tử dâng đèn hoa sen trong đêm hội",
|
||||||
|
alt: "Phật tử áo tràng chắp tay bên đèn hoa sen trong lễ cầu an ban đêm tại Chùa Pháp Vân",
|
||||||
|
span: "lg:col-span-2 lg:row-span-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("toan-canh-le-hoi.jpg"),
|
||||||
|
title: "Toàn cảnh đám rước",
|
||||||
|
note: "Múa rồng và đoàn rước rợp sắc cờ",
|
||||||
|
alt: "Toàn cảnh đám rước lễ hội với hai đoàn múa rồng vàng giữa hàng cây",
|
||||||
|
span: "lg:col-span-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("ruoc-kieu.jpg"),
|
||||||
|
title: "Rước kiệu thánh",
|
||||||
|
note: "Kiệu sơn son thếp vàng giữa sân lễ",
|
||||||
|
alt: "Kiệu thánh sơn son thếp vàng được trai làng khiêng trong đám rước",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("doan-khieng-kieu.jpg"),
|
||||||
|
title: "Trai làng rước kiệu",
|
||||||
|
note: "Khí thế đoàn rước qua đường làng",
|
||||||
|
alt: "Đoàn trai làng cởi trần thắt đai đỏ vàng khiêng kiệu thánh qua đường làng",
|
||||||
|
span: "lg:row-span-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("mua-rong-cong-chua.jpg"),
|
||||||
|
title: "Múa rồng trước Tam quan",
|
||||||
|
note: "Sắc áo dài rực rỡ hai bên lối vào",
|
||||||
|
alt: "Màn múa rồng trước Tam quan chùa với hàng người mặc áo dài cầm quạt hai bên",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("nghi-thuc-san-chua.jpg"),
|
||||||
|
title: "Nghi thức tế lễ",
|
||||||
|
note: "Đội tế xếp hàng giữa sân chùa",
|
||||||
|
alt: "Đội tế xếp hàng làm nghi thức giữa sân gạch chùa rợp cờ Phật giáo nhìn từ trên cao",
|
||||||
|
span: "lg:col-span-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("kieu-long-vang.jpg"),
|
||||||
|
title: "Kiệu dưới tán lọng",
|
||||||
|
note: "Lọng vàng che kiệu trong đám rước",
|
||||||
|
alt: "Kiệu thánh dưới tán lọng vàng giữa dòng người trẩy hội",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("san-chua-tu-tren-cao.jpg"),
|
||||||
|
title: "Sân chùa ngày hội",
|
||||||
|
note: "Rợp lọng vàng nhìn từ trên cao",
|
||||||
|
alt: "Sân chùa ngày hội rợp lọng vàng và dòng người nhìn từ trên cao",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("doan-ruoc-bo-lao.jpg"),
|
||||||
|
title: "Đoàn rước bô lão",
|
||||||
|
note: "Các cụ trong trang phục tế truyền thống",
|
||||||
|
alt: "Đoàn các cụ bô lão mặc áo tế truyền thống cầm gậy đi trong đám rước",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: leHoi("tru-tri-le-hoi.jpg"),
|
||||||
|
title: "Sư trụ trì chủ lễ",
|
||||||
|
note: "Chủ lễ giữa đoàn rước long trọng",
|
||||||
|
alt: "Sư trụ trì khoác áo cà sa vàng đi giữa hai hàng người cầm lọng và cờ lễ",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ảnh phần đầu (hero) của các trang con — dùng ảnh lễ hội thật thay cho ảnh panorama bị méo.
|
||||||
|
export const subpageHero = {
|
||||||
|
gioiThieu: leHoi("mua-rong-cong-chua.jpg"),
|
||||||
|
diTich: leHoi("nghi-thuc-san-chua.jpg"),
|
||||||
|
diTichFeature: leHoi("kieu-long-vang.jpg"),
|
||||||
|
coVat: leHoi("tru-tri-le-hoi.jpg"),
|
||||||
|
lienHe: leHoi("le-cau-an-ban-dem.jpg"), // Đêm cầu an — trầm lắng, mời gọi, hợp trang liên hệ
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const navigation = [
|
export const navigation = [
|
||||||
{ href: "/", label: "Trang chủ" },
|
{ href: "/", label: "Trang chủ" },
|
||||||
{ href: "/gioi-thieu", label: "Giới thiệu" },
|
{ href: "/gioi-thieu", label: "Giới thiệu" },
|
||||||
@@ -99,13 +222,13 @@ export type HeritageSite = {
|
|||||||
location?: string;
|
location?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function nationalStub(id: string, name: string, type: string, desc: string): HeritageSite {
|
function nationalStub(id: string, name: string, type: string, imageFile: string, desc: string): HeritageSite {
|
||||||
return stub(id, name, type, "Cấp quốc gia", desc);
|
return stub(id, name, type, "Cấp quốc gia", imageFile, desc);
|
||||||
}
|
}
|
||||||
function provincialStub(id: string, name: string, type: string, desc: string): HeritageSite {
|
function provincialStub(id: string, name: string, type: string, imageFile: string, desc: string): HeritageSite {
|
||||||
return stub(id, name, type, "Cấp tỉnh", desc);
|
return stub(id, name, type, "Cấp tỉnh", imageFile, desc);
|
||||||
}
|
}
|
||||||
function stub(id: string, name: string, type: string, level: HeritageLevel, desc: string): HeritageSite {
|
function stub(id: string, name: string, type: string, level: HeritageLevel, imageFile: string, desc: string): HeritageSite {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -113,7 +236,9 @@ function stub(id: string, name: string, type: string, level: HeritageLevel, desc
|
|||||||
level,
|
level,
|
||||||
status: "Đang bổ sung tư liệu",
|
status: "Đang bổ sung tư liệu",
|
||||||
shortDescription: desc,
|
shortDescription: desc,
|
||||||
image: img("08.jpg"), // ảnh tạm (Chính sảnh) dùng chung cho các di tích chưa có tư liệu
|
// Ảnh đại diện tạm — chọn các góc khác nhau từ kho 26 ảnh để lưới di tích không bị
|
||||||
|
// lặp một ảnh. Các di tích này chưa số hóa riêng nên dùng ảnh không gian tiêu biểu.
|
||||||
|
image: img(imageFile),
|
||||||
detailUrl: `/di-tich/${id}`,
|
detailUrl: `/di-tich/${id}`,
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
location: "Xã Như Quỳnh, tỉnh Hưng Yên",
|
location: "Xã Như Quỳnh, tỉnh Hưng Yên",
|
||||||
@@ -140,32 +265,32 @@ export const heritageSites: HeritageSite[] = [
|
|||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
location: "Xã Như Quỳnh, tỉnh Hưng Yên",
|
location: "Xã Như Quỳnh, tỉnh Hưng Yên",
|
||||||
},
|
},
|
||||||
nationalStub("den-ghenh", "Đền Ghênh (Đền Ỷ Lan)", "Đền",
|
nationalStub("den-ghenh", "Đền Ghênh (Đền Ỷ Lan)", "Đền", "02.jpg",
|
||||||
"Đề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."),
|
"Đề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",
|
nationalStub("tu-vu-ho-truong", "Từ Vũ họ Trương", "Di tích khác", "05.jpg",
|
||||||
"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."),
|
"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",
|
nationalStub("bai-da-den-tu-vu", "Bãi đá đền Từ Vũ", "Di tích khác", "06.jpg",
|
||||||
"Cụm di tích khảo cổ và 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."),
|
"Cụm di tích khảo cổ và 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",
|
nationalStub("dinh-at", "Đình Ất", "Đình", "09.jpg",
|
||||||
"Đì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."),
|
"Đì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",
|
nationalStub("chua-hanh-lac", "Chùa Hành Lạc", "Chùa", "10.jpg",
|
||||||
"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."),
|
"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",
|
nationalStub("dinh-hanh-lac", "Đình Hành Lạc", "Đình", "07.jpg",
|
||||||
"Đì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."),
|
"Đì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",
|
nationalStub("chua-ong", "Chùa Ông", "Chùa", "24.jpg",
|
||||||
"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."),
|
"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",
|
nationalStub("chua-vinh-thai", "Chùa Vĩnh Thái", "Chùa", "25.jpg",
|
||||||
"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."),
|
"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."),
|
||||||
provincialStub("dinh-le-xa", "Đình Lê Xá", "Đình",
|
provincialStub("dinh-le-xa", "Đình Lê Xá", "Đình", "15.jpg",
|
||||||
"Đì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."),
|
"Đì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",
|
provincialStub("dinh-ngo-xuyen", "Đình Ngô Xuyên", "Đình", "16.jpg",
|
||||||
"Đì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."),
|
"Đì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",
|
provincialStub("chua-tu-kinh", "Chùa Từ Kính", "Chùa", "26.jpg",
|
||||||
"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."),
|
"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",
|
provincialStub("cung-chi-nguyen", "Cung Chí Nguyên", "Di tích khác", "17.jpg",
|
||||||
"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\"."),
|
"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",
|
provincialStub("van-mieu-nhu-quynh", "Văn miếu Như Quỳnh", "Văn miếu", "19.jpg",
|
||||||
"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."),
|
"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",
|
provincialStub("dau-tich-chua-la", "Dấu tích Chùa Lá", "Chùa", "18.jpg",
|
||||||
"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."),
|
"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."),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ export default async function HeritageDetailPage({
|
|||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<section className="public-subpage-hero relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
<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" />
|
<Image src={site.image} alt={site.name} fill preload sizes="100vw" className="public-subpage-hero-image public-parallax-bg 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-subpage-hero-content mx-auto flex min-h-[inherit] max-w-7xl flex-col justify-end 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)]">
|
<Link href="/di-tich" className="inline-flex w-fit items-center gap-2 text-sm font-bold text-[rgb(243_236_221_/_0.86)] transition hover:text-[var(--tour-gold-light)]">
|
||||||
<ArrowLeft className="h-4 w-4" strokeWidth={1.8} />
|
<ArrowLeft className="h-4 w-4" strokeWidth={1.8} />
|
||||||
Tất cả di tích
|
Tất cả di tích
|
||||||
</Link>
|
</Link>
|
||||||
@@ -54,12 +54,12 @@ export default async function HeritageDetailPage({
|
|||||||
<span className="public-chip">{site.type}</span>
|
<span className="public-chip">{site.type}</span>
|
||||||
<span className="public-chip">{site.status}</span>
|
<span className="public-chip">{site.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="public-gradient-text public-heading-safe mt-4 text-[clamp(2.4rem,5.4vw,4.6rem)] font-extrabold">
|
<h1 className="public-hero-display public-heading-safe mt-5 text-[clamp(2.4rem,5.4vw,4.6rem)] font-extrabold">
|
||||||
{site.name}
|
{site.name}
|
||||||
</h1>
|
</h1>
|
||||||
{site.location ? (
|
{site.location ? (
|
||||||
<p className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[var(--foreground)]/82">
|
<p className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[rgb(243_236_221_/_0.86)]">
|
||||||
<MapPin className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
|
<MapPin className="h-4 w-4 text-[var(--tour-gold-light)]" strokeWidth={1.8} />
|
||||||
{site.location}
|
{site.location}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -68,9 +68,12 @@ export default async function HeritageDetailPage({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mx-auto max-w-3xl px-4 py-14 sm:px-6 lg:py-20">
|
<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">
|
<p className="public-kicker">Về di tích</p>
|
||||||
|
<div className="mt-5 grid gap-5 text-base leading-8 text-[var(--foreground)]/82">
|
||||||
{paragraphs.map((text, i) => (
|
{paragraphs.map((text, i) => (
|
||||||
<p key={i}>{text}</p>
|
<p key={i} className={i === 0 ? "text-lg leading-9 text-[var(--tour-ink)]" : undefined}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import Link from "next/link";
|
|||||||
import { Mail, MapPinned } from "lucide-react";
|
import { Mail, MapPinned } from "lucide-react";
|
||||||
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
||||||
import { CulturalSpaceExplorer } from "@/app/components/landing/CulturalSpaceExplorer";
|
import { CulturalSpaceExplorer } from "@/app/components/landing/CulturalSpaceExplorer";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
import { activeTour, heritageSites } from "@/app/content";
|
import { heritageSites, subpageHero } from "@/app/content";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Di tích | VR360 Như Quỳnh",
|
title: "Di tích | VR360 Như Quỳnh",
|
||||||
@@ -21,21 +22,21 @@ export default function HeritageListPage() {
|
|||||||
|
|
||||||
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
||||||
<Image
|
<Image
|
||||||
src={activeTour.gateImage}
|
src={subpageHero.diTich}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
priority
|
preload
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
className="public-subpage-hero-image object-cover"
|
className="public-subpage-hero-image public-parallax-bg object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="public-subpage-hero-content mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:min-h-[580px] lg:grid-cols-[0.95fr_0.7fr] lg:items-center lg:py-22">
|
<div className="public-subpage-hero-content mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:min-h-[600px] lg:grid-cols-[0.95fr_0.7fr] lg:items-center lg:py-24">
|
||||||
<div className="public-rise public-hero-rule w-full min-w-0 max-w-4xl">
|
<div className="public-rise public-hero-rule w-full min-w-0 max-w-4xl">
|
||||||
<p className="public-kicker">Danh sách di tích và tour VR360</p>
|
<p className="public-kicker">Danh sách di tích và tour VR360</p>
|
||||||
<h1 className="public-gradient-text public-heading-safe public-subpage-title mt-4 text-[clamp(2.55rem,5.8vw,5.35rem)] font-extrabold">
|
<h1 className="public-hero-display public-heading-safe public-subpage-title mt-5 text-[clamp(2.55rem,5.8vw,5.35rem)] font-extrabold">
|
||||||
Hệ thống di tích Xã Như Quỳnh
|
Hệ thống di tích <span className="public-display-accent">Xã Như Quỳnh</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="public-subpage-copy mt-6 text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
|
<p className="public-subpage-copy mt-6 text-base leading-8 sm:text-lg">
|
||||||
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-7 flex flex-wrap gap-3">
|
<div className="mt-7 flex flex-wrap gap-3">
|
||||||
@@ -50,32 +51,37 @@ export default function HeritageListPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="public-image-stage public-hero-photo public-stagger-item hidden lg:block">
|
<div className="public-image-stage public-hero-photo public-stagger-item relative hidden lg:block">
|
||||||
<Image
|
<Image
|
||||||
src={activeTour.coverImage}
|
src={subpageHero.diTichFeature}
|
||||||
alt="Di sản văn hóa Xã Như Quỳnh"
|
alt="Kiệu rước dưới tán lọng vàng trong lễ hội Chùa Pháp Vân (Thái Lạc)"
|
||||||
fill
|
fill
|
||||||
sizes="38vw"
|
sizes="38vw"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
|
<HeritageSeal className="absolute right-4 top-4 z-10 h-9 w-9 text-[var(--tour-gold-light)] opacity-90 drop-shadow-[0_2px_8px_rgb(6_22_16_/_0.55)]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<CulturalSpaceExplorer sites={heritageSites} />
|
<CulturalSpaceExplorer sites={heritageSites} />
|
||||||
|
|
||||||
<section className="px-4 py-14 sm:px-6 lg:py-20">
|
<section className="px-4 py-16 sm:px-6 lg:py-24">
|
||||||
<div className="public-panel public-glow mx-auto grid max-w-7xl gap-5 rounded-[8px] p-6 lg:grid-cols-[1fr_auto] lg:items-center lg:p-8">
|
<div className="public-on-dark relative mx-auto grid max-w-7xl gap-6 overflow-hidden rounded-[22px] border border-[rgb(214_180_104_/_0.3)] bg-[linear-gradient(135deg,#11483593,#0a261d_72%),linear-gradient(135deg,#0e3a2c,#0a261d)] p-8 text-white shadow-[0_40px_110px_rgb(8_31_23_/_0.42)] lg:grid-cols-[1fr_auto] lg:items-center lg:p-12">
|
||||||
<div>
|
<HeritageSeal
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute -right-10 -top-10 h-48 w-48 text-[var(--tour-gold-light)] opacity-[0.08]"
|
||||||
|
/>
|
||||||
|
<div className="relative min-w-0">
|
||||||
<p className="public-kicker">Đề xuất địa điểm</p>
|
<p className="public-kicker">Đề xuất địa điểm</p>
|
||||||
<h2 className="public-heading-safe mt-2 text-3xl font-bold text-[var(--tour-ink)] sm:text-4xl">
|
<h2 className="public-display public-heading-safe mt-3 text-3xl font-bold text-[#f8f1e2] sm:text-[2.4rem]">
|
||||||
Bạn muốn đề xuất thêm địa điểm văn hóa?
|
Bạn muốn đề xuất thêm <span className="public-display-accent text-[var(--tour-gold-light)]">địa điểm văn hóa?</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-[66ch] text-sm leading-6 text-[var(--foreground)]/76 sm:text-base">
|
<p className="mt-4 max-w-[66ch] text-sm leading-7 text-[rgb(243_236_221_/_0.82)] sm:text-base">
|
||||||
Gửi thông tin, hình ảnh hoặc câu chuyện liên quan để cùng hoàn thiện kho tư liệu số của Xã Như Quỳnh.
|
Gửi thông tin, hình ảnh hoặc câu chuyện liên quan để cùng hoàn thiện kho tư liệu số của Xã Như Quỳnh.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/lien-he" className="public-cta w-fit">
|
<Link href="/lien-he" className="public-cta relative w-fit">
|
||||||
<Mail className="h-4 w-4" strokeWidth={1.8} />
|
<Mail className="h-4 w-4" strokeWidth={1.8} />
|
||||||
Liên hệ với chúng tôi
|
Liên hệ với chúng tôi
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Home, RotateCcw } from "lucide-react";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Ghi log lỗi để theo dõi (có thể nối với dịch vụ giám sát sau này).
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="public-page flex min-h-[100dvh] flex-col items-center justify-center bg-[var(--background)] px-4 py-24 text-center text-[var(--foreground)] sm:px-6">
|
||||||
|
<HeritageSeal className="h-16 w-16 text-[var(--primary)] opacity-85" />
|
||||||
|
<p className="public-kicker public-kicker--center mt-7">Đã xảy ra sự cố</p>
|
||||||
|
<h1 className="public-display public-heading-safe mt-3 text-[clamp(1.9rem,5vw,3rem)] font-bold text-[var(--tour-ink)]">
|
||||||
|
Không thể tải nội dung
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-[52ch] text-base leading-7 text-[var(--muted-foreground)] sm:text-lg">
|
||||||
|
Đã có lỗi không mong muốn khi hiển thị trang. Vui lòng thử lại; nếu sự cố tiếp diễn, hãy
|
||||||
|
quay về trang chủ và truy cập lại sau ít phút.
|
||||||
|
</p>
|
||||||
|
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
<button type="button" onClick={reset} className="public-cta">
|
||||||
|
<RotateCcw className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
Thử lại
|
||||||
|
</button>
|
||||||
|
<Link href="/" className="public-cta-secondary">
|
||||||
|
<Home className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
Về trang chủ
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
||||||
|
import { CountUp } from "@/app/components/landing/CountUp";
|
||||||
|
import { HeritageSeal, SectionDivider } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
import { activeTour, antiquities, heritageSites, img } from "@/app/content";
|
import { activeTour, antiquities, heritageSites, img, subpageHero } from "@/app/content";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Giới thiệu | VR360 Như Quỳnh",
|
title: "Giới thiệu | VR360 Như Quỳnh",
|
||||||
@@ -60,14 +62,14 @@ export default function AboutPage() {
|
|||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
<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" />
|
<Image src={subpageHero.gioiThieu} alt="" fill preload sizes="100vw" className="public-subpage-hero-image public-parallax-bg 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-subpage-hero-content mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:py-28">
|
||||||
<div className="public-rise public-hero-rule max-w-4xl">
|
<div className="public-rise public-hero-rule max-w-4xl">
|
||||||
<p className="public-kicker">Giới thiệu</p>
|
<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">
|
<h1 className="public-hero-display public-heading-safe mt-5 text-[clamp(2.55rem,5.8vw,5rem)] font-extrabold">
|
||||||
Di sản văn hóa Xã Như Quỳnh
|
Di sản văn hóa <span className="public-display-accent">Xã Như Quỳnh</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
|
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 sm:text-lg">
|
||||||
Một vùng đất văn hiến với hệ thống di tích trải dài nhiều thế kỷ, nay được gìn giữ và lan tỏa
|
Một vùng đất văn hiến với hệ thống di tích trải dài nhiều thế kỷ, nay được gìn giữ và lan tỏa
|
||||||
bằng công nghệ số.
|
bằng công nghệ số.
|
||||||
</p>
|
</p>
|
||||||
@@ -94,13 +96,17 @@ export default function AboutPage() {
|
|||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
|
<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-2 lg:grid-cols-4 lg:py-18">
|
<div className="mx-auto grid max-w-7xl gap-px overflow-hidden rounded-[16px] border border-[var(--surface-border)] bg-[var(--surface-border)] px-0 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{stats.map((s) => (
|
{stats.map((s) => (
|
||||||
<div key={s.label} className="public-panel public-stagger-item rounded-[14px] p-6 text-center">
|
<div
|
||||||
<p className="public-gradient-text font-display-vn text-[2.6rem] font-extrabold leading-none [font-variant-numeric:tabular-nums]">
|
key={s.label}
|
||||||
{s.value}
|
className="public-stagger-item flex flex-col items-center bg-[rgb(255_255_255_/_0.5)] px-6 py-10 text-center"
|
||||||
|
>
|
||||||
|
<p className="public-display text-[clamp(2.6rem,5.4vw,3.5rem)] font-bold leading-none text-[var(--primary)] [font-variant-numeric:tabular-nums]">
|
||||||
|
<CountUp value={Number(s.value)} />
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)]">{s.label}</p>
|
<span aria-hidden className="mt-3 h-px w-8 bg-[rgb(142_95_11_/_0.4)]" />
|
||||||
|
<p className="mt-3 max-w-[18ch] text-sm leading-6 text-[var(--muted-foreground)]">{s.label}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +148,9 @@ export default function AboutPage() {
|
|||||||
Từ tư liệu đến trải nghiệm số
|
Từ tư liệu đến trải nghiệm số
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<ol className="mt-10 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="public-timeline relative mt-10">
|
||||||
|
<span className="public-timeline-line hidden xl:block" aria-hidden="true" />
|
||||||
|
<ol className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{steps.map((step, index) => {
|
{steps.map((step, index) => {
|
||||||
const Icon = step.icon;
|
const Icon = step.icon;
|
||||||
return (
|
return (
|
||||||
@@ -158,10 +166,12 @@ export default function AboutPage() {
|
|||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Featured tour CTA */}
|
{/* Featured tour CTA */}
|
||||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-20">
|
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-24">
|
||||||
|
<SectionDivider className="mx-auto mb-14 max-w-3xl" />
|
||||||
<div className="public-card group grid overflow-hidden rounded-[18px] lg:grid-cols-[1.05fr_0.95fr]">
|
<div className="public-card group grid overflow-hidden rounded-[18px] lg:grid-cols-[1.05fr_0.95fr]">
|
||||||
<div className="relative min-h-[20rem]">
|
<div className="relative min-h-[20rem]">
|
||||||
<Image
|
<Image
|
||||||
@@ -171,6 +181,11 @@ export default function AboutPage() {
|
|||||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
className="object-cover transition-transform duration-700 group-hover:scale-[1.03]"
|
className="object-cover transition-transform duration-700 group-hover:scale-[1.03]"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0 bg-[linear-gradient(120deg,transparent_55%,rgb(8_31_23_/_0.32))]"
|
||||||
|
/>
|
||||||
|
<HeritageSeal className="absolute right-4 top-4 h-9 w-9 text-[var(--tour-gold-light)] opacity-90 drop-shadow-[0_2px_8px_rgb(6_22_16_/_0.55)]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-center p-7 sm:p-10">
|
<div className="flex flex-col justify-center p-7 sm:p-10">
|
||||||
<p className="public-kicker">Điểm đến tiêu biểu</p>
|
<p className="public-kicker">Điểm đến tiêu biểu</p>
|
||||||
|
|||||||
@@ -315,8 +315,12 @@ a {
|
|||||||
max-width: min(15ch, 100%);
|
max-width: min(15ch, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hero trang con — bản ĐIỆN ẢNH: ảnh thật chiếm sáng, phủ lớp sẫm xanh rêu để chữ ngà
|
||||||
|
nổi bật. Tạo "mảng sẫm" tương phản với thân trang sáng, cảm giác triển lãm cao cấp. */
|
||||||
.public-subpage-hero {
|
.public-subpage-hero {
|
||||||
min-height: min(760px, calc(100dvh - 72px));
|
min-height: min(760px, calc(100dvh - 72px));
|
||||||
|
background: #0a261d;
|
||||||
|
color: #f3ecdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-subpage-hero--compact {
|
.public-subpage-hero--compact {
|
||||||
@@ -324,8 +328,30 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.public-subpage-hero-image {
|
.public-subpage-hero-image {
|
||||||
opacity: 0.62;
|
opacity: 0.96;
|
||||||
filter: saturate(1.08) contrast(1.05) brightness(1.02);
|
filter: saturate(1.12) contrast(1.06) brightness(0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trong hero điện ảnh: nhãn triện sáng hơn, chữ phụ màu ngà mờ. */
|
||||||
|
.public-subpage-hero .public-kicker {
|
||||||
|
color: var(--tour-gold-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-kicker::before {
|
||||||
|
border-color: color-mix(in oklab, var(--tour-gold-light) 70%, transparent);
|
||||||
|
background: radial-gradient(circle, var(--tour-gold-light) 0 26%, transparent 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-chip {
|
||||||
|
border-color: rgb(224 207 159 / 0.32);
|
||||||
|
background: rgb(10 38 29 / 0.5);
|
||||||
|
color: #f3ecdd;
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-chip svg {
|
||||||
|
color: var(--tour-gold-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-subpage-hero::before,
|
.public-subpage-hero::before,
|
||||||
@@ -339,16 +365,29 @@ a {
|
|||||||
.public-subpage-hero::before {
|
.public-subpage-hero::before {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at 24% 48%, rgb(243 247 240 / 0.98) 0%, rgb(243 247 240 / 0.88) 36%, rgb(243 247 240 / 0.48) 58%, transparent 78%),
|
linear-gradient(90deg, rgb(7 25 19 / 0.94) 0%, rgb(8 30 23 / 0.78) 34%, rgb(9 34 26 / 0.32) 66%, transparent 100%),
|
||||||
linear-gradient(90deg, rgb(243 247 240 / 0.98) 0%, rgb(243 247 240 / 0.9) 40%, rgb(243 247 240 / 0.36) 72%, rgb(243 247 240 / 0.12) 100%),
|
linear-gradient(0deg, rgb(6 22 16 / 0.88) 0%, rgb(7 26 20 / 0.34) 38%, transparent 66%),
|
||||||
linear-gradient(180deg, rgb(243 247 240 / 0.18), rgb(243 247 240 / 0.96) 100%);
|
radial-gradient(125% 130% at 28% 26%, transparent 38%, rgb(5 19 14 / 0.62) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mép dưới hòa nhẹ vào thân trang sáng + một sợi chỉ vàng mảnh phân tách tinh tế. */
|
||||||
.public-subpage-hero::after {
|
.public-subpage-hero::after {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
inset: auto 0 0 0;
|
inset: auto 0 0 0;
|
||||||
height: 44%;
|
height: 30%;
|
||||||
background: linear-gradient(180deg, transparent, rgb(243 247 240 / 0.98));
|
background: linear-gradient(180deg, transparent 0%, transparent 58%, rgb(243 247 240 / 0.14) 86%, rgb(243 247 240 / 0.32) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-subpage-copy {
|
||||||
|
color: rgb(243 236 221 / 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-hero-rule::before {
|
||||||
|
background: linear-gradient(90deg, transparent, rgb(224 207 159 / 0.5), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-subpage-hero .public-hero-rule::after {
|
||||||
|
background: linear-gradient(90deg, rgb(224 207 159 / 0.46), rgb(141 146 118 / 0.32), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-subpage-hero-content {
|
.public-subpage-hero-content {
|
||||||
@@ -562,13 +601,13 @@ a {
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.public-subpage-hero-image {
|
.public-subpage-hero-image {
|
||||||
opacity: 0.44;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-subpage-hero::before {
|
.public-subpage-hero::before {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgb(243 247 240 / 0.84) 0%, rgb(243 247 240 / 0.78) 52%, rgb(243 247 240 / 0.96) 100%),
|
linear-gradient(180deg, rgb(7 25 19 / 0.46) 0%, rgb(7 25 19 / 0.34) 30%, rgb(6 22 16 / 0.82) 100%),
|
||||||
linear-gradient(90deg, rgb(243 247 240 / 0.94), rgb(243 247 240 / 0.64));
|
linear-gradient(90deg, rgb(7 25 19 / 0.6), rgb(7 25 19 / 0.28) 80%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,8 +636,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.public-page .public-heading-safe {
|
.public-page .public-heading-safe {
|
||||||
font-size: clamp(1.85rem, 8.7vw, 2.35rem);
|
font-size: clamp(1.6rem, 7.4vw, 2.15rem);
|
||||||
line-height: 1.12;
|
line-height: 1.14;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-page .public-hero-title {
|
.public-page .public-hero-title {
|
||||||
@@ -850,12 +889,44 @@ a {
|
|||||||
transform: translateX(22%);
|
transform: translateX(22%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nhãn "triện di sản" — thay cho eyebrow chữ hoa lặp lại (vốn là dấu hiệu AI rõ rệt).
|
||||||
|
Một dấu triện vàng hình thoi + chữ thường, đọc như bảng tên hiện vật trong bảo tàng;
|
||||||
|
là một thành tố thương hiệu CÓ CHỦ ĐÍCH, không phải khung sườn phản xạ. */
|
||||||
.public-kicker {
|
.public-kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0.005em;
|
||||||
text-transform: uppercase;
|
line-height: 1.25;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-kicker::before {
|
||||||
|
content: "";
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 0.82rem;
|
||||||
|
height: 0.82rem;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border: 1.5px solid color-mix(in oklab, var(--primary) 70%, transparent);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: radial-gradient(circle, var(--primary) 0 26%, transparent 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-kicker--center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trên nền sẫm (panel điện ảnh, CTA tối): nhãn triện chuyển sang vàng sáng cho dễ đọc. */
|
||||||
|
.public-on-dark .public-kicker {
|
||||||
|
color: var(--tour-gold-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-on-dark .public-kicker::before {
|
||||||
|
border-color: color-mix(in oklab, var(--tour-gold-light) 70%, transparent);
|
||||||
|
background: radial-gradient(circle, var(--tour-gold-light) 0 26%, transparent 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-cta,
|
.public-cta,
|
||||||
@@ -978,13 +1049,29 @@ a {
|
|||||||
padding-top: 0.9rem;
|
padding-top: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tiêu đề hiển thị: màu mực xanh rêu đậm ĐẶC (bỏ gradient-clip để tránh "tell" AI và
|
||||||
|
tăng tương phản, độ nét). Nhấn nhá chuyển sang từ điểm nhấn .public-display-accent. */
|
||||||
.public-gradient-text {
|
.public-gradient-text {
|
||||||
color: transparent;
|
color: var(--tour-ink);
|
||||||
background: linear-gradient(90deg, #0e3427, #8e5f0b 54%, #245f4b);
|
}
|
||||||
background-size: 180% 100%;
|
|
||||||
-webkit-background-clip: text;
|
/* Từ/cụm nhấn trong tiêu đề: serif nghiêng, màu vàng đồng — sang, có nhịp, đặc một màu. */
|
||||||
background-clip: text;
|
.public-display-accent {
|
||||||
-webkit-text-fill-color: transparent;
|
color: var(--primary);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tiêu đề hero trên nền ảnh sẫm (điện ảnh): chữ sáng ngà, viền sáng nhẹ cho dễ đọc. */
|
||||||
|
.public-hero-display {
|
||||||
|
color: #f8f1e2;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-shadow: 0 1px 28px rgb(6 22 16 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-hero-display .public-display-accent {
|
||||||
|
color: var(--tour-gold-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-shimmer::after {
|
.public-shimmer::after {
|
||||||
@@ -1344,3 +1431,226 @@ a {
|
|||||||
transform: translate3d(-50%, 0, 0);
|
transform: translate3d(-50%, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
HIỆU ỨNG CHUYỂN ĐỘNG NÂNG CAO (bản "mạnh tay" — vẫn giữ gu di sản)
|
||||||
|
Tất cả gắn vào animation-timeline: view()/scroll() (gốc trình duyệt, không
|
||||||
|
tốn JS theo cuộn) nên cực mượt; trình duyệt không hỗ trợ sẽ nhảy thẳng tới
|
||||||
|
trạng thái cuối (hiển thị đầy đủ) — an toàn, không kẹt nội dung.
|
||||||
|
─────────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* 1) Nâng cấp grade ảnh — ấm & sâu hơn nhưng vẫn tự nhiên (giữ ảnh thật). */
|
||||||
|
.public-image-stage img,
|
||||||
|
.public-card img,
|
||||||
|
.public-media img,
|
||||||
|
.public-gallery-card img {
|
||||||
|
filter: saturate(1.12) contrast(1.06) brightness(1.015);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2) SPLIT REVEAL — thanh màu quét ngang tiết lộ nội dung khi cuộn tới. */
|
||||||
|
.public-split-reveal {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-split-reveal::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5;
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--tour-jade) 88%);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.public-split-reveal::after {
|
||||||
|
animation: public-split-bar both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 4% cover 34%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-split-reveal > * {
|
||||||
|
animation: public-split-content both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 6% cover 34%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes public-split-bar {
|
||||||
|
0% {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
46% {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
54% {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes public-split-content {
|
||||||
|
0%,
|
||||||
|
42% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, 14px, 0);
|
||||||
|
}
|
||||||
|
64%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3) TIMELINE — đường nối vẽ dần theo cuộn (hàng mốc "Hành trình số hóa"). */
|
||||||
|
.public-timeline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
top: var(--timeline-top, 2.6rem);
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--tour-jade) 64%, transparent);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.public-timeline-line {
|
||||||
|
animation: public-line-grow both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 16% cover 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes public-line-grow {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4) PARALLAX HERO — lớp ảnh nền trôi chậm hơn nội dung khi cuộn.
|
||||||
|
Bọc trong @supports để trình duyệt thiếu scroll-timeline giữ bố cục tĩnh. */
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.public-parallax-bg {
|
||||||
|
animation: public-parallax-drift linear both;
|
||||||
|
animation-timeline: scroll(root);
|
||||||
|
animation-range: 0 80vh;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes public-parallax-drift {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0) scale(1.12);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 9%, 0) scale(1.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5) HORIZONTAL SCROLL — dải cuộn ngang có scroll-snap (thuần CSS, không jank). */
|
||||||
|
.public-hscroll {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-padding-inline: 1rem;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--primary) transparent;
|
||||||
|
padding-bottom: 0.9rem;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-hscroll > * {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-hscroll::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-hscroll::-webkit-scrollbar-track {
|
||||||
|
background: rgb(25 76 55 / 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-hscroll::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--tour-jade));
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-gallery-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: clamp(15rem, 72vw, 21rem);
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
background: rgb(14 52 39 / 0.1);
|
||||||
|
box-shadow:
|
||||||
|
0 24px 66px rgb(37 75 54 / 0.16),
|
||||||
|
inset 0 1px 0 rgb(255 255 255 / 0.4);
|
||||||
|
transition:
|
||||||
|
transform 360ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
box-shadow 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-gallery-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow:
|
||||||
|
0 34px 92px rgb(37 75 54 / 0.24),
|
||||||
|
0 0 0 1px rgb(169 111 18 / 0.24),
|
||||||
|
inset 0 1px 0 rgb(255 255 255 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-gallery-card img {
|
||||||
|
transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-gallery-card:hover img {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6) TYPEWRITER — con trỏ nhấp nháy cho dòng chữ tự gõ ở hero. */
|
||||||
|
.public-typewriter-caret {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5ch;
|
||||||
|
height: 1.02em;
|
||||||
|
margin-left: 0.1em;
|
||||||
|
vertical-align: -0.14em;
|
||||||
|
background: linear-gradient(180deg, var(--primary), var(--tour-jade));
|
||||||
|
border-radius: 1px;
|
||||||
|
animation: public-caret-blink 1.05s steps(1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes public-caret-blink {
|
||||||
|
0%,
|
||||||
|
48% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
49%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<rect width="48" height="48" rx="11" fill="#0e3a2c" />
|
||||||
|
<g fill="none" stroke="#dcc07f" stroke-width="1.6" stroke-linejoin="round">
|
||||||
|
<circle cx="24" cy="24" r="16.5" />
|
||||||
|
<rect x="14" y="14" width="20" height="20" />
|
||||||
|
<rect x="14" y="14" width="20" height="20" transform="rotate(45 24 24)" />
|
||||||
|
<circle cx="24" cy="24" r="4" />
|
||||||
|
</g>
|
||||||
|
<circle cx="24" cy="24" r="1.7" fill="#dcc07f" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 491 B |
@@ -1,9 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
import { Clock, Globe2, ImagePlus, Landmark, MapPin, MessageSquareText, Phone, Send } from "lucide-react";
|
import { Clock, Globe2, ImagePlus, Landmark, MapPin, MessageSquareText, Phone, Send } from "lucide-react";
|
||||||
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
import { contact } from "@/app/content";
|
import { contact, subpageHero } from "@/app/content";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Liên hệ | VR360 Như Quỳnh",
|
title: "Liên hệ | VR360 Như Quỳnh",
|
||||||
@@ -28,17 +30,20 @@ export default function ContactPage() {
|
|||||||
<main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
|
<main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<section className="mx-auto max-w-7xl px-4 pt-16 sm:px-6 lg:pt-24">
|
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
||||||
<div className="public-rise max-w-3xl">
|
<Image src={subpageHero.lienHe} alt="" fill preload sizes="100vw" className="public-subpage-hero-image public-parallax-bg object-cover" />
|
||||||
|
<div className="public-subpage-hero-content mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:py-28">
|
||||||
|
<div className="public-rise public-hero-rule max-w-4xl">
|
||||||
<p className="public-kicker">Liên hệ</p>
|
<p className="public-kicker">Liên hệ</p>
|
||||||
<h1 className="public-gradient-text public-heading-safe mt-3 text-[clamp(2.2rem,5vw,3.8rem)] font-extrabold">
|
<h1 className="public-hero-display public-heading-safe mt-5 text-[clamp(2.2rem,5vw,4.2rem)] font-extrabold">
|
||||||
Liên hệ & đóng góp tư liệu
|
Liên hệ & <span className="public-display-accent">đóng góp tư liệu</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-5 text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
|
<p className="public-subpage-copy mt-6 max-w-[58ch] text-base leading-8 sm:text-lg">
|
||||||
Người dân, tổ chức có tư liệu, hình ảnh hoặc câu chuyện về các di tích của Xã Như Quỳnh
|
Người dân, tổ chức có tư liệu, hình ảnh hoặc câu chuyện về các di tích của Xã Như Quỳnh
|
||||||
vui lòng liên hệ để cùng hoàn thiện kho dữ liệu số di sản.
|
vui lòng liên hệ để cùng hoàn thiện kho dữ liệu số di sản.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:grid lg:grid-cols-[0.56fr_0.44fr] lg:gap-8 lg:py-16">
|
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:grid lg:grid-cols-[0.56fr_0.44fr] lg:gap-8 lg:py-16">
|
||||||
@@ -116,6 +121,15 @@ export default function ContactPage() {
|
|||||||
|
|
||||||
{/* Map */}
|
{/* Map */}
|
||||||
<section className="mx-auto max-w-7xl px-4 pb-16 sm:px-6 lg:pb-24">
|
<section className="mx-auto max-w-7xl px-4 pb-16 sm:px-6 lg:pb-24">
|
||||||
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
<HeritageSeal className="h-8 w-8 shrink-0 text-[var(--primary)] opacity-80" />
|
||||||
|
<div>
|
||||||
|
<p className="public-kicker">Vị trí trên bản đồ</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-1.5 text-xl font-bold text-[var(--tour-ink)] sm:text-2xl">
|
||||||
|
{contact.address}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="public-map-frame overflow-hidden rounded-[16px] border border-[var(--surface-border)]">
|
<div className="public-map-frame overflow-hidden rounded-[16px] border border-[var(--surface-border)]">
|
||||||
<iframe
|
<iframe
|
||||||
title="Bản đồ Thôn Đình Dù, Xã Như Quỳnh, tỉnh Hưng Yên"
|
title="Bản đồ Thôn Đình Dù, Xã Như Quỳnh, tỉnh Hưng Yên"
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Compass, Home } from "lucide-react";
|
||||||
|
import { HeritageSeal } from "@/app/components/landing/HeritageOrnament";
|
||||||
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Không tìm thấy trang | VR360 Như Quỳnh",
|
||||||
|
description: "Trang bạn tìm không tồn tại hoặc đã được di chuyển.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main className="public-page flex min-h-[100dvh] flex-col bg-[var(--background)] text-[var(--foreground)]">
|
||||||
|
<SiteHeader />
|
||||||
|
|
||||||
|
<section className="mx-auto flex w-full max-w-3xl flex-1 flex-col items-center justify-center px-4 py-24 text-center sm:px-6">
|
||||||
|
<HeritageSeal className="h-16 w-16 text-[var(--primary)] opacity-85" />
|
||||||
|
<p className="public-display mt-7 text-[clamp(3.5rem,12vw,6rem)] font-bold leading-none text-[var(--tour-ink)]">
|
||||||
|
404
|
||||||
|
</p>
|
||||||
|
<p className="public-kicker public-kicker--center mt-6">Không tìm thấy trang</p>
|
||||||
|
<h1 className="public-display public-heading-safe mt-3 text-[clamp(1.7rem,4vw,2.5rem)] font-bold text-[var(--tour-ink)]">
|
||||||
|
Trang này chưa được số hóa
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-[52ch] text-base leading-7 text-[var(--muted-foreground)] sm:text-lg">
|
||||||
|
Đường dẫn bạn truy cập có thể đã thay đổi hoặc nội dung đang được bổ sung. Hãy quay về
|
||||||
|
trang chủ hoặc khám phá hệ thống di tích của Xã Như Quỳnh.
|
||||||
|
</p>
|
||||||
|
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
<Link href="/" className="public-cta">
|
||||||
|
<Home className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
Về trang chủ
|
||||||
|
</Link>
|
||||||
|
<Link href="/di-tich" className="public-cta-secondary">
|
||||||
|
<Compass className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
Khám phá di tích
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<SiteFooter />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,11 +3,15 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, Box, Globe2, Landmark, PlayCircle, Rotate3d, School, Sparkles } from "lucide-react";
|
import { ArrowRight, Box, Globe2, Landmark, PlayCircle, Rotate3d, School, Sparkles } from "lucide-react";
|
||||||
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
|
||||||
|
import { FestivalGallery } from "@/app/components/landing/FestivalGallery";
|
||||||
|
import { FestivalVideo } from "@/app/components/landing/FestivalVideo";
|
||||||
import { HeritageMarquee } from "@/app/components/landing/HeritageMarquee";
|
import { HeritageMarquee } from "@/app/components/landing/HeritageMarquee";
|
||||||
|
import { HeritageSeal, SectionDivider } from "@/app/components/landing/HeritageOrnament";
|
||||||
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
import { SiteFooter } from "@/app/components/landing/SiteFooter";
|
||||||
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
import { SiteHeader } from "@/app/components/landing/SiteHeader";
|
||||||
|
import { Typewriter } from "@/app/components/landing/Typewriter";
|
||||||
import { HeroShaderBackground } from "@/app/components/ui/hero-shader-background";
|
import { HeroShaderBackground } from "@/app/components/ui/hero-shader-background";
|
||||||
import { activeTour, heritageSites, img } from "@/app/content";
|
import { activeTour, featuredSpaces, festivalVideo, heritageSites, img } from "@/app/content";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Trang chủ | VR360 Như Quỳnh",
|
title: "Trang chủ | VR360 Như Quỳnh",
|
||||||
@@ -51,11 +55,24 @@ export default function Home() {
|
|||||||
<div className="public-rise min-w-0">
|
<div className="public-rise min-w-0">
|
||||||
<p className="public-kicker">VR360 số hóa di sản Xã Như Quỳnh</p>
|
<p className="public-kicker">VR360 số hóa di sản Xã Như Quỳnh</p>
|
||||||
<h1 className="public-gradient-text public-heading-safe mt-5 max-w-[18ch] text-[clamp(2.85rem,5.35vw,5rem)] font-extrabold">
|
<h1 className="public-gradient-text public-heading-safe mt-5 max-w-[18ch] text-[clamp(2.85rem,5.35vw,5rem)] font-extrabold">
|
||||||
Khám phá di sản văn hóa Xã Như Quỳnh qua trải nghiệm VR360
|
Khám phá <span className="public-display-accent">di sản văn hóa</span> Xã Như Quỳnh qua trải nghiệm VR360
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 max-w-[58ch] text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
|
<p className="mt-6 max-w-[58ch] text-base leading-8 text-[var(--ink-on-light)]/78 sm:text-lg">
|
||||||
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 độ.
|
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 độ.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-5 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-base font-semibold sm:text-lg">
|
||||||
|
<span className="text-[var(--muted-foreground)]">Khám phá</span>
|
||||||
|
<Typewriter
|
||||||
|
words={[
|
||||||
|
"đình làng cổ kính",
|
||||||
|
"ngôi chùa thời Trần",
|
||||||
|
"đền thờ Nguyên phi Ỷ Lan",
|
||||||
|
"văn miếu khoa bảng",
|
||||||
|
"cổ vật số hóa 3D",
|
||||||
|
]}
|
||||||
|
className="font-bold text-[var(--primary)]"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap items-center gap-3">
|
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||||
<Link href="/di-tich" className="public-cta">
|
<Link href="/di-tich" className="public-cta">
|
||||||
<PlayCircle className="h-4 w-4" strokeWidth={1.8} />
|
<PlayCircle className="h-4 w-4" strokeWidth={1.8} />
|
||||||
@@ -67,21 +84,108 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="public-image-stage public-stagger-item relative hidden min-h-[31rem] lg:block">
|
<Link
|
||||||
|
href={activeTour.href}
|
||||||
|
aria-label={`Tham quan VR360 ${activeTour.title}`}
|
||||||
|
className="public-image-stage public-stagger-item group relative hidden min-h-[31rem] lg:block"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={activeTour.coverImage}
|
src={activeTour.heroFeatureImage}
|
||||||
alt="Không gian di sản Xã Như Quỳnh được số hóa VR360"
|
alt="Nghi lễ rước kiệu trong lễ hội Chùa Thái Lạc (Pháp Vân tự), Xã Như Quỳnh"
|
||||||
fill
|
fill
|
||||||
priority
|
preload
|
||||||
sizes="42vw"
|
sizes="42vw"
|
||||||
className="object-cover"
|
className="object-cover transition-transform duration-[1200ms] ease-[cubic-bezier(.16,1,.3,1)] group-hover:scale-[1.04]"
|
||||||
/>
|
/>
|
||||||
|
<HeritageSeal className="absolute right-4 top-4 z-10 h-9 w-9 text-[var(--tour-gold-light)] opacity-90 drop-shadow-[0_2px_8px_rgb(6_22_16_/_0.6)]" />
|
||||||
|
<div className="absolute inset-x-3 bottom-3 z-10 flex items-end justify-between gap-4 rounded-[8px] border border-white/15 bg-[rgb(8_31_23_/_0.6)] p-4 text-white backdrop-blur-md">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-[0.68rem] font-bold uppercase tracking-[0.14em] text-[var(--tour-gold-light)]">
|
||||||
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--tour-gold-light)]" />
|
||||||
|
Đang mở tour
|
||||||
|
</span>
|
||||||
|
<p className="public-display mt-1.5 truncate text-lg font-bold leading-tight">{activeTour.title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="shrink-0 text-right text-[0.72rem] font-semibold leading-snug text-white/82">
|
||||||
|
{activeTour.sceneCount} điểm
|
||||||
|
<br />
|
||||||
|
ảnh 360°
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
</HeroShaderBackground>
|
</HeroShaderBackground>
|
||||||
|
|
||||||
<HeritageMarquee />
|
<HeritageMarquee />
|
||||||
|
|
||||||
|
{/* Không gian tiêu biểu — gallery cuộn ngang, khoe ảnh thật của di tích */}
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
||||||
|
<div className="public-split-reveal max-w-3xl rounded-[6px]">
|
||||||
|
<p className="public-kicker">Không gian tiêu biểu</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
|
Dạo qua từng không gian của Chùa Thái Lạc
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 max-w-[64ch] text-sm leading-6 text-[var(--muted-foreground)] sm:text-base">
|
||||||
|
Kéo ngang để lướt qua cổng tam quan, sân chùa, hành lang tượng và thượng điện — tất cả là hình ảnh thật được số hóa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-hscroll mt-9">
|
||||||
|
{featuredSpaces.map((space) => (
|
||||||
|
<Link key={space.file} href={activeTour.href} className="public-gallery-card group">
|
||||||
|
<div className="relative aspect-[3/4] w-full overflow-hidden rounded-[14px]">
|
||||||
|
<Image
|
||||||
|
src={img(space.file)}
|
||||||
|
alt={space.title}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 640px) 72vw, 21rem"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0 bg-[linear-gradient(180deg,transparent_36%,rgb(8_31_23_/_0.8))]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 p-5">
|
||||||
|
<p className="text-lg font-extrabold leading-tight text-white drop-shadow-sm">{space.title}</p>
|
||||||
|
<p className="mt-1 text-sm leading-5 text-white/85">{space.note}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Lễ hội & văn hóa — tư liệu video động, gắn với tín ngưỡng Tứ Pháp của vùng */}
|
||||||
|
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-[0.82fr_1.18fr] lg:items-center lg:py-20">
|
||||||
|
<div className="public-split-reveal min-w-0 rounded-[6px]">
|
||||||
|
<p className="public-kicker">{festivalVideo.kicker}</p>
|
||||||
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
|
{festivalVideo.heading}
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 grid gap-3 text-sm leading-7 text-[var(--muted-foreground)] sm:text-base">
|
||||||
|
{festivalVideo.description.map((para) => (
|
||||||
|
<p key={para}>{para}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={festivalVideo.ctaHref}
|
||||||
|
className="mt-6 inline-flex items-center gap-2 text-sm font-extrabold text-[var(--tour-ink)] transition hover:text-[var(--primary)]"
|
||||||
|
>
|
||||||
|
{festivalVideo.ctaLabel}
|
||||||
|
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-stagger-item min-w-0">
|
||||||
|
<FestivalVideo src={festivalVideo.src} poster={festivalVideo.poster} title={festivalVideo.title} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Khoảnh khắc lễ hội — gallery bento ảnh thật, nối tiếp mạch "Lễ hội & văn hóa" */}
|
||||||
|
<FestivalGallery />
|
||||||
|
|
||||||
<section className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-[0.8fr_1.2fr] lg:items-center lg:py-20">
|
<section className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-[0.8fr_1.2fr] lg:items-center lg:py-20">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="public-kicker">Giới thiệu nhanh</p>
|
<p className="public-kicker">Giới thiệu nhanh</p>
|
||||||
@@ -165,7 +269,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{upcomingSites.map((site) => (
|
{upcomingSites.slice(0, 6).map((site) => (
|
||||||
<Link
|
<Link
|
||||||
key={site.id}
|
key={site.id}
|
||||||
href={site.detailUrl}
|
href={site.detailUrl}
|
||||||
@@ -186,25 +290,46 @@ export default function Home() {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{upcomingSites.length > 6 && (
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<Link
|
||||||
|
href="/di-tich"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] bg-[var(--surface-band)] px-6 py-2.5 text-sm font-bold text-[var(--tour-ink)] transition hover:border-[rgb(200_164_106_/_0.5)] hover:text-[var(--primary)]"
|
||||||
|
>
|
||||||
|
Xem thêm {upcomingSites.length - 6} di tích khác
|
||||||
|
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
|
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
|
||||||
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
|
||||||
<div className="max-w-3xl">
|
<div className="public-split-reveal max-w-3xl rounded-[6px]">
|
||||||
<p className="public-kicker">Điểm nổi bật</p>
|
<p className="public-kicker">Điểm nổi bật</p>
|
||||||
<h2 className="public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
|
||||||
Giá trị của nền tảng VR360.
|
Vì sao một nền tảng VR360 cho di sản?
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mt-3 max-w-[60ch] text-sm leading-6 text-[var(--muted-foreground)] sm:text-base">
|
||||||
|
Bốn giá trị cốt lõi định hình cách dự án gìn giữ và lan tỏa di sản Như Quỳnh.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="mt-12 grid gap-x-8 gap-y-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{benefits.map((benefit) => {
|
{benefits.map((benefit) => {
|
||||||
const Icon = benefit.icon;
|
const Icon = benefit.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={benefit.title} className="public-panel public-stagger-item rounded-[16px] p-6">
|
<article key={benefit.title} className="public-stagger-item relative pt-6">
|
||||||
<Icon className="h-7 w-7 text-[var(--primary)]" strokeWidth={1.8} />
|
<span
|
||||||
<h3 className="mt-5 text-xl font-bold text-[var(--tour-ink)]">{benefit.title}</h3>
|
aria-hidden
|
||||||
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)]">{benefit.detail}</p>
|
className="absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,rgb(142_95_11_/_0.55),rgb(142_95_11_/_0.08))]"
|
||||||
|
/>
|
||||||
|
<span className="grid h-12 w-12 place-items-center rounded-full border border-[rgb(142_95_11_/_0.24)] bg-[rgb(200_164_106_/_0.1)] text-[var(--primary)] shadow-[inset_0_1px_0_rgb(255_255_255_/_0.6)]">
|
||||||
|
<Icon className="h-6 w-6" strokeWidth={1.7} />
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-5 text-lg font-bold leading-snug text-[var(--tour-ink)]">{benefit.title}</h3>
|
||||||
|
<p className="mt-2.5 text-sm leading-6 text-[var(--muted-foreground)]">{benefit.detail}</p>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -212,18 +337,27 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="px-4 py-14 sm:px-6 lg:py-20">
|
<section className="px-4 py-16 sm:px-6 lg:py-24">
|
||||||
<div className="public-panel public-glow mx-auto grid max-w-7xl gap-5 rounded-[18px] p-6 lg:grid-cols-[1fr_auto] lg:items-center lg:p-8">
|
<SectionDivider className="mx-auto mb-14 max-w-7xl" />
|
||||||
<div>
|
<div className="public-on-dark relative mx-auto grid max-w-7xl gap-6 overflow-hidden rounded-[22px] border border-[rgb(214_180_104_/_0.3)] bg-[linear-gradient(135deg,#11483593,#0a261d_72%),linear-gradient(135deg,#0e3a2c,#0a261d)] p-8 text-white shadow-[0_40px_110px_rgb(8_31_23_/_0.42)] lg:grid-cols-[1fr_auto] lg:items-center lg:p-12">
|
||||||
|
<HeritageSeal
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute -right-10 -top-10 h-48 w-48 text-[var(--tour-gold-light)] opacity-[0.08]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 bg-[radial-gradient(120%_120%_at_85%_-10%,rgb(214_180_104_/_0.14),transparent_55%)]"
|
||||||
|
/>
|
||||||
|
<div className="relative min-w-0">
|
||||||
<p className="public-kicker">Bắt đầu khám phá</p>
|
<p className="public-kicker">Bắt đầu khám phá</p>
|
||||||
<h2 className="public-heading-safe mt-2 text-3xl font-bold text-[var(--tour-ink)] sm:text-4xl">
|
<h2 className="public-display public-heading-safe mt-3 text-3xl font-bold text-[#f8f1e2] sm:text-[2.6rem]">
|
||||||
Bắt đầu hành trình khám phá Như Quỳnh
|
Mở cánh cửa di sản <span className="public-display-accent text-[var(--tour-gold-light)]">Như Quỳnh</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-[66ch] text-sm leading-6 text-[var(--foreground)]/76 sm:text-base">
|
<p className="mt-4 max-w-[60ch] text-sm leading-7 text-[rgb(243_236_221_/_0.82)] sm:text-base">
|
||||||
Mở danh sách không gian văn hóa để tìm kiếm, lọc và lựa chọn điểm tham quan phù hợp.
|
Mở danh sách không gian văn hóa để tìm kiếm, lọc và lựa chọn điểm tham quan phù hợp — bắt đầu ngay từ ngôi chùa cổ Thái Lạc.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/di-tich" className="public-cta w-fit">
|
<Link href="/di-tich" className="public-cta relative w-fit">
|
||||||
<Sparkles className="h-4 w-4" strokeWidth={1.8} />
|
<Sparkles className="h-4 w-4" strokeWidth={1.8} />
|
||||||
Xem không gian văn hóa
|
Xem không gian văn hóa
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const base = "/images/chua-thai-lac";
|
|||||||
// đánh số lại — nhờ vậy có thể bỏ cảnh giữa chừng mà không phải đổi tên ảnh.
|
// đánh số lại — nhờ vậy có thể bỏ cảnh giữa chừng mà không phải đổi tên ảnh.
|
||||||
const imageForFile = (fileNumber: number) => `${base}/${String(fileNumber).padStart(2, "0")}.webp`;
|
const imageForFile = (fileNumber: number) => `${base}/${String(fileNumber).padStart(2, "0")}.webp`;
|
||||||
|
|
||||||
|
// Âm thanh thuyết minh (lồng tiếng) đặt tại public/media/chua-thai-lac/*.mp3 (tên ASCII).
|
||||||
|
const audioBase = "/media/chua-thai-lac";
|
||||||
|
const audioForName = (name: string) => `${audioBase}/${name}.mp3`;
|
||||||
|
|
||||||
type SceneSeed = {
|
type SceneSeed = {
|
||||||
title: string;
|
title: string;
|
||||||
location: string;
|
location: string;
|
||||||
@@ -19,171 +23,453 @@ type SceneSeed = {
|
|||||||
infoMarkers?: InfoMarker[];
|
infoMarkers?: InfoMarker[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Eyebrow dùng chung cho mọi popup thuyết minh.
|
||||||
|
const EYEBROW = "Chùa Thái Lạc · Pháp Vân tự";
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Danh sách cảnh sau khi XOÁ 3 cảnh (ảnh 20/21/22) và ĐÁNH SỐ LẠI liền mạch 1..23.
|
// Danh sách cảnh sau khi XOÁ 5 cảnh (ảnh 04/13/20/21/22 không dùng) và ĐÁNH SỐ LẠI liền mạch 1..21.
|
||||||
// Mỗi cảnh giữ nguyên ảnh gốc qua `imageFile`; số cảnh hiển thị = vị trí trong mảng.
|
// Mỗi cảnh giữ nguyên ảnh gốc qua `imageFile`; số cảnh hiển thị = vị trí trong mảng.
|
||||||
// Tên cảnh & nhóm khu vực được đặt lại theo đúng nội dung từng ảnh panorama:
|
// Tên điểm tham quan đặt theo yêu cầu (Tam quan, cổng Thanh Long/Bạch Hổ, Sân Tịnh An, …);
|
||||||
// 1-4 Khu cổng · 5-7 Cảnh quan vườn · 8-11 Sân chùa & Tam quan ·
|
// các cảnh có nội dung thuyết minh được gắn icon "info" (giới thiệu) hiển thị popup.
|
||||||
// 12-17 Khu nhà Tổ & nhà khách · 18-23 Điện thờ & hành lang tượng.
|
// 1-3 Khu cổng · 4-6 Cảnh quan vườn · 7-10 Sân chùa & Tam quan ·
|
||||||
|
// 11-15 Khu nhà Tổ & nhà khách · 16-21 Điện thờ & hành lang tượng.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
const seeds: SceneSeed[] = [
|
const seeds: SceneSeed[] = [
|
||||||
// 1 — ảnh 01: cổng chùa, cây cổ thụ và ghế đá trong sân
|
// 1 — ảnh 01: cổng chùa, cây cổ thụ và ghế đá trong sân
|
||||||
{
|
{
|
||||||
title: "Cổng chùa Thái Lạc",
|
title: "Tam quan",
|
||||||
location: "Khu cổng",
|
location: "Khu cổng",
|
||||||
imageFile: 1,
|
imageFile: 1,
|
||||||
infoMarkers: [
|
infoMarkers: [
|
||||||
{
|
{
|
||||||
id: "gioi-thieu",
|
id: "tam-quan",
|
||||||
title: "Giới thiệu",
|
title: "Tam quan",
|
||||||
eyebrow: "Chùa Thái Lạc · Pháp Vân tự",
|
eyebrow: EYEBROW,
|
||||||
yaw: 145,
|
yaw: 145,
|
||||||
pitch: 5,
|
pitch: 5,
|
||||||
iconType: "info",
|
iconType: "info",
|
||||||
content: [
|
content: [
|
||||||
"Chùa Thái Lạc, tên chữ là Pháp Vân tự, là ngôi chùa cổ 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ộ.",
|
"Tam quan được xây kiểu chồng diêm hai tầng tám mái, cổng chính giữa được làm vượt hẳn lên so với hai cổng phụ. Trung tâm đường bờ nóc cổng cái trang trí hình tháp chín tầng.",
|
||||||
"Di tích được xếp hạng cấp quốc gia, nổi tiếng với hệ thống ván 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.",
|
"Phần mái với các đầu đao tạo tác kiểu hồi long vân cuốn, mái không rộng nhưng đều được đắp ngói ống, đường bờ nóc hình cong mui thuyền. Các cột trụ xây vuông, đắp gờ, kẻ nổi để thể hiện đôi câu đối chữ Hán miêu tả cảnh đẹp và ca ngợi uy linh của Tứ Pháp.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 2 — ảnh 02: cổng chùa nhìn từ phía đường nhựa
|
// 2 — ảnh 02: cổng chùa nhìn từ phía đường nhựa
|
||||||
{ title: "Cổng chùa bên đường", location: "Khu cổng", imageFile: 2 },
|
|
||||||
// 3 — ảnh 03: đường trước cổng, cột cờ
|
|
||||||
{ title: "Đường vào cổng chùa", location: "Khu cổng", imageFile: 3 },
|
|
||||||
// 4 — ảnh 04: cổng vòm nhỏ dẫn vào sân chùa
|
|
||||||
{ title: "Cổng vào khuôn viên", location: "Khu cổng", imageFile: 4 },
|
|
||||||
// 5 — ảnh 05: sân vườn, ghế đá, bảng nội quy
|
|
||||||
{ title: "Vườn chùa", location: "Cảnh quan vườn chùa", imageFile: 5 },
|
|
||||||
// 6 — ảnh 06: lối đi rợp bóng hàng cây, tường vàng
|
|
||||||
{ title: "Lối vào rợp bóng cây", location: "Cảnh quan vườn chùa", imageFile: 6 },
|
|
||||||
// 7 — ảnh 07: hồ nước cảnh quan, lan can đá, gò non bộ
|
|
||||||
{ title: "Hồ nước cảnh quan", location: "Cảnh quan vườn chùa", imageFile: 7 },
|
|
||||||
// 8 — ảnh 08: sân lớn lát đá trước gác chuông tam quan đồ sộ
|
|
||||||
{ title: "Gác chuông Tam quan", location: "Sân chùa & Tam quan", imageFile: 8 },
|
|
||||||
// 9 — ảnh 09: sân gạch, tam quan bên trái, đài Quan Âm bên phải
|
|
||||||
{ title: "Sân chùa và đài Quan Âm", location: "Sân chùa & Tam quan", imageFile: 9 },
|
|
||||||
// 10 — ảnh 10: đài Quan Âm lộ thiên, sân lát gạch đỏ
|
|
||||||
{ title: "Đài Quan Âm lộ thiên", location: "Sân chùa & Tam quan", imageFile: 10 },
|
|
||||||
// 11 — ảnh 11: vườn tháp, tháp mộ gạch cổ
|
|
||||||
{ title: "Vườn tháp cổ", location: "Sân chùa & Tam quan", imageFile: 11 },
|
|
||||||
// 12 — ảnh 12: lối đi hẹp giữa các dãy nhà, chậu cảnh
|
|
||||||
{ title: "Lối vào khu nhà Tổ", location: "Khu nhà Tổ & nhà khách", imageFile: 12 },
|
|
||||||
// 13 — ảnh 13: sân trong khu nhà ngang, cây cổ thụ
|
|
||||||
{ title: "Sân nhà Tổ", location: "Khu nhà Tổ & nhà khách", imageFile: 13 },
|
|
||||||
// 14 — ảnh 14: hiên nhà có hàng cột, giàn cây xanh che mát
|
|
||||||
{ title: "Hiên nhà dưới giàn cây", location: "Khu nhà Tổ & nhà khách", imageFile: 14 },
|
|
||||||
// 15 — ảnh 15: sân sau, hiên gạch đỏ của dãy nhà
|
|
||||||
{ title: "Sân sau nhà khách", location: "Khu nhà Tổ & nhà khách", imageFile: 15 },
|
|
||||||
// 16 — ảnh 16: sân trong giếng trời, cây cổ thụ tỏa bóng, bàn ghế đá
|
|
||||||
{ title: "Sân trong giếng trời", location: "Khu nhà Tổ & nhà khách", imageFile: 16 },
|
|
||||||
// 17 — ảnh 17: nhà khách, hàng ghế gỗ dài, tường treo ảnh
|
|
||||||
{ title: "Nhà khách", location: "Khu nhà Tổ & nhà khách", imageFile: 17 },
|
|
||||||
// 18 — ảnh 18: hành lang gỗ cổ với hàng tượng, cột & vì kèo cổ (chạm khắc thời Trần)
|
|
||||||
{
|
{
|
||||||
title: "Hành lang tượng cổ",
|
title: "Cổng Thanh Long (bên trái)",
|
||||||
|
location: "Khu cổng",
|
||||||
|
imageFile: 2,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "cong-thanh-long",
|
||||||
|
title: "Cổng Thanh Long (bên trái)",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Cổng Thanh Long nằm bên trái Tam Quan, là một phần quan trọng trong tổng thể kiến trúc truyền thống của chùa. Theo quan niệm phương Đông, Thanh Long là linh vật đại diện cho sức mạnh, sự phát triển và nguồn sinh khí tốt lành. Cùng với cổng Bạch Hổ ở phía đối diện, công trình góp phần tạo nên thế phong thủy hài hòa, thể hiện mong ước về sự bình an, thịnh vượng và trường tồn cho ngôi chùa.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 3 — ảnh 03: đường trước cổng, cột cờ
|
||||||
|
{
|
||||||
|
title: "Cổng Bạch Hổ (bên phải)",
|
||||||
|
location: "Khu cổng",
|
||||||
|
imageFile: 3,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "cong-bach-ho",
|
||||||
|
title: "Cổng Bạch Hổ (bên phải)",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Cổng Bạch Hổ nằm bên phải Tam Quan, tượng trưng cho sự vững chãi, bảo hộ và cân bằng. Trong văn hóa truyền thống, Bạch Hổ là linh vật canh giữ phía Tây, mang ý nghĩa trấn giữ và bảo vệ. Sự kết hợp giữa Thanh Long và Bạch Hổ không chỉ tạo nên vẻ đẹp cân đối cho kiến trúc mà còn phản ánh quan niệm hài hòa giữa thiên nhiên, con người và đời sống tâm linh.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 4 — ảnh 05: sân vườn, ghế đá, bảng nội quy
|
||||||
|
{
|
||||||
|
title: "Sân Tịnh An",
|
||||||
|
location: "Cảnh quan vườn chùa",
|
||||||
|
imageFile: 5,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "san-tinh-an",
|
||||||
|
title: "Sân Tịnh An",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 120,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Sân Tịnh An là khoảng không gian mở, xanh mát và yên bình trong khuôn viên chùa. Đây là nơi để tăng ni, phật tử và du khách nghỉ chân, thư giãn, chiêm ngưỡng cảnh quan và cảm nhận sự thanh tịnh của chốn thiền môn. Không gian này góp phần tạo nên môi trường trong lành, giúp mỗi người tạm gác những bộn bề của cuộc sống để tìm về sự an nhiên trong tâm hồn.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 5 — ảnh 06: lối đi rợp bóng hàng cây, tường vàng
|
||||||
|
{ title: "Hữu đạo", location: "Cảnh quan vườn chùa", imageFile: 6 },
|
||||||
|
// 6 — ảnh 07: hồ nước cảnh quan, lan can đá, gò non bộ
|
||||||
|
{
|
||||||
|
title: "Hồ Di Lặc",
|
||||||
|
location: "Cảnh quan vườn chùa",
|
||||||
|
imageFile: 7,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "ho-di-lac",
|
||||||
|
title: "Hồ Di Lặc",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 110,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Hồ Di Lặc là điểm nhấn cảnh quan trong khuôn viên chùa, nổi bật với tượng Phật Di Lặc bằng đá được đặt trang trọng phía trên hồ cá Koi. Hình tượng Phật với nụ cười hiền hòa tượng trưng cho niềm vui, hạnh phúc và sự sung túc. Sự kết hợp giữa tượng Phật, mặt nước trong xanh và đàn cá Koi tạo nên không gian thanh tịnh, mang ý nghĩa cầu chúc bình an và may mắn cho mọi người.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 7 — ảnh 08: sân lớn lát đá trước gác chuông tam quan đồ sộ
|
||||||
|
{
|
||||||
|
title: "Tiền sảnh",
|
||||||
|
location: "Sân chùa & Tam quan",
|
||||||
|
imageFile: 8,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "tien-sanh",
|
||||||
|
title: "Tiền sảnh",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 90,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khu vực sân trong là một khoảng sân rộng lớn, thông thoáng, được lát phối hợp giữa gạch xám và các dải gạch đỏ tạo thành những đường vòng cung định hướng không gian. Khoảng sân được bao bọc bởi dãy nhà cổ lợp mái ngói mũi hài rêu phong, tường sơn vàng và hệ thống cửa gỗ lá bàng khép kín. Tại vị trí chính diện tòa nhà trung tâm, một đỉnh hương đá lớn được tôn trí giữa hai cột đèn đá nhỏ.",
|
||||||
|
"Bao quanh sân là hệ thống cây cảnh và các cây duối cổ thụ được cắt tỉa, uốn thế dạng tầng tán mâm xôi bề thế, đặt trong các bồn đá tròn. Phía góc sân hướng ra bên ngoài là khoảng không gian trống có treo cờ Phật giáo, kết hợp với các dãy chậu cây cảnh xếp dọc tường bao. Phía đối diện là một lễ đài lớn ngoài trời có dựng bức bạt tạo hình đóa hoa sen tôn trí hình ảnh Đức Phật, hoàn thiện kết cấu cảnh quan xung quanh sân chính.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Marker VIDEO đặt ngay đỉnh hương đá giữa sân — bấm để mở modal phát phim lễ hội.
|
||||||
|
// yaw/pitch tinh chỉnh cho trùng vị trí lư hương: cùng hướng marker chữ (yaw 90)
|
||||||
|
// nhưng hạ thấp xuống mặt sân (pitch âm) để hai marker không chồng nhau.
|
||||||
|
{
|
||||||
|
id: "video-le-hoi-cau-mua",
|
||||||
|
title: "Lễ hội cầu mưa",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 178,
|
||||||
|
pitch: 2,
|
||||||
|
iconType: "video",
|
||||||
|
video: {
|
||||||
|
src: "/video/le-hoi-cau-mua-lac-hong-van-lam.mp4",
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
"Lễ hội cầu mưa gắn với tín ngưỡng Tứ Pháp của cư dân nông nghiệp đồng bằng Bắc Bộ — cầu cho mưa thuận gió hòa, mùa màng tươi tốt. Tại vùng Như Quỳnh, dòng tín ngưỡng ấy hiện diện ngay ở Chùa Thái Lạc (Pháp Vân tự), nơi thờ vị thần Mây Pháp Vân.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 8 — ảnh 09: sân gạch, tam quan bên trái, đài Quan Âm bên phải
|
||||||
|
{ title: "Sân chùa", location: "Sân chùa & Tam quan", imageFile: 9 },
|
||||||
|
// 9 — ảnh 10: đài Quan Âm lộ thiên, sân lát gạch đỏ
|
||||||
|
{
|
||||||
|
title: "Tôn đài Đức Phật Thích Ca",
|
||||||
|
location: "Sân chùa & Tam quan",
|
||||||
|
imageFile: 10,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "ton-dai-duc-phat",
|
||||||
|
title: "Tôn đài Đức Phật Thích Ca",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 110,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Tôn đài Đức Phật Thích Ca là nơi tôn thờ Đức Phật Thích Ca Mâu Ni – người sáng lập Phật giáo. Tượng Phật được tôn trí trang nghiêm trên tòa sen, biểu tượng cho sự giác ngộ, trí tuệ và lòng từ bi. Đây là không gian để phật tử và du khách dâng hương, chiêm bái, hướng tâm đến những giá trị tốt đẹp của đạo Phật và cầu nguyện cho cuộc sống bình an, an lạc.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 10 — ảnh 11: vườn tháp, tháp mộ gạch cổ
|
||||||
|
{
|
||||||
|
title: "Vườn tháp cổ",
|
||||||
|
location: "Sân chùa & Tam quan",
|
||||||
|
imageFile: 11,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "vuon-thap-co",
|
||||||
|
title: "Vườn tháp cổ",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 115,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khu vực Vườn tháp cổ được xây dựng trên một đài nền xi măng cao hơn mặt sân, có hệ thống bậc tam cấp thấp trải dài làm lối dẫn lên. Trung tâm khuôn viên là một lối đi lát đá xám phẳng phiu, hai bên bố trí đối xứng các bảo tháp ba tầng được xây bằng gạch trần màu đỏ cam kiên cố.",
|
||||||
|
"Mỗi bảo tháp có phần chân đế đắp nổi phù điêu đá xám, thân tháp trổ các vòm cửa nhỏ hướng ra ngoài và phần mái tầng trên cùng được tạo tác cong vút với các họa tiết mây đao bằng đá xám, đỉnh tháp nhọn hướng lên nền trời. Bao bọc xung quanh các ngôi bảo tháp là hệ thống cây xanh cổ thụ tán rộng và các chậu cây cảnh tầm trung đặt trong chum gốm nung màu nâu mộc mạc.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 11 — ảnh 12: lối đi hẹp giữa các dãy nhà, chậu cảnh
|
||||||
|
{ title: "Tả đạo", location: "Khu nhà Tổ & nhà khách", imageFile: 12 },
|
||||||
|
// 12 — ảnh 14: hiên nhà có hàng cột, giàn cây xanh che mát (Khách xá)
|
||||||
|
{
|
||||||
|
title: "Khách xá",
|
||||||
|
location: "Khu nhà Tổ & nhà khách",
|
||||||
|
imageFile: 14,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "khach-xa",
|
||||||
|
title: "Khách xá",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khách Xá là nơi lưu trú và nghỉ ngơi dành cho tăng ni, phật tử và khách thập phương khi đến chùa tham dự các khóa tu, sinh hoạt Phật giáo hoặc chiêm bái. Không gian được bố trí đơn giản, trang nhã và thanh tịnh, tạo điều kiện thuận lợi cho việc nghỉ ngơi, tu học và tìm hiểu các giá trị văn hóa, tâm linh của nhà chùa.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 13 — ảnh 15: sân sau, hiên gạch đỏ của dãy nhà (Khách xá)
|
||||||
|
{
|
||||||
|
title: "Khách xá",
|
||||||
|
location: "Khu nhà Tổ & nhà khách",
|
||||||
|
imageFile: 15,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "khach-xa",
|
||||||
|
title: "Khách xá",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khách Xá là nơi lưu trú và nghỉ ngơi dành cho tăng ni, phật tử và khách thập phương khi đến chùa tham dự các khóa tu, sinh hoạt Phật giáo hoặc chiêm bái. Không gian được bố trí đơn giản, trang nhã và thanh tịnh, tạo điều kiện thuận lợi cho việc nghỉ ngơi, tu học và tìm hiểu các giá trị văn hóa, tâm linh của nhà chùa.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 14 — ảnh 16: sân trong giếng trời, cây cổ thụ tỏa bóng, bàn ghế đá (Khách xá)
|
||||||
|
{
|
||||||
|
title: "Khách xá",
|
||||||
|
location: "Khu nhà Tổ & nhà khách",
|
||||||
|
imageFile: 16,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "khach-xa",
|
||||||
|
title: "Khách xá",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khách Xá là nơi lưu trú và nghỉ ngơi dành cho tăng ni, phật tử và khách thập phương khi đến chùa tham dự các khóa tu, sinh hoạt Phật giáo hoặc chiêm bái. Không gian được bố trí đơn giản, trang nhã và thanh tịnh, tạo điều kiện thuận lợi cho việc nghỉ ngơi, tu học và tìm hiểu các giá trị văn hóa, tâm linh của nhà chùa.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 15 — ảnh 17: nhà khách, hàng ghế gỗ dài, tường treo ảnh (Khách Đường)
|
||||||
|
{
|
||||||
|
title: "Khách Đường",
|
||||||
|
location: "Khu nhà Tổ & nhà khách",
|
||||||
|
imageFile: 17,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "khach-duong",
|
||||||
|
title: "Khách Đường",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: -60,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Khách Đường là nơi đón tiếp chư tôn đức, tăng ni, phật tử và du khách thập phương khi đến tham quan, chiêm bái tại chùa. Không gian được bố trí trang nhã, thuận tiện cho các hoạt động gặp gỡ, trao đổi và sinh hoạt Phật giáo. Đây cũng là nơi lưu giữ nhiều câu chuyện, dấu ấn gắn liền với lịch sử phát triển của nhà chùa và các hoạt động hoằng pháp qua nhiều thời kỳ.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 16 — ảnh 18: hành lang gỗ cổ với hàng tượng La Hán (Hữu Vu La Hán)
|
||||||
|
{
|
||||||
|
title: "Hữu Vu La Hán",
|
||||||
location: "Điện thờ & hành lang tượng",
|
location: "Điện thờ & hành lang tượng",
|
||||||
imageFile: 18,
|
imageFile: 18,
|
||||||
infoMarkers: [
|
infoMarkers: [
|
||||||
{
|
{
|
||||||
id: "cham-khac-go",
|
id: "huu-vu-la-han",
|
||||||
title: "Chạm khắc gỗ thời Trần",
|
title: "Hữu Vu La Hán",
|
||||||
eyebrow: "Giá trị di sản",
|
eyebrow: EYEBROW,
|
||||||
yaw: 0,
|
yaw: -80,
|
||||||
pitch: -4,
|
pitch: 0,
|
||||||
iconType: "landmark",
|
iconType: "info",
|
||||||
content: [
|
content: [
|
||||||
"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ữ và hình tượng Phật giáo với kỹ thuật điêu luyện.",
|
"Hành lang La Hán là công trình nối liền các khu vực trong chùa và là nơi tôn trí tượng các vị La Hán. Mỗi pho tượng được thể hiện với hình dáng và thần thái riêng, phản ánh những phẩm hạnh cao quý của người tu hành trên con đường giác ngộ. Không gian yên tĩnh của hành lang giúp du khách có cơ hội chiêm nghiệm về ý nghĩa của sự tu tập, lòng từ bi và trí tuệ trong Phật giáo.",
|
||||||
"Đây là nguồn tư liệu quý cho nghiên cứu mỹ thuật cổ Việt Nam, làm nên giá trị đặc biệt của Chùa Thái Lạc.",
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 17 — ảnh 19: sân thiên tỉnh, hòm công đức, hòn non bộ (Nhà Tổ)
|
||||||
|
{
|
||||||
|
title: "Nhà Tổ",
|
||||||
|
location: "Điện thờ & hành lang tượng",
|
||||||
|
imageFile: 19,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "nha-to",
|
||||||
|
title: "Nhà Tổ",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 110,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Nhà Tổ là nơi thờ các vị tổ sư và các thế hệ tăng ni đã có công xây dựng, gìn giữ và phát triển Chùa Thái Lạc qua nhiều thời kỳ. Công trình mang kiến trúc truyền thống với không gian trang nghiêm, cổ kính và giàu giá trị văn hóa. Đây là nơi thể hiện đạo lý “uống nước nhớ nguồn”, nhắc nhở các thế hệ hôm nay luôn trân trọng và biết ơn công lao của những người đi trước.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 18 — ảnh 23: hàng tượng La Hán bên hành lang, sân vườn cây lựu (Tả Vu La Hán)
|
||||||
|
{
|
||||||
|
title: "Tả Vu La Hán",
|
||||||
|
location: "Điện thờ & hành lang tượng",
|
||||||
|
imageFile: 23,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "ta-vu-la-han",
|
||||||
|
title: "Tả Vu La Hán",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 140,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Hành lang La Hán là công trình nối liền các khu vực trong chùa và là nơi tôn trí tượng các vị La Hán. Mỗi pho tượng được thể hiện với hình dáng và thần thái riêng, phản ánh những phẩm hạnh cao quý của người tu hành trên con đường giác ngộ. Không gian yên tĩnh của hành lang giúp du khách có cơ hội chiêm nghiệm về ý nghĩa của sự tu tập, lòng từ bi và trí tuệ trong Phật giáo.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 19 — ảnh 24: gian thờ bên trong, vách gỗ bức bàn, ban thờ hai bên (Tiền đường)
|
||||||
|
{
|
||||||
|
title: "Tiền đường",
|
||||||
|
location: "Điện thờ & hành lang tượng",
|
||||||
|
imageFile: 24,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "tien-duong",
|
||||||
|
title: "Tiền đường",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 130,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Tiền Đường là công trình trung tâm và có quy mô lớn nhất trong tổng thể kiến trúc Chùa Thái Lạc. Nơi đây nổi bật với hệ thống cột gỗ cổ đồ sộ, kết cấu kiến trúc thời Trần cùng các mảng chạm khắc tinh xảo mang giá trị nghệ thuật đặc biệt. Đây là không gian diễn ra các nghi lễ Phật giáo quan trọng và là nơi thờ tự chính của chùa. Trải qua hơn 700 năm lịch sử, Tiền Đường vẫn lưu giữ được nhiều nét kiến trúc nguyên gốc, trở thành minh chứng quý giá cho trình độ xây dựng, điêu khắc và mỹ thuật của cha ông thời Trần.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 20 — ảnh 25: thượng điện, khám thờ gỗ, cột cổ, ban thờ hai bên (Tiền đường)
|
||||||
|
{
|
||||||
|
title: "Tiền đường",
|
||||||
|
location: "Điện thờ & hành lang tượng",
|
||||||
|
imageFile: 25,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "tien-duong",
|
||||||
|
title: "Tiền đường",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 110,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Tiền Đường là công trình trung tâm và có quy mô lớn nhất trong tổng thể kiến trúc Chùa Thái Lạc. Nơi đây nổi bật với hệ thống cột gỗ cổ đồ sộ, kết cấu kiến trúc thời Trần cùng các mảng chạm khắc tinh xảo mang giá trị nghệ thuật đặc biệt. Đây là không gian diễn ra các nghi lễ Phật giáo quan trọng và là nơi thờ tự chính của chùa. Trải qua hơn 700 năm lịch sử, Tiền Đường vẫn lưu giữ được nhiều nét kiến trúc nguyên gốc, trở thành minh chứng quý giá cho trình độ xây dựng, điêu khắc và mỹ thuật của cha ông thời Trần.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 21 — ảnh 26: hậu đường, hàng cột, ban thờ tượng dọc tường (Tiền đường)
|
||||||
|
{
|
||||||
|
title: "Tiền đường",
|
||||||
|
location: "Điện thờ & hành lang tượng",
|
||||||
|
imageFile: 26,
|
||||||
|
infoMarkers: [
|
||||||
|
{
|
||||||
|
id: "tien-duong",
|
||||||
|
title: "Tiền đường",
|
||||||
|
eyebrow: EYEBROW,
|
||||||
|
yaw: 150,
|
||||||
|
pitch: 0,
|
||||||
|
iconType: "info",
|
||||||
|
content: [
|
||||||
|
"Tiền Đường là công trình trung tâm và có quy mô lớn nhất trong tổng thể kiến trúc Chùa Thái Lạc. Nơi đây nổi bật với hệ thống cột gỗ cổ đồ sộ, kết cấu kiến trúc thời Trần cùng các mảng chạm khắc tinh xảo mang giá trị nghệ thuật đặc biệt. Đây là không gian diễn ra các nghi lễ Phật giáo quan trọng và là nơi thờ tự chính của chùa. Trải qua hơn 700 năm lịch sử, Tiền Đường vẫn lưu giữ được nhiều nét kiến trúc nguyên gốc, trở thành minh chứng quý giá cho trình độ xây dựng, điêu khắc và mỹ thuật của cha ông thời Trần.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 19 — ảnh 19: sân thiên tỉnh, hòm công đức, hòn non bộ
|
|
||||||
{ title: "Sân thiên tỉnh", location: "Điện thờ & hành lang tượng", imageFile: 19 },
|
|
||||||
// 20 — ảnh 23: hàng tượng thờ bên hành lang, sân vườn cây lựu
|
|
||||||
{ title: "Hành lang tượng và sân vườn", location: "Điện thờ & hành lang tượng", imageFile: 23 },
|
|
||||||
// 21 — ảnh 24: gian thờ bên trong, vách gỗ bức bàn, ban thờ hai bên
|
|
||||||
{ title: "Gian thờ bên trong", location: "Điện thờ & hành lang tượng", imageFile: 24 },
|
|
||||||
// 22 — ảnh 25: thượng điện, khám thờ gỗ, cột cổ, ban thờ hai bên
|
|
||||||
{ title: "Thượng điện", location: "Điện thờ & hành lang tượng", imageFile: 25 },
|
|
||||||
// 23 — ảnh 26: hậu đường, hàng cột, ban thờ tượng dọc tường
|
|
||||||
{ title: "Hậu đường", location: "Điện thờ & hành lang tượng", imageFile: 26 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// ĐỒ THỊ HƯỚNG ĐI (có hướng) — khoá là SỐ CẢNH MỚI (1..23), giá trị là danh sách
|
// ĐỒ THỊ HƯỚNG ĐI (có hướng) — khoá là SỐ CẢNH (1..21), giá trị là danh sách
|
||||||
// các cảnh mà từ đó có thể đi tới. Đây chính là các mũi tên hiển thị trong ảnh 360.
|
// các cảnh mà từ đó có thể đi tới. Đây chính là các mũi tên hiển thị trong ảnh 360.
|
||||||
// Lấy theo yêu cầu (đã quy đổi số cũ → số mới: 23→20, 24→21, 25→22, 26→23):
|
// Đã xoá cảnh 04 (Cổng vào khuôn viên) và cảnh 13 (Sân nhà Tổ); hướng đi bypass:
|
||||||
// cũ 8→…,25 ⇒ 8→…,22 | cũ 19→…,23 ⇒ 19→…,20 | cũ 23→19,(24),25 ⇒ 20→19,21,22
|
// cũ 3→4→5 ⇒ 3→4(mới) | cũ 12→13→14 ⇒ 11→12(mới) (số mới sau đánh lại).
|
||||||
// cũ 25→23,26,8 ⇒ 22→20,23,8 | cũ 24→23 ⇒ 21→20 | thêm 23→22 để có lối quay lại.
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
const adjacency: Record<number, number[]> = {
|
const adjacency: Record<number, number[]> = {
|
||||||
1: [2, 3, 8],
|
1: [2, 3, 7],
|
||||||
2: [1, 9],
|
2: [1, 8],
|
||||||
3: [1, 4],
|
3: [1, 4],
|
||||||
4: [5, 7],
|
4: [3, 5, 6, 7],
|
||||||
5: [4, 6, 7, 8],
|
5: [4, 15],
|
||||||
6: [5, 17],
|
6: [4],
|
||||||
7: [4, 5],
|
7: [1, 4, 8, 20],
|
||||||
8: [1, 5, 9, 22],
|
8: [2, 7, 9, 11],
|
||||||
9: [2, 8, 10, 12],
|
9: [8, 10],
|
||||||
10: [9, 11],
|
10: [8, 9],
|
||||||
11: [9, 10],
|
11: [8, 12],
|
||||||
12: [9, 13],
|
12: [11, 13],
|
||||||
13: [12, 14],
|
13: [12, 14],
|
||||||
14: [13, 15],
|
14: [13, 15],
|
||||||
15: [14, 16],
|
15: [14, 17, 5],
|
||||||
16: [15, 17],
|
16: [15, 17],
|
||||||
17: [16, 19, 6],
|
17: [15, 16, 18],
|
||||||
18: [17, 19],
|
18: [17, 19],
|
||||||
19: [17, 18, 20],
|
19: [18, 20],
|
||||||
20: [19, 21],
|
20: [19, 21, 7],
|
||||||
21: [20, 22],
|
21: [20],
|
||||||
22: [21, 23, 8],
|
|
||||||
23: [22],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toạ độ sơ đồ (mini-map): x trái→phải, y trên(0)→dưới(100).
|
// Toạ độ sơ đồ (mini-map): x trái→phải, y trên(0)→dưới(100).
|
||||||
// Cổng ở dưới, tiến dần lên trên qua chính điện → khu thờ → cảnh quan sau chùa.
|
// Cổng ở dưới, tiến dần lên trên qua chính điện → khu thờ → cảnh quan sau chùa.
|
||||||
// Bố cục này quyết định luôn HƯỚNG mũi tên trong ảnh 360 (tính theo phương vị bản đồ).
|
// Bố cục này quyết định luôn HƯỚNG mũi tên trong ảnh 360 (tính theo phương vị bản đồ).
|
||||||
// Ràng buộc hình học:
|
// Ràng buộc hình học:
|
||||||
// • 1-2-9-8 là một hình vuông (2,9 bên trái), 1-3-5-8 là một hình vuông (3,5 bên phải);
|
// • 1-2-8-7 là một hình vuông (2,8 bên trái), 1-3-4-7 là một hình vuông (3,4 bên phải);
|
||||||
// hai hình vuông dùng chung cạnh đứng 1-8 ở giữa (x=50).
|
// hai hình vuông dùng chung cạnh đứng 1-7 ở giữa (x=50).
|
||||||
// • 3,4,5,6 nằm trên một cột dọc bên phải (x=68); 3-4-5 thẳng hàng tạo cạnh phải hình vuông.
|
// • 3,4,5 nằm trên một cột dọc bên phải (x=68); 3-4 thẳng hàng tạo cạnh phải hình vuông.
|
||||||
// • 9,12,13,14 nằm trên một cột dọc bên trái (x=32); 10,11 nằm trên một cột dọc (x=18).
|
// • 8,11,12 nằm trên một cột dọc bên trái (x=32); 9,10 nằm trên cột dọc (x~16-18).
|
||||||
// • Cột dọc 12-13-14 (x=32) VUÔNG GÓC với hàng ngang 14-15-16 (y=22), gặp nhau tại 14;
|
// • Cột dọc 11-12 (x=32) VUÔNG GÓC với hàng ngang 12-13-14 (y=12), gặp nhau tại 12.
|
||||||
// khoảng cách 13↔14 được giãn rộng.
|
// • 1-7-20-17-13 cùng cột dọc giữa (x=50); 3-4-5-14 cùng cột dọc phải (x=68).
|
||||||
// • 1-8-22-19-15 cùng cột dọc giữa (x=50); 3-4-5-6-16 cùng cột dọc phải (x=68);
|
// • 19-20-21 cùng một hàng ngang (y=56); 16-18 cùng khu hàng ngang (y~40-42).
|
||||||
// 7-17 cùng cột dọc (x=82).
|
// • Nhóm giữa 16-17-18 được kéo lên cao, giãn xa khỏi hàng dưới 11-19-20-21.
|
||||||
// • 21-22-23 cùng một hàng ngang (y=56); 13-20 cùng một hàng ngang (y=40).
|
// • Band 11-19-20-21-5 (y~56) được giãn cách khỏi hàng 8-7-4 (y=72).
|
||||||
// • Nhóm giữa 13-18-19-20 được kéo lên cao, giãn xa khỏi hàng dưới 12-21-22-23;
|
|
||||||
// 19 cao hơn (y nhỏ hơn) cả 20 và 13.
|
|
||||||
// • Band 12-21-22-23-6 (y=56) được giãn cách khỏi hàng 9-8-5 (y=72).
|
|
||||||
const mapPositions: Record<number, { x: number; y: number }> = {
|
const mapPositions: Record<number, { x: number; y: number }> = {
|
||||||
1: { x: 50, y: 100 },
|
1: { x: 50, y: 100 },
|
||||||
2: { x: 32, y: 90 },
|
2: { x: 32, y: 90 },
|
||||||
3: { x: 68, y: 90 },
|
3: { x: 68, y: 90 },
|
||||||
4: { x: 68, y: 81 },
|
4: { x: 68, y: 72 },
|
||||||
5: { x: 68, y: 72 },
|
5: { x: 68, y: 56 },
|
||||||
6: { x: 68, y: 56 },
|
6: { x: 78, y: 76 },
|
||||||
7: { x: 78, y: 76 },
|
7: { x: 50, y: 72 },
|
||||||
8: { x: 50, y: 72 },
|
8: { x: 32, y: 72 },
|
||||||
9: { x: 32, y: 72 },
|
9: { x: 16, y: 72 },
|
||||||
10: { x: 16, y: 72 },
|
10: { x: 18, y: 58 },
|
||||||
11: { x: 18, y: 58 },
|
11: { x: 32, y: 56 },
|
||||||
12: { x: 32, y: 56 },
|
12: { x: 32, y: 12 },
|
||||||
13: { x: 32, y: 40 },
|
13: { x: 50, y: 12 },
|
||||||
14: { x: 32, y: 12 },
|
14: { x: 68, y: 12 },
|
||||||
15: { x: 50, y: 12 },
|
15: { x: 82, y: 34 },
|
||||||
16: { x: 68, y: 12 },
|
16: { x: 64, y: 42 },
|
||||||
17: { x: 82, y: 34 },
|
17: { x: 50, y: 32 },
|
||||||
18: { x: 64, y: 42 },
|
18: { x: 42, y: 40 },
|
||||||
19: { x: 50, y: 32 },
|
19: { x: 40, y: 56 },
|
||||||
20: { x: 42, y: 40 },
|
20: { x: 50, y: 56 },
|
||||||
21: { x: 40, y: 56 },
|
21: { x: 60, y: 56 },
|
||||||
22: { x: 50, y: 56 },
|
|
||||||
23: { x: 60, y: 56 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sceneId = (sceneNumber: number) => `scene-${sceneNumber}`;
|
const sceneId = (sceneNumber: number) => `scene-${sceneNumber}`;
|
||||||
@@ -236,112 +522,103 @@ type ArrowAdjust = {
|
|||||||
// │ Kết hợp nhiều thứ cùng lúc: { yaw: 90, pitch: -15, scale: 1.2, nextYaw: 0 } │
|
// │ Kết hợp nhiều thứ cùng lúc: { yaw: 90, pitch: -15, scale: 1.2, nextYaw: 0 } │
|
||||||
// └─────────────────────────────────────────────────────────────────────────────┘
|
// └─────────────────────────────────────────────────────────────────────────────┘
|
||||||
const arrowConfig: Record<string, ArrowAdjust> = {
|
const arrowConfig: Record<string, ArrowAdjust> = {
|
||||||
// Cảnh 1 — Cổng chùa Thái Lạc
|
// Cảnh 1 — Tam quan
|
||||||
"1->2" : { yaw: 88, pitch: -2, rotation: 20 }, // → 2. Cổng chùa bên đường
|
"1->2": { yaw: 88, pitch: -2, rotation: 20 }, // → 2. Cổng Thanh Long (bên trái)
|
||||||
"1->3" : { yaw: -170, pitch: -5, rotation: -10 }, // → 3. Đường vào cổng chùa
|
"1->3": { yaw: -170, pitch: -5, rotation: -10 }, // → 3. Cổng Bạch Hổ (bên phải)
|
||||||
"1->8" : { yaw: 140, pitch: -6, rotation: 0 }, // → 8. Gác chuông Tam quan
|
"1->7": { yaw: 140, pitch: -6, rotation: 0 }, // → 7. Tiền sảnh
|
||||||
// Cảnh 2 — Cổng chùa bên đường
|
// Cảnh 2 — Cổng Thanh Long (bên trái)
|
||||||
"2->1" : { yaw: -90, pitch: -18, rotation: 0 }, // → 1. Cổng chùa Thái Lạc
|
"2->1": { yaw: -90, pitch: -18, rotation: 0 }, // → 1. Tam quan
|
||||||
"2->9" : { yaw: 140, pitch: -20, rotation: 0 }, // → 9. Sân chùa và đài Quan Âm
|
"2->8": { yaw: 140, pitch: -20, rotation: 0 }, // → 8. Sân chùa
|
||||||
// Cảnh 3 — Đường vào cổng chùa
|
// Cảnh 3 — Cổng Bạch Hổ (bên phải)
|
||||||
"3->1" : { yaw: 10, pitch: -18, rotation: 0 }, // → 1. Cổng chùa Thái Lạc
|
"3->1": { yaw: 10, pitch: -18, rotation: 0 }, // → 1. Tam quan
|
||||||
"3->4" : { yaw: 120, pitch: -18, rotation: 0 }, // → 4. Cổng vào khuôn viên
|
"3->4": { yaw: 121, pitch: -18, rotation: 0 }, // → 4. Sân Tịnh An (chỉnh lại nếu lệch)
|
||||||
// Cảnh 4 — Cổng vào khuôn viên
|
// Cảnh 4 — Sân Tịnh An
|
||||||
"4->5" : { yaw: 120, pitch: -5, rotation: 0 }, // → 5. Vườn chùa
|
"4->3": { yaw: 273, pitch: -18, rotation: 0 }, // → 3. Cổng Bạch Hổ (bên phải)
|
||||||
"4->7" : { yaw: 150, pitch: -8, rotation: 0 }, // → 7. Hồ nước cảnh quan
|
"4->5": { yaw: 100, pitch: -18, rotation: 0 }, // → 5. Hữu đạo
|
||||||
// Cảnh 5 — Vườn chùa
|
"4->6": { yaw: -120, pitch: -22, rotation: -20 }, // → 6. Hồ Di Lặc
|
||||||
"5->4" : { yaw: -88, pitch: -15, rotation: 0 }, // → 4. Cổng vào khuôn viên
|
"4->7": { yaw: -60, pitch: -20, rotation: 30 }, // → 7. Tiền sảnh
|
||||||
"5->6" : { yaw: 100, pitch: -18, rotation: 0 }, // → 6. Lối vào rợp bóng cây
|
// Cảnh 5 — Hữu đạo
|
||||||
"5->7" : { yaw: -120, pitch: -22, rotation: -20 }, // → 7. Hồ nước cảnh quan
|
"5->4": { yaw: 290, pitch: -26, rotation: -30 }, // → 4. Sân Tịnh An
|
||||||
"5->8" : { yaw: -60, pitch: -20, rotation: 30 }, // → 8. Gác chuông Tam quan
|
"5->15": { yaw: 32, pitch: -8, rotation: 0 }, // → 15. Khách Đường
|
||||||
// Cảnh 6 — Lối vào rợp bóng cây
|
// Cảnh 6 — Hồ Di Lặc
|
||||||
"6->5" : { yaw: 290, pitch: -26, rotation: -30 }, // → 5. Vườn chùa
|
"6->4": { yaw: 39, pitch: -18, rotation: 30 }, // → 4. Sân Tịnh An
|
||||||
"6->17" : { yaw: 32, pitch: -8, rotation: 0 }, // → 17. Nhà khách
|
// Cảnh 7 — Tiền sảnh
|
||||||
// Cảnh 7 — Hồ nước cảnh quan
|
"7->1": { yaw: 313, pitch: -8, rotation: 0 }, // → 1. Tam quan
|
||||||
"7->4" : { yaw: 0, pitch: -18, rotation: -20 }, // → 4. Cổng vào khuôn viên
|
"7->4": { yaw: 210, pitch: -8, rotation: 0 }, // → 4. Sân Tịnh An
|
||||||
"7->5" : { yaw: 39, pitch: -18, rotation: 30 }, // → 5. Vườn chùa
|
"7->8": { yaw: 30, pitch: -8, rotation: 0 }, // → 8. Sân chùa
|
||||||
// Cảnh 8 — Gác chuông Tam quan
|
"7->20": { yaw: 115, pitch: -15, rotation: 0 }, // → 20. Tiền đường
|
||||||
"8->1" : { yaw: 313, pitch: -8, rotation: 0 }, // → 1. Cổng chùa Thái Lạc
|
// Cảnh 8 — Sân chùa
|
||||||
"8->5" : { yaw: 210, pitch: -8, rotation: 0 }, // → 5. Vườn chùa
|
"8->2": { yaw: 300, pitch: -8, rotation: 0 }, // → 2. Cổng Thanh Long (bên trái)
|
||||||
"8->9" : { yaw: 30, pitch: -8, rotation: 0 }, // → 9. Sân chùa và đài Quan Âm
|
"8->7": { yaw: 200, pitch: -13, rotation: 0 }, // → 7. Tiền sảnh
|
||||||
"8->22" : { yaw: 115, pitch: -15, rotation: 0 }, // → 22. Thượng điện
|
"8->9": { yaw: 23, pitch: -8, rotation: 0 }, // → 9. Tôn đài Đức Phật Thích Ca
|
||||||
// Cảnh 9 — Sân chùa và đài Quan Âm
|
"8->11": { yaw: 117, pitch: -8, rotation: 0 }, // → 11. Tả đạo
|
||||||
"9->2" : { yaw: 300, pitch: -8, rotation: 0 }, // → 2. Cổng chùa bên đường
|
// Cảnh 9 — Tôn đài Đức Phật Thích Ca
|
||||||
"9->8" : { yaw: 200, pitch: -13, rotation: 0 }, // → 8. Gác chuông Tam quan
|
"9->8": { yaw: 290, pitch: -18, rotation: 0 }, // → 8. Sân chùa
|
||||||
"9->10" : { yaw: 23, pitch: -8, rotation: 0 }, // → 10. Đài Quan Âm lộ thiên
|
"9->10": { yaw: 210, pitch: -22, rotation: 0 }, // → 10. Vườn tháp cổ
|
||||||
"9->12" : { yaw: 117, pitch: -8, rotation: 0 }, // → 12. Lối vào khu nhà Tổ
|
// Cảnh 10 — Vườn tháp cổ
|
||||||
// Cảnh 10 — Đài Quan Âm lộ thiên
|
"10->8": { yaw: 231, pitch: -28, rotation: 0 }, // → 8. Sân chùa
|
||||||
"10->9" : { yaw: 290, pitch: -18, rotation: 0 }, // → 9. Sân chùa và đài Quan Âm
|
"10->9": { yaw: 350, pitch: -11, rotation: -20 }, // → 9. Tôn đài Đức Phật Thích Ca
|
||||||
"10->11": { yaw: 210, pitch: -22, rotation: 0 }, // → 11. Vườn tháp cổ
|
// Cảnh 11 — Tả đạo
|
||||||
// Cảnh 11 — Vườn tháp cổ
|
"11->8": { yaw: 292, pitch: -8, rotation: 0 }, // → 8. Sân chùa
|
||||||
"11->9" : { yaw: 231, pitch: -28, rotation: 0 }, // → 9. Sân chùa và đài Quan Âm
|
"11->12": {yaw: 122, pitch: -8, rotation: 0}, // → 12. Khách xá (chỉnh lại nếu lệch)
|
||||||
"11->10": { yaw: 350, pitch: -11, rotation: -20 }, // → 10. Đài Quan Âm lộ thiên
|
// Cảnh 12 — Khách xá
|
||||||
// Cảnh 12 — Lối vào khu nhà Tổ
|
"12->11": {yaw: 222, pitch: -18, rotation: 0}, // → 11. Tả đạo (chỉnh lại nếu lệch)
|
||||||
"12->9" : { yaw: 292, pitch: -8, rotation: 0 }, // → 9. Sân chùa và đài Quan Âm
|
"12->13": { yaw: 115, pitch: -12, rotation: 0 }, // → 13. Khách xá
|
||||||
"12->13": { yaw: 122, pitch: -8, rotation: 0 }, // → 13. Sân nhà Tổ
|
// Cảnh 13 — Khách xá
|
||||||
// Cảnh 13 — Sân nhà Tổ
|
"13->12": { yaw: -70, pitch: -8, rotation: 0 }, // → 12. Khách xá
|
||||||
"13->12": { yaw: -30, pitch: -18, rotation: 0 }, // → 12. Lối vào khu nhà Tổ
|
"13->14": { yaw: 115, pitch: -8, rotation: 0 }, // → 14. Khách xá
|
||||||
"13->14": { yaw: -150, pitch: -18, rotation: 0 }, // → 14. Hiên nhà dưới giàn cây
|
// Cảnh 14 — Khách xá
|
||||||
// Cảnh 14 — Hiên nhà dưới giàn cây
|
"14->13": { yaw: -22, pitch: -18, rotation: 0 }, // → 13. Khách xá
|
||||||
"14->13": { yaw: 228, pitch: -12, rotation: 0 }, // → 13. Sân nhà Tổ
|
"14->15": { yaw: 230, pitch: -8, rotation: 0 }, // → 15. Khách Đường
|
||||||
"14->15": { yaw: 115, pitch: -12, rotation: 0 }, // → 15. Sân sau nhà khách
|
// Cảnh 15 — Khách Đường
|
||||||
// Cảnh 15 — Sân sau nhà khách
|
"15->14": { yaw: -28, pitch: -18, rotation: 30 }, // → 14. Khách xá
|
||||||
"15->14": { yaw: -70, pitch: -8, rotation: 0 }, // → 14. Hiên nhà dưới giàn cây
|
"15->17": { yaw: -90, pitch: -18, rotation: 0 }, // → 17. Nhà Tổ
|
||||||
"15->16": { yaw: 115, pitch: -8, rotation: 0 }, // → 16. Sân trong giếng trời
|
"15->5": { yaw: -97, pitch: -18, rotation: -40 }, // → 5. Hữu đạo
|
||||||
// Cảnh 16 — Sân trong giếng trời
|
// Cảnh 16 — Hữu Vu La Hán
|
||||||
"16->15": { yaw: -22, pitch: -18, rotation: 0 }, // → 15. Sân sau nhà khách
|
"16->15": { yaw: -62, pitch: -8, rotation: 20 }, // → 15. Khách Đường
|
||||||
"16->17": { yaw: 230, pitch: -8, rotation: 0 }, // → 17. Nhà khách
|
"16->17": { yaw: -80, pitch: -8, rotation: -20 }, // → 17. Nhà Tổ
|
||||||
// Cảnh 17 — Nhà khách
|
// Cảnh 17 — Nhà Tổ
|
||||||
"17->16": { yaw: -28, pitch: -18, rotation: 30 }, // → 16. Sân trong giếng trời
|
"17->15": { yaw: 205, pitch: -12, rotation: 0 }, // → 15. Khách Đường
|
||||||
"17->19": { yaw: -90, pitch: -18, rotation: 0 }, // → 19. Sân thiên tỉnh
|
"17->16": { yaw: 220, pitch: -12, rotation: 10 }, // → 16. Hữu Vu La Hán
|
||||||
"17->6" : { yaw: -97, pitch: -18, rotation: -40 }, // → 6. Lối vào rợp bóng cây
|
"17->18": { yaw: 10, pitch: -12, rotation: -10 }, // → 18. Tả Vu La Hán
|
||||||
// Cảnh 18 — Hành lang tượng cổ
|
// Cảnh 18 — Tả Vu La Hán
|
||||||
"18->17": { yaw: -62, pitch: -8, rotation: 20 }, // → 17. Nhà khách
|
"18->17": { yaw: -110, pitch: -18, rotation: 0 }, // → 17. Nhà Tổ
|
||||||
"18->19": { yaw: -80, pitch: -8, rotation: -20 }, // → 19. Sân thiên tỉnh
|
"18->19": { yaw: 78, pitch: -18, rotation: 0 }, // → 19. Tiền đường
|
||||||
// Cảnh 19 — Sân thiên tỉnh
|
// Cảnh 19 — Tiền đường
|
||||||
"19->17": { yaw: 205, pitch: -12, rotation: 0 }, // → 17. Nhà khách
|
"19->18": { yaw: 50, pitch: -22, rotation: 10 }, // → 18. Tả Vu La Hán
|
||||||
"19->18": { yaw: 220, pitch: -12, rotation: 10 }, // → 18. Hành lang tượng cổ
|
"19->20": { yaw: 190, pitch: -18, rotation: 0 }, // → 20. Tiền đường
|
||||||
"19->20": { yaw: 10, pitch: -12, rotation: -10 }, // → 20. Hành lang tượng và sân vườn
|
// Cảnh 20 — Tiền đường
|
||||||
// Cảnh 20 — Hành lang tượng và sân vườn
|
"20->19": { yaw: 40, pitch: -18, rotation: 0 }, // → 19. Tiền đường
|
||||||
"20->19": { yaw: -110, pitch: -18, rotation: 0 }, // → 19. Sân thiên tỉnh
|
"20->21": { yaw: 200, pitch: -18, rotation: 0 }, // → 21. Tiền đường
|
||||||
"20->21": { yaw: 78, pitch: -18, rotation: 0 }, // → 21. Gian thờ bên trong
|
"20->7": { yaw: 295, pitch: -18, rotation: 0 }, // → 7. Tiền sảnh
|
||||||
// Cảnh 21 — Gian thờ bên trong
|
// Cảnh 21 — Tiền đường
|
||||||
"21->20": { yaw: 50, pitch: -22, rotation: 10 }, // → 20. Hành lang tượng và sân vườn
|
"21->20": { yaw: 30, pitch: -18, rotation: 0 }, // → 20. Tiền đường
|
||||||
"21->22": { yaw: 190, pitch: -18, rotation: 0 }, // → 22. Thượng điện
|
|
||||||
// Cảnh 22 — Thượng điện
|
|
||||||
"22->21": { yaw: 40, pitch: -18, rotation: 0 }, // → 21. Gian thờ bên trong
|
|
||||||
"22->23": { yaw: 200, pitch: -18, rotation: 0 }, // → 23. Hậu đường
|
|
||||||
"22->8" : { yaw: 295, pitch: -18, rotation: 0 }, // → 8. Gác chuông Tam quan
|
|
||||||
// Cảnh 23 — Hậu đường
|
|
||||||
"23->22": { yaw: 30, pitch: -18, rotation: 0 }, // → 22. Thượng điện
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ③ Góc nhìn MẶC ĐỊNH của mỗi cảnh — camera tự quay về hướng này khi vừa vào cảnh.
|
// ③ Góc nhìn MẶC ĐỊNH của mỗi cảnh — camera tự quay về hướng này khi vừa vào cảnh.
|
||||||
// yaw = quay ngang (0 = hướng gốc của ảnh), pitch = ngẩng/cúi (0 = ngang tầm mắt).
|
// yaw = quay ngang (0 = hướng gốc của ảnh), pitch = ngẩng/cúi (0 = ngang tầm mắt).
|
||||||
const sceneDefaultView: Record<number, { yaw: number; pitch: number }> = {
|
const sceneDefaultView: Record<number, { yaw: number; pitch: number }> = {
|
||||||
1: { yaw: 140, pitch: 18 }, // Cổng chùa Thái Lạc
|
1: { yaw: 140, pitch: 18 }, // Tam quan
|
||||||
2: { yaw: 140, pitch: 10 }, // Cổng chùa bên đường
|
2: { yaw: 140, pitch: 10 }, // Cổng Thanh Long (bên trái)
|
||||||
3: { yaw: 120, pitch: 0 }, // Đường vào cổng chùa
|
3: { yaw: 120, pitch: 0 }, // Cổng Bạch Hổ (bên phải)
|
||||||
4: { yaw: 120, pitch: 10 }, // Cổng vào khuôn viên
|
4: { yaw: 120, pitch: 0 }, // Sân Tịnh An
|
||||||
5: { yaw: 120, pitch: 0 }, // Vườn chùa
|
5: { yaw: -80, pitch: -3 }, // Hữu đạo
|
||||||
6: { yaw: -80, pitch: -3 }, // Lối vào rợp bóng cây
|
6: { yaw: 110, pitch: 0 }, // Hồ Di Lặc
|
||||||
7: { yaw: 110, pitch: 0 }, // Hồ nước cảnh quan
|
7: { yaw: 90, pitch: 0 }, // Tiền sảnh
|
||||||
8: { yaw: 90, pitch: 0 }, // Gác chuông Tam quan
|
8: { yaw: 20, pitch: 8 }, // Sân chùa
|
||||||
9: { yaw: 20, pitch: 8 }, // Sân chùa và đài Quan Âm
|
9: { yaw: 110, pitch: 0 }, // Tôn đài Đức Phật Thích Ca
|
||||||
10: { yaw: 110, pitch: 0 }, // Đài Quan Âm lộ thiên
|
10: { yaw: 115, pitch: 0 }, // Vườn tháp cổ
|
||||||
11: { yaw: 115, pitch: 0 }, // Vườn tháp cổ
|
11: { yaw: 120, pitch: 0 }, // Tả đạo
|
||||||
12: { yaw: 120, pitch: 0 }, // Lối vào khu nhà Tổ
|
12: { yaw: -150, pitch: 0 }, // Khách xá
|
||||||
13: { yaw: 220, pitch: 0 }, // Sân nhà Tổ
|
13: { yaw: 130, pitch: 0 }, // Khách xá
|
||||||
14: { yaw: -150, pitch: 0 }, // Hiên nhà dưới giàn cây
|
14: { yaw: 230, pitch: 0 }, // Khách xá
|
||||||
15: { yaw: 130, pitch: 0 }, // Sân sau nhà khách
|
15: { yaw: -60, pitch: 0 }, // Khách Đường
|
||||||
16: { yaw: 230, pitch: 0 }, // Sân trong giếng trời
|
16: { yaw: -80, pitch: 0 }, // Hữu Vu La Hán
|
||||||
17: { yaw: -60, pitch: 0 }, // Nhà khách
|
17: { yaw: 110, pitch: 0 }, // Nhà Tổ
|
||||||
18: { yaw: -80, pitch: 0 }, // Hành lang tượng cổ
|
18: { yaw: 140, pitch: 0 }, // Tả Vu La Hán
|
||||||
19: { yaw: 110, pitch: 0 }, // Sân thiên tỉnh
|
19: { yaw: 130, pitch: 0 }, // Tiền đường
|
||||||
20: { yaw: 140, pitch: 0 }, // Hành lang tượng và sân vườn
|
20: { yaw: 110, pitch: 0 }, // Tiền đường
|
||||||
21: { yaw: 130, pitch: 0 }, // Gian thờ bên trong
|
21: { yaw: 150, pitch: 0 }, // Tiền đường
|
||||||
22: { yaw: 110, pitch: 0 }, // Thượng điện
|
|
||||||
23: { yaw: 150, pitch: 0 }, // Hậu đường
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const scenes: TourScene[] = seeds.map((seed, index) => {
|
const scenes: TourScene[] = seeds.map((seed, index) => {
|
||||||
@@ -400,6 +677,48 @@ const mapRoutes = (() => {
|
|||||||
return routes;
|
return routes;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// LỒNG TIẾNG (thuyết minh) — gán file âm thanh cho từng nhóm cảnh.
|
||||||
|
// Mỗi nhóm dùng CHUNG (các) file; khi khách đi giữa các cảnh trong cùng nhóm thì
|
||||||
|
// audio phát LIỀN MẠCH (không tua lại) nhờ `continuousNarrationGroups` bên dưới.
|
||||||
|
// `audio` có thể là một tên hoặc MẢNG tên — mảng sẽ phát NỐI TIẾP nhau (hàng đợi),
|
||||||
|
// vd cảnh 1-3 phát "lời chào" xong tự phát tiếp "Tam quan".
|
||||||
|
// Cảnh không có trong bảng (cảnh 11) sẽ không phát thuyết minh.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
const narrationGroups: { audio: string | string[]; scenes: number[] }[] = [
|
||||||
|
{ audio: ["loi-chao", "tam-quan"], scenes: [1] },
|
||||||
|
{ audio: "cong-thanh-long-ta", scenes: [2] },
|
||||||
|
{ audio: "cong-bach-ho-huu", scenes: [3] },
|
||||||
|
{ audio: "san-tinh-an", scenes: [4, 5] },
|
||||||
|
{ audio: "ho-di-lac", scenes: [6] },
|
||||||
|
{ audio: "tien-sanh", scenes: [7, 8] },
|
||||||
|
{ audio: "ton-dai-duc-phat", scenes: [9] },
|
||||||
|
{ audio: "vuon-thap-co", scenes: [10] },
|
||||||
|
{ audio: "khach-xa", scenes: [12, 13, 14] },
|
||||||
|
{ audio: "khach-duong", scenes: [15] },
|
||||||
|
{ audio: "huu-vu-ta-vu-la-han", scenes: [16, 18] },
|
||||||
|
{ audio: "nha-to", scenes: [17] },
|
||||||
|
{ audio: "tien-duong", scenes: [19, 20, 21] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sceneNarration = new Map<string, string[]>();
|
||||||
|
for (const group of narrationGroups) {
|
||||||
|
const names = Array.isArray(group.audio) ? group.audio : [group.audio];
|
||||||
|
const sources = names.map(audioForName);
|
||||||
|
for (const scene of group.scenes) {
|
||||||
|
sceneNarration.set(sceneId(scene), sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nhóm cảnh phát liền mạch: chỉ những cảnh dùng chung file VÀ kề nhau trên lối đi
|
||||||
|
// (16 & 18 dùng chung file nhưng cách nhau bởi cảnh 17 nên không gom — tự phát lại).
|
||||||
|
const continuousNarrationGroups: string[][] = [
|
||||||
|
["scene-4", "scene-5"],
|
||||||
|
["scene-7", "scene-8"],
|
||||||
|
["scene-12", "scene-13", "scene-14"],
|
||||||
|
["scene-19", "scene-20", "scene-21"],
|
||||||
|
];
|
||||||
|
|
||||||
export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> = {
|
export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> = {
|
||||||
id: "chua-thai-lac",
|
id: "chua-thai-lac",
|
||||||
title: "Chùa Thái Lạc (Pháp Vân tự)",
|
title: "Chùa Thái Lạc (Pháp Vân tự)",
|
||||||
@@ -408,19 +727,27 @@ export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> =
|
|||||||
welcomeAccent: "Pháp Vân tự",
|
welcomeAccent: "Pháp Vân tự",
|
||||||
welcomeSubtitle:
|
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.",
|
"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: imageForFile(8), // 08.webp — Chính sảnh
|
welcomeBackgroundImage: imageForFile(8), // 08.webp — Tiền sảnh
|
||||||
welcomeCards: [
|
welcomeCards: [
|
||||||
{ image: imageForFile(1), label: "Cổng chùa", rotate: "-rotate-6", sceneId: "scene-1" },
|
{ image: imageForFile(1), label: "Tam quan", rotate: "-rotate-6", sceneId: "scene-1" },
|
||||||
{ image: imageForFile(10), label: "Đài Quan Âm", rotate: "rotate-5", sceneId: "scene-10" },
|
{
|
||||||
|
image: imageForFile(10),
|
||||||
|
label: "Tôn đài Đức Phật",
|
||||||
|
rotate: "rotate-5",
|
||||||
|
sceneId: "scene-9",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
backgroundAudio: "", // chưa có nhạc nền; engine bỏ qua chuỗi rỗng (không render thẻ audio)
|
backgroundAudio: "/media/nhac-nen/nhac-phat-giao.mp3", // nhạc nền nhẹ, tự hạ thấp khi có thuyết minh
|
||||||
practicalInfo: {
|
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ự).",
|
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: [
|
items: [
|
||||||
{ label: "Địa chỉ", value: "Xã Như Quỳnh, tỉnh Hưng Yên." },
|
{ 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: "Người phụ trách", value: "Đang cập nhật." },
|
||||||
{ label: "Số điện thoại", 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." },
|
{
|
||||||
|
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: [
|
notes: [
|
||||||
"Giữ trang phục lịch sự, đi nhẹ nói khẽ, không tự ý di chuyển đồ thờ.",
|
"Giữ trang phục lịch sự, đi nhẹ nói khẽ, không tự ý di chuyển đồ thờ.",
|
||||||
@@ -428,7 +755,7 @@ export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> =
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
scenes,
|
scenes,
|
||||||
sceneNarration: new Map<string, string[]>(),
|
sceneNarration,
|
||||||
continuousNarrationGroups: [],
|
continuousNarrationGroups,
|
||||||
mapRoutes,
|
mapRoutes,
|
||||||
};
|
};
|
||||||
|
|||||||