mirror of
https://github.com/domfelipe/portfolio.git
synced 2026-08-07 09:56:42 +00:00
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
This commit is contained in:
commit
be348f70f6
32 changed files with 8631 additions and 0 deletions
135
src/components/Navbar.tsx
Normal file
135
src/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue