From 68c64502709dc6f6818464e01cca3aa19c9c939c Mon Sep 17 00:00:00 2001 From: Suyog241005 <166608895+Suyog241005@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:32:49 +0530 Subject: [PATCH 1/5] feat: add table of contents navigation for documentation pages --- components/Sidebar.tsx | 16 +- components/TableOfContents.tsx | 187 ++++++++++++++++++ pages/learn/[slug].page.tsx | 4 +- pages/overview/[slug].page.tsx | 4 +- .../understanding-json-schema/[slug].page.tsx | 4 +- .../reference/[slug].page.tsx | 4 +- 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 components/TableOfContents.tsx diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 463e92a4b..812fc0010 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -17,6 +17,7 @@ import { CollapsibleTrigger, } from './ui/collapsible'; import { Button } from './ui/button'; +import TableOfContents from './TableOfContents'; const DocLink = ({ uri, @@ -276,9 +277,9 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => { -
+
{!shouldHideSidebar && ( -
+
{
)}
{children}
+ {!shouldHideSidebar && ( +
+ +
+ )}
diff --git a/components/TableOfContents.tsx b/components/TableOfContents.tsx new file mode 100644 index 000000000..fe386d9b4 --- /dev/null +++ b/components/TableOfContents.tsx @@ -0,0 +1,187 @@ +'use client'; + +import React, { useEffect, useState, useCallback } from 'react'; +import { useRouter } from 'next/router'; +import { cn } from '~/lib/utils'; + +interface TocItem { + id: string; + text: string; + level: number; +} + +interface TableOfContentsProps { + className?: string; +} + +export const TableOfContents: React.FC = ({ + className, +}) => { + const router = useRouter(); + const [tocItems, setTocItems] = useState([]); + const [activeId, setActiveId] = useState(''); + + // Extract headings from the page + useEffect(() => { + const headings = document.querySelectorAll('h2, h3'); + const items: TocItem[] = []; + + // Skip the first heading and add "Introduction" as the first item + if (headings.length > 0) { + items.push({ + id: 'introduction', + text: 'Introduction', + level: 2, // Same level as h2 + }); + } + + // Start from index 1 to skip the first heading + for (let i = 1; i < headings.length; i++) { + const heading = headings[i]; + const text = heading.textContent || ''; + const id = heading.id || text.toLowerCase().replace(/\s+/g, '-'); + + if (!heading.id && id) { + heading.id = id; + } + + items.push({ + id, + text, + level: parseInt(heading.tagName.substring(1), 10), // Get heading level (2 for h2, 3 for h3, etc.) + }); + } + + setTocItems(items); + }, [router.asPath]); + + // Intersection Observer to track which section is visible + useEffect(() => { + if (tocItems.length === 0) return; + + const observer = new IntersectionObserver( + (entries) => { + let newActiveId = ''; + let isAtTop = window.scrollY < 100; // 100px from top + + if (isAtTop) { + // If at the top, highlight Introduction + newActiveId = 'introduction'; + } else { + // Otherwise, find the first visible heading + entries.forEach((entry) => { + if (entry.isIntersecting && !newActiveId) { + newActiveId = entry.target.id; + } + }); + } + + if (newActiveId) { + setActiveId(newActiveId); + } + }, + { + rootMargin: '-20% 0px -60% 0px', + threshold: 0.1, + } + ); + + // Observe all headings + tocItems.forEach(({ id }) => { + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + }); + + + return () => { + tocItems.forEach(({ id }) => { + const element = document.getElementById(id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [tocItems]); + + useEffect(() => { + const handleScroll = () => { + if (window.scrollY < 100) { + setActiveId('introduction'); + } + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const handleClick = useCallback((e: React.MouseEvent, id: string) => { + e.preventDefault(); + const element = id === 'introduction' + ? document.documentElement // Scroll to top for introduction + : document.getElementById(id); + + if (element) { + const yOffset = -80; // Adjust this value to match your header height + const y = id === 'introduction' + ? 0 + : element.getBoundingClientRect().top + window.pageYOffset + yOffset; + + window.scrollTo({ top: y, behavior: 'smooth' }); + } + }, []); + + if (tocItems.length === 0) { + return null; + } + + return ( + + ); +}; + +export default TableOfContents; diff --git a/pages/learn/[slug].page.tsx b/pages/learn/[slug].page.tsx index d186f1a4d..c60723ed4 100644 --- a/pages/learn/[slug].page.tsx +++ b/pages/learn/[slug].page.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Head from 'next/head'; +import { useRouter } from 'next/router'; import StyledMarkdown from '~/components/StyledMarkdown'; import { getLayout } from '~/components/Sidebar'; import getStaticMarkdownPaths from '~/lib/getStaticMarkdownPaths'; @@ -23,6 +24,7 @@ export default function StaticMarkdownPage({ frontmatter: any; content: any; }) { + const router = useRouter(); const fileRenderType = '_md'; const newTitle = 'JSON Schema - ' + frontmatter.title; return ( @@ -31,7 +33,7 @@ export default function StaticMarkdownPage({ {newTitle} {frontmatter.title} - + {newTitle} {frontmatter.title} - + {newTitle} {frontmatter.title || 'NO TITLE!'} - + {newTitle} {frontmatter.title || 'NO TITLE!'} - + Date: Thu, 23 Oct 2025 12:46:22 +0530 Subject: [PATCH 2/5] Fix: Resolve linting and formatting issues --- components/Sidebar.tsx | 4 +-- components/TableOfContents.tsx | 61 +++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 812fc0010..221f4d5df 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -291,8 +291,8 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => { )}
diff --git a/components/TableOfContents.tsx b/components/TableOfContents.tsx index fe386d9b4..ea6f5b46b 100644 --- a/components/TableOfContents.tsx +++ b/components/TableOfContents.tsx @@ -62,7 +62,7 @@ export const TableOfContents: React.FC = ({ const observer = new IntersectionObserver( (entries) => { let newActiveId = ''; - let isAtTop = window.scrollY < 100; // 100px from top + const isAtTop = window.scrollY < 100; // 100px from top if (isAtTop) { // If at the top, highlight Introduction @@ -83,9 +83,9 @@ export const TableOfContents: React.FC = ({ { rootMargin: '-20% 0px -60% 0px', threshold: 0.1, - } + }, ); - + // Observe all headings tocItems.forEach(({ id }) => { const element = document.getElementById(id); @@ -93,8 +93,7 @@ export const TableOfContents: React.FC = ({ observer.observe(element); } }); - - + return () => { tocItems.forEach(({ id }) => { const element = document.getElementById(id); @@ -104,7 +103,7 @@ export const TableOfContents: React.FC = ({ }); }; }, [tocItems]); - + useEffect(() => { const handleScroll = () => { if (window.scrollY < 100) { @@ -116,21 +115,28 @@ export const TableOfContents: React.FC = ({ return () => window.removeEventListener('scroll', handleScroll); }, []); - const handleClick = useCallback((e: React.MouseEvent, id: string) => { - e.preventDefault(); - const element = id === 'introduction' - ? document.documentElement // Scroll to top for introduction - : document.getElementById(id); - - if (element) { - const yOffset = -80; // Adjust this value to match your header height - const y = id === 'introduction' - ? 0 - : element.getBoundingClientRect().top + window.pageYOffset + yOffset; + const handleClick = useCallback( + (e: React.MouseEvent, id: string) => { + e.preventDefault(); + const element = + id === 'introduction' + ? document.documentElement // Scroll to top for introduction + : document.getElementById(id); - window.scrollTo({ top: y, behavior: 'smooth' }); - } - }, []); + if (element) { + const yOffset = -80; // Adjust this value to match your header height + const y = + id === 'introduction' + ? 0 + : element.getBoundingClientRect().top + + window.pageYOffset + + yOffset; + + window.scrollTo({ top: y, behavior: 'smooth' }); + } + }, + [], + ); if (tocItems.length === 0) { return null; @@ -141,19 +147,19 @@ export const TableOfContents: React.FC = ({ className={cn( 'hidden xl:block sticky top-24 h-[calc(100vh-6rem)] overflow-y-auto', 'pr-4', - className + className, )} - aria-label="Table of contents" + aria-label='Table of contents' style={{ scrollbarWidth: 'thin', scrollbarColor: 'rgb(203 213 225) transparent', }} > -
-

+
+

On This Page

-
    +
      {tocItems.map((item) => (
    • = ({ onClick={(e) => handleClick(e, item.id)} className={cn( 'block py-2 text-sm transition-colors duration-200', - activeId === item.id || (item.id === 'introduction' && !activeId) + activeId === item.id || + (item.id === 'introduction' && !activeId) ? 'text-primary font-medium' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-300', - item.level === 3 ? 'pl-2' : '' + item.level === 3 ? 'pl-2' : '', )} > {item.text} From bd6a10f401aeded2ecbebced37b29fd1803b76ab Mon Sep 17 00:00:00 2001 From: Suyog241005 <166608895+Suyog241005@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:10:20 +0530 Subject: [PATCH 3/5] Fix: Resolve build and linting issues From f62470f180d35d0c659e57d8c39c7fbeffddd089 Mon Sep 17 00:00:00 2001 From: Suyog241005 <166608895+Suyog241005@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:48:24 +0530 Subject: [PATCH 4/5] resolved the deployments issue --- components/TableOfContents.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/TableOfContents.tsx b/components/TableOfContents.tsx index ea6f5b46b..76910e727 100644 --- a/components/TableOfContents.tsx +++ b/components/TableOfContents.tsx @@ -145,8 +145,7 @@ export const TableOfContents: React.FC = ({ return (