-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add dashboard for table of contents
- Loading branch information
1 parent
a851a9e
commit 884bd3a
Showing
7 changed files
with
277 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { rawText } from "./block-parse"; | ||
|
||
const heading_level_1 = "heading_1"; | ||
const heading_level_2 = "heading_2"; | ||
const heading_level_3 = "heading_3"; | ||
|
||
interface Item { | ||
title: string; | ||
id: string; | ||
items: Item[]; | ||
} | ||
|
||
interface Items { | ||
items: Item[]; | ||
} | ||
|
||
export type TableOfContents = Items; | ||
|
||
export function getTableOfContents(blocks: Array<any>): TableOfContents { | ||
const root: TableOfContents = { | ||
items: new Array(), | ||
}; | ||
|
||
blocks.map((block: any) => { | ||
const { id, type } = block; | ||
if (type != heading_level_1 && type != heading_level_2 && type != heading_level_3) { | ||
return; | ||
} | ||
|
||
const heading = block[type]; | ||
const raw_context = rawText(heading?.rich_text); | ||
if (!raw_context) { | ||
return; | ||
} | ||
|
||
const item: Item = { | ||
title: raw_context, | ||
id: id, | ||
items: new Array(), | ||
}; | ||
|
||
if (type == heading_level_1) { | ||
root.items.push(item); | ||
} else if (type == heading_level_2) { | ||
const parent = root.items[root.items.length - 1]; | ||
if (parent) { | ||
parent.items.push(item); | ||
} else { | ||
root.items.push(item); | ||
} | ||
} else { | ||
const grandparent = root.items[root.items.length - 1]; | ||
if (grandparent) { | ||
const parent = grandparent.items[grandparent.items.length - 1]; | ||
if (parent) { | ||
parent.items.push(item); | ||
} else { | ||
grandparent.items.push(item); | ||
} | ||
} else { | ||
root.items.push(item); | ||
} | ||
} | ||
}); | ||
return root; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
"use client"; | ||
|
||
import * as React from "react"; | ||
|
||
import { TableOfContents } from "@/app/notion/toc"; | ||
import { cn } from "@/lib/utils"; | ||
import { useMounted } from "@/lib/hooks/use-mounted"; | ||
|
||
interface TocProps { | ||
toc: TableOfContents; | ||
} | ||
|
||
export function DashboardTableOfContents({ toc }: TocProps) { | ||
const itemIds = React.useMemo( | ||
() => | ||
toc.items | ||
? toc.items | ||
.flatMap((heading1) => [ | ||
heading1.id, | ||
heading1.items?.map((heading2) => [heading2.id, heading2.items?.map((heading3) => [heading3.id])]), | ||
]) | ||
.flat(4) | ||
.filter(Boolean) | ||
: [], | ||
[toc] | ||
); | ||
const activeHeading = useActiveItem(itemIds); | ||
const mounted = useMounted(); | ||
|
||
if (!toc?.items) { | ||
return null; | ||
} | ||
|
||
return mounted ? ( | ||
<div className="space-y-2"> | ||
<p className="font-medium">On This Page</p> | ||
{/* <Tree tree={toc} activeItem={activeHeading} /> */} | ||
<ul className={cn("m-0 list-none")}> | ||
<IndentTree tree={toc} activeItem={activeHeading} /> | ||
</ul> | ||
</div> | ||
) : null; | ||
} | ||
|
||
function useActiveItem(itemIds: (string | undefined)[]) { | ||
const [activeId, setActiveId] = React.useState<string>(""); | ||
|
||
React.useEffect(() => { | ||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
// entries.forEach((entry) => { | ||
// if (entry.isIntersecting) { | ||
// console.log("actived", entry.target.id); | ||
// // setActiveId(entry.target.id); | ||
// } | ||
// }); | ||
|
||
entries.some((entry) => { | ||
if (entry.isIntersecting) { | ||
// console.log("the active", entry.target.id); | ||
setActiveId(entry.target.id); | ||
return true; | ||
} | ||
}); | ||
}, | ||
{ rootMargin: `-64px 0px 0px 0px` } | ||
// {threshold: 0.5,} | ||
); | ||
|
||
itemIds?.forEach((id) => { | ||
if (!id) { | ||
return; | ||
} | ||
const element = document.getElementById(id); | ||
if (element) { | ||
observer.observe(element); | ||
} | ||
}); | ||
|
||
return () => { | ||
itemIds?.forEach((id) => { | ||
if (!id) { | ||
return; | ||
} | ||
|
||
const element = document.getElementById(id); | ||
if (element) { | ||
observer.unobserve(element); | ||
} | ||
}); | ||
}; | ||
}, [itemIds]); | ||
|
||
return activeId; | ||
} | ||
|
||
interface TreeProps { | ||
tree: TableOfContents; | ||
level?: number; | ||
activeItem?: string | null; | ||
} | ||
|
||
function Tree({ tree, level = 1, activeItem }: TreeProps) { | ||
return tree?.items?.length && level <= 3 ? ( | ||
<ul className={cn("m-0 list-none", { "pl-4": level !== 1 })}> | ||
{tree.items.map((item, index) => { | ||
return ( | ||
<li key={index} className={cn("mt-0 pt-2")}> | ||
<a | ||
href={`#${item.id}`} | ||
className={cn( | ||
"inline-block no-underline", | ||
item.id === `${activeItem}` ? "font-medium text-primary" : "text-sm text-muted-foreground" | ||
)} | ||
> | ||
{item.title} | ||
</a> | ||
{item.items?.length ? <Tree tree={item} level={level + 1} activeItem={activeItem} /> : null} | ||
</li> | ||
); | ||
})} | ||
</ul> | ||
) : null; | ||
} | ||
|
||
function IndentTree({ tree, level = 1, activeItem }: TreeProps) { | ||
let style = "pl-4"; | ||
if (level == 2) { | ||
style = "pl-8"; | ||
} else if (level == 3) { | ||
style = "pl-12"; | ||
} | ||
return tree?.items?.length && level <= 3 ? ( | ||
<React.Fragment> | ||
{tree.items.map((item, index) => { | ||
return ( | ||
<> | ||
<li key={index} className={cn("mt-0 pt-2", style)}> | ||
<a | ||
href={`#${item.id}`} | ||
className={cn( | ||
"inline-block", | ||
item.id === `${activeItem}` ? "font-medium text-primary" : "text-sm text-muted-foreground" | ||
)} | ||
> | ||
{item.title} | ||
</a> | ||
</li> | ||
{item.items?.length ? <IndentTree tree={item} level={level + 1} activeItem={activeItem} /> : null} | ||
</> | ||
); | ||
})} | ||
</React.Fragment> | ||
) : null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import * as React from "react" | ||
|
||
export function useMounted() { | ||
const [mounted, setMounted] = React.useState(false) | ||
|
||
React.useEffect(() => { | ||
setMounted(true) | ||
}, []) | ||
|
||
return mounted | ||
} |