Files
blog/components/table-of-contents.tsx

64 lines
1.7 KiB
TypeScript

"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>
);
}