Files
supabase/apps/learn/components/toc.tsx
Terry Sutton dda0b526ac Feat/learn (#41566)
wip

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

# Release Notes

* **New Features**
* Added a new Learn application offering foundational Supabase courses
with interactive documentation
* Courses include Architecture, Authentication, Data Fundamentals,
Security, Storage, Realtime, and Edge Functions
  * Chapter tracking and progress indicators for course completions
  * Responsive sidebar navigation with search/command menu
  * Theme switching support (light, dark, classic dark modes)
  * Mobile-friendly course interface

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 21:36:24 -03:30

108 lines
2.5 KiB
TypeScript

// @ts-nocheck
'use client'
import * as React from 'react'
import { TableOfContents } from '@/lib/toc'
import { cn } from '@/lib/utils'
import { useMounted } from '@/hooks/use-mounted'
interface TocProps {
toc: TableOfContents
}
export function DashboardTableOfContents({ toc }: TocProps) {
const itemIds = React.useMemo(
() =>
toc.items
? toc.items
.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
.flat()
.filter(Boolean)
.map((id) => id?.split('#')[1])
: [],
[toc]
)
const activeHeading = useActiveItem(itemIds)
const mounted = useMounted()
if (!toc?.items || !mounted) {
return null
}
return (
<div className="space-y-2">
<p className="font-medium text-foreground-light">On This Page</p>
<Tree tree={toc} activeItem={activeHeading} />
</div>
)
}
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = React.useState(null)
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{ rootMargin: `0% 0% -80% 0%` }
)
itemIds?.forEach((id) => {
const element = document.getElementById(id)
if (element) {
observer.observe(element)
}
})
return () => {
itemIds?.forEach((id) => {
const element = document.getElementById(id)
if (element) {
observer.unobserve(element)
}
})
}
}, [itemIds])
return activeId
}
interface TreeProps {
tree: TableOfContents
level?: number
activeItem?: string
}
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.url}
className={cn(
'inline-block no-underline transition-colors hover:text-foreground',
item.url === `#${activeItem}`
? 'font-medium text-foreground'
: 'text-foreground-muted'
)}
>
{item.title}
</a>
{item.items?.length ? (
<Tree tree={item} level={level + 1} activeItem={activeItem} />
) : null}
</li>
)
})}
</ul>
) : null
}