feat: add TableOfContents component with IntersectionObserver
This commit is contained in:
63
components/table-of-contents.tsx
Normal file
63
components/table-of-contents.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Heading } from "@/lib/posts";
|
||||
|
||||
interface TableOfContentsProps {
|
||||
headings: Heading[];
|
||||
}
|
||||
|
||||
export default function TableOfContents({ headings }: TableOfContentsProps) {
|
||||
const [activeSlug, setActiveSlug] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSlug(entry.target.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0% 0% -70% 0%", threshold: 0 }
|
||||
);
|
||||
|
||||
headings.forEach(({ slug }) => {
|
||||
const el = document.getElementById(slug);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="bg-white/20 backdrop-blur border border-white/30 rounded-lg p-4 lg:sticky lg:top-8">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-800 mb-3">
|
||||
Contents
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{headings.map(({ slug, text, level }) => (
|
||||
<li
|
||||
key={slug}
|
||||
style={{ paddingLeft: level === 3 ? "0.75rem" : "0" }}
|
||||
>
|
||||
<a
|
||||
href={`#${slug}`}
|
||||
className={`text-sm leading-snug transition-colors duration-150 ${
|
||||
activeSlug === slug
|
||||
? "text-blue-700 font-semibold"
|
||||
: "text-gray-600 hover:text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user