84 أسطر
2.1 KiB
TypeScript
84 أسطر
2.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useState } from "react";
|
|
|
|
type NavItem = {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
|
|
type NavLinksProps = {
|
|
items: NavItem[];
|
|
alignEnd: boolean;
|
|
};
|
|
|
|
function getHashId(href: string) {
|
|
return href.split("#")[1] ?? "";
|
|
}
|
|
|
|
export function NavLinks({ items, alignEnd }: NavLinksProps) {
|
|
const [activeId, setActiveId] = useState("");
|
|
|
|
useEffect(() => {
|
|
const sectionIds = items.map((item) => getHashId(item.href)).filter(Boolean);
|
|
const sections = sectionIds
|
|
.map((id) => document.getElementById(id))
|
|
.filter((section): section is HTMLElement => Boolean(section));
|
|
|
|
if (sections.length === 0) {
|
|
setActiveId("");
|
|
return;
|
|
}
|
|
|
|
function syncFromHash() {
|
|
const nextId = window.location.hash.replace("#", "");
|
|
if (sectionIds.includes(nextId)) {
|
|
setActiveId(nextId);
|
|
}
|
|
}
|
|
|
|
syncFromHash();
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const visible = entries
|
|
.filter((entry) => entry.isIntersecting)
|
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
|
|
if (visible?.target.id) {
|
|
setActiveId(visible.target.id);
|
|
}
|
|
},
|
|
{ rootMargin: "-28% 0px -58% 0px", threshold: [0.12, 0.24, 0.36] }
|
|
);
|
|
|
|
sections.forEach((section) => observer.observe(section));
|
|
window.addEventListener("hashchange", syncFromHash);
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
window.removeEventListener("hashchange", syncFromHash);
|
|
};
|
|
}, [items]);
|
|
|
|
return (
|
|
<nav aria-label="Primary" className="nav-scroll">
|
|
<ul className={`nav-list ${alignEnd ? "md:justify-end" : "md:justify-start"}`}>
|
|
{items.map((item) => {
|
|
const itemId = getHashId(item.href);
|
|
const isActive = activeId === itemId;
|
|
|
|
return (
|
|
<li key={item.href}>
|
|
<Link href={item.href} className="nav-link-pill" data-active={isActive || undefined}>
|
|
{item.label}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
);
|
|
}
|