| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 |
- "use client";
- import { useEffect, useState } from "react";
- import { usePathname } from "next/navigation";
- import { cn } from "@/lib/utils";
- type Heading = {
- id: string;
- text: string;
- level: number;
- };
- function getHeadings(): Heading[] {
- const article = document.querySelector("article");
- if (!article) return [];
- const elements = article.querySelectorAll("h2[id], h3[id]");
- return Array.from(elements).map((el) => ({
- id: el.id,
- text: el.textContent?.replace(/#$/, "").trim() ?? "",
- level: el.tagName === "H3" ? 3 : 2,
- }));
- }
- export function TableOfContents() {
- const pathname = usePathname();
- const [headings, setHeadings] = useState<Heading[]>([]);
- const [activeId, setActiveId] = useState<string>("");
- useEffect(() => {
- const timer = setTimeout(() => setHeadings(getHeadings()), 100);
- return () => clearTimeout(timer);
- }, [pathname]);
- useEffect(() => {
- if (headings.length === 0) return;
- const observer = new IntersectionObserver(
- (entries) => {
- for (const entry of entries) {
- if (entry.isIntersecting) {
- setActiveId(entry.target.id);
- }
- }
- },
- { rootMargin: "0px 0px -75% 0px", threshold: 0.1 },
- );
- for (const h of headings) {
- const el = document.getElementById(h.id);
- if (el) observer.observe(el);
- }
- return () => observer.disconnect();
- }, [headings]);
- if (headings.length === 0) return null;
- return (
- <nav aria-label="On this page">
- <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
- On this page
- </h4>
- <ul className="space-y-1">
- {headings.map((h) => (
- <li key={h.id}>
- <a
- href={`#${h.id}`}
- className={cn(
- "block text-xs leading-relaxed py-0.5 transition-colors",
- h.level === 3 && "pl-3",
- activeId === h.id
- ? "text-foreground"
- : "text-muted-foreground hover:text-foreground",
- )}
- >
- {h.text}
- </a>
- </li>
- ))}
- </ul>
- </nav>
- );
- }
|