هذا الالتزام موجود في:
2026-04-29 16:55:25 +03:00
التزام 7885be6c2e
82 ملفات معدلة مع 31672 إضافات و0 حذوفات

83
components/nav-links.tsx Normal file
عرض الملف

@@ -0,0 +1,83 @@
"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>
);
}