828a81f3a5
- 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
166 lines
5.7 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|