diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index ad52e2ab8..b0e466595 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -10,6 +10,7 @@ import CarbonAds from './CarbonsAds'; import { useTheme } from 'next-themes'; import ExternalLinkIcon from '../public/icons/external-link-black.svg'; import Image from 'next/image'; +import TableOfContents from './TableOfContents'; const DocLink = ({ uri, label, @@ -252,9 +253,17 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => { /> -
+ +
{children}
+ +
+ +
diff --git a/components/TableOfContents.tsx b/components/TableOfContents.tsx new file mode 100644 index 000000000..86cfc65db --- /dev/null +++ b/components/TableOfContents.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +interface Heading { + id: string; + text: string; + level: number; +} + +const TableOfContents: React.FC = () => { + const router = useRouter(); + const [headings, setHeadings] = useState([]); + const [activeId, setActiveId] = useState(null); + + useEffect(() => { + const mainContent = document.getElementById('main-content'); + const elements = mainContent + ? mainContent.querySelectorAll('h1, h2, h3, h4') + : []; + const newHeadings: Heading[] = []; + + elements.forEach((el) => { + const text = el.textContent || ''; + if (text.trim().toLowerCase() === 'on this page') return; + if (el.closest('#sidebar')) return; + + const currentFolder = router.pathname.split('/').pop()?.toLowerCase(); + if (text.trim().toLowerCase() === currentFolder) return; + if ( + text.includes('/') || + text.includes('\\') || + /\.md$|\.tsx$|\.jsx$|\.js$/i.test(text.trim()) + ) + return; + const level = parseInt(el.tagName.replace('H', '')); + if (!el.id) { + const generatedId = text + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^\w-]+/g, ''); + if (generatedId) { + el.id = generatedId; + } + } + + newHeadings.push({ + id: el.id, + text, + level, + }); + }); + + setHeadings(newHeadings); + + const handleScroll = () => { + let currentId: string | null = null; + elements.forEach((el) => { + const rect = (el as HTMLElement).getBoundingClientRect(); + if (rect.top <= 100) { + currentId = el.id; + } + }); + setActiveId(currentId); + }; + + window.addEventListener('scroll', handleScroll); + handleScroll(); + return () => window.removeEventListener('scroll', handleScroll); + }, [router.asPath]); + + return ( + + ); +}; + +export default TableOfContents;