Files
portifolio-evandrodevbr/app/components/Header.tsx
T
Evandro Fonseca Junior 10f170abf4 feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site
H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
2026-10-14 14:15:25 -03:00

198 lines
7.5 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { type KeyboardEvent, useEffect, useRef, useState } from "react";
import { NAVIGATION_ITEMS } from "@/lib/site-navigation";
import {
isServiceOrCasePath,
type WhatsAppContexts,
whatsAppSubjectForPath,
} from "@/lib/whatsapp";
import HoverPrefetchLink from "./HoverPrefetchLink";
import { Logo } from "./Logo";
import WhatsAppLink from "./WhatsAppLink";
const MENU_ID = "site-navigation-menu";
const CTA_LABEL = "Falar sobre meu projeto";
/**
* Botão de contato do cabeçalho. Nas páginas de serviço e de case ele abre o WhatsApp já com o assunto da página;
* nas demais leva ao formulário da home.
*/
function ContactCta({
className,
placement,
viaWhatsApp,
subject,
onNavigate,
}: {
className: string;
placement: string;
viaWhatsApp: boolean;
subject?: string;
onNavigate?: () => void;
}) {
if (viaWhatsApp) {
return (
<WhatsAppLink
className={className}
service={subject}
placement={placement}
onClick={onNavigate}
>
{CTA_LABEL}
</WhatsAppLink>
);
}
return (
<Link href="/#contact" className={className} onClick={onNavigate}>
{CTA_LABEL}
</Link>
);
}
export function Header({
whatsAppContexts,
}: {
/** Nomes dos serviços e dos cases, montados no servidor (app/layout.tsx), para o WhatsApp falar da página aberta. */
whatsAppContexts?: WhatsAppContexts;
}) {
const pathname = usePathname();
const viaWhatsApp = isServiceOrCasePath(pathname);
const subject = whatsAppSubjectForPath(pathname, whatsAppContexts);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const menuButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
let wasScrolled = false;
const updateScrolled = () => {
const nextScrolled = window.scrollY > 0;
if (nextScrolled === wasScrolled) return;
wasScrolled = nextScrolled;
setScrolled(nextScrolled);
};
updateScrolled();
window.addEventListener("scroll", updateScrolled, { passive: true });
return () => window.removeEventListener("scroll", updateScrolled);
}, []);
const closeMobileMenu = () => setMobileMenuOpen(false);
const handleMenuKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (event.key !== "Escape") return;
event.preventDefault();
closeMobileMenu();
menuButtonRef.current?.focus();
};
return (
<header
className={`site-header fixed inset-x-0 top-0 z-[9000] h-[var(--site-header-height)] w-full border-b px-sp-6 text-[var(--white)] transition-colors duration-200 lg:px-sp-8 xl:px-sp-12 ${
scrolled
? "border-white/12 bg-[var(--ink)]/95 backdrop-blur-md"
: "border-transparent bg-[var(--ink)]"
}`}
>
<div className="ds-section-shell flex h-full items-center justify-between gap-sp-6">
<HoverPrefetchLink
href="/#home"
aria-label="Evandro.dev.br — início"
className="flex min-h-11 shrink-0 items-center no-underline"
>
<Logo theme="dark" loading="eager" className="w-40 sm:w-44" />
</HoverPrefetchLink>
<nav
aria-label="Navegação principal"
className="hidden items-center xl:flex"
>
<ul className="flex list-none items-center gap-sp-4">
{NAVIGATION_ITEMS.map((item) => (
<li key={item.href}>
<HoverPrefetchLink
href={item.href}
className="inline-flex min-h-11 items-center whitespace-nowrap rounded-sm px-sp-3 font-display text-sm font-medium normal-case tracking-normal text-white/80 no-underline transition-colors hover:bg-white/5 hover:text-white"
>
{item.label}
</HoverPrefetchLink>
</li>
))}
</ul>
</nav>
<div className="flex shrink-0 items-center">
<div className="hidden xl:block">
<ContactCta
className="inline-flex min-h-11 items-center whitespace-nowrap rounded-sm bg-[var(--slash)] px-sp-4 font-display text-sm font-semibold normal-case tracking-normal text-[var(--ink)] no-underline transition-colors hover:bg-[var(--white)]"
placement="header"
viaWhatsApp={viaWhatsApp}
subject={subject}
/>
</div>
<button
ref={menuButtonRef}
type="button"
className="flex h-11 w-11 flex-col items-center justify-center gap-sp-2 border-none bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)] xl:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-controls={mobileMenuOpen ? MENU_ID : undefined}
aria-label={mobileMenuOpen ? "Fechar menu" : "Abrir menu"}
aria-expanded={mobileMenuOpen}
onKeyDown={handleMenuKeyDown}
>
<span
className={`h-0.5 w-5 bg-white transition-transform duration-300 ${mobileMenuOpen ? "translate-y-[6px] rotate-45" : ""}`}
/>
<span
className={`h-0.5 w-5 bg-white transition-opacity duration-300 ${mobileMenuOpen ? "opacity-0" : ""}`}
/>
<span
className={`h-0.5 w-5 bg-white transition-transform duration-300 ${mobileMenuOpen ? "-translate-y-[6px] -rotate-45" : ""}`}
/>
</button>
</div>
</div>
{mobileMenuOpen ? (
// Tela cheia abaixo da barra, com fundo opaco: o conteúdo da página não aparece por trás dos links.
<div className="absolute inset-x-0 top-[var(--site-header-height)] flex h-[calc(100dvh-var(--site-header-height))] flex-col overflow-y-auto overscroll-contain bg-[var(--ink)] px-sp-6 pb-[calc(var(--sp-6)+env(safe-area-inset-bottom))] lg:px-sp-8 xl:hidden">
<div className="ds-section-shell flex flex-1 flex-col">
<nav
id={MENU_ID}
aria-label="Navegação mobile"
className="flex flex-1 flex-col"
onKeyDown={handleMenuKeyDown}
>
<ul className="flex list-none flex-col">
{NAVIGATION_ITEMS.map((item) => (
<li key={item.href} className="border-b border-white/10">
<HoverPrefetchLink
href={item.href}
className="flex min-h-14 items-center font-display text-[22px] font-medium normal-case leading-tight tracking-normal text-white/85 no-underline transition-colors hover:text-white"
onClick={closeMobileMenu}
>
{item.label}
</HoverPrefetchLink>
</li>
))}
</ul>
<div className="mt-auto pt-sp-8">
<ContactCta
className="flex min-h-14 w-full items-center justify-center rounded-sm bg-[var(--slash)] px-sp-4 font-display text-[18px] font-semibold normal-case tracking-normal text-[var(--ink)] no-underline transition-colors hover:bg-[var(--white)]"
placement="header_menu"
viaWhatsApp={viaWhatsApp}
subject={subject}
onNavigate={closeMobileMenu}
/>
</div>
</nav>
</div>
</div>
) : null}
</header>
);
}