Initial commit
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Menu, X, ArrowRight } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Locale, Dictionary } from "@/lib/dictionaries";
|
||||
import LanguageSwitcher from "@/components/ui/LanguageSwitcher";
|
||||
import IPInfoCard from "@/components/ui/IPInfoCard";
|
||||
import { LogoMark } from "@/components/ui/Logo";
|
||||
|
||||
interface NavbarProps {
|
||||
locale: Locale;
|
||||
dict: Dictionary;
|
||||
}
|
||||
|
||||
export default function Navbar({ locale, dict }: NavbarProps) {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const navLinks = [
|
||||
{ href: `/${locale}/#features`, label: dict.nav.features },
|
||||
{ href: `/${locale}/projects`, label: dict.nav.projects },
|
||||
|
||||
{ href: `/${locale}/services`, label: dict.nav.services },
|
||||
{ href: `/${locale}/about`, label: dict.nav.about },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setScrolled(window.scrollY > 12);
|
||||
handler();
|
||||
window.addEventListener("scroll", handler, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = open ? "hidden" : "";
|
||||
return () => { document.body.style.overflow = ""; };
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.header
|
||||
initial={{ y: -32, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={{ top: "var(--banner-h, 0px)" }}
|
||||
className={cn(
|
||||
"fixed inset-x-0 z-50 transition-all duration-500",
|
||||
scrolled
|
||||
? "border-b border-white/5 bg-background/70 backdrop-blur-xl"
|
||||
: "bg-transparent",
|
||||
)}
|
||||
>
|
||||
<nav className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6 lg:h-[72px] lg:px-8">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="group flex items-center gap-2.5 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<div className="relative flex h-8 w-8 items-center justify-center rounded-lg border border-purple-400/30 bg-purple-500/10 shadow-glow-sm">
|
||||
<LogoMark className="w-[19px] text-white" />
|
||||
<div className="absolute inset-0 rounded-lg bg-purple-500/20 opacity-0 blur-md transition-opacity group-hover:opacity-100" />
|
||||
</div>
|
||||
<span className="font-display text-base font-medium tracking-tight text-text-primary">
|
||||
ksan.dev
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Center: nav links + contact CTA — desktop */}
|
||||
<div className="hidden items-center gap-1 md:flex">
|
||||
{navLinks.map((link) => {
|
||||
// Hash links (e.g. /en/#features) never appear in pathname,
|
||||
// so they are deliberately excluded from active highlighting
|
||||
const isActive =
|
||||
!link.href.includes("#") &&
|
||||
(pathname === link.href ||
|
||||
pathname.startsWith(link.href + "/"));
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"relative rounded-lg px-4 py-2 text-sm transition-colors",
|
||||
isActive
|
||||
? "text-text-primary"
|
||||
: "text-text-secondary hover:text-text-primary",
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="nav-active"
|
||||
className="absolute inset-0 -z-10 rounded-lg bg-white/5"
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Contact CTA — sits right next to About */}
|
||||
<Link
|
||||
href={`/${locale}/contact`}
|
||||
className="group relative ml-1 inline-flex h-9 items-center gap-1.5 overflow-hidden rounded-xl bg-gradient-to-r from-purple-300 via-purple-500 to-purple-700 px-4 text-sm font-medium text-white shadow-glow-md transition-all hover:shadow-glow-lg hover:brightness-110"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/25 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
|
||||
/>
|
||||
<span className="relative z-10">{dict.nav.contact}</span>
|
||||
<ArrowRight className="relative z-10 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right side: lang + IP — desktop */}
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<LanguageSwitcher locale={locale} />
|
||||
<IPInfoCard dict={dict} />
|
||||
</div>
|
||||
|
||||
{/* Mobile right side */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<LanguageSwitcher locale={locale} />
|
||||
<IPInfoCard dict={dict} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="rounded-lg p-2 text-text-secondary hover:bg-white/5 hover:text-text-primary"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.header>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-40 bg-background/95 backdrop-blur-xl md:hidden"
|
||||
>
|
||||
<div className="flex h-full flex-col items-center justify-center gap-8 px-6 pt-16">
|
||||
{navLinks.map((link, i) => (
|
||||
<motion.div
|
||||
key={link.href}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.05 * i, duration: 0.4 }}
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="font-display text-3xl font-medium tracking-tight text-text-primary transition-colors hover:text-purple-300"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
>
|
||||
<Link
|
||||
href={`/${locale}/contact`}
|
||||
onClick={() => setOpen(false)}
|
||||
className="group inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-purple-300 via-purple-500 to-purple-700 px-6 py-3 font-display text-lg font-medium text-white shadow-glow-lg"
|
||||
>
|
||||
{dict.nav.contact}
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user