'use client'; import { useEffect, useRef, useState } from 'react'; import { usePathname } from 'next/navigation'; import { FiList } from 'react-icons/fi'; interface TocItem { id: string; text: string; depth: number; } export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) { const [items, setItems] = useState([]); const [activeId, setActiveId] = useState(null); const listRef = useRef(null); const itemRefs = useRef>({}); const [indicator, setIndicator] = useState({ top: 0, opacity: 0 }); const pathname = usePathname(); useEffect(() => { // Clear items immediately when pathname changes setItems([]); setActiveId(null); let observer: IntersectionObserver | null = null; // Small delay to ensure DOM has been updated with new article content const timeoutId = setTimeout(() => { const headings = Array.from( document.querySelectorAll('article h2, article h3') ); const mapped = headings .filter((el) => el.id) .map((el) => ({ id: el.id, text: el.innerText, depth: el.tagName === 'H3' ? 3 : 2 })); setItems(mapped); observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const id = (entry.target as HTMLElement).id; if (id) { setActiveId(id); } } }); }, { // Trigger when heading is in upper 40% of viewport rootMargin: '0px 0px -60% 0px', threshold: 0.1 } ); headings.forEach((el) => observer?.observe(el)); }, 50); // 50ms delay to ensure DOM is updated return () => { clearTimeout(timeoutId); if (observer) { observer.disconnect(); } }; }, [pathname]); useEffect(() => { if (!activeId || !listRef.current) { setIndicator({ top: 0, opacity: 0 }); return; } const activeEl = itemRefs.current[activeId]; if (!activeEl) return; const listTop = listRef.current.getBoundingClientRect().top; const { top, height } = activeEl.getBoundingClientRect(); setIndicator({ top: top - listTop + height / 2, opacity: 1 }); }, [activeId, items.length]); const handleClick = (id: string) => (event: React.MouseEvent) => { event.preventDefault(); const el = document.getElementById(id); if (!el) return; el.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Temporary highlight el.classList.add('toc-target-highlight'); setTimeout(() => { el.classList.remove('toc-target-highlight'); }, 700); // Update hash without instant jump if (history.replaceState) { const url = new URL(window.location.href); url.hash = id; history.replaceState(null, '', url.toString()); } // Trigger callback if provided (e.g. to close mobile menu) if (onLinkClick) { onLinkClick(); } }; if (items.length === 0) return null; return ( ); }