nomadweb/frontend/src/components/Navbar.tsx
eric 18fb3318b1 Rebrand to nomadro and add wifi score, savings goal, daily quote
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 23:19:27 -05:00

109 lines
4.2 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: "/#timezone", label: "🕐 时区", section: "timezone" },
{ href: "/#calculator", label: "🧮 计算器", section: "calculator" },
{ 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 sections = LINKS.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={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>
);
}