Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
CollapsibleTrigger,
} from './ui/collapsible';
import { Button } from './ui/button';
import TableOfContents from './TableOfContents';

const DocLink = ({
uri,
Expand Down Expand Up @@ -276,9 +277,9 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
<DocsNav open={open} setOpen={setOpen} />
</div>
</div>
<div className='dark:bg-slate-800 max-w-[1400px] grid grid-cols-1 lg:grid-cols-4 mx-4 md:mx-12'>
<div className='dark:bg-slate-800 max-w-[1400px] grid grid-cols-1 lg:grid-cols-12 mx-4 md:mx-12 gap-4'>
{!shouldHideSidebar && (
<div className='hidden lg:block mt-24 sticky top-24 h-[calc(100vh-6rem)] overflow-hidden'>
<div className='hidden lg:block lg:col-span-3 mt-24 sticky top-24 h-[calc(100vh-6rem)] overflow-hidden'>
<div className='h-full overflow-y-auto scrollbar-hidden'>
<DocsNav open={open} setOpen={setOpen} />
<CarbonAds
Expand All @@ -289,10 +290,19 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
</div>
)}
<div
className={`lg:mt-20 mx-4 md:mx-0 ${shouldHideSidebar ? 'col-span-4 w-full' : 'col-span-4 md:col-span-3 lg:w-5/6'}`}
className={`lg:mt-20 mx-4 md:mx-0 ${
shouldHideSidebar
? 'col-span-12 w-full'
: 'col-span-12 lg:col-span-6 xl:col-span-6'
}`}
>
{children}
</div>
{!shouldHideSidebar && (
<div className='hidden xl:block xl:col-span-3 mt-20'>
<TableOfContents />
</div>
)}
</div>
</section>
</div>
Expand Down
210 changes: 210 additions & 0 deletions components/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
'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<TableOfContentsProps> = ({
className,
}) => {
const router = useRouter();
const [tocItems, setTocItems] = useState<TocItem[]>([]);
const [activeId, setActiveId] = useState<string>('');

// 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++) {
// Get the heading element and its text content
const heading = headings[i];
// Get the text content of the heading
const text = heading.textContent || '';
// Get the ID of the heading, or generate one from the text content
const id = heading.id || text.toLowerCase().replace(/\s+/g, '-');

// If the heading doesn't have an ID, set one
if (!heading.id && id) {
heading.id = id;
}
// Add the heading to the table of contents
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(
// Callback function to handle intersection events
(entries) => {
// Track the currently active section
let newActiveId = '';

// Check if we are at the top of the page
const isAtTop = window.scrollY < 100; // 100px from top

// If at the top, highlight Introduction
if (isAtTop) {
newActiveId = 'introduction';
} else {
// Otherwise, find the first visible heading
entries.forEach((entry) => {
if (entry.isIntersecting && !newActiveId) {
newActiveId = entry.target.id;
}
});
}

// Update the active 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) {
// Observe the element
observer.observe(element);
}
});

return () => {
tocItems.forEach(({ id }) => {
const element = document.getElementById(id);
if (element) {
// Unobserve the element
observer.unobserve(element);
}
});
};
}, [tocItems]);

