114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useAuth } from "@/lib/auth";
|
|
|
|
const LINKS = [
|
|
{ href: "/#map", label: "🗺️ 地图", section: "map" },
|
|
{ href: "/#destinations", label: "🌍 目的地", section: "destinations" },
|
|
{ href: "/#trip", label: "🗓️ 行程", section: "trip" },
|
|
{ href: "/#calculator", label: "🧮 计算器", section: "calculator" },
|
|
{ href: "/tools", label: "🛠️ 工具箱", section: "tools" },
|
|
{ href: "/#visa", label: "📋 签证", section: "visa" },
|
|
{ href: "/#blog", label: "📝 博客", section: "blog" },
|
|
];
|
|
|
|
export default function Navbar() {
|
|
const { user } = useAuth();
|
|
const pathname = usePathname();
|
|
const isHome = pathname === "/";
|
|
const [theme, setTheme] = useState<"dark" | "light">("dark");
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [active, setActive] = useState("");
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem("nomad-theme");
|
|
if (saved === "light") {
|
|
setTheme("light");
|
|
document.documentElement.setAttribute("data-theme", "light");
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const navbar = document.getElementById("navbar");
|
|
const onScroll = () => navbar?.classList.toggle("scrolled", window.scrollY > 50);
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
onScroll();
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isHome) return;
|
|
const homeLinks = LINKS.filter((l) => l.href.startsWith("/#"));
|
|
const sections = homeLinks.map((l) => document.getElementById(l.section)).filter(Boolean);
|
|
const observer = new IntersectionObserver(
|
|
(entries) => entries.forEach((e) => { if (e.isIntersecting) setActive(e.target.id); }),
|
|
{ threshold: 0.3, rootMargin: "-80px 0px -50% 0px" }
|
|
);
|
|
sections.forEach((s) => observer.observe(s!));
|
|
return () => observer.disconnect();
|
|
}, [isHome]);
|
|
|
|
useEffect(() => {
|
|
document.body.style.overflow = menuOpen ? "hidden" : "";
|
|
return () => { document.body.style.overflow = ""; };
|
|
}, [menuOpen]);
|
|
|
|
const toggleTheme = () => {
|
|
const next = theme === "dark" ? "light" : "dark";
|
|
setTheme(next);
|
|
if (next === "light") document.documentElement.setAttribute("data-theme", "light");
|
|
else document.documentElement.removeAttribute("data-theme");
|
|
localStorage.setItem("nomad-theme", next);
|
|
};
|
|
|
|
return (
|
|
<header className="navbar" id="navbar">
|
|
<div className="nav-container">
|
|
<Link href="/" className="logo">
|
|
<svg className="logo-icon" viewBox="0 0 40 40" fill="none">
|
|
<circle cx="20" cy="20" r="18" stroke="url(#logoGrad)" strokeWidth="2.5" />
|
|
<path d="M12 22c4-8 12-8 16 0" stroke="url(#logoGrad)" strokeWidth="2" strokeLinecap="round" />
|
|
<circle cx="28" cy="14" r="3" fill="url(#logoGrad)" />
|
|
<defs>
|
|
<linearGradient id="logoGrad" x1="0" y1="0" x2="40" y2="40">
|
|
<stop offset="0%" stopColor="#FF6B6B" />
|
|
<stop offset="50%" stopColor="#FFE66D" />
|
|
<stop offset="100%" stopColor="#4ECDC4" />
|
|
</linearGradient>
|
|
</defs>
|
|
</svg>
|
|
<span>nomadro</span>
|
|
</Link>
|
|
<nav className={`nav-links${menuOpen ? " open" : ""}`}>
|
|
{LINKS.map((l) => (
|
|
<Link key={l.section} href={l.href} data-section={l.section}
|
|
className={
|
|
l.href === "/tools"
|
|
? (pathname === "/tools" ? "active" : "")
|
|
: (isHome && active === l.section ? "active" : "")
|
|
}
|
|
onClick={() => setMenuOpen(false)}>{l.label}</Link>
|
|
))}
|
|
</nav>
|
|
<button className="nav-cta" onClick={toggleTheme} aria-label="切换主题">
|
|
<span className="theme-icon">{theme === "dark" ? "🌙" : "☀️"}</span>
|
|
</button>
|
|
{user ? (
|
|
<Link href="/profile" className="nav-user">
|
|
<span>{user.avatar}</span>
|
|
<span className="nav-user-name">{user.name}</span>
|
|
</Link>
|
|
) : (
|
|
<Link href="/login" className="nav-login-btn">登录</Link>
|
|
)}
|
|
<button className={`mobile-menu-btn${menuOpen ? " open" : ""}`} onClick={() => setMenuOpen(!menuOpen)} aria-label="菜单">
|
|
<span /><span /><span />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|