145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Sticky header.
|
|
* - Condenses (smaller, blurred, bordered) after the hero via scroll listener.
|
|
* - Top scroll-progress bar reads the --scroll-progress CSS var fed by Lenis.
|
|
* - Mobile: accessible disclosure menu (real <button aria-expanded>, Esc to
|
|
* close, focus returns to the toggle, body scroll locked while open).
|
|
*/
|
|
import { useEffect, useRef, useState } from "react";
|
|
import Link from "next/link";
|
|
import Magnetic from "./Magnetic";
|
|
import { SITE } from "../content";
|
|
|
|
const NAV = [
|
|
{ href: "#services", label: "Services" },
|
|
{ href: "#work", label: "Work" },
|
|
{ href: "#process", label: "About" },
|
|
{ href: "#faq", label: "FAQ" },
|
|
];
|
|
|
|
export default function SiteHeader() {
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
const [active, setActive] = useState<string>("");
|
|
const toggleRef = useRef<HTMLButtonElement>(null);
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 40);
|
|
onScroll();
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
|
|
// Scroll-spy: highlight the nav item for the section currently in view, so
|
|
// location is always legible (navigation best practice + a touch of life).
|
|
useEffect(() => {
|
|
const ids = NAV.map((n) => n.href.slice(1));
|
|
const sections = ids
|
|
.map((id) => document.getElementById(id))
|
|
.filter((el): el is HTMLElement => !!el);
|
|
if (!sections.length) return;
|
|
const io = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((e) => {
|
|
if (e.isIntersecting) setActive(`#${e.target.id}`);
|
|
});
|
|
},
|
|
{ rootMargin: "-45% 0px -50% 0px", threshold: 0 }
|
|
);
|
|
sections.forEach((s) => io.observe(s));
|
|
return () => io.disconnect();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
document.body.style.overflow = "hidden";
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
setOpen(false);
|
|
toggleRef.current?.focus();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
window.removeEventListener("keydown", onKey);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<header className={`site-head ${scrolled ? "is-scrolled" : ""}`}>
|
|
<div className="progress-bar" aria-hidden="true" />
|
|
<div className="site-head__inner">
|
|
<Link href="#main" className="brand" aria-label={`${SITE.name} — home`}>
|
|
<span className="brand__mark" aria-hidden="true">
|
|
<span className="brand__dot" />
|
|
</span>
|
|
<span className="brand__name">Feedback Studios</span>
|
|
</Link>
|
|
|
|
<nav className="site-nav" aria-label="Primary">
|
|
<ul>
|
|
{NAV.map((n) => {
|
|
const isActive = active === n.href;
|
|
return (
|
|
<li key={n.href}>
|
|
<a
|
|
href={n.href}
|
|
className={isActive ? "is-active" : ""}
|
|
aria-current={isActive ? "true" : undefined}
|
|
>
|
|
{n.label}
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
|
|
<div className="site-head__cta">
|
|
<Magnetic strength={0.5}>
|
|
<Link href={SITE.booking} className="btn btn--accent btn--sm" data-cursor="Let's talk">
|
|
Get a growth audit
|
|
</Link>
|
|
</Magnetic>
|
|
</div>
|
|
|
|
<button
|
|
ref={toggleRef}
|
|
className={`burger ${open ? "is-open" : ""}`}
|
|
aria-expanded={open}
|
|
aria-controls="mobile-menu"
|
|
aria-label={open ? "Close menu" : "Open menu"}
|
|
onClick={() => setOpen((v) => !v)}
|
|
>
|
|
<span /><span /><span />
|
|
</button>
|
|
</div>
|
|
|
|
<div id="mobile-menu" className={`mobile-menu ${open ? "is-open" : ""}`} hidden={!open}>
|
|
<nav aria-label="Mobile">
|
|
<ul>
|
|
{NAV.map((n) => (
|
|
<li key={n.href}>
|
|
<a href={n.href} onClick={() => setOpen(false)}>
|
|
{n.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
<li>
|
|
<Link
|
|
href={SITE.booking}
|
|
className="btn btn--accent"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Get a growth audit
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|