feat: port SiteFooter (liên hệ Như Quỳnh) + BackToTopButton

This commit is contained in:
tuyenio
2026-06-23 11:10:20 +07:00
parent fcd002d9af
commit 735e44b158
2 changed files with 117 additions and 0 deletions
@@ -0,0 +1,68 @@
"use client";
import { ArrowUp } from "lucide-react";
import { CSSProperties, useEffect, useState } from "react";
export function BackToTopButton() {
const [isVisible, setIsVisible] = useState(false);
const [scrollProgress, setScrollProgress] = useState(0);
useEffect(() => {
let frameId: number | null = null;
const updateScrollState = () => {
const scrollTop = window.scrollY;
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
const nextProgress = scrollableHeight > 0 ? Math.min(100, Math.max(0, (scrollTop / scrollableHeight) * 100)) : 0;
setIsVisible(scrollTop > 120);
setScrollProgress(nextProgress);
frameId = null;
};
const requestUpdate = () => {
if (frameId === null) {
frameId = window.requestAnimationFrame(updateScrollState);
}
};
requestUpdate();
window.addEventListener("scroll", requestUpdate, { passive: true });
window.addEventListener("resize", requestUpdate);
return () => {
window.removeEventListener("scroll", requestUpdate);
window.removeEventListener("resize", requestUpdate);
if (frameId !== null) {
window.cancelAnimationFrame(frameId);
}
};
}, []);
const handleClick = () => {
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
window.scrollTo({
top: 0,
behavior: prefersReducedMotion ? "auto" : "smooth",
});
};
return (
<button
type="button"
aria-label="Lên đầu trang"
aria-hidden={!isVisible}
onClick={handleClick}
className="public-back-to-top"
data-visible={isVisible ? "true" : "false"}
tabIndex={isVisible ? 0 : -1}
style={{ "--scroll-progress": `${scrollProgress}%` } as CSSProperties}
>
<span className="public-back-to-top-ring" aria-hidden="true" />
<span className="public-back-to-top-core">
<ArrowUp className="h-5 w-5" strokeWidth={2.2} />
</span>
</button>
);
}
+49
View File
@@ -0,0 +1,49 @@
import Link from "next/link";
import { Globe2, Mail, MapPin } from "lucide-react";
import { navigation } from "@/app/content";
export function SiteFooter() {
return (
<footer id="contact" className="px-4 pb-8 sm:px-6">
<div className="mx-auto grid max-w-7xl gap-8 border-t border-[var(--surface-border)] pt-8 lg:grid-cols-[1.1fr_0.72fr_0.82fr]">
<div className="min-w-0">
<p className="text-lg font-bold text-[var(--tour-ink)]">VR360 Như Quỳnh</p>
<p className="mt-3 max-w-[62ch] text-sm leading-6 text-[var(--muted-foreground)]">
Cổng số hóa di tích không gian văn hóa Như Quỳnh, tỉnh Hưng Yên bằng công nghệ VR360.
</p>
</div>
<nav className="grid gap-2 text-sm" aria-label="Liên kết nhanh">
<p className="font-bold text-[var(--tour-ink)]">Liên kết nhanh</p>
{navigation.map((item) => (
<Link key={item.href} href={item.href} className="w-fit text-[var(--muted-foreground)] transition-colors hover:text-[var(--tour-ink)]">
{item.label}
</Link>
))}
</nav>
<div className="grid gap-3 text-sm leading-6 text-[var(--muted-foreground)] lg:text-right">
<p className="font-bold text-[var(--tour-ink)]">Thông tin liên hệ</p>
<p className="inline-flex items-center gap-2 lg:justify-end">
<MapPin className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
Như Quỳnh, tỉnh Hưng Yên
</p>
<a
href="#"
className="inline-flex items-center gap-2 font-semibold text-[var(--tour-ink)] hover:text-[var(--primary)] lg:justify-end"
>
<Globe2 className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
Cổng thông tin Như Quỳnh
</a>
<Link href="/lien-he" className="inline-flex items-center gap-2 font-semibold text-[var(--tour-ink)] hover:text-[var(--primary)] lg:justify-end">
<Mail className="h-4 w-4 text-[var(--primary)]" strokeWidth={1.8} />
Gửi góp ý hoặc liệu
</Link>
</div>
</div>
<div className="mx-auto mt-8 max-w-7xl border-t border-[var(--surface-border)] pt-5 text-center text-sm font-semibold text-[var(--muted-foreground)]">
© 2026 VR360 Di sản Như Quỳnh.
</div>
</footer>
);
}