useEffect(() => {
const handleScroll = () => {
if (window.scrollY < 100) {
// If at the top, highlight Introduction
setActiveId('introduction');
}
};

window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);

const handleClick = useCallback(
// Callback function to handle click events
(e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
// Get the element to scroll to
const element =
id === 'introduction'
? document.documentElement // Scroll to top for introduction
: document.getElementById(id);

if (element) {
// Calculate the scroll position
const yOffset = -80; // Adjust this value to match your header height
const y =
id === 'introduction'
? 0
: element.getBoundingClientRect().top +
window.pageYOffset +
yOffset;

// Scroll to the element
window.scrollTo({ top: y, behavior: 'smooth' });
}
},
[],
);

if (tocItems.length === 0) {
return null;
}

return (
<nav
className={cn(
'hidden xl:block sticky top-24 h-[calc(100vh-6rem)] overflow-y-auto pr-4',
className,
)}
aria-label='Table of contents'
style={{
scrollbarWidth: 'thin',
scrollbarColor: 'rgb(203 213 225) transparent',
}}
>
<div className='space-y-2 pb-8'>
<h4 className='font-semibold text-slate-900 dark:text-slate-100 mb-4 text-sm uppercase tracking-wide'>
On This Page
</h4>
<ul className='space-y-2 text-sm border-l-2 border-slate-200 dark:border-slate-700'>
{tocItems.map((item) => (
<li
key={item.id}
className={cn('transition-all duration-200', {
'pl-4': item.level === 2,
'pl-8': item.level === 3,
})}
>
<a
key={item.id}
href={`#${item.id}`}

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML Medium

DOM text
is reinterpreted as HTML without escaping meta-characters.
// click function to handle smooth scrolling
onClick={(e) => handleClick(e, item.id)}
className={cn(
'block py-2 text-sm transition-colors duration-200',
activeId === item.id ||
(item.id === 'introduction' && !activeId)
? 'text-primary font-medium' // active state
: 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-300',
item.level === 3 ? 'pl-2' : '', // indentation
)}
>
{item.text}
</a>
</li>
))}
</ul>
</div>
</nav>
);
};

export default TableOfContents;
4 changes: 3 additions & 1 deletion pages/learn/[slug].page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
Expand All @@ -31,7 +33,7 @@ export default function StaticMarkdownPage({
<title>{newTitle}</title>
</Head>
<Headline1>{frontmatter.title}</Headline1>
<StyledMarkdown markdown={content} />
<StyledMarkdown key={router.asPath} markdown={content} />
<NextPrevButton
prevLabel={frontmatter?.prev?.label}
prevURL={frontmatter?.prev?.url}
Expand Down
4 changes: 3 additions & 1 deletion pages/overview/[slug].page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { getLayout } from '~/components/Sidebar';
import StyledMarkdown from '~/components/StyledMarkdown';
import getStaticMarkdownPaths from '~/lib/getStaticMarkdownPaths';
Expand All @@ -23,6 +24,7 @@ export default function StaticMarkdownPage({
frontmatter: any;
content: any;
}) {
const router = useRouter();
const fileRenderType = '_md';
const newTitle = 'JSON Schema - ' + frontmatter.title;

Expand All @@ -32,7 +34,7 @@ export default function StaticMarkdownPage({
<title>{newTitle}</title>
</Head>
<Headline1>{frontmatter.title}</Headline1>
<StyledMarkdown markdown={content} />
<StyledMarkdown key={router.asPath} markdown={content} />
<NextPrevButton
prevLabel={frontmatter.prev?.label}
prevURL={frontmatter.prev?.url}
Expand Down
4 changes: 3 additions & 1 deletion pages/understanding-json-schema/[slug].page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
Expand All @@ -31,7 +33,7 @@ export default function StaticMarkdownPage({
<title>{newTitle}</title>
</Head>
<Headline1>{frontmatter.title || 'NO TITLE!'}</Headline1>
<StyledMarkdown markdown={content} />
<StyledMarkdown key={router.asPath} markdown={content} />
<NextPrevButton
prevLabel={frontmatter?.prev?.label}
prevURL={frontmatter?.prev?.url}
Expand Down
4 changes: 3 additions & 1 deletion pages/understanding-json-schema/reference/[slug].page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { getLayout } from '~/components/Sidebar';
import { Headline1 } from '~/components/Headlines';
import StyledMarkdown from '~/components/StyledMarkdown';
Expand All @@ -26,6 +27,7 @@ export default function StaticMarkdownPage({
frontmatter: any;
content: any;
}) {
const router = useRouter();
const newTitle = 'JSON Schema - ' + frontmatter.title;
const fileRenderType = '_md';
return (
Expand All @@ -34,7 +36,7 @@ export default function StaticMarkdownPage({
<title>{newTitle}</title>
</Head>
<Headline1>{frontmatter.title || 'NO TITLE!'}</Headline1>
<StyledMarkdown markdown={content} />
<StyledMarkdown key={router.asPath} markdown={content} />
<NextPrevButton
prevLabel={frontmatter?.prev?.label}
prevURL={frontmatter?.prev?.url}
Expand Down