Compare commits
2 Commits
17a7866cd2
...
25772c9c25
| Author | SHA1 | Date | |
|---|---|---|---|
| 25772c9c25 | |||
| f518c07763 |
Binary file not shown.
Binary file not shown.
@@ -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 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.
|
||||
</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 đá 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.
|
||||
</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 mô 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 mô hình 3D. Vui lòng kiểm tra kết nối và 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} />
|
||||
Mô 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}
|
||||
|
||||
@@ -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 Xã 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}
|
||||
|
||||
@@ -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
@@ -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 2026–2030.",
|
||||
];
|
||||
|
||||
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 Xã 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ữ và 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ử và 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ừ 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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 tư 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 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.
|
||||
</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 tư 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 và 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
@@ -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} />
|
||||
Mô 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 mô hình ba chiều — xoay, phóng to và 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 tư 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 tư 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>
|
||||
|
||||
Reference in New Issue
Block a user