portfolio/src/components/Navbar.tsx
Felipe Domingues be348f70f6 feat: portfolio bilíngue (PT/EN) — Next.js 16 + Tailwind v4
- Hero com terminal animado e identidade de engenheiro de agentes de IA
- 10 projetos curados (destaques: Mika e Agentes de IA com métricas)
- Skills, contato, dark mode, SEO/OG por locale
- Rotas /pt e /en com redirect por Accept-Language
2026-07-21 01:47:54 -03:00

135 lines
4.8 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { Menu, Moon, Sun, X } from "lucide-react";
import { useI18n } from "@/i18n/context";
import { locales } from "@/i18n/config";
import type { Locale } from "@/i18n/config";
export default function Navbar() {
const { t } = useI18n();
const pathname = usePathname();
const { resolvedTheme, setTheme } = useTheme();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const links = [
{ href: "#projects", label: t.nav.projects },
{ href: "#skills", label: t.nav.skills },
{ href: "#contact", label: t.nav.contact },
];
const currentLocale = (pathname.split("/")[1] ?? "pt") as Locale;
return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ${
scrolled
? "border-b border-line bg-bg/85 backdrop-blur-md"
: "border-b border-transparent bg-transparent"
}`}
>
<nav className="mx-auto flex h-16 max-w-6xl items-center justify-between px-5 sm:px-8">
<a
href={`/${currentLocale}`}
className="group flex items-center gap-1 font-mono text-sm font-semibold text-ink"
>
<span className="text-brand">~/</span>felipe-domingues
<span className="inline-block h-4 w-[7px] translate-y-[2px] animate-blink bg-brand" />
</a>
<div className="hidden items-center gap-7 md:flex">
{links.map((link) => (
<a
key={link.href}
href={link.href}
className="font-mono text-[13px] text-mute transition-colors hover:text-brand"
>
{link.label}
</a>
))}
<div className="flex items-center gap-1 rounded-lg border border-line bg-surface p-1">
{locales.map((l) => (
<Link
key={l}
href={`/${l}`}
className={`rounded-md px-2 py-0.5 font-mono text-[11px] uppercase transition-colors ${
l === currentLocale
? "bg-brand text-bg font-bold"
: "text-mute hover:text-ink"
}`}
>
{l}
</Link>
))}
</div>
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
aria-label="toggle theme"
className="rounded-lg border border-line bg-surface p-2 text-mute transition-colors hover:border-brand hover:text-brand"
>
{resolvedTheme === "dark" ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>
</div>
<div className="flex items-center gap-2 md:hidden">
<div className="flex items-center gap-1 rounded-lg border border-line bg-surface p-1">
{locales.map((l) => (
<Link
key={l}
href={`/${l}`}
className={`rounded-md px-2 py-0.5 font-mono text-[11px] uppercase ${
l === currentLocale ? "bg-brand text-bg font-bold" : "text-mute"
}`}
>
{l}
</Link>
))}
</div>
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
aria-label="toggle theme"
className="rounded-lg border border-line bg-surface p-2 text-mute"
>
{resolvedTheme === "dark" ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>
<button
onClick={() => setOpen(!open)}
aria-label="menu"
className="rounded-lg border border-line bg-surface p-2 text-ink"
>
{open ? <X className="size-4" /> : <Menu className="size-4" />}
</button>
</div>
</nav>
{open && (
<div className="border-b border-line bg-bg/95 backdrop-blur-md md:hidden">
<div className="flex flex-col gap-1 px-5 py-4">
{links.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setOpen(false)}
className="rounded-lg px-3 py-2 font-mono text-sm text-mute transition-colors hover:bg-surface hover:text-brand"
>
{"// "}
{link.label}
</a>
))}
</div>
</div>
)}
</header>
);
}