Compare commits

...

2 Commits

Author SHA1 Message Date
tuyenio 25772c9c25 feat: redesign landing pages — bố cục giàu hơn, mượt & cao cấp hơn
- Trang chủ: thêm thẻ trải nghiệm Cổ vật 3D, khu "sắp ra mắt" dạng thẻ gọn
  (bỏ ảnh lặp), bo góc mềm, badge vuông có chấm trạng thái
- Giới thiệu: bố cục editorial bất đối xứng, dải thống kê, 4 trụ giá trị di sản,
  hành trình số hóa 4 bước, thẻ CTA tour nổi bật
- Liên hệ: 2 cột (hướng dẫn đóng góp + thẻ liên hệ), nhúng bản đồ
- CTA: sửa màu chữ tương phản trên nền xanh/nền sáng

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:07:03 +07:00
tuyenio f518c07763 feat: trang trưng bày cổ vật 3D + nâng cấp typography/độ tương phản
- Trình xem mô hình .glb (three GLTFLoader + OrbitControls): xoay/zoom/tự xoay,
  môi trường studio, trạng thái tải & lỗi; trang /co-vat với 2 cổ vật chuông đồng
- Đổi tên file .glb Unicode -> ASCII (chuong-1/2.glb) tránh lỗi đường dẫn
- Thêm font serif hiển thị (Noto Serif Display) cho tiêu đề
- Sửa độ tương phản chữ trên bề mặt sáng (header, ô tìm kiếm, bộ lọc), badge vuông

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:45:41 +07:00
13 changed files with 815 additions and 80 deletions
Binary file not shown.
Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
import type { Metadata } from "next";
import Image from "next/image";
import { Boxes, ScanLine, Sparkles } from "lucide-react";
import { AntiquityShowcase } from "@/app/components/antiquity/AntiquityShowcase";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
import { activeTour, antiquities } from "@/app/content";
export const metadata: Metadata = {
title: "Cổ vật 3D | VR360 Như Quỳnh",
description:
"Phòng trưng bày số các cổ vật, hiện vật tiêu biểu của hệ thống di tích Xã Như Quỳnh được số hóa thành mô hình 3D xem được mọi góc.",
};
const highlights = [
{ icon: ScanLine, title: "Số hóa 3D độ chi tiết cao", detail: "Hiện vật được quét và dựng mô hình ba chiều, lưu giữ hình khối và hoa văn nguyên bản." },
{ icon: Boxes, title: "Xem mọi góc, mọi lúc", detail: "Xoay, phóng to và quan sát từng chi tiết ngay trên trình duyệt, không cần tiếp xúc hiện vật gốc." },
{ icon: Sparkles, title: "Bảo tồn cho mai sau", detail: "Tư liệu số phục vụ trưng bày, giáo dục và nghiên cứu di sản lâu dài." },
];
export default function AntiquityPage() {
return (
<main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
<SiteHeader />
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
<Image src={activeTour.shrineImage} alt="" fill priority sizes="100vw" className="public-subpage-hero-image object-cover" />
<div className="public-subpage-hero-content mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
<div className="public-rise public-hero-rule max-w-4xl">
<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">
Cổ vật & hiện vật 3D
</h1>
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 text-[var(--foreground)]/82 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 Như Quỳnh. Mỗi hiện vật đưc
dựng thành hình ba chiều đ chiêm ngưỡng trọn vẹn từ mọi góc nhìn.
</p>
</div>
</div>
</section>
<AntiquityShowcase items={antiquities} />
<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">
{highlights.map((item) => {
const Icon = item.icon;
return (
<article key={item.title} className="public-panel public-stagger-item rounded-[14px] p-6">
<Icon className="h-7 w-7 text-[var(--primary)]" strokeWidth={1.7} />
<h3 className="mt-4 text-lg font-bold text-[var(--tour-ink)]">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-[var(--muted-foreground)]">{item.detail}</p>
</article>
);
})}
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:py-16">
<div className="public-panel public-glow rounded-[16px] p-6 text-center sm:p-9">
<p className="public-kicker justify-center">Mở rộng bộ sưu tập</p>
<h2 className="public-display mt-3 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
</h2>
<p className="mx-auto mt-3 max-w-[64ch] text-sm leading-7 text-[var(--foreground)]/76 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 đá đ thờ tự tiêu biểu
của toàn bộ hệ thống di tích Như Quỳnh trong giai đoạn 20262030.
</p>
</div>
</section>
<SiteFooter />
<BackToTopButton />
</main>
);
}
@@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { CalendarClock, Layers, Landmark } from "lucide-react";
import { AntiquityViewer } from "./AntiquityViewer";
import type { Antiquity } from "@/app/content";
export function AntiquityShowcase({ items }: { items: Antiquity[] }) {
const [activeId, setActiveId] = useState(items[0]?.id);
const active = items.find((item) => item.id === activeId) ?? items[0];
if (!active) {
return (
<section className="mx-auto max-w-3xl px-4 py-20 text-center sm:px-6">
<p className="text-base text-[var(--muted-foreground)]">Bộ sưu tập cổ vật số đang đưc cập nhật.</p>
</section>
);
}
return (
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:py-16">
<div className="grid gap-6 lg:grid-cols-[1.32fr_0.68fr] lg:items-stretch">
<div className="public-panel public-glow rounded-[16px] p-3 sm:p-4">
<div className="h-[clamp(360px,58vh,640px)] w-full">
{/* key buộc viewer dựng lại sạch khi đổi hiện vật */}
<AntiquityViewer key={active.model} src={active.model} />
</div>
</div>
<div className="public-panel flex flex-col rounded-[16px] p-6 sm:p-7">
<p className="public-kicker">Hiện vật đang xem</p>
<h2 className="public-display public-heading-safe mt-3 text-[clamp(1.7rem,3vw,2.3rem)] font-bold text-[var(--tour-ink)]">
{active.name}
</h2>
<dl className="mt-5 grid gap-2.5">
<Meta icon={Landmark} label="Di tích" value={active.site} />
<Meta icon={Layers} label="Chất liệu" value={active.material} />
<Meta icon={CalendarClock} label="Niên đại" value={active.period} />
</dl>
<div className="mt-5 grid gap-3 border-t border-[var(--surface-border)] pt-5 text-sm leading-7 text-[var(--foreground)]/82">
{active.description.map((text, index) => (
<p key={index}>{text}</p>
))}
</div>
</div>
</div>
<div className="mt-8">
<p className="text-sm font-bold text-[var(--muted-foreground)]">
Bộ sưu tập · {items.length} hiện vật đã số hóa 3D
</p>
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item, index) => {
const isActive = item.id === active.id;
return (
<button
key={item.id}
type="button"
onClick={() => setActiveId(item.id)}
aria-pressed={isActive}
className={`group flex items-center gap-4 rounded-[14px] border p-4 text-left transition active:scale-[0.99] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[rgb(200_164_106_/_0.22)] ${
isActive
? "border-[var(--primary)] bg-[rgb(200_164_106_/_0.12)] shadow-[0_14px_40px_rgb(0_0_0_/_0.3)]"
: "border-[var(--surface-border)] bg-[var(--surface-glass)] hover:border-[rgb(200_164_106_/_0.45)]"
}`}
>
<span
className={`grid h-12 w-12 shrink-0 place-items-center rounded-[10px] font-display-vn text-lg font-bold transition ${
isActive
? "bg-[var(--primary)] text-[var(--primary-foreground)]"
: "bg-[rgb(200_164_106_/_0.14)] text-[var(--primary)]"
}`}
>
{String(index + 1).padStart(2, "0")}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-bold text-[var(--tour-ink)]">{item.name}</span>
<span className="mt-0.5 block text-xs text-[var(--muted-foreground)]">
{item.material} · {item.period}
</span>
</span>
</button>
);
})}
</div>
</div>
</section>
);
}
function Meta({
icon: Icon,
label,
value,
}: {
icon: typeof Landmark;
label: string;
value: string;
}) {
return (
<div className="flex items-start gap-3">
<span className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-[8px] bg-[rgb(200_164_106_/_0.14)]">
<Icon className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
</span>
<span>
<span className="block text-xs font-bold uppercase tracking-[0.1em] text-[var(--muted-foreground)]">{label}</span>
<span className="block text-sm font-semibold text-[var(--tour-ink)]">{value}</span>
</span>
</div>
);
}
@@ -0,0 +1,223 @@
"use client";
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { Rotate3d, Pause, Play, Maximize2 } from "lucide-react";
type Status = "loading" | "ready" | "error";
/**
* Trình xem mô hình 3D (.glb) cho cổ vật: xoay, phóng to, tự xoay.
* Dùng three.js (đã có sẵn trong dự án) — không thêm thư viện mới.
*/
export function AntiquityViewer({ src }: { src: string }) {
const mountRef = useRef<HTMLDivElement>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const homeRef = useRef<{ pos: THREE.Vector3; target: THREE.Vector3 } | null>(null);
const autoRef = useRef(true);
const [status, setStatus] = useState<Status>("loading");
const [progress, setProgress] = useState(0);
const [autoRotate, setAutoRotate] = useState(true);
useEffect(() => {
const mount = mountRef.current;
if (!mount) return;
setStatus("loading");
setProgress(0);
let raf = 0;
let disposed = false;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:grab;";
mount.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 1000);
cameraRef.current = camera;
const pmrem = new THREE.PMREMGenerator(renderer);
const envTexture = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
scene.environment = envTexture;
const keyLight = new THREE.DirectionalLight(0xfff1d6, 2.4);
keyLight.position.set(2.5, 3.5, 2.2);
const rimLight = new THREE.DirectionalLight(0xcdd9ff, 1.2);
rimLight.position.set(-3, 1.8, -2.4);
const ambient = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(keyLight, rimLight, ambient);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enablePan = false;
controls.autoRotate = autoRef.current;
controls.autoRotateSpeed = 1.1;
controls.addEventListener("start", () => {
renderer.domElement.style.cursor = "grabbing";
});
controls.addEventListener("end", () => {
renderer.domElement.style.cursor = "grab";
});
controlsRef.current = controls;
const resize = () => {
const w = mount.clientWidth;
const h = mount.clientHeight;
if (!w || !h) return;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
};
const observer = new ResizeObserver(resize);
observer.observe(mount);
resize();
const loader = new GLTFLoader();
loader.load(
src,
(gltf) => {
if (disposed) return;
const model = gltf.scene;
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
const maxDim = Math.max(size.x, size.y, size.z) || 1;
model.scale.setScalar(2 / maxDim);
scene.add(model);
const radius = 1; // mô hình đã chuẩn hóa về ~2 đơn vị
const dist = (radius / Math.sin((camera.fov * Math.PI) / 360)) * 1.25;
camera.position.set(dist * 0.62, dist * 0.32, dist * 0.92);
controls.target.set(0, 0, 0);
controls.minDistance = dist * 0.55;
controls.maxDistance = dist * 2.6;
controls.update();
homeRef.current = { pos: camera.position.clone(), target: controls.target.clone() };
setStatus("ready");
},
(event) => {
if (event.total) setProgress(Math.round((event.loaded / event.total) * 100));
},
() => {
if (!disposed) setStatus("error");
},
);
const tick = () => {
controls.update();
renderer.render(scene, camera);
raf = requestAnimationFrame(tick);
};
tick();
return () => {
disposed = true;
cancelAnimationFrame(raf);
observer.disconnect();
controls.dispose();
scene.traverse((object) => {
const mesh = object as THREE.Mesh;
if (mesh.geometry) mesh.geometry.dispose();
const material = mesh.material;
if (Array.isArray(material)) material.forEach((m) => m.dispose());
else if (material) (material as THREE.Material).dispose();
});
envTexture.dispose();
pmrem.dispose();
renderer.dispose();
if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
};
}, [src]);
const toggleAutoRotate = () => {
const next = !autoRotate;
setAutoRotate(next);
autoRef.current = next;
if (controlsRef.current) controlsRef.current.autoRotate = next;
};
const resetView = () => {
const camera = cameraRef.current;
const controls = controlsRef.current;
const home = homeRef.current;
if (!camera || !controls || !home) return;
camera.position.copy(home.pos);
controls.target.copy(home.target);
controls.update();
};
return (
<div className="relative h-full w-full overflow-hidden rounded-[14px]">
<div
ref={mountRef}
className="h-full w-full"
style={{
background:
"radial-gradient(circle at 50% 36%, rgb(63 70 54 / 0.55), rgb(18 21 16 / 0.92) 72%)",
}}
/>
{status === "loading" ? (
<div className="absolute inset-0 grid place-items-center bg-[rgb(18_21_16_/_0.55)] backdrop-blur-[2px]">
<div className="flex flex-col items-center gap-3 text-center">
<span className="grid h-12 w-12 animate-spin place-items-center rounded-full border-2 border-[rgb(200_164_106_/_0.25)] border-t-[var(--primary)]" />
<p className="text-sm font-semibold text-[var(--tour-ink)]">Đang tải hình 3D {progress > 0 ? `${progress}%` : ""}</p>
<p className="max-w-[24ch] text-xs text-[var(--muted-foreground)]">Hiện vật đ phân giải cao, vui lòng đi trong giây lát.</p>
</div>
</div>
) : null}
{status === "error" ? (
<div className="absolute inset-0 grid place-items-center bg-[rgb(18_21_16_/_0.7)]">
<p className="max-w-[28ch] px-6 text-center text-sm font-semibold text-[var(--tour-ink)]">
Không tải đưc hình 3D. Vui lòng kiểm tra kết nối thử lại.
</p>
</div>
) : null}
{status === "ready" ? (
<>
<div className="pointer-events-none absolute left-1/2 top-4 -translate-x-1/2 rounded-full border border-[rgb(215_199_157_/_0.18)] bg-[rgb(18_21_16_/_0.55)] px-3.5 py-1.5 text-[0.72rem] font-medium text-[var(--tour-ink)] backdrop-blur-md">
Kéo đ xoay · Lăn chuột đ phóng to
</div>
<div className="absolute bottom-4 right-4 flex gap-2">
<button
type="button"
onClick={toggleAutoRotate}
aria-label={autoRotate ? "Tạm dừng tự xoay" : "Bật tự xoay"}
className="grid h-10 w-10 place-items-center rounded-full border border-[rgb(215_199_157_/_0.22)] bg-[rgb(18_21_16_/_0.6)] text-[var(--tour-ink)] backdrop-blur-md transition hover:border-[var(--primary)] hover:text-[var(--primary)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[rgb(200_164_106_/_0.25)]"
>
{autoRotate ? <Pause className="h-4 w-4" strokeWidth={1.8} /> : <Play className="h-4 w-4" strokeWidth={1.8} />}
</button>
<button
type="button"
onClick={resetView}
aria-label="Đặt lại góc nhìn"
className="grid h-10 w-10 place-items-center rounded-full border border-[rgb(215_199_157_/_0.22)] bg-[rgb(18_21_16_/_0.6)] text-[var(--tour-ink)] backdrop-blur-md transition hover:border-[var(--primary)] hover:text-[var(--primary)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[rgb(200_164_106_/_0.25)]"
>
<Maximize2 className="h-4 w-4" strokeWidth={1.8} />
</button>
</div>
<div className="pointer-events-none absolute bottom-4 left-4 inline-flex items-center gap-1.5 text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-[rgb(248_242_229_/_0.62)]">
<Rotate3d className="h-3.5 w-3.5" strokeWidth={1.8} />
hình 3D
</div>
</>
) : null}
</div>
);
}
@@ -50,7 +50,7 @@ export function CulturalSpaceExplorer({ sites }: { sites: HeritageSite[] }) {
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Nhập tên di tích, loại địa điểm hoặc từ khóa"
className="h-12 w-full rounded-[8px] border border-[var(--surface-border)] bg-white/82 pl-11 pr-4 text-sm font-semibold text-[var(--tour-ink)] outline-none transition focus:border-[var(--primary)] focus:ring-4 focus:ring-[rgb(142_95_11_/_0.12)]"
className="h-12 w-full rounded-[10px] border border-[var(--surface-border)] bg-white/85 pl-11 pr-4 text-sm font-semibold text-[var(--ink-on-light)] outline-none transition placeholder:text-[var(--ink-on-light-muted)] focus:border-[var(--primary)] focus:ring-4 focus:ring-[rgb(142_95_11_/_0.14)]"
/>
</span>
</label>
@@ -110,12 +110,13 @@ export function CulturalSpaceExplorer({ sites }: { sites: HeritageSite[] }) {
</div>
<div className="flex min-h-[19rem] flex-col p-5">
<span
className={`w-fit rounded-full border px-3 py-1 text-xs font-extrabold ${
className={`inline-flex w-fit items-center gap-1.5 rounded-md border px-2.5 py-1 text-[0.7rem] font-bold uppercase tracking-[0.08em] ${
site.isAvailable
? "border-[rgb(14_52_39_/_0.18)] bg-[rgb(14_92_62_/_0.12)] text-[var(--tour-ink)]"
: "border-[rgb(142_95_11_/_0.18)] bg-[rgb(142_95_11_/_0.1)] text-[var(--primary)]"
? "border-[rgb(141_146_118_/_0.4)] bg-[rgb(141_146_118_/_0.16)] text-[var(--tour-gold-light)]"
: "border-[rgb(200_164_106_/_0.28)] bg-[rgb(200_164_106_/_0.1)] text-[var(--primary)]"
}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${site.isAvailable ? "bg-[var(--tour-jade)]" : "bg-[var(--primary)]"}`} />
{site.status}
</span>
<p className="mt-4 text-sm font-bold text-[var(--primary)]">{site.type}</p>
@@ -165,10 +166,10 @@ function FilterGroup<T extends readonly string[]>({
key={option}
type="button"
onClick={() => onChange(option)}
className={`h-12 shrink-0 rounded-full border px-4 text-sm font-extrabold transition active:scale-[0.98] ${
className={`h-12 shrink-0 rounded-[10px] border px-4 text-sm font-bold transition active:scale-[0.98] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[rgb(142_95_11_/_0.18)] ${
value === option
? "border-[var(--primary)] bg-[var(--tour-ink)] text-[var(--primary-foreground)]"
: "border-[var(--surface-border)] bg-white/76 text-[var(--tour-ink)] hover:border-[var(--primary)]/40"
? "border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)] shadow-[0_8px_20px_rgb(142_95_11_/_0.28)]"
: "border-[var(--surface-border)] bg-white/72 text-[var(--ink-on-light-muted)] hover:border-[rgb(142_95_11_/_0.4)] hover:text-[var(--ink-on-light)]"
}`}
>
{option}
+8 -8
View File
@@ -22,10 +22,10 @@ export function SiteHeader() {
<Landmark className="h-5 w-5 text-[var(--primary)]" strokeWidth={1.8} />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-bold leading-tight text-[var(--tour-ink)] sm:text-base">
<span className="public-display block truncate text-base font-bold leading-tight text-[var(--ink-on-light)] sm:text-lg">
VR360 Như Quỳnh
</span>
<span className="hidden truncate text-xs leading-5 text-[var(--muted-foreground)] sm:block">
<span className="hidden truncate text-xs leading-5 text-[var(--ink-on-light-muted)] sm:block">
Di sản văn hóa Như Quỳnh
</span>
</span>
@@ -39,8 +39,8 @@ export function SiteHeader() {
key={item.href}
className={`public-nav-link text-sm font-semibold transition-colors ${
isActive
? "text-[var(--primary)]"
: "text-[var(--muted-foreground)] hover:text-[var(--tour-ink)]"
? "font-bold text-[var(--ink-on-light)]"
: "text-[var(--ink-on-light-muted)] hover:text-[var(--ink-on-light)]"
}`}
href={item.href}
aria-current={isActive ? "page" : undefined}
@@ -62,7 +62,7 @@ export function SiteHeader() {
<button
onClick={() => setMobileMenuOpen((open) => !open)}
className="grid h-10 w-10 place-items-center rounded-[8px] border border-[var(--surface-border)] bg-[rgb(255_255_255_/_0.8)] text-[var(--tour-ink)] shadow-[0_10px_28px_rgb(37_75_54_/_0.08)] transition-transform active:scale-[0.98] lg:hidden"
className="grid h-10 w-10 place-items-center rounded-[8px] border border-[var(--surface-border)] bg-[rgb(255_255_255_/_0.8)] text-[var(--ink-on-light)] shadow-[0_10px_28px_rgb(37_75_54_/_0.08)] transition-transform active:scale-[0.98] lg:hidden"
aria-label="Menu"
aria-expanded={mobileMenuOpen}
>
@@ -84,10 +84,10 @@ export function SiteHeader() {
key={item.href}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className={`rounded-[8px] border px-4 py-3 text-sm font-medium transition-colors ${
className={`rounded-[10px] border px-4 py-3 text-sm font-semibold transition-colors ${
isActive
? "border-[var(--primary)]/35 bg-[var(--surface-glass-strong)] text-[var(--primary)]"
: "border-[var(--surface-border)] bg-[var(--surface-glass)] text-[var(--tour-ink)] hover:bg-[var(--surface-glass-strong)]"
? "border-[rgb(142_95_11_/_0.35)] bg-[rgb(200_164_106_/_0.18)] text-[var(--ink-on-light)]"
: "border-[rgb(28_36_25_/_0.1)] bg-[rgb(255_255_255_/_0.5)] text-[var(--ink-on-light-muted)] hover:text-[var(--ink-on-light)]"
}`}
>
{item.label}
+45
View File
@@ -25,9 +25,54 @@ export const navigation = [
{ href: "/", label: "Trang chủ" },
{ href: "/gioi-thieu", label: "Giới thiệu" },
{ href: "/di-tich", label: "Di tích" },
{ href: "/co-vat", label: "Cổ vật 3D" },
{ href: "/lien-he", label: "Liên hệ" },
];
export type Antiquity = {
id: string;
name: string;
site: string; // di tích lưu giữ
material: string;
period: string; // niên đại
model: string; // đường dẫn file .glb
description: string[];
isAvailable: boolean; // đã có mô hình 3D
};
// Bộ sưu tập cổ vật số hóa 3D. Hiện có 2 hiện vật đã dựng mô hình .glb;
// các trường niên đại/di tích để "Đang cập nhật" cho tới khi có dữ liệu xác thực.
export const antiquities: Antiquity[] = [
{
id: "chuong-dong-1",
name: "Chuông đồng — Hiện vật I",
site: "Cụm di tích Xã Như Quỳnh",
material: "Đồng",
period: "Đang cập nhật",
model: "/antiquity/chuong-1.glb",
description: [
"Chuông đồng là pháp khí quan trọng trong không gian thờ tự của các ngôi chùa Việt, dùng để báo hiệu thời khắc hành lễ và gắn bó với đời sống tâm linh của cộng đồng.",
"Hiện vật đã được số hóa thành mô hình 3D, cho phép xoay và phóng to để quan sát hình khối, hoa văn và minh văn từ mọi góc nhìn.",
"Thông tin chi tiết về niên đại, minh văn và di tích lưu giữ đang được bổ sung.",
],
isAvailable: true,
},
{
id: "chuong-dong-2",
name: "Chuông đồng — Hiện vật II",
site: "Cụm di tích Xã Như Quỳnh",
material: "Đồng",
period: "Đang cập nhật",
model: "/antiquity/chuong-2.glb",
description: [
"Một hiện vật chuông đồng khác trong kho di sản của Xã Như Quỳnh, mang những đặc điểm tạo hình và trang trí riêng.",
"Mô hình 3D giúp người xem chiêm ngưỡng từng chi tiết quai chuông, thân chuông và họa tiết trang trí mà không cần tiếp xúc trực tiếp với hiện vật gốc.",
"Niên đại và nguồn gốc chi tiết đang được nghiên cứu, bổ sung.",
],
isAvailable: true,
},
];
export type HeritageLevel = "Cấp quốc gia" | "Cấp tỉnh";
export type HeritageSite = {
+144 -11
View File
@@ -1,9 +1,22 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRight,
BookOpenText,
Building2,
Compass,
FileText,
Landmark,
PlayCircle,
ScanLine,
Sparkles,
Users,
} from "lucide-react";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
import { activeTour, heritageSites } from "@/app/content";
import { activeTour, antiquities, heritageSites, img } from "@/app/content";
export const metadata: Metadata = {
title: "Giới thiệu | VR360 Như Quỳnh",
@@ -15,9 +28,10 @@ const nationalCount = heritageSites.filter((s) => s.level === "Cấp quốc gia"
const provincialCount = heritageSites.filter((s) => s.level === "Cấp tỉnh").length;
const stats = [
{ value: String(nationalCount), label: "di tích xếp hạng cấp quốc gia" },
{ value: String(nationalCount), label: "di tích cấp quốc gia" },
{ value: String(provincialCount), label: "di tích cấp tỉnh" },
{ value: activeTour.sceneCount, label: "điểm 360° trong tour đầu tiên" },
{ value: activeTour.sceneCount, label: "điểm ảnh 360°" },
{ value: String(antiquities.length), label: "cổ vật số hóa 3D" },
];
const paragraphs = [
@@ -26,6 +40,20 @@ const paragraphs = [
"Mỗi không gian được giới thiệu bằng hình ảnh thật, điểm tương tác, chú thích tiếng Việt và cấu trúc nội dung dễ theo dõi, hướng tới mục tiêu chuyển đổi số di sản giai đoạn 20262030.",
];
const values = [
{ icon: Landmark, title: "Không gian tín ngưỡng", detail: "Đình, chùa, đền, văn miếu — nơi gìn giữ kiến trúc và đời sống tâm linh của cộng đồng làng quê." },
{ icon: BookOpenText, title: "Vùng đất khoa bảng", detail: "Truyền thống hiếu học được tôn vinh qua hệ thống văn miếu và các bậc danh nhân của quê hương." },
{ icon: Building2, title: "Kiến trúc nghệ thuật", detail: "Những mảng chạm khắc, đồ thờ tự và hiện vật mang giá trị mỹ thuật cổ tiêu biểu." },
{ icon: Users, title: "Ký ức cộng đồng", detail: "Lễ hội, phong tục và câu chuyện dân gian gắn liền với từng di tích qua nhiều thế hệ." },
];
const steps = [
{ icon: Compass, title: "Khảo sát", detail: "Xác định danh mục di tích, tuyến tham quan và đầu mối duyệt nội dung tại địa phương." },
{ icon: ScanLine, title: "Số hóa", detail: "Ghi nhận ảnh 360°, mô hình 3D cổ vật và dữ liệu không gian độ phân giải cao." },
{ icon: FileText, title: "Biên tập", detail: "Tổ chức nội dung thành trải nghiệm dễ hiểu cho người dân, học sinh và du khách." },
{ icon: Sparkles, title: "Vận hành", detail: "Đưa lên nền tảng trực tuyến, sẵn sàng mở rộng thêm các tuyến di sản mới." },
];
export default function AboutPage() {
return (
<main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
@@ -34,34 +62,139 @@ export default function AboutPage() {
<section className="public-subpage-hero public-subpage-hero--compact relative isolate overflow-hidden border-b border-[var(--surface-border)]">
<Image src={activeTour.gateImage} alt="" fill priority sizes="100vw" className="public-subpage-hero-image object-cover" />
<div className="public-subpage-hero-content mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
<div className="public-rise max-w-4xl">
<div className="public-rise public-hero-rule max-w-4xl">
<p className="public-kicker">Giới thiệu</p>
<h1 className="public-gradient-text public-heading-safe mt-4 text-[clamp(2.55rem,5.8vw,5rem)] font-extrabold">
Di sản văn hóa Như Quỳnh
</h1>
<p className="public-subpage-copy mt-6 max-w-[60ch] text-base leading-8 text-[var(--foreground)]/82 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ữ lan tỏa
bằng công nghệ số.
</p>
</div>
</div>
</section>
<section className="mx-auto max-w-3xl px-4 py-14 sm:px-6 lg:py-20">
<div className="grid gap-4 text-base leading-8 text-[var(--foreground)]/82">
{/* Intro — bố cục bất đối xứng: nhãn dính bên trái, nội dung bên phải */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:grid lg:grid-cols-[0.42fr_0.58fr] lg:gap-12 lg:py-24">
<div className="lg:sticky lg:top-28 lg:self-start">
<p className="public-kicker">Về vùng đt</p>
<h2 className="public-display public-heading-safe mt-3 text-[clamp(1.9rem,3.2vw,2.8rem)] font-bold text-[var(--tour-ink)]">
Nơi lịch sử tín ngưỡng cùng hội tụ
</h2>
</div>
<div className="mt-6 grid max-w-[65ch] gap-5 text-base leading-8 text-[var(--foreground)]/82 lg:mt-0">
{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>
</section>
{/* Stats */}
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
<div className="mx-auto grid max-w-7xl gap-4 px-4 py-14 sm:px-6 sm:grid-cols-3 lg:py-20">
<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">
{stats.map((s) => (
<div key={s.label} className="public-panel rounded-[8px] p-6 text-center">
<p className="public-gradient-text text-4xl font-extrabold">{s.value}</p>
<p className="mt-2 text-sm leading-6 text-[var(--muted-foreground)]">{s.label}</p>
<div key={s.label} className="public-panel public-stagger-item rounded-[14px] p-6 text-center">
<p className="public-gradient-text font-display-vn text-[2.6rem] font-extrabold leading-none [font-variant-numeric:tabular-nums]">
{s.value}
</p>
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)]">{s.label}</p>
</div>
))}
</div>
</section>
{/* Heritage values */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-24">
<div className="max-w-3xl">
<p className="public-kicker">Giá trị di sản</p>
<h2 className="public-display public-heading-safe mt-3 text-[clamp(1.9rem,3.4vw,3rem)] font-bold text-[var(--tour-ink)]">
Bốn lớp giá trị đưc gìn giữ
</h2>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2">
{values.map((value, index) => {
const Icon = value.icon;
return (
<article
key={value.title}
className={`public-panel public-stagger-item rounded-[16px] p-7 ${index % 2 === 1 ? "md:mt-8" : ""}`}
>
<span className="grid h-12 w-12 place-items-center rounded-[12px] bg-[rgb(200_164_106_/_0.14)]">
<Icon className="h-6 w-6 text-[var(--primary)]" strokeWidth={1.7} />
</span>
<h3 className="mt-5 text-xl font-bold text-[var(--tour-ink)]">{value.title}</h3>
<p className="mt-2.5 text-sm leading-7 text-[var(--muted-foreground)]">{value.detail}</p>
</article>
);
})}
</div>
</section>
{/* Digitization journey */}
<section className="border-y border-[var(--surface-border)] bg-[var(--surface-band)]">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-24">
<div className="max-w-3xl">
<p className="public-kicker">Hành trình số hóa</p>
<h2 className="public-display public-heading-safe mt-3 text-[clamp(1.9rem,3.4vw,3rem)] font-bold text-[var(--tour-ink)]">
Từ liệu đến trải nghiệm số
</h2>
</div>
<ol className="mt-10 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{steps.map((step, index) => {
const Icon = step.icon;
return (
<li key={step.title} className="public-panel public-stagger-item relative rounded-[16px] p-6">
<span className="font-display-vn text-sm font-extrabold tracking-[0.2em] text-[var(--primary)]">
{String(index + 1).padStart(2, "0")}
</span>
<Icon className="mt-4 h-7 w-7 text-[var(--tour-jade)]" strokeWidth={1.7} />
<h3 className="mt-4 text-lg font-bold text-[var(--tour-ink)]">{step.title}</h3>
<p className="mt-2 text-sm leading-6 text-[var(--muted-foreground)]">{step.detail}</p>
</li>
);
})}
</ol>
</div>
</section>
{/* Featured tour CTA */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-20">
<div className="public-card group grid overflow-hidden rounded-[18px] lg:grid-cols-[1.05fr_0.95fr]">
<div className="relative min-h-[20rem]">
<Image
src={img("11.jpg")}
alt="Chùa Thái Lạc (Pháp Vân tự)"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover transition-transform duration-700 group-hover:scale-[1.03]"
/>
</div>
<div className="flex flex-col justify-center p-7 sm:p-10">
<p className="public-kicker">Điểm đến tiêu biểu</p>
<h2 className="public-display public-heading-safe mt-3 text-[clamp(1.8rem,3vw,2.6rem)] font-bold text-[var(--tour-ink)]">
{activeTour.title}
</h2>
<p className="mt-4 max-w-[52ch] text-sm leading-7 text-[var(--foreground)]/80 sm:text-base">
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 di tích đu tiên
đưc số hóa thành tour VR360 hoàn chỉnh.
</p>
<div className="mt-7 flex flex-wrap gap-3">
<Link href={activeTour.href} className="public-cta">
<PlayCircle className="h-4 w-4" strokeWidth={1.8} />
Tham quan VR360
</Link>
<Link href="/co-vat" className="public-cta-secondary">
Xem cổ vật 3D
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
</Link>
</div>
</div>
</div>
</section>
<SiteFooter />
<BackToTopButton />
</main>
+27 -6
View File
@@ -22,8 +22,10 @@
--font-sans: var(--font-noto-sans), system-ui, sans-serif;
--secondary: rgb(244 238 222 / 0.12);
--background: #121510;
--font-serif: var(--font-noto-sans), system-ui, sans-serif;
--font-serif: var(--font-noto-serif), Georgia, "Times New Roman", serif;
--foreground: #f0eadc;
--ink-on-light: #1c2419;
--ink-on-light-muted: #51604a;
--destructive: #d85a42;
--shadow-blur: 5px;
--shadow-color: hsl(95 16% 8%);
@@ -257,6 +259,25 @@ a {
word-break: normal;
}
/* Tiêu đề hiển thị dùng serif trang trọng + tracking âm để có sức nặng thị giác. */
.public-page h1,
.public-page h2 {
font-family: var(--font-serif);
letter-spacing: -0.018em;
font-feature-settings: "liga" 1, "kern" 1;
}
.public-display {
font-family: var(--font-serif);
letter-spacing: -0.02em;
}
.public-display-italic {
font-family: var(--font-serif);
font-style: italic;
font-weight: 500;
}
.public-page p,
.public-page dd {
overflow-wrap: break-word;
@@ -869,7 +890,7 @@ a {
background:
linear-gradient(180deg, rgb(35 95 74), rgb(13 55 41)),
#123a2c;
color: var(--primary-foreground);
color: #f7f0e0;
box-shadow:
0 18px 44px rgb(14 52 39 / 0.22),
inset 0 1px 0 rgb(255 255 255 / 0.2),
@@ -939,17 +960,17 @@ a {
.public-cta-secondary {
border: 1px solid rgb(14 52 39 / 0.18);
background: rgb(255 255 255 / 0.78);
color: var(--tour-ink);
background: rgb(255 255 255 / 0.82);
color: var(--ink-on-light);
box-shadow:
0 14px 32px rgb(37 75 54 / 0.08),
inset 0 1px 0 rgb(255 255 255 / 0.82);
}
.public-cta-secondary:hover {
background: var(--surface-glass-strong);
background: rgb(255 255 255 / 0.95);
transform: translateY(-2px);
border-color: rgb(142 95 11 / 0.28);
border-color: rgb(142 95 11 / 0.4);
box-shadow:
0 18px 40px rgb(37 75 54 / 0.12),
inset 0 1px 0 rgb(255 255 255 / 0.92);
+10 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from "next";
import { Noto_Sans } from "next/font/google";
import { Noto_Sans, Noto_Serif_Display } from "next/font/google";
import "./globals.css";
const notoSans = Noto_Sans({
@@ -8,6 +8,14 @@ const notoSans = Noto_Sans({
weight: ["400", "500", "600", "700", "800"],
});
// Serif hiển thị cho tiêu đề — tăng tính trang trọng, phù hợp di sản; hỗ trợ tiếng Việt đầy đủ.
const notoSerifDisplay = Noto_Serif_Display({
subsets: ["latin", "vietnamese"],
variable: "--font-noto-serif",
weight: ["500", "600", "700"],
style: ["normal", "italic"],
});
export const metadata: Metadata = {
title: "VR360 Di sản Xã Như Quỳnh",
description:
@@ -25,7 +33,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="vi" className={`${notoSans.variable} h-full antialiased`}>
<html lang="vi" className={`${notoSans.variable} ${notoSerifDisplay.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
+71 -9
View File
@@ -1,18 +1,25 @@
import type { Metadata } from "next";
import { MapPin, Mail, Phone } from "lucide-react";
import { Clock, ImagePlus, Landmark, Mail, MapPin, MessageSquareText, Phone, Send } from "lucide-react";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
export const metadata: Metadata = {
title: "Liên hệ | VR360 Như Quỳnh",
description: "Thông tin liên hệ và đầu mối cung cấp tư liệu cho dự án số hóa di sản Xã Như Quỳnh.",
description: "Thông tin liên hệ và đầu mối cung cấp tư liệu cho dự án số hóa di sản Xã Như Quỳnh, tỉnh Hưng Yên.",
};
const contacts = [
{ icon: MapPin, label: "Địa chỉ", value: "Xã Như Quỳnh, tỉnh Hưng Yên" },
{ icon: Phone, label: "Điện thoại", value: "Đang cập nhật" },
{ icon: Mail, label: "Email", value: "Đang cập nhật" },
{ icon: Clock, label: "Thời gian tiếp nhận", value: "Trong giờ hành chính các ngày làm việc" },
];
const steps = [
{ icon: MessageSquareText, title: "Gửi thông tin", detail: "Mô tả ngắn về di tích, cổ vật hoặc câu chuyện bạn muốn đóng góp." },
{ icon: ImagePlus, title: "Kèm hình ảnh, tư liệu", detail: "Hình ảnh, tài liệu hoặc nguồn tham khảo giúp xác minh và biên tập nội dung." },
{ icon: Landmark, title: "Cùng hoàn thiện", detail: "Ban quản lý sẽ rà soát, đối chiếu và đưa nội dung phù hợp lên nền tảng số." },
];
export default function ContactPage() {
@@ -20,32 +27,87 @@ export default function ContactPage() {
<main className="public-page min-h-[100dvh] bg-[var(--background)] text-[var(--foreground)]">
<SiteHeader />
<section className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:py-24">
<section className="mx-auto max-w-7xl px-4 pt-16 sm:px-6 lg:pt-24">
<div className="public-rise max-w-3xl">
<p className="public-kicker">Liên hệ</p>
<h1 className="public-gradient-text public-heading-safe mt-3 text-[clamp(2.2rem,5vw,3.6rem)] font-extrabold">
<h1 className="public-gradient-text public-heading-safe mt-3 text-[clamp(2.2rem,5vw,3.8rem)] font-extrabold">
Liên hệ & đóng góp liệu
</h1>
<p className="mt-5 text-base leading-8 text-[var(--foreground)]/82">
<p className="mt-5 text-base leading-8 text-[var(--foreground)]/82 sm:text-lg">
Người dân, tổ chức liệu, hình nh hoặc câu chuyện về các di tích của Như Quỳnh
vui lòng liên hệ đ cùng hoàn thiện kho dữ liệu số di sản.
</p>
</div>
</section>
<div className="mt-10 grid gap-4">
<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">
{/* Contribution guidance */}
<div>
<h2 className="public-display public-heading-safe text-2xl font-bold text-[var(--tour-ink)] sm:text-3xl">
Cách đóng góp liệu
</h2>
<ol className="mt-6 grid gap-4">
{steps.map((step, index) => {
const Icon = step.icon;
return (
<li key={step.title} className="public-panel public-stagger-item flex items-start gap-4 rounded-[14px] p-5">
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-[10px] bg-[rgb(200_164_106_/_0.14)]">
<Icon className="h-5 w-5 text-[var(--primary)]" strokeWidth={1.8} />
</span>
<span>
<span className="flex items-center gap-2">
<span className="font-display-vn text-xs font-extrabold tracking-[0.18em] text-[var(--primary)]">
{String(index + 1).padStart(2, "0")}
</span>
<span className="text-base font-bold text-[var(--tour-ink)]">{step.title}</span>
</span>
<span className="mt-1 block text-sm leading-6 text-[var(--muted-foreground)]">{step.detail}</span>
</span>
</li>
);
})}
</ol>
</div>
{/* Contact cards */}
<div className="mt-8 lg:mt-0">
<h2 className="public-display public-heading-safe text-2xl font-bold text-[var(--tour-ink)] sm:text-3xl">
Thông tin liên hệ
</h2>
<div className="mt-6 grid gap-3">
{contacts.map((c) => {
const Icon = c.icon;
return (
<div key={c.label} className="public-panel flex items-center gap-4 rounded-[8px] p-5">
<div key={c.label} className="public-panel flex items-center gap-4 rounded-[14px] p-5">
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-full bg-[rgb(200_164_106_/_0.18)]">
<Icon className="h-5 w-5 text-[var(--primary)]" strokeWidth={1.8} />
</span>
<span>
<span className="block text-sm font-bold text-[var(--muted-foreground)]">{c.label}</span>
<span className="block text-base font-bold text-[var(--tour-ink)]">{c.value}</span>
<span className="block text-xs font-bold uppercase tracking-[0.1em] text-[var(--muted-foreground)]">{c.label}</span>
<span className="block text-base font-semibold text-[var(--tour-ink)]">{c.value}</span>
</span>
</div>
);
})}
</div>
<p className="mt-4 inline-flex items-center gap-2 text-xs leading-5 text-[var(--muted-foreground)]">
<Send className="h-3.5 w-3.5 text-[var(--primary)]" strokeWidth={1.8} />
Thông tin số điện thoại email chính thức sẽ đưc cập nhật khi sẵn sàng.
</p>
</div>
</section>
{/* Map */}
<section className="mx-auto max-w-7xl px-4 pb-16 sm:px-6 lg:pb-24">
<div className="public-map-frame overflow-hidden rounded-[16px] border border-[var(--surface-border)]">
<iframe
title="Bản đồ Xã Như Quỳnh, tỉnh Hưng Yên"
src="https://www.google.com/maps?q=Nh%C6%B0%20Qu%E1%BB%B3nh%2C%20H%C6%B0ng%20Y%C3%AAn&output=embed"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="h-[360px] w-full sm:h-[440px]"
/>
</div>
</section>
<SiteFooter />
+68 -16
View File
@@ -1,12 +1,12 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { ArrowRight, Box, Globe2, Landmark, PlayCircle, 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 { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
import { HeroShaderBackground } from "@/app/components/ui/hero-shader-background";
import { activeTour, heritageSites } from "@/app/content";
import { activeTour, heritageSites, img } from "@/app/content";
export const metadata: Metadata = {
title: "Trang chủ | VR360 Như Quỳnh",
@@ -100,13 +100,13 @@ export default function Home() {
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
<div className="flex flex-col justify-between gap-4 lg:flex-row lg:items-end">
<div className="max-w-3xl">
<p className="public-kicker">Di tích nổi bật</p>
<h2 className="public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
Không gian đang mở tour VR360.
<p className="public-kicker">Trải nghiệm nổi bật</p>
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
Đang mở đ khám phá.
</h2>
</div>
<Link href="/di-tich" className="inline-flex items-center gap-2 text-sm font-extrabold text-[var(--tour-ink)] transition hover:text-[var(--primary)]">
Xem toàn bộ không gian
Xem toàn bộ di tích
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
</Link>
</div>
@@ -115,21 +115,72 @@ export default function Home() {
{availableSites.map((site) => (
<HomeSiteCard key={site.id} site={site} />
))}
{/* Trải nghiệm cổ vật 3D */}
<article className="public-card public-stagger-item group grid overflow-hidden rounded-[16px] md:grid-cols-[0.92fr_1.08fr]">
<div className="relative min-h-[18rem]">
<Image
src={img("20.jpg")}
alt="Cổ vật số hóa 3D"
fill
sizes="(max-width: 768px) 100vw, 42vw"
className="object-cover transition-transform duration-700 group-hover:scale-[1.025]"
/>
<span className="absolute left-4 top-4 inline-flex items-center gap-1.5 rounded-md border border-[rgb(215_199_157_/_0.22)] bg-[rgb(18_21_16_/_0.55)] px-2.5 py-1 text-[0.7rem] font-bold uppercase tracking-[0.08em] text-[var(--tour-gold-light)] backdrop-blur-md">
<Rotate3d className="h-3.5 w-3.5" strokeWidth={1.8} />
hình 3D
</span>
</div>
<div className="flex min-h-[18rem] flex-col p-5 sm:p-6">
<p className="mt-1 text-sm font-bold text-[var(--primary)]">Phòng trưng bày số</p>
<h3 className="mt-2 text-2xl font-extrabold leading-tight text-[var(--tour-ink)]">Cổ vật & hiện vật 3D</h3>
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)]">
Chiêm ngưỡng các cổ vật tiêu biểu đưc dựng thành hình ba chiều xoay, phóng to quan sát từng chi tiết.
</p>
<Link href="/co-vat" className="mt-auto inline-flex items-center gap-2 pt-5 text-sm font-extrabold text-[var(--tour-ink)] transition hover:text-[var(--primary)]">
Khám phá cổ vật 3D
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
</Link>
</div>
</article>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:py-20">
<div className="flex flex-col justify-between gap-4 lg:flex-row lg:items-end">
<div className="max-w-3xl">
<p className="public-kicker">Sắp ra mắt</p>
<h2 className="public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
Các điểm đang bổ sung liệu.
<h2 className="public-display public-heading-safe mt-3 text-[2rem] font-bold text-[var(--tour-ink)] sm:text-4xl">
Các di tích đang bổ sung liệu.
</h2>
</div>
<Link href="/di-tich" className="inline-flex items-center gap-2 text-sm font-extrabold text-[var(--tour-ink)] transition hover:text-[var(--primary)]">
Xem tất cả {heritageSites.length} di tích
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
</Link>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2">
<div className="mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{upcomingSites.map((site) => (
<HomeSiteCard key={site.id} site={site} />
<Link
key={site.id}
href={site.detailUrl}
className="public-panel public-stagger-item group flex flex-col rounded-[14px] p-5 transition hover:-translate-y-0.5"
>
<span className="flex items-center justify-between gap-3">
<span className="text-sm font-bold text-[var(--primary)]">{site.type}</span>
<span className="rounded-md border border-[rgb(200_164_106_/_0.24)] bg-[rgb(200_164_106_/_0.08)] px-2 py-0.5 text-[0.64rem] font-bold uppercase tracking-[0.06em] text-[var(--primary)]">
{site.level === "Cấp quốc gia" ? "Quốc gia" : "Cấp tỉnh"}
</span>
</span>
<span className="mt-2 text-lg font-bold leading-snug text-[var(--tour-ink)]">{site.name}</span>
<span className="mt-2 line-clamp-2 text-sm leading-6 text-[var(--muted-foreground)]">{site.shortDescription}</span>
<span className="mt-auto inline-flex items-center gap-1.5 pt-4 text-sm font-bold text-[var(--tour-ink)] transition group-hover:text-[var(--primary)]">
Xem thông tin
<ArrowRight className="h-4 w-4" strokeWidth={1.8} />
</span>
</Link>
))}
</div>
</section>
@@ -147,7 +198,7 @@ export default function Home() {
const Icon = benefit.icon;
return (
<article key={benefit.title} className="public-panel public-stagger-item rounded-[8px] p-5">
<article key={benefit.title} className="public-panel public-stagger-item rounded-[16px] p-6">
<Icon className="h-7 w-7 text-[var(--primary)]" strokeWidth={1.8} />
<h3 className="mt-5 text-xl font-bold text-[var(--tour-ink)]">{benefit.title}</h3>
<p className="mt-3 text-sm leading-6 text-[var(--muted-foreground)]">{benefit.detail}</p>
@@ -159,7 +210,7 @@ export default function Home() {
</section>
<section className="px-4 py-14 sm:px-6 lg:py-20">
<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-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">
<div>
<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">
@@ -184,7 +235,7 @@ export default function Home() {
function HomeSiteCard({ site }: { site: (typeof heritageSites)[number] }) {
return (
<article className="public-card public-stagger-item group grid overflow-hidden rounded-[8px] md:grid-cols-[0.92fr_1.08fr]">
<article className="public-card public-stagger-item group grid overflow-hidden rounded-[16px] md:grid-cols-[0.92fr_1.08fr]">
<div className="relative min-h-[18rem]">
<Image
src={site.image}
@@ -196,12 +247,13 @@ function HomeSiteCard({ site }: { site: (typeof heritageSites)[number] }) {
</div>
<div className="flex min-h-[18rem] flex-col p-5 sm:p-6">
<span
className={`w-fit rounded-full border px-3 py-1 text-xs font-extrabold ${
className={`inline-flex w-fit items-center gap-1.5 rounded-md border px-2.5 py-1 text-[0.7rem] font-bold uppercase tracking-[0.08em] ${
site.isAvailable
? "border-[rgb(14_52_39_/_0.18)] bg-[rgb(14_92_62_/_0.12)] text-[var(--tour-ink)]"
: "border-[rgb(142_95_11_/_0.18)] bg-[rgb(142_95_11_/_0.1)] text-[var(--primary)]"
? "border-[rgb(141_146_118_/_0.4)] bg-[rgb(141_146_118_/_0.16)] text-[var(--tour-gold-light)]"
: "border-[rgb(200_164_106_/_0.28)] bg-[rgb(200_164_106_/_0.1)] text-[var(--primary)]"
}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${site.isAvailable ? "bg-[var(--tour-jade)]" : "bg-[var(--primary)]"}`} />
{site.status}
</span>
<p className="mt-4 text-sm font-bold text-[var(--primary)]">{site.type}</p>