diff --git a/components/blog/TableOfContents.tsx b/components/blog/TableOfContents.tsx new file mode 100644 index 0000000..815c3bb --- /dev/null +++ b/components/blog/TableOfContents.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; + +interface TOCItem { + id: string; + text: string; + level: number; +} + +export function TableOfContents() { + const [headings, setHeadings] = useState([]); + const [activeId, setActiveId] = useState(""); + const observerRef = useRef(null); + + useEffect(() => { + const elements = Array.from( + document.querySelectorAll("article h2, article h3") + ) as HTMLElement[]; + + const parsed = elements.map((el) => ({ + id: el.id, + text: el.textContent ?? "", + level: el.tagName === "H2" ? 2 : 3, + })); + + setHeadings(parsed); + + observerRef.current = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "-20% 0px -70% 0px", threshold: 0 } + ); + + elements.forEach((el) => observerRef.current?.observe(el)); + + return () => observerRef.current?.disconnect(); + }, []); + + if (headings.length === 0) return null; + + return ( + + ); +}