merge: tối ưu hiệu năng web (hero/render-gate, nén ảnh+model, LRU, CSS, bundle)

This commit is contained in:
tuyenio
2026-06-25 09:43:45 +07:00
72 changed files with 2191 additions and 300 deletions
+3
View File
@@ -44,3 +44,6 @@ next-env.d.ts
# ảnh gốc trước khi nén (backup cục bộ)
/image-originals
# model .glb gốc trước khi nén (backup cục bộ)
/model-originals
+5 -1
View File
@@ -1,7 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [360, 640, 768, 1024, 1280, 1536, 1920],
minimumCacheTTL: 2678400,
},
};
export default nextConfig;
+1902 -3
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -6,7 +6,9 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"optimize:images": "node scripts/optimize-images.mjs",
"optimize:models": "node scripts/optimize-models.mjs"
},
"dependencies": {
"@paper-design/shaders-react": "^0.0.76",
@@ -19,6 +21,7 @@
"three": "^0.184.0"
},
"devDependencies": {
"@gltf-transform/cli": "^4.4.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 810 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 999 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

+20 -5
View File
@@ -10,7 +10,8 @@ const MODAL_DIR = path.join(ROOT, "public/modal");
const BACKUP_DIR = path.join(ROOT, "image-originals");
const LQIP_OUT = path.join(ROOT, "src/app/tours/chua-thai-lac.lqip.ts");
const PANO_WIDTH = 6144;
const PANO_WIDTH = 4096;
const PANO_WEBP_QUALITY = 72;
const PANO_QUALITY = 80;
const LQIP_WIDTH = 32;
const MODAL_FILES = ["modal-1.png", "modal-2.png"];
@@ -38,17 +39,31 @@ async function optimizePanoramas() {
const src = await sourceFrom(dest, "chua-thai-lac");
const before = (await fs.stat(dest)).size;
const buf = await sharp(src)
// JPEG (giữ cho next/image ở trang landing) — cũng hạ về 4096
const jpgBuf = await sharp(src)
.rotate()
.resize({ width: PANO_WIDTH, withoutEnlargement: true })
.jpeg({ quality: PANO_QUALITY, mozjpeg: true, progressive: true })
.toBuffer();
await fs.writeFile(dest, buf);
await fs.writeFile(dest, jpgBuf);
// WebP (tour viewer nạp bản này) — cùng tên, đuôi .webp
const webpName = f.replace(/\.jpe?g$/i, ".webp");
const webpDest = path.join(PANO_DIR, webpName);
const webpBuf = await sharp(src)
.rotate()
.resize({ width: PANO_WIDTH, withoutEnlargement: true })
.webp({ quality: PANO_WEBP_QUALITY, effort: 6 })
.toBuffer();
await fs.writeFile(webpDest, webpBuf);
// LQIP khóa theo path .webp (đúng path tour yêu cầu)
const lqipBuf = await sharp(src).rotate().resize({ width: LQIP_WIDTH }).jpeg({ quality: 40 }).toBuffer();
lqip["/images/chua-thai-lac/" + f] = "data:image/jpeg;base64," + lqipBuf.toString("base64");
lqip["/images/chua-thai-lac/" + webpName] = "data:image/jpeg;base64," + lqipBuf.toString("base64");
console.log(`${f}: ${fmt(before)} -> ${fmt(buf.length)} (-${Math.round((1 - buf.length / before) * 100)}%)`);
console.log(
`${f}: ${fmt(before)} -> jpg ${fmt(jpgBuf.length)} | webp ${fmt(webpBuf.length)}`,
);
}
return lqip;
}
+53
View File
@@ -0,0 +1,53 @@
// scripts/optimize-models.mjs
// Nén .glb: backup bản gốc rồi tối ưu (meshopt + nén texture webp).
// Chạy: node scripts/optimize-models.mjs (an toàn chạy lại — luôn nén từ backup gốc)
import { promises as fs } from "node:fs";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
const ROOT = process.cwd();
const MODEL_DIR = path.join(ROOT, "public/antiquity");
const BACKUP_DIR = path.join(ROOT, "model-originals");
const fmt = (b) => (b / 1024 / 1024).toFixed(2) + "MB";
const exists = (p) => fs.access(p).then(() => true).catch(() => false);
async function optimizeModel(file) {
const dest = path.join(MODEL_DIR, file);
const backup = path.join(BACKUP_DIR, file);
await fs.mkdir(BACKUP_DIR, { recursive: true });
if (!(await exists(backup))) {
await fs.copyFile(dest, backup);
}
const before = (await fs.stat(dest)).size;
// Luôn tối ưu TỪ backup gốc; ghi đè ra dest.
const isWin = process.platform === "win32";
const bin = path.join(ROOT, "node_modules/.bin", isWin ? "gltf-transform.cmd" : "gltf-transform");
const q = (s) => (isWin ? `"${s}"` : s);
await run(
q(bin),
[
"optimize",
q(backup),
q(dest),
"--compress", "meshopt",
"--texture-compress", "webp",
],
// Trên Windows phải chạy .cmd qua shell (Node >=18 ném EINVAL nếu spawn .cmd trực tiếp).
{ shell: isWin }
);
const after = (await fs.stat(dest)).size;
console.log(`${file}: ${fmt(before)} -> ${fmt(after)} (-${Math.round((1 - after / before) * 100)}%)`);
}
try {
const files = (await fs.readdir(MODEL_DIR)).filter((f) => /\.glb$/i.test(f)).sort();
for (const f of files) {
await optimizeModel(f);
}
console.log("Done.");
} catch (err) {
console.error("optimize-models failed:", err);
process.exit(1);
}
@@ -0,0 +1,20 @@
"use client";
import dynamic from "next/dynamic";
import type { Antiquity } from "@/app/content";
const AntiquityShowcase = dynamic(
() => import("@/app/components/antiquity/AntiquityShowcase").then((m) => m.AntiquityShowcase),
{
ssr: false,
loading: () => (
<div className="mx-auto max-w-7xl px-4 py-16 text-center text-sm text-[var(--muted-foreground)] sm:px-6">
Đang tải phòng trưng bày 3D
</div>
),
},
);
export default function AntiquityShowcaseClient({ items }: { items: Antiquity[] }) {
return <AntiquityShowcase items={items} />;
}
+2 -2
View File
@@ -1,7 +1,7 @@
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 AntiquityShowcaseClient from "./AntiquityShowcaseClient";
import { BackToTopButton } from "@/app/components/landing/BackToTopButton";
import { SiteFooter } from "@/app/components/landing/SiteFooter";
import { SiteHeader } from "@/app/components/landing/SiteHeader";
@@ -40,7 +40,7 @@ export default function AntiquityPage() {
</div>
</section>
<AntiquityShowcase items={antiquities} />
<AntiquityShowcaseClient 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">
+40
View File
@@ -216,6 +216,38 @@ const panoramaTexturePromises = new Map<string, Promise<THREE.Texture>>();
const panoramaTextureProgress = new Map<string, number>();
const panoramaTextureProgressListeners = new Map<string, Set<(progress: number) => void>>();
const MAX_CACHED_PANORAMAS = 8; // đủ cho cảnh hiện tại + các cảnh lân cận
const pinnedPanoramaUrls = new Set<string>();
// Ghim các ảnh không được phép dispose (cảnh hiện tại + lân cận).
function setPinnedPanoramas(urls: string[]) {
pinnedPanoramaUrls.clear();
urls.forEach((url) => pinnedPanoramaUrls.add(url));
}
// Đánh dấu ảnh là mới-dùng-nhất (Map giữ thứ tự chèn) rồi dọn nếu vượt giới hạn.
function touchPanoramaCache(image: string, texture: THREE.Texture) {
panoramaTextureCache.delete(image);
panoramaTextureCache.set(image, texture);
evictPanoramaTextures();
}
function evictPanoramaTextures() {
if (panoramaTextureCache.size <= MAX_CACHED_PANORAMAS) {
return;
}
for (const [url, texture] of panoramaTextureCache) {
if (panoramaTextureCache.size <= MAX_CACHED_PANORAMAS) {
break;
}
if (pinnedPanoramaUrls.has(url)) {
continue; // không dispose ảnh đang ghim (đang hiển thị/lân cận)
}
panoramaTextureCache.delete(url);
texture.dispose();
}
}
function notifyPanoramaProgress(image: string, progress: number) {
panoramaTextureProgress.set(image, progress);
panoramaTextureProgressListeners.get(image)?.forEach((listener) => listener(progress));
@@ -243,6 +275,7 @@ function loadPanoramaTexture(image: string, onProgress?: (progress: number) => v
const cachedTexture = panoramaTextureCache.get(image);
if (cachedTexture) {
touchPanoramaCache(image, cachedTexture);
onProgress?.(100);
return Promise.resolve(cachedTexture);
}
@@ -263,6 +296,7 @@ function loadPanoramaTexture(image: string, onProgress?: (progress: number) => v
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 8;
panoramaTextureCache.set(image, texture);
evictPanoramaTextures();
panoramaTexturePromises.delete(image);
notifyPanoramaProgress(image, 100);
resolve(texture);
@@ -1311,6 +1345,12 @@ function TourExperience({
const preloadTourScene = useCallback(
(scene: TourScene) => {
// Ghim ảnh đang dùng + các cảnh lân cận để LRU không dispose nhầm.
const neighborImages = scene.hotspots
.map((hotspot) => tourConfig.sceneById.get(hotspot.targetId)?.image)
.filter((image): image is string => Boolean(image));
setPinnedPanoramas([scene.image, ...neighborImages]);
const preloadImage = (image: string) => {
loadPanoramaTexture(image)
.then((texture) => warmPanoramaTexture(image, texture))
@@ -5,6 +5,8 @@ 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 { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
import { createRenderGate } from "@/app/lib/render-gate";
import { Rotate3d, Pause, Play, Maximize2 } from "lucide-react";
type Status = "loading" | "ready" | "error";
@@ -35,7 +37,7 @@ export function AntiquityViewer({ src }: { src: string }) {
let disposed = false;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:grab;";
@@ -70,6 +72,18 @@ export function AntiquityViewer({ src }: { src: string }) {
});
controlsRef.current = controls;
let needsRender = true;
let active = true;
const requestRender = () => {
needsRender = true;
};
controls.addEventListener("change", requestRender);
const stopGate = createRenderGate(mount, (isActive) => {
active = isActive;
if (isActive) requestRender();
});
const resize = () => {
const w = mount.clientWidth;
const h = mount.clientHeight;
@@ -77,12 +91,14 @@ export function AntiquityViewer({ src }: { src: string }) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
needsRender = true;
};
const observer = new ResizeObserver(resize);
observer.observe(mount);
resize();
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
src,
(gltf) => {
@@ -108,6 +124,7 @@ export function AntiquityViewer({ src }: { src: string }) {
homeRef.current = { pos: camera.position.clone(), target: controls.target.clone() };
setStatus("ready");
requestRender();
},
(event) => {
if (event.total) setProgress(Math.round((event.loaded / event.total) * 100));
@@ -118,9 +135,14 @@ export function AntiquityViewer({ src }: { src: string }) {
);
const tick = () => {
controls.update();
renderer.render(scene, camera);
raf = requestAnimationFrame(tick);
if (!active) return;
// controls.update() trả về true khi camera còn chuyển động (damping/autoRotate)
const moving = controls.update();
if (needsRender || moving || controls.autoRotate) {
renderer.render(scene, camera);
needsRender = false;
}
};
tick();
@@ -128,6 +150,8 @@ export function AntiquityViewer({ src }: { src: string }) {
disposed = true;
cancelAnimationFrame(raf);
observer.disconnect();
stopGate();
controls.removeEventListener("change", requestRender);
controls.dispose();
scene.traverse((object) => {
const mesh = object as THREE.Mesh;
@@ -1,111 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import * as THREE from "three";
interface HeroPanoramaBackgroundProps {
imageSrc: string;
}
export function HeroPanoramaBackground({ imageSrc }: HeroPanoramaBackgroundProps) {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const mount = mountRef.current;
if (!mount) {
return;
}
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
powerPreference: "high-performance",
});
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6));
renderer.setSize(mount.clientWidth, mount.clientHeight);
renderer.domElement.className = "h-full w-full";
mount.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(
58,
mount.clientWidth / Math.max(mount.clientHeight, 1),
0.1,
1000,
);
camera.position.set(0, 0, 0.1);
const scene = new THREE.Scene();
const geometry = new THREE.SphereGeometry(500, 96, 64);
geometry.scale(-1, 1, 1);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0,
});
const sphere = new THREE.Mesh(geometry, material);
sphere.rotation.y = -0.28;
scene.add(sphere);
let frame = 0;
let disposed = false;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const loader = new THREE.TextureLoader();
loader.load(
imageSrc,
(texture) => {
if (disposed) {
texture.dispose();
return;
}
texture.colorSpace = THREE.SRGBColorSpace;
texture.minFilter = THREE.LinearFilter;
material.map = texture;
material.opacity = 1;
material.needsUpdate = true;
},
undefined,
() => {
material.opacity = 0;
},
);
const resize = () => {
const width = mount.clientWidth;
const height = Math.max(mount.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
const animate = () => {
if (!reduceMotion) {
sphere.rotation.y += 0.00075;
}
renderer.render(scene, camera);
frame = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(mount);
animate();
return () => {
disposed = true;
cancelAnimationFrame(frame);
resizeObserver.disconnect();
geometry.dispose();
material.map?.dispose();
material.dispose();
renderer.dispose();
renderer.domElement.remove();
};
}, [imageSrc]);
return <div ref={mountRef} aria-hidden className="absolute inset-0" />;
}
@@ -2,7 +2,8 @@
import { MeshGradient } from "@paper-design/shaders-react";
import type React from "react";
import { HeroPanoramaBackground } from "@/app/components/ui/hero-panorama-background";
import { useEffect, useRef, useState } from "react";
import { createRenderGate } from "@/app/lib/render-gate";
interface HeroShaderBackgroundProps {
children: React.ReactNode;
@@ -10,45 +11,37 @@ interface HeroShaderBackgroundProps {
}
export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) {
return (
<div className="relative isolate overflow-hidden border-b border-[var(--surface-border)]">
<svg aria-hidden className="absolute inset-0 h-0 w-0">
<defs>
<filter id="heritage-glass-effect" x="-50%" y="-50%" width="200%" height="200%">
<feTurbulence baseFrequency="0.005" numOctaves="1" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="0.3" />
<feColorMatrix
type="matrix"
values="1 0 0 0 0.03
0 1 0 0 0.04
0 0 1 0 0.02
0 0 0 0.88 0"
result="tint"
/>
</filter>
</defs>
</svg>
const rootRef = useRef<HTMLDivElement>(null);
const [shaderActive, setShaderActive] = useState(false);
useEffect(() => {
const el = rootRef.current;
if (!el) return;
return createRenderGate(el, setShaderActive);
}, []);
return (
<div
ref={rootRef}
className="relative isolate overflow-hidden border-b border-[var(--surface-border)]"
>
<div
aria-hidden
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
style={{ backgroundImage: `url("${imageSrc}")` }}
/>
<HeroPanoramaBackground imageSrc={imageSrc} />
<div aria-hidden className="absolute inset-0 bg-[rgb(243_247_240_/_0.12)]" />
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-48 mix-blend-soft-light">
{/* Một lớp shader duy nhất, chỉ vẽ khi hero trong tầm nhìn + tab hiển thị */}
{shaderActive ? (
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-40">
<MeshGradient
className="absolute inset-0 h-full w-full"
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
speed={0.28}
/>
<MeshGradient
className="absolute inset-0 h-full w-full opacity-45"
colors={["#ffffff", "#e7cb81", "#dcebdd", "#0e3427"]}
speed={0.2}
speed={0.24}
/>
</div>
) : null}
<div
aria-hidden
+4 -111
View File
@@ -240,7 +240,6 @@ a {
background-size: 180% 100%, 84px 84px, 84px 84px;
filter: none;
opacity: 0.9;
animation: public-ambient-pan 18s ease-in-out infinite alternate;
}
.public-page::after {
@@ -722,9 +721,8 @@ a {
box-shadow:
var(--surface-shadow),
inset 0 1px 0 rgb(255 255 255 / 0.88);
backdrop-filter: blur(18px) saturate(132%);
-webkit-backdrop-filter: blur(18px) saturate(132%);
will-change: transform;
backdrop-filter: blur(10px) saturate(120%);
-webkit-backdrop-filter: blur(10px) saturate(120%);
transition:
transform 280ms cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 280ms cubic-bezier(0.16, 1, 0.3, 1),
@@ -778,9 +776,8 @@ a {
box-shadow:
0 24px 66px rgb(37 75 54 / 0.13),
inset 0 1px 0 rgb(255 255 255 / 0.9);
backdrop-filter: blur(12px) saturate(125%);
-webkit-backdrop-filter: blur(12px) saturate(125%);
will-change: transform;
backdrop-filter: blur(8px) saturate(118%);
-webkit-backdrop-filter: blur(8px) saturate(118%);
transition:
transform 280ms cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 280ms cubic-bezier(0.16, 1, 0.3, 1),
@@ -988,7 +985,6 @@ a {
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: public-gradient-flow 8s ease-in-out infinite alternate;
}
.public-shimmer::after {
@@ -1161,19 +1157,6 @@ a {
animation: public-header-drop 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.public-header::after {
animation: public-header-glint 7s ease-in-out 1.2s infinite;
}
.public-logo-mark {
animation: public-logo-breathe 4.8s ease-in-out infinite;
}
.public-header-cta,
.public-cta {
animation: public-cta-breathe 4.2s ease-in-out infinite;
}
.public-hero-inner::before {
animation: public-line-draw 980ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
}
@@ -1182,14 +1165,6 @@ a {
animation: public-line-draw 1100ms cubic-bezier(0.16, 1, 0.3, 1) 260ms both;
}
.public-subpage-hero-image {
animation: public-photo-breathe 12s ease-in-out infinite alternate;
}
.public-hero-inner .public-image-stage {
animation: public-stage-float 7.2s ease-in-out 900ms infinite alternate;
}
.public-hero-rule::before {
animation: public-line-draw 980ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
}
@@ -1228,10 +1203,6 @@ a {
.public-card:hover::after {
animation: public-sheen 820ms cubic-bezier(0.16, 1, 0.3, 1);
}
.public-back-to-top[data-visible="true"] {
animation: public-back-to-top-float 4.8s ease-in-out infinite;
}
}
@media (prefers-reduced-motion: reduce) {
@@ -1267,45 +1238,8 @@ a {
}
}
@keyframes public-header-glint {
0%,
58%,
100% {
opacity: 0;
transform: translateX(0) skewX(-18deg);
}
68% {
opacity: 0.62;
}
82% {
opacity: 0;
transform: translateX(760%) skewX(-18deg);
}
}
@keyframes public-logo-breathe {
0%,
100% {
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.92),
0 10px 24px rgb(37 75 54 / 0.08);
}
50% {
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.98),
0 16px 34px rgb(142 95 11 / 0.14);
}
}
@keyframes public-cta-breathe {
0%,
100% {
filter: saturate(1);
}
50% {
filter: saturate(1.08) brightness(1.03);
}
}
@keyframes public-rise {
from {
@@ -1329,23 +1263,7 @@ a {
}
}
@keyframes public-ambient-pan {
from {
background-position: 0% 0%, 0 0, 0 0;
}
to {
background-position: 100% 0%, 84px 0, 0 84px;
}
}
@keyframes public-gradient-flow {
from {
background-position: 0% 50%;
}
to {
background-position: 100% 50%;
}
}
@keyframes public-line-draw {
from {
@@ -1372,33 +1290,8 @@ a {
}
}
@keyframes public-photo-breathe {
from {
transform: scale(1);
}
to {
transform: scale(1.035);
}
}
@keyframes public-stage-float {
from {
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg);
}
to {
transform: translate3d(0, -10px, 0) rotateX(0.4deg) rotateY(-0.55deg);
}
}
@keyframes public-back-to-top-float {
0%,
100% {
filter: saturate(1);
}
50% {
filter: saturate(1.08) brightness(1.03);
}
}
/* Lớp nhiễu hạt phim cố định — tạo chiều sâu, chất liệu cao cấp cho nền tối. */
.site-grain {
+39
View File
@@ -0,0 +1,39 @@
// src/app/lib/render-gate.ts
// Chỉ cho phép vòng lặp vẽ (rAF/setAnimationLoop) chạy khi phần tử nằm trong vùng nhìn
// VÀ tab đang hiển thị. Gọi onChange(active) mỗi khi trạng thái đổi. Trả về hàm dọn dẹp.
export function createRenderGate(
element: Element,
onChange: (active: boolean) => void,
): () => void {
let onScreen = false;
let visible = typeof document === "undefined" ? true : !document.hidden;
let last = false;
const emit = () => {
const next = onScreen && visible;
if (next !== last) {
last = next;
onChange(next);
}
};
const io = new IntersectionObserver(
(entries) => {
onScreen = entries.some((entry) => entry.isIntersecting);
emit();
},
{ threshold: 0.01 },
);
io.observe(element);
const onVisibility = () => {
visible = !document.hidden;
emit();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
io.disconnect();
document.removeEventListener("visibilitychange", onVisibility);
};
}
+16
View File
@@ -0,0 +1,16 @@
"use client";
import dynamic from "next/dynamic";
const VirtualTour = dynamic(() => import("@/app/components/VirtualTour"), {
ssr: false,
loading: () => (
<div className="grid min-h-[100dvh] place-items-center bg-[#f5f1e6] text-[#4a3f35]">
<p className="text-sm font-bold">Đang tải không gian 360°</p>
</div>
),
});
export default function TourClient({ tourId }: { tourId?: string }) {
return <VirtualTour tourId={tourId} />;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import VirtualTour from "@/app/components/VirtualTour";
import TourClient from "./TourClient";
export const metadata: Metadata = {
title: "VR360 Chùa Thái Lạc (Pháp Vân tự)",
@@ -8,5 +8,5 @@ export const metadata: Metadata = {
};
export default function ChuaThaiLacTourPage() {
return <VirtualTour tourId="chua-thai-lac" />;
return <TourClient tourId="chua-thai-lac" />;
}
+26 -26
View File
@@ -1,30 +1,30 @@
// TỰ ĐỘNG SINH bởi scripts/optimize-images.mjs — KHÔNG sửa tay.
// Ảnh mờ tạm (blur-up/LQIP) base64 cho mỗi panorama, khoá theo đường dẫn ảnh.
export const lqip: Record<string, string> = {
"/images/chua-thai-lac/01.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABQECBP/EACgQAAIBAwEFCQAAAAAAAAAAAAECAwAEETEFEhMhUSIjMjNBYXGBof/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAwDAQACEQMRAD8As77OlaO3Ktuq3mnByPjpz1oW7sRC5KhSh5Aqxx9eppwzk+HBIOoIq8SKUFJHQsMEHUA1mrIDtrWMwtLIyDA3gC35SFlLwpxFIUaBVzJ3ecg9PeutrZJY+0iFeoIraPHFGI0IyNd3mBTTH//Z",
"/images/chua-thai-lac/02.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEBf/EACMQAAIBBAICAgMAAAAAAAAAAAECAwAEERIFMSFxEyJhcoH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABcRAQEBAQAAAAAAAAAAAAAAAAARARL/2gAMAwEAAhEDEQA/ACLmgZUjkttWIBOCfFXScvaxWssjRnKEAAr3no+qmexQvtoVcjGw7p0FikcJSNXkLeTkFvVTrSM+Ll5EkDXADxk4KxqNgexVZ5SJrB50t3WQHVY27b8/ymixRSSYTn9KbJZJND8ckZKn7DKkYNLpH//Z",
"/images/chua-thai-lac/03.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEBf/EACQQAAIBAwMEAwEAAAAAAAAAAAECAwAREgQFITFBcYEUFSKR/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/ALNHvME72YCMY5XzB9eaZrNyHw45dOyAEkMWHI9VP9dGyhWCoAMQc+170WhgIEaqMF4AtUukQR7xqYGDTAzRvexva9u4rUbcYAiOLkOuQ5/g80mPSwLb8Jx046U2TRQakiRSM4+AA1gaXcSP/9k=",
"/images/chua-thai-lac/04.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwQC/8QAIxAAAgIBBAICAwAAAAAAAAAAAQIDEQQABRIhEzEiQWGh4f/EABQBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEgH/2gAMAwEAAhEDEQA/ALMfdMOReXl8YuvkPf8ANOd0x5MRpIeLEGqNAnRT4uPNGAWUGqHXr8ami2yGI9OoX3Siu9FmWId2idh54nRT3YN6pizsd+QYrG6mirH96zJhYjKF4lSKAYN9aOLEg5uVd+JNkX0dF6Jf/9k=",
"/images/chua-thai-lac/05.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGQAAAQUAAAAAAAAAAAAAAAAABAECAwUG/8QAIxAAAgEDAwQDAAAAAAAAAAAAAQIDAAQREiExBUFRgRMUkf/EABYBAQEBAAAAAAAAAAAAAAAAAAEAAv/EABcRAQEBAQAAAAAAAAAAAAAAAAAREgH/2gAMAwEAAhEDEQA/ACrS/wCmC2iaQIuvtIMnx6qCf6l9FK9i6oyYJDbD1ReI5LcqHXjY4GKEtLK1hlDNcK5Bzpz3rOllQtJKFUtE++4OCM+aSMvqJIcHkbYGK0FykEkufmiYDhS3H5TVit8DOhGHOptyKL0x/9k=",
"/images/chua-thai-lac/06.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAgQBAwUG/8QAJRAAAgICAQMDBQAAAAAAAAAAAQIDBAARIRIxYQUyQRVRUnHB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgH/xAAaEQACAwEBAAAAAAAAAAAAAAAAAQIRIRJR/9oADAMBAAIRAxEAPwALiU7EDmvGI50UFtNpd/bWZcdGad0H5nR6T7R3JzoJqSKVavMIjrTcA78/vFVgsSWiXnCgjlwvOC34QGKD06tMERprBX50Cr+PGMPIjzKfp6JpSSAQRv44/mRYqtG3VWeP29Oie+VR17UzASOiKO+jhcp3iEua0//Z",
"/images/chua-thai-lac/07.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQCAwX/xAAiEAACAQMFAAMBAAAAAAAAAAABAgMABBEFEiEiMRVBgZH/xAAVAQEBAAAAAAAAAAAAAAAAAAABAP/EABkRAAMAAwAAAAAAAAAAAAAAAAABAgMREv/aAAwDAQACEQMRAD8A0k1C0jCl5lUEZ8zUItatrqSROsYTxiR2pJLCBWyIGBGOWer5rOGSMKm0bh2AFDsVOw+RtGcgyMozw2ODRPf20Eayb94bwLyf5S1tpiQSbgDkjHY+U3Nbq8AjTn6fadrfhoWQuD//2Q==",
"/images/chua-thai-lac/08.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEAP/EACkQAAIBAgIIBwAAAAAAAAAAAAECEQADBAUSExQVIUFRgSJCUmFxkdH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABcRAAMBAAAAAAAAAAAAAAAAAAABERL/2gAMAwEAAhEDEQA/AK97ZeonXp9U21nGAvhlR1keZkianuHDtytmOgFEPbA8JUdxU0xEK3lhbl0jiqesn55dqaMwwCaJN8AMJEg/lbWWQRxSesChtGHdtFnU+zRV0xEf/9k=",
"/images/chua-thai-lac/09.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIEA//EACMQAAIBAwQBBQAAAAAAAAAAAAECAwAEEQUSITFBIiNRcpH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABYRAQEBAAAAAAAAAAAAAAAAAAASAf/aAAwDAQACEQMRAD8Ast9Xs5I42kdYnYZIY9GmutZs40Qh1cHsoOqMQjG1EHHgDihhE+NwDY+cGpWkornW7dFjaBGmLH1LnbtFa32qw2sCSQoZWboHIAGPNU+0vACfgpSIZOGSMj6ilaTj/9k=",
"/images/chua-thai-lac/10.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAABAUAAgP/xAAkEAACAQMCBwEBAAAAAAAAAAABAgMABBEFMRITFCEiYXFRgf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/8QAGBEBAAMBAAAAAAAAAAAAAAAAAAEDEhP/2gAMAwEAAhEDEQA/ADZdZigQPMrKpOAV74+1JNdtYngUlzzdyBng+0OUik8X4WX5WhitvHgjVSuxo2vnK+qazyYV6MxSSE4O/Yfvul1hrF686dQUaLOCcAH+UwuAksYBKE+xQyW0CHOIwRsSBRoxW//Z",
"/images/chua-thai-lac/11.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAgMEAQX/xAAlEAACAQMEAgEFAAAAAAAAAAABAgMABBEFEiExEyJRFEFSYYH/xAAUAQEAAAAAAAAAAAAAAAAAAAAB/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQAREv/aAAwDAQACEQMRAD8AZqurMsEa2E+yV+WOB6j4PwaVpTXyXm++u5XhRD2cgmr7i0tpguSW29ZPVAtonj2FjtDZ4JBo1nCK31eK58uyOUCMZ98DP6FQxa4wdpJAWRuBH91/tdAWlqQN4LY/NycVv0ViHZ1jUMeyOKOljC//2Q==",
"/images/chua-thai-lac/12.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAABAP/xAAnEAACAQMCAwkAAAAAAAAAAAABAgMABBEFIRJBYRMVIiMyUXGRof/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/EABgRAAMBAQAAAAAAAAAAAAAAAAABAhEh/9oADAMBAAIRAxEAPwBR1LyTJGwaVV9IBwT1HtQu9dRJZnUADB8KjHxTruwSVENs4R1XG52YdaKum3DkJIvAnMqwNZbocLyamyxKjMgmfccAOB986LJrNwkhjQR5GNypxmn3FgknZlHEbKOHcZGKMbAs2HmjG+5C5/aOqJh//9k=",
"/images/chua-thai-lac/13.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAABAIDBf/EACMQAAICAQIHAQEAAAAAAAAAAAECAxEABBIFEyEiMVFhQRT/xAAWAQEBAQAAAAAAAAAAAAAAAAABAAL/xAAZEQACAwEAAAAAAAAAAAAAAAAAEQECElH/2gAMAwEAAhEDEQA/ACPMDp5DzZGlDdjDr0+3izxCTU6YwgkBVo0m8k+/GJk4dC6s6m2K1TH995KHQvCq8rbuqmLfuZdhUMy0eKPUxW8oViQ5ksLXsfcvGsDyqg1M38yjt7e4+b6ivWN1XDRIqyGTc6eFrocJyWDBBHIisKZt10Pl4atwlB//2Q==",
"/images/chua-thai-lac/14.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABAMBBf/EACUQAAIBAwIFBQAAAAAAAAAAAAECAwAEERIhEyMxQVEUMmFxkf/EABYBAQEBAAAAAAAAAAAAAAAAAAIAA//EABkRAAMAAwAAAAAAAAAAAAAAAAABEQISIf/aAAwDAQACEQMRAD8APLbBghQM506mBPt+Kk8aqoYZHbIBI+669w1nJIhkSNVC6TliMDtRZIbJITH6hAhOVOsGs3gSSChRCnEWR8/gNagZ445Uzzd13Jz560mERSqyxyZC9CW26eKpaQyRnnSRsg3VUOMUdOQah//Z",
"/images/chua-thai-lac/15.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAABAIDBf/EACMQAAICAgICAQUAAAAAAAAAAAECAxEABAUhEyISFTEyM3H/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/EABgRAAMBAQAAAAAAAAAAAAAAAAABAhEh/9oADAMBAAIRAxEAPwBsfNxvqRFmBn6DqRXeR+rkyyEqBEnRA7N/3E7Wlo7SqG8SuoADWLFYccdpoWZpEotdAisO0XEA5Dcn3NgrpPJ4kA/Wasn74OCfcLP4JJy0Z9uz1m/LriR1l1thY6FfEGhlUfHWX8kigMfam/LC3WjlTnT/2Q==",
"/images/chua-thai-lac/16.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEAv/EACEQAAICAgMAAgMAAAAAAAAAAAECAxEAEgQhMUFREyJx/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAED/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQACEf/aAAwDAQACEQMRAD8AUvJvjKyMRfZ2HRxZeQwgvG6sR0D85Y8HGcBASF9Ftd/3A61CY3ZWQEVZFA5m5aoNiBDHCzu0RfWvxte1/YycpKpYvsGUbaeULyvjiKOczPIu2pXrw3jJV48nrsLFfq3x9YRYcL//2Q==",
"/images/chua-thai-lac/17.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABQMG/8QAJBAAAgIBAwIHAAAAAAAAAAAAAQIDEQAEEiExQQUTFCJRgaH/xAAWAQEBAQAAAAAAAAAAAAAAAAADAAH/xAAaEQACAgMAAAAAAAAAAAAAAAAAAQMREyFB/9oADAMBAAIRAxEAPwBYy8Hb2NEnoMlLq3iVAIxIz8rR4/czom1BdTvZU6n3VzlfVvI4Ll5CtgBjYw7Y6iY2urlWPzZoNsYPJRgSPrKLroHra1k9B3wrUeJARiOMKhHcR2DhxMrQrU7ijzTE38ZW+GY3uz//2Q==",
"/images/chua-thai-lac/18.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAED/8QAIRAAAgEEAgIDAAAAAAAAAAAAAQIDAAQRIRITImEUQdH/xAAWAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwAHWwlCRCVig8goIJFLhmmiUE2UrKD5bDYrb45knuJo2ALgceRyCfyrbW9zJKnbxjjXZKnJPqjJ2iNIbu+IjgcoWIXOtD1Vmh6mKyRMFxrioNPmtl7i+wcnBFDWGPsZps5RsAknQ+qjU1//2Q==",
"/images/chua-thai-lac/19.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAgMEBf/EACUQAAIBAwQBBAMAAAAAAAAAAAECAwAEERIhMUEFExQiUSRhcf/EABYBAQEBAAAAAAAAAAAAAAAAAAIBA//EABcRAQEBAQAAAAAAAAAAAAAAAAEAAhH/2gAMAwEAAhEDEQA/AJ4vIe5DARMpHG3NaPj54HtzI7a3zsgxsPs0qO2iRhobSOxqzn978U6WC2lh0EIp41KBmga7a6EIze2pYqVxjsEEfzmsmTyP5cygFowcpgcjqr4LCCGQP6hkb6bBo7mCKRmLYBOPl3VdMcnb/9k=",
"/images/chua-thai-lac/20.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAQIFAAT/xAAkEAACAQMDBAMBAAAAAAAAAAABAgMABBEFEiETMUFxIlFhYv/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/8QAGBEBAQADAAAAAAAAAAAAAAAAAQACEVH/2gAMAwEAAhEDEQA/AO6bVjbKDPDsUnAZW349jio0+oztemSJk6bN4zwPuqUmnQzbRliB/VONMtou0aj9zUVYHZ7u9PSjeBXXPLbozz6oLqkZChY3LngruwM+6ElvgIhmcIO6k5yKxtLRhkJ8gc5Hig5TRf/Z",
"/images/chua-thai-lac/21.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAID/8QAIhAAAgEEAgIDAQAAAAAAAAAAAQIDAAQRIRIxBUETFDKB/8QAFgEBAQEAAAAAAAAAAAAAAAAABAEC/8QAGBEAAwEBAAAAAAAAAAAAAAAAABESIRP/2gAMAwEAAhEDEQA/AMpLCyikdtMq6XA/Rotxaq7ZijVR0Me6uO2EkTqJ8nGgw0KTBbgyc/lRZSMZ6AFHt4J5rTCLx0Uh4SJwcAaHs1MfjLYuqknJbeegKZLEebs8o5dDjvIoU1pGwJ+w6nJJB7P9rTRJZ//Z",
"/images/chua-thai-lac/22.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAABAP/xAAiEAACAQQCAgMBAAAAAAAAAAABAgMABBExEiETQWGBkaH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAwT/xAAZEQEBAQADAAAAAAAAAAAAAAABAAIREzH/2gAMAwEAAhEDEQA/AIzXEnhwlvEGXZGDmi8JbkBiRGvyQKVPZwuV8cgQrvB3R5beXI4Trjvotkn+VI02hwHkV4WSZAUYq28mlZQkGGIDBxxY7+6qtlcOqiV+Sb67NVSxRAQWYD2Dv99UG0nq5v/Z",
"/images/chua-thai-lac/23.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAgME/8QAIxAAAgEEAAYDAAAAAAAAAAAAAQIDAAQRIQUSMTJBURMikf/EABcBAAMBAAAAAAAAAAAAAAAAAAABAwT/xAAZEQACAwEAAAAAAAAAAAAAAAAAAQIREkH/2gAMAwEAAhEDEQA/ACJE2wim5R0OD0pxzLLN8SqqNnucHQ85oSWTsw5CwX0TmtlpaKA4cdw81FUaHskXRQAzIHzjAqN2qxrHKJ8c3cM6G8ftaJOEAn6ysN7yM69VR7GKK1ESgtvJJO6MrolOdn//2Q==",
"/images/chua-thai-lac/24.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAMB/8QAIhAAAgEEAgIDAQAAAAAAAAAAAQIDAAQREiExE1EUInGx/8QAFgEBAQEAAAAAAAAAAAAAAAAAAwAB/8QAGhEAAgMBAQAAAAAAAAAAAAAAAAECEkERIf/aAAwDAQACEQMRAD8AF5L1pmAJAxkBjilJLcQIkayCSY847B56o+gEqs10HUceuKS3xzNFNGRvEQwOeCaNSfPRHBYXW/vlXJjjEYOGYYOD6/alK90WEks4WJusD+it2thbNCFKh38hO+ftUDbW7RAJcmMnJYbd1tmVFp//2Q==",
"/images/chua-thai-lac/25.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwIE/8QAJBAAAgEEAQMFAQAAAAAAAAAAAQIDAAQRIRITMXEFFCJBQoH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAA//EABkRAQADAQEAAAAAAAAAAAAAAAEAAhESQf/aAAwDAQACEQMRAD8AOWNRbxlRKJydqdCoje5t0EjcXJ3wJzgeaeaCzCgK7Hee/ajmtldgIkbiR+c/Gpjb2U5I7+pzOFX28agjOdHX8rDcRXKyO3UyoOQeWiPFKLAIRzeUj6BGKZ7dG6ZkYFgSWxRs5BU2f//Z",
"/images/chua-thai-lac/26.jpg": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAwQBAgX/xAAjEAACAgICAgEFAAAAAAAAAAABAgMRAAQSIRMUBSIjJDJh/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAP/xAAYEQADAQEAAAAAAAAAAAAAAAAAAQISEf/aAAwDAQACEQMRAD8AC/togWGZpZWPSEUBh4/kdyG1j4OFq2YgViiPohgW2mYHo2xwPlgUhX2PtH9lFWTdjJp0UzJpS7W3wb2x4/qKWpHRq8Qlj3omDNOHH8Y5abc053X8jnbciG6s1VnJl2dWRI4jOvJBTMGq8N1wZk//2Q==",
"/images/chua-thai-lac/01.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABQECBP/EACgQAAIBAwEFCQAAAAAAAAAAAAECAwAEETEFEhMhUSIjMjNBYXGBof/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAwDAQACEQMRAD8As77OlaO3Ktuq3mnByPjpz1oW7sRC5KhSh5Aqxx9eppwzk+HBIOoIq8SKUFJHQsMEHUA1mrIDtrWMwtLIyDA3gC35SFlLwpxFIUaBVzJ3ecg9PeutrZJY+0iFeoIraPHFGI0IyNd3mBTTH//Z",
"/images/chua-thai-lac/02.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEBf/EACMQAAIBBAICAgMAAAAAAAAAAAECAwAEERIFMSFxEyJhcoH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABcRAQEBAQAAAAAAAAAAAAAAAAARARL/2gAMAwEAAhEDEQA/ACLmgZUjkttWIBOCfFXScvaxWssjRnKEAAr3no+qmexQvtoVcjGw7p0FikcJSNXkLeTkFvVTrSM+Ll5EkDXADxk4KxqNgexVZ5SJrB50t3WQHVY27b8/ymixRSSYTn9KbJZJND8ckZKn7DKkYNLpH//Z",
"/images/chua-thai-lac/03.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEBf/EACQQAAIBAwMEAwEAAAAAAAAAAAECAwAREgQFITFBcYEUFSKR/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/ALNHvME72YCMY5XzB9eaZrNyHw45dOyAEkMWHI9VP9dGyhWCoAMQc+170WhgIEaqMF4AtUukQR7xqYGDTAzRvexva9u4rUbcYAiOLkOuQ5/g80mPSwLb8Jx046U2TRQakiRSM4+AA1gaXcSP/9k=",
"/images/chua-thai-lac/04.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwQC/8QAIxAAAgIBBAICAwAAAAAAAAAAAQIDEQQABRIhEzEiQWGh4f/EABQBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEgH/2gAMAwEAAhEDEQA/ALMfdMOReXl8YuvkPf8ANOd0x5MRpIeLEGqNAnRT4uPNGAWUGqHXr8ami2yGI9OoX3Siu9FmWId2idh54nRT3YN6pizsd+QYrG6mirH96zJhYjKF4lSKAYN9aOLEg5uVd+JNkX0dF6Jf/9k=",
"/images/chua-thai-lac/05.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGQAAAQUAAAAAAAAAAAAAAAAABAECAwUG/8QAIxAAAgEDAwQDAAAAAAAAAAAAAQIDAAQREiExBUFRgRMUkf/EABYBAQEBAAAAAAAAAAAAAAAAAAEAAv/EABcRAQEBAQAAAAAAAAAAAAAAAAAREgH/2gAMAwEAAhEDEQA/ACrS/wCmC2iaQIuvtIMnx6qCf6l9FK9i6oyYJDbD1ReI5LcqHXjY4GKEtLK1hlDNcK5Bzpz3rOllQtJKFUtE++4OCM+aSMvqJIcHkbYGK0FykEkufmiYDhS3H5TVit8DOhGHOptyKL0x/9k=",
"/images/chua-thai-lac/06.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAgQBAwUG/8QAJRAAAgICAQMDBQAAAAAAAAAAAQIDBAARIRIxYQUyQRVRUnHB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgH/xAAaEQACAwEBAAAAAAAAAAAAAAAAAQIRIRJR/9oADAMBAAIRAxEAPwALiU7EDmvGI50UFtNpd/bWZcdGad0H5nR6T7R3JzoJqSKVavMIjrTcA78/vFVgsSWiXnCgjlwvOC34QGKD06tMERprBX50Cr+PGMPIjzKfp6JpSSAQRv44/mRYqtG3VWeP29Oie+VR17UzASOiKO+jhcp3iEua0//Z",
"/images/chua-thai-lac/07.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQCAwX/xAAiEAACAQMFAAMBAAAAAAAAAAABAgMABBEFEiEiMRVBgZH/xAAVAQEBAAAAAAAAAAAAAAAAAAABAP/EABkRAAMAAwAAAAAAAAAAAAAAAAABAgMREv/aAAwDAQACEQMRAD8A0k1C0jCl5lUEZ8zUItatrqSROsYTxiR2pJLCBWyIGBGOWer5rOGSMKm0bh2AFDsVOw+RtGcgyMozw2ODRPf20Eayb94bwLyf5S1tpiQSbgDkjHY+U3Nbq8AjTn6fadrfhoWQuD//2Q==",
"/images/chua-thai-lac/08.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEAP/EACkQAAIBAgIIBwAAAAAAAAAAAAECEQADBAUSExQVIUFRgSJCUmFxkdH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABcRAAMBAAAAAAAAAAAAAAAAAAABERL/2gAMAwEAAhEDEQA/AK97ZeonXp9U21nGAvhlR1keZkianuHDtytmOgFEPbA8JUdxU0xEK3lhbl0jiqesn55dqaMwwCaJN8AMJEg/lbWWQRxSesChtGHdtFnU+zRV0xEf/9k=",
"/images/chua-thai-lac/09.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIEA//EACMQAAIBAwQBBQAAAAAAAAAAAAECAwAEEQUSITFBIiNRcpH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABYRAQEBAAAAAAAAAAAAAAAAAAASAf/aAAwDAQACEQMRAD8Ast9Xs5I42kdYnYZIY9GmutZs40Qh1cHsoOqMQjG1EHHgDihhE+NwDY+cGpWkornW7dFjaBGmLH1LnbtFa32qw2sCSQoZWboHIAGPNU+0vACfgpSIZOGSMj6ilaTj/9k=",
"/images/chua-thai-lac/10.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAABAUAAgP/xAAkEAACAQMCBwEBAAAAAAAAAAABAgMABBEFMRITFCEiYXFRgf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/8QAGBEBAAMBAAAAAAAAAAAAAAAAAAEDEhP/2gAMAwEAAhEDEQA/ADZdZigQPMrKpOAV74+1JNdtYngUlzzdyBng+0OUik8X4WX5WhitvHgjVSuxo2vnK+qazyYV6MxSSE4O/Yfvul1hrF686dQUaLOCcAH+UwuAksYBKE+xQyW0CHOIwRsSBRoxW//Z",
"/images/chua-thai-lac/11.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAgMEAQX/xAAlEAACAQMEAgEFAAAAAAAAAAABAgMABBEFEiExEyJRFEFSYYH/xAAUAQEAAAAAAAAAAAAAAAAAAAAB/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQAREv/aAAwDAQACEQMRAD8AZqurMsEa2E+yV+WOB6j4PwaVpTXyXm++u5XhRD2cgmr7i0tpguSW29ZPVAtonj2FjtDZ4JBo1nCK31eK58uyOUCMZ98DP6FQxa4wdpJAWRuBH91/tdAWlqQN4LY/NycVv0ViHZ1jUMeyOKOljC//2Q==",
"/images/chua-thai-lac/12.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAABAP/xAAnEAACAQMCAwkAAAAAAAAAAAABAgMABBEFIRJBYRMVIiMyUXGRof/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/EABgRAAMBAQAAAAAAAAAAAAAAAAABAhEh/9oADAMBAAIRAxEAPwBR1LyTJGwaVV9IBwT1HtQu9dRJZnUADB8KjHxTruwSVENs4R1XG52YdaKum3DkJIvAnMqwNZbocLyamyxKjMgmfccAOB986LJrNwkhjQR5GNypxmn3FgknZlHEbKOHcZGKMbAs2HmjG+5C5/aOqJh//9k=",
"/images/chua-thai-lac/13.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAABAIDBf/EACMQAAICAQIHAQEAAAAAAAAAAAECAxEABBIFEyEiMVFhQRT/xAAWAQEBAQAAAAAAAAAAAAAAAAABAAL/xAAZEQACAwEAAAAAAAAAAAAAAAAAEQECElH/2gAMAwEAAhEDEQA/ACPMDp5DzZGlDdjDr0+3izxCTU6YwgkBVo0m8k+/GJk4dC6s6m2K1TH995KHQvCq8rbuqmLfuZdhUMy0eKPUxW8oViQ5ksLXsfcvGsDyqg1M38yjt7e4+b6ivWN1XDRIqyGTc6eFrocJyWDBBHIisKZt10Pl4atwlB//2Q==",
"/images/chua-thai-lac/14.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABAMBBf/EACUQAAIBAwIFBQAAAAAAAAAAAAECAwAEERIhEyMxQVEUMmFxkf/EABYBAQEBAAAAAAAAAAAAAAAAAAIAA//EABkRAAMAAwAAAAAAAAAAAAAAAAABEQISIf/aAAwDAQACEQMRAD8APLbBghQM506mBPt+Kk8aqoYZHbIBI+669w1nJIhkSNVC6TliMDtRZIbJITH6hAhOVOsGs3gSSChRCnEWR8/gNagZ445Uzzd13Jz560mERSqyxyZC9CW26eKpaQyRnnSRsg3VUOMUdOQah//Z",
"/images/chua-thai-lac/15.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAABAIDBf/EACMQAAICAgICAQUAAAAAAAAAAAECAxEABAUhEyISFTEyM3H/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/EABgRAAMBAQAAAAAAAAAAAAAAAAABAhEh/9oADAMBAAIRAxEAPwBsfNxvqRFmBn6DqRXeR+rkyyEqBEnRA7N/3E7Wlo7SqG8SuoADWLFYccdpoWZpEotdAisO0XEA5Dcn3NgrpPJ4kA/Wasn74OCfcLP4JJy0Z9uz1m/LriR1l1thY6FfEGhlUfHWX8kigMfam/LC3WjlTnT/2Q==",
"/images/chua-thai-lac/16.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMEAv/EACEQAAICAgMAAgMAAAAAAAAAAAECAxEAEgQhMUFREyJx/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAED/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQACEf/aAAwDAQACEQMRAD8AUvJvjKyMRfZ2HRxZeQwgvG6sR0D85Y8HGcBASF9Ftd/3A61CY3ZWQEVZFA5m5aoNiBDHCzu0RfWvxte1/YycpKpYvsGUbaeULyvjiKOczPIu2pXrw3jJV48nrsLFfq3x9YRYcL//2Q==",
"/images/chua-thai-lac/17.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABQMG/8QAJBAAAgIBAwIHAAAAAAAAAAAAAQIDEQAEEiExQQUTFCJRgaH/xAAWAQEBAQAAAAAAAAAAAAAAAAADAAH/xAAaEQACAgMAAAAAAAAAAAAAAAAAAQMREyFB/9oADAMBAAIRAxEAPwBYy8Hb2NEnoMlLq3iVAIxIz8rR4/czom1BdTvZU6n3VzlfVvI4Ll5CtgBjYw7Y6iY2urlWPzZoNsYPJRgSPrKLroHra1k9B3wrUeJARiOMKhHcR2DhxMrQrU7ijzTE38ZW+GY3uz//2Q==",
"/images/chua-thai-lac/18.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAED/8QAIRAAAgEEAgIDAAAAAAAAAAAAAQIDAAQRIRITImEUQdH/xAAWAQEBAQAAAAAAAAAAAAAAAAADAQL/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwAHWwlCRCVig8goIJFLhmmiUE2UrKD5bDYrb45knuJo2ALgceRyCfyrbW9zJKnbxjjXZKnJPqjJ2iNIbu+IjgcoWIXOtD1Vmh6mKyRMFxrioNPmtl7i+wcnBFDWGPsZps5RsAknQ+qjU1//2Q==",
"/images/chua-thai-lac/19.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAgMEBf/EACUQAAIBAwQBBAMAAAAAAAAAAAECAwAEERIhMUEFExQiUSRhcf/EABYBAQEBAAAAAAAAAAAAAAAAAAIBA//EABcRAQEBAQAAAAAAAAAAAAAAAAEAAhH/2gAMAwEAAhEDEQA/AJ4vIe5DARMpHG3NaPj54HtzI7a3zsgxsPs0qO2iRhobSOxqzn978U6WC2lh0EIp41KBmga7a6EIze2pYqVxjsEEfzmsmTyP5cygFowcpgcjqr4LCCGQP6hkb6bBo7mCKRmLYBOPl3VdMcnb/9k=",
"/images/chua-thai-lac/20.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAQIFAAT/xAAkEAACAQMDBAMBAAAAAAAAAAABAgMABBEFEiETMUFxIlFhYv/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/8QAGBEBAQADAAAAAAAAAAAAAAAAAQACEVH/2gAMAwEAAhEDEQA/AO6bVjbKDPDsUnAZW349jio0+oztemSJk6bN4zwPuqUmnQzbRliB/VONMtou0aj9zUVYHZ7u9PSjeBXXPLbozz6oLqkZChY3LngruwM+6ElvgIhmcIO6k5yKxtLRhkJ8gc5Hig5TRf/Z",
"/images/chua-thai-lac/21.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAID/8QAIhAAAgEEAgIDAQAAAAAAAAAAAQIDAAQRIRIxBUETFDKB/8QAFgEBAQEAAAAAAAAAAAAAAAAABAEC/8QAGBEAAwEBAAAAAAAAAAAAAAAAABESIRP/2gAMAwEAAhEDEQA/AMpLCyikdtMq6XA/Rotxaq7ZijVR0Me6uO2EkTqJ8nGgw0KTBbgyc/lRZSMZ6AFHt4J5rTCLx0Uh4SJwcAaHs1MfjLYuqknJbeegKZLEebs8o5dDjvIoU1pGwJ+w6nJJB7P9rTRJZ//Z",
"/images/chua-thai-lac/22.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAABAP/xAAiEAACAQQCAgMBAAAAAAAAAAABAgMABBExEiETQWGBkaH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAwT/xAAZEQEBAQADAAAAAAAAAAAAAAABAAIREzH/2gAMAwEAAhEDEQA/AIzXEnhwlvEGXZGDmi8JbkBiRGvyQKVPZwuV8cgQrvB3R5beXI4Trjvotkn+VI02hwHkV4WSZAUYq28mlZQkGGIDBxxY7+6qtlcOqiV+Sb67NVSxRAQWYD2Dv99UG0nq5v/Z",
"/images/chua-thai-lac/23.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAgME/8QAIxAAAgEEAAYDAAAAAAAAAAAAAQIDAAQRIQUSMTJBURMikf/EABcBAAMBAAAAAAAAAAAAAAAAAAABAwT/xAAZEQACAwEAAAAAAAAAAAAAAAAAAQIREkH/2gAMAwEAAhEDEQA/ACJE2wim5R0OD0pxzLLN8SqqNnucHQ85oSWTsw5CwX0TmtlpaKA4cdw81FUaHskXRQAzIHzjAqN2qxrHKJ8c3cM6G8ftaJOEAn6ysN7yM69VR7GKK1ESgtvJJO6MrolOdn//2Q==",
"/images/chua-thai-lac/24.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAMB/8QAIhAAAgEEAgIDAQAAAAAAAAAAAQIDAAQREiExE1EUInGx/8QAFgEBAQEAAAAAAAAAAAAAAAAAAwAB/8QAGhEAAgMBAQAAAAAAAAAAAAAAAAECEkERIf/aAAwDAQACEQMRAD8AF5L1pmAJAxkBjilJLcQIkayCSY847B56o+gEqs10HUceuKS3xzNFNGRvEQwOeCaNSfPRHBYXW/vlXJjjEYOGYYOD6/alK90WEks4WJusD+it2thbNCFKh38hO+ftUDbW7RAJcmMnJYbd1tmVFp//2Q==",
"/images/chua-thai-lac/25.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwIE/8QAJBAAAgEEAQMFAQAAAAAAAAAAAQIDAAQRIRITMXEFFCJBQoH/xAAVAQEBAAAAAAAAAAAAAAAAAAAAA//EABkRAQADAQEAAAAAAAAAAAAAAAEAAhESQf/aAAwDAQACEQMRAD8AOWNRbxlRKJydqdCoje5t0EjcXJ3wJzgeaeaCzCgK7Hee/ajmtldgIkbiR+c/Gpjb2U5I7+pzOFX28agjOdHX8rDcRXKyO3UyoOQeWiPFKLAIRzeUj6BGKZ7dG6ZkYFgSWxRs5BU2f//Z",
"/images/chua-thai-lac/26.webp": "data:image/jpeg;base64,/9j/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAAQACADASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAwQBAgX/xAAjEAACAgICAgEFAAAAAAAAAAABAgMRAAQSIRMUBSIjJDJh/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAP/xAAYEQADAQEAAAAAAAAAAAAAAAAAAQISEf/aAAwDAQACEQMRAD8AC/togWGZpZWPSEUBh4/kdyG1j4OFq2YgViiPohgW2mYHo2xwPlgUhX2PtH9lFWTdjJp0UzJpS7W3wb2x4/qKWpHRq8Qlj3omDNOHH8Y5abc053X8jnbciG6s1VnJl2dWRI4jOvJBTMGq8N1wZk//2Q==",
};
+3 -3
View File
@@ -2,14 +2,14 @@
// File chỉ chứa DỮ LIỆU (type-only import từ engine để tránh circular import runtime).
// Engine `VirtualTour.tsx` import `chuaThaiLacTour` rồi bọc qua createTourConfig().
//
// Ảnh panorama đặt tại public/images/chua-thai-lac/01.jpg … 26.jpg
// Ảnh panorama đặt tại public/images/chua-thai-lac/01.webp … 26.webp
// (đặt tên ASCII theo thứ tự cảnh để tránh lỗi mã hóa Unicode/đường dẫn khi triển khai).
import type { TourConfig, TourScene, InfoMarker, Hotspot } from "@/app/components/VirtualTour";
const base = "/images/chua-thai-lac";
// Trỏ tới đúng file ảnh gốc theo SỐ FILE (01..26), độc lập với thứ tự cảnh sau khi
// đánh số lại — nhờ vậy có thể bỏ cảnh giữa chừng mà không phải đổi tên ảnh.
const imageForFile = (fileNumber: number) => `${base}/${String(fileNumber).padStart(2, "0")}.jpg`;
const imageForFile = (fileNumber: number) => `${base}/${String(fileNumber).padStart(2, "0")}.webp`;
type SceneSeed = {
title: string;
@@ -408,7 +408,7 @@ export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> =
welcomeAccent: "Pháp Vân tự",
welcomeSubtitle:
"Tham quan 360° ngôi chùa cổ thờ Pháp Vân, nổi tiếng với mảng chạm khắc gỗ thời Trần.",
welcomeBackgroundImage: imageForFile(8), // 08.jpg — Chính sảnh
welcomeBackgroundImage: imageForFile(8), // 08.webp — Chính sảnh
welcomeCards: [
{ image: imageForFile(1), label: "Cổng chùa", rotate: "-rotate-6", sceneId: "scene-1" },
{ image: imageForFile(10), label: "Đài Quan Âm", rotate: "rotate-5", sceneId: "scene-10" },