magicui
106 строк · 2.6 Кб
1"use client";
2
3import { TableOfContents } from "@/lib/toc";
4import { useMounted } from "@/lib/use-mounted";
5import { cn } from "@/lib/utils";
6import { useEffect, useMemo, useState } from "react";
7
8interface TocProps {
9toc: TableOfContents;
10}
11
12export function DashboardTableOfContents({ toc }: TocProps) {
13const itemIds: string[] = useMemo(
14() =>
15toc.items
16? toc.items
17.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
18.flat()
19.filter(Boolean)
20.map((id) => id?.split("#")[1])
21: [],
22[toc],
23) as string[];
24
25const activeHeading = useActiveItem(itemIds);
26const mounted = useMounted();
27
28if (!toc?.items || !mounted) {
29return null;
30}
31
32return (
33<div className="space-y-2">
34<p className="font-medium">On This Page</p>
35<Tree tree={toc} activeItem={activeHeading} />
36</div>
37);
38}
39
40function useActiveItem(itemIds: string[]): string | null {
41const [activeId, setActiveId] = useState<string | null>(null);
42
43useEffect(() => {
44const observer = new IntersectionObserver(
45(entries) => {
46entries.forEach((entry) => {
47if (entry.isIntersecting) {
48setActiveId(entry.target.id);
49}
50});
51},
52{ rootMargin: `0% 0% -80% 0%` },
53);
54
55itemIds?.forEach((id) => {
56const element = document.getElementById(id);
57if (element) {
58observer.observe(element);
59}
60});
61
62return () => {
63itemIds?.forEach((id) => {
64const element = document.getElementById(id);
65if (element) {
66observer.unobserve(element);
67}
68});
69};
70}, [itemIds]);
71
72return activeId;
73}
74
75interface TreeProps {
76tree: TableOfContents;
77level?: number;
78activeItem?: string | null;
79}
80
81function Tree({ tree, level = 1, activeItem }: TreeProps) {
82return tree?.items?.length && level < 3 ? (
83<ul className={cn("m-0 list-none", { "pl-4": level !== 1 })}>
84{tree.items.map((item, index) => {
85return (
86<li key={index} className={cn("mt-0 pt-2")}>
87<a
88href={item.url}
89className={cn(
90"inline-block no-underline transition-colors hover:text-foreground",
91item.url === `#${activeItem}`
92? "font-medium text-foreground"
93: "text-muted-foreground",
94)}
95>
96{item.title}
97</a>
98{item.items?.length ? (
99<Tree tree={item} level={level + 1} activeItem={activeItem} />
100) : null}
101</li>
102);
103})}
104</ul>
105) : null;
106}
107