@@ -1,4 +1,3 @@
1−import { useState, useEffect, useRef } from "react";
2import { useRouter } from "next/router";
1import { useRouter } from "next/router";
3import Head from "next/head";
2import Head from "next/head";
4import ErrorPage from "../pages/404";
3import ErrorPage from "../pages/404";
@@ -9,116 +8,7 @@ import SingleColumn from "./SingleColumn";
9import Section from "./Section";
8import Section from "./Section";
10import markdownStyles from "../styles/markdown.module.css";
9import markdownStyles from "../styles/markdown.module.css";
11import { decode } from "html-entities";
10import { decode } from "html-entities";
12−
11+import { TableOfContents } from "./TableOfContents";
13−const TableOfContents = () => {
14− const { nestedHeadings } = useHeadingsData();
15− const [activeId, setActiveId] = useState();
16− useIntersectionObserver(setActiveId);
19− className="sticky min-h-0 overflow-y-scroll w-52 flex-shrink-0 pb-24 hidden lg:block"
20− style={{ top: "8rem", height: "calc(100vh - 16rem)" }}
22− <Headings headings={nestedHeadings} activeId={activeId} />
27−const getNestedHeadings = (headingElements) => {
28− const nestedHeadings = [];
30− headingElements.forEach((heading, index) => {
31− const { innerText: title, id } = heading;
33− if (heading.nodeName === "H2") {
34− nestedHeadings.push({ id, title, items: [] });
35− } else if (heading.nodeName === "H3" && nestedHeadings.length > 0) {
36− nestedHeadings[nestedHeadings.length - 1].items.push({
43− return nestedHeadings;
46−const useHeadingsData = () => {
47− const [nestedHeadings, setNestedHeadings] = useState([]);
50− const headingElements = Array.from(document.querySelectorAll("h2, h3"));
52− const newNestedHeadings = getNestedHeadings(headingElements);
53− setNestedHeadings(newNestedHeadings);
56− return { nestedHeadings };
59−const Headings = ({ headings, activeId }) => (
61− {headings.map((heading) => (
62− <li key={heading.id}>
63− <a className="font-semibold" href={`#${heading.id}`}>
66− {heading.items.length > 0 && (
67− <ul className="pl-2">
68− {heading.items.map((child) => (
71− className={child.id === activeId ? "text-green" : "text-gray"}
73− <a href={`#${child.id}`}>{child.title}</a>
83−const useIntersectionObserver = (setActiveId) => {
84− const headingElementsRef = useRef({});
86− const callback = (headings) => {
87− headingElementsRef.current = headings.reduce((map, headingElement) => {
88− map[headingElement.target.id] = headingElement;
90− }, headingElementsRef.current);
92− const visibleHeadings = [];
93− Object.keys(headingElementsRef.current).forEach((key) => {
94− const headingElement = headingElementsRef.current[key];
95− if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
98− const getIndexFromId = (id) =>
99− headingElements.findIndex((heading) => heading.id === id);
101− if (visibleHeadings.length === 1) {
102− setActiveId(visibleHeadings[0].target.id);
103− } else if (visibleHeadings.length > 1) {
104− const sortedVisibleHeadings = visibleHeadings.sort(
105− (a, b) => getIndexFromId(a.target.id) > getIndexFromId(b.target.id)
107− setActiveId(sortedVisibleHeadings[0].target.id);
111− const observer = new IntersectionObserver(callback, {
112− rootMargin: "0px 0px -50% 0px",
115− const headingElements = Array.from(document.querySelectorAll("h2, h3"));
117− headingElements.forEach((element) => observer.observe(element));
119− return () => observer.disconnect();
123export default function PageWithIndex({ post, markdown, search }) {
13export default function PageWithIndex({ post, markdown, search }) {
124 const router = useRouter();
14 const router = useRouter();