mirror of
https://github.com/supabase/supabase.git
synced 2026-06-24 01:43:09 +08:00
Co-authored-by: Kang Ming <kang.ming1996@gmail.com> Co-authored-by: Joel Lee <lee.yi.jie.joel@gmail.com>
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import { usePathname } from 'next/navigation'
|
|
import { useEffect, useState } from 'react'
|
|
import { cn } from 'ui'
|
|
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
|
import { proxy, useSnapshot } from 'valtio'
|
|
import {
|
|
highlightSelectedTocItem,
|
|
removeAnchor,
|
|
} from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
|
|
import { Feedback } from '~/components/Feedback'
|
|
import useHash from '~/hooks/useHash'
|
|
|
|
const formatSlug = (slug: string) => {
|
|
// [Joshen] We will still provide support for headers declared like this:
|
|
// ## REST API {#rest-api-overview}
|
|
// At least for now, this was a docusaurus thing.
|
|
if (slug.includes('#')) return slug.split('#')[1]
|
|
return slug
|
|
}
|
|
|
|
const formatTOCHeader = (content: string) => {
|
|
let begin = false
|
|
const res = []
|
|
for (const x of content) {
|
|
if (x === '`') {
|
|
if (!begin) {
|
|
begin = true
|
|
res.push(`<code class="text-xs border rounded bg-muted">`)
|
|
} else {
|
|
begin = false
|
|
res.push(`</code>`)
|
|
}
|
|
} else {
|
|
res.push(x)
|
|
}
|
|
}
|
|
return res.join('')
|
|
}
|
|
|
|
const tocRenderSwitch = proxy({
|
|
renderFlag: 0,
|
|
toggleRenderFlag: () => void (tocRenderSwitch.renderFlag = (tocRenderSwitch.renderFlag + 1) % 2),
|
|
})
|
|
|
|
const useSubscribeTocRerender = () => {
|
|
const { renderFlag } = useSnapshot(tocRenderSwitch)
|
|
return void renderFlag // Prevent it from being detected as unused code
|
|
}
|
|
|
|
const useTocRerenderTrigger = () => {
|
|
const { toggleRenderFlag } = useSnapshot(tocRenderSwitch)
|
|
return toggleRenderFlag
|
|
}
|
|
|
|
const GuidesTableOfContents = ({
|
|
className,
|
|
overrideToc,
|
|
video,
|
|
}: {
|
|
className?: string
|
|
overrideToc?: Array<{ text: string; link: string; level: number }>
|
|
video?: string
|
|
}) => {
|
|
useSubscribeTocRerender()
|
|
const [tocList, setTocList] = useState([])
|
|
const pathname = usePathname()
|
|
const [hash] = useHash()
|
|
|
|
const displayedList = overrideToc ?? tocList
|
|
|
|
useEffect(() => {
|
|
if (overrideToc) return
|
|
|
|
/**
|
|
* Because we're directly querying the DOM, needs the setTimeout so the DOM
|
|
* update will happen first.
|
|
*/
|
|
const timeoutHandle = setTimeout(() => {
|
|
const headings = Array.from(
|
|
document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? []
|
|
)
|
|
const newHeadings = headings
|
|
.filter((heading) => heading.id)
|
|
.map((heading) => {
|
|
const text = heading.textContent.replace('#', '')
|
|
const link = heading.querySelector('a')?.getAttribute('href')
|
|
if (!link) return null
|
|
|
|
const level = heading.tagName === 'H2' ? 2 : 3
|
|
return { text, link, level }
|
|
})
|
|
.filter(Boolean)
|
|
setTocList(newHeadings)
|
|
})
|
|
|
|
return () => clearTimeout(timeoutHandle)
|
|
/**
|
|
* window.location.href needed to recalculate toc when page changes,
|
|
* `useSubscribeTocRerender` above will trigger the rerender
|
|
*/
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [overrideToc, typeof window !== 'undefined' && window.location.href])
|
|
|
|
useEffect(() => {
|
|
if (hash && displayedList.length > 0) {
|
|
highlightSelectedTocItem(hash)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [hash, JSON.stringify(displayedList)])
|
|
|
|
if (!displayedList.length) return
|
|
|
|
const tocVideoPreview = `http://img.youtube.com/vi/${video}/0.jpg`
|
|
|
|
return (
|
|
<div className={cn('border-l', 'thin-scrollbar overflow-y-auto', 'px-2', className)}>
|
|
{video && (
|
|
<div className="relative mb-6 pl-5">
|
|
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
|
</div>
|
|
)}
|
|
<Feedback key={pathname} />
|
|
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
|
|
On this page
|
|
</span>
|
|
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
|
|
{displayedList.map((item, i) => (
|
|
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
|
<a
|
|
href={`#${formatSlug(item.link)}`}
|
|
className="text-foreground-lighter hover:text-brand-link transition-colors"
|
|
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default GuidesTableOfContents
|
|
export { useTocRerenderTrigger }
|