Files
aigrowthtech/src/components/site-header.tsx
T
Alejandro Veras 828a81f3a5 Add Spanish and Arabic versions, a contact form captcha and a multilingual sitemap
- English, Spanish (/es) and Arabic (/ar, right to left) versions of every page, with a language switch
- Self-hosted "I'm not a robot" check on the contact form; the message field is optional
- Google Tag Manager, GA4 and Search Console verification in the shared layout
- Sitemap, robots.txt and canonical links point at aigrowthtechnologies.com
2026-10-13 11:53:18 -04:00

166 lines
5.7 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import Logo from "./logo";
import { localeNames, localeShort, locales, localize, splitLocale, type Locale } from "@/i18n/config";
import type { NavItem } from "@/i18n/nav";
export type HeaderLabels = {
primary: string;
bookCall: string;
openMenu: string;
closeMenu: string;
language: string;
logoHome: string;
};
function LanguageSwitch({ locale, path, label }: { locale: Locale; path: string; label: string }) {
return (
<nav className="lang-switch" aria-label={label}>
{locales.map((l) =>
// A plain link: switching language swaps the document's lang and direction.
l === locale ? (
<span key={l} aria-current="true" lang={l} title={localeNames[l]}>
{localeShort[l]}
</span>
) : (
<a key={l} href={localize(path, l)} hrefLang={l} lang={l} title={localeNames[l]}>
{localeShort[l]}
</a>
),
)}
</nav>
);
}
export default function SiteHeader({ locale, nav, t }: { locale: Locale; nav: NavItem[]; t: HeaderLabels }) {
const pathname = usePathname();
const { path } = splitLocale(pathname);
const current = localize(path, locale);
const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false);
const [open, setOpen] = useState(false);
const [menuPath, setMenuPath] = useState(pathname);
// Close the mobile menu whenever the route changes.
if (menuPath !== pathname) {
setMenuPath(pathname);
setOpen(false);
}
// Drop focus left on a nav link after navigating so its dropdown doesn't stay pinned open.
useEffect(() => {
const el = document.activeElement;
if (el instanceof HTMLElement && el.closest(".site-header")) el.blur();
}, [pathname]);
useEffect(() => {
let lastY = window.scrollY;
const onScroll = () => {
const y = window.scrollY;
setScrolled(y > 24);
setHidden(y > 480 && y > lastY + 4);
if (y < lastY - 4 || y < 480) setHidden(false);
lastY = y;
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
const isActive = (href: string) => current === href || current.startsWith(href + "/");
const contactHref = localize("/contact", locale);
const classes = ["site-header", scrolled || open ? "is-scrolled" : "", hidden && !open ? "is-hidden" : ""]
.filter(Boolean)
.join(" ");
return (
<>
<header className={classes}>
<div className="wrap bar">
<Logo href={localize("/", locale)} label={t.logoHome} />
<nav className="nav" aria-label={t.primary}>
{nav.map((l) =>
l.children ? (
<div key={l.href} className="nav-group">
<Link href={l.href} className={isActive(l.href) ? "is-active" : undefined}>
{l.label}
</Link>
<div className="nav-panel">
<div className="nav-panel-inner">
<Link href={l.href} className="nav-panel-all">
{l.allLabel} <span className="arrow" aria-hidden="true">→</span>
</Link>
<ul>
{l.children.map((c) => (
<li key={c.href}>
<Link href={c.href} className={current === c.href ? "is-active" : undefined}>
{c.label}
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
) : (
<Link key={l.href} href={l.href} className={isActive(l.href) ? "is-active" : undefined}>
{l.label}
</Link>
),
)}
</nav>
<LanguageSwitch locale={locale} path={path} label={t.language} />
<Link href={contactHref} className="btn btn-solid header-cta">
{t.bookCall} <span className="arrow">→</span>
</Link>
<button
type="button"
className="menu-toggle"
aria-expanded={open}
aria-controls="mobile-menu"
aria-label={open ? t.closeMenu : t.openMenu}
onClick={() => setOpen((v) => !v)}
>
<span />
<span />
</button>
</div>
</header>
<div id="mobile-menu" className={`mobile-menu${open ? " is-open" : ""}`} aria-hidden={!open} inert={!open}>
<ul>
{nav.map((l) => (
<li key={l.href}>
<Link href={l.href}>
{l.label} <span className="arrow" aria-hidden="true">→</span>
</Link>
{l.children && (
<div className="mobile-sub">
{l.children.map((c) => (
<Link key={c.href} href={c.href}>
{c.label}
</Link>
))}
</div>
)}
</li>
))}
</ul>
<LanguageSwitch locale={locale} path={path} label={t.language} />
<Link href={contactHref} className="btn btn-solid">
{t.bookCall} <span className="arrow">→</span>
</Link>
</div>
</>
);
}