B2B-88: add starter kit structure and elements
This commit is contained in:
90
app/(marketing)/docs/[...slug]/page.tsx
Normal file
90
app/(marketing)/docs/[...slug]/page.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { ContentRenderer, createCmsClient } from '@kit/cms';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
import { DocsCards } from '../_components/docs-cards';
|
||||
import { DocsTableOfContents } from '../_components/docs-table-of-contents';
|
||||
import { extractHeadingsFromJSX } from '../_lib/utils';
|
||||
|
||||
const getPageBySlug = cache(pageLoader);
|
||||
|
||||
interface DocumentationPageProps {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}
|
||||
|
||||
async function pageLoader(slug: string) {
|
||||
const client = await createCmsClient();
|
||||
|
||||
return client.getContentItemBySlug({ slug, collection: 'documentation' });
|
||||
}
|
||||
|
||||
export const generateMetadata = async ({ params }: DocumentationPageProps) => {
|
||||
const slug = (await params).slug.join('/');
|
||||
const page = await getPageBySlug(slug);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { title, description } = page;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
};
|
||||
|
||||
async function DocumentationPage({ params }: DocumentationPageProps) {
|
||||
const slug = (await params).slug.join('/');
|
||||
const page = await getPageBySlug(slug);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const description = page?.description ?? '';
|
||||
|
||||
const headings = extractHeadingsFromJSX(
|
||||
page.content as {
|
||||
props: { children: React.ReactElement[] };
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden py-5'}>
|
||||
<div className={'flex overflow-y-hidden'}>
|
||||
<article className={cn('gap-y-12 overflow-y-auto px-6')}>
|
||||
<section className={'flex flex-col gap-y-2.5'}>
|
||||
<h1 className={'text-foreground text-3xl font-semibold'}>
|
||||
{page.title}
|
||||
</h1>
|
||||
|
||||
<h2 className={'text-muted-foreground text-lg'}>{description}</h2>
|
||||
</section>
|
||||
|
||||
<div className={'markdoc'}>
|
||||
<ContentRenderer content={page.content} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<DocsTableOfContents data={headings} />
|
||||
</div>
|
||||
|
||||
<If condition={page.children.length > 0}>
|
||||
<Separator />
|
||||
|
||||
<DocsCards cards={page.children ?? []} />
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(DocumentationPage);
|
||||
53
app/(marketing)/docs/_components/docs-card.tsx
Normal file
53
app/(marketing)/docs/_components/docs-card.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function DocsCard({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
link,
|
||||
}: React.PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
link: { url: string; label?: string };
|
||||
}>) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`bg-background flex grow flex-col gap-y-2 border p-6 ${link ? 'rounded-t-lg border-b-0' : 'rounded-lg'}`}
|
||||
>
|
||||
<h3 className="mt-0 text-lg font-semibold hover:underline dark:text-white">
|
||||
<Link href={link.url}>{title}</Link>
|
||||
</h3>
|
||||
|
||||
{subtitle && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<p dangerouslySetInnerHTML={{ __html: subtitle }}></p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children && <div className="text-sm">{children}</div>}
|
||||
</div>
|
||||
|
||||
{link && (
|
||||
<div className="bg-muted/50 rounded-b-lg border p-6 py-4">
|
||||
<Link
|
||||
className={
|
||||
'flex items-center space-x-2 text-sm font-medium hover:underline'
|
||||
}
|
||||
href={link.url}
|
||||
>
|
||||
<span>
|
||||
{link.label ?? <Trans i18nKey={'marketing:readMore'} />}
|
||||
</span>
|
||||
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
app/(marketing)/docs/_components/docs-cards.tsx
Normal file
24
app/(marketing)/docs/_components/docs-cards.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Cms } from '@kit/cms';
|
||||
|
||||
import { DocsCard } from './docs-card';
|
||||
|
||||
export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
|
||||
const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order);
|
||||
|
||||
return (
|
||||
<div className={'grid grid-cols-1 gap-6 lg:grid-cols-2'}>
|
||||
{cardsSortedByOrder.map((item) => {
|
||||
return (
|
||||
<DocsCard
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
subtitle={item.description}
|
||||
link={{
|
||||
url: `/docs/${item.slug}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
app/(marketing)/docs/_components/docs-nav-link.tsx
Normal file
39
app/(marketing)/docs/_components/docs-nav-link.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
|
||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||
|
||||
export function DocsNavLink({
|
||||
label,
|
||||
url,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
||||
const currentPath = usePathname();
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const isCurrent = isRouteActive(url, currentPath, true);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isCurrent}
|
||||
className={cn('transition-background font-normal!', {
|
||||
'text-secondary-foreground font-bold': isCurrent,
|
||||
})}
|
||||
>
|
||||
<Link href={url}>
|
||||
<span ref={ref} className="block max-w-full truncate">
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{children}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Cms } from '@kit/cms';
|
||||
import { Collapsible } from '@kit/ui/collapsible';
|
||||
import { isRouteActive } from '@kit/ui/utils';
|
||||
|
||||
export function DocsNavigationCollapsible(
|
||||
props: React.PropsWithChildren<{
|
||||
node: Cms.ContentItem;
|
||||
prefix: string;
|
||||
}>,
|
||||
) {
|
||||
const currentPath = usePathname();
|
||||
const prefix = props.prefix;
|
||||
|
||||
const isChildActive = props.node.children.some((child) =>
|
||||
isRouteActive(prefix + '/' + child.url, currentPath, false),
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
className={'group/collapsible'}
|
||||
defaultOpen={isChildActive ? true : !props.node.collapsed}
|
||||
>
|
||||
{props.children}
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
143
app/(marketing)/docs/_components/docs-navigation.tsx
Normal file
143
app/(marketing)/docs/_components/docs-navigation.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { Cms } from '@kit/cms';
|
||||
import { CollapsibleContent, CollapsibleTrigger } from '@kit/ui/collapsible';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
|
||||
|
||||
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||
|
||||
function Node({
|
||||
node,
|
||||
level,
|
||||
prefix,
|
||||
}: {
|
||||
node: Cms.ContentItem;
|
||||
level: number;
|
||||
prefix: string;
|
||||
}) {
|
||||
const url = `${prefix}/${node.slug}`;
|
||||
const label = node.label ? node.label : node.title;
|
||||
|
||||
const Container = (props: React.PropsWithChildren) => {
|
||||
if (node.collapsible) {
|
||||
return (
|
||||
<DocsNavigationCollapsible node={node} prefix={prefix}>
|
||||
{props.children}
|
||||
</DocsNavigationCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
const ContentContainer = (props: React.PropsWithChildren) => {
|
||||
if (node.collapsible) {
|
||||
return <CollapsibleContent>{props.children}</CollapsibleContent>;
|
||||
}
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
const Trigger = () => {
|
||||
if (node.collapsible) {
|
||||
return (
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
{label}
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return <DocsNavLink label={label} url={url} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Trigger />
|
||||
|
||||
<ContentContainer>
|
||||
<Tree pages={node.children ?? []} level={level + 1} prefix={prefix} />
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function Tree({
|
||||
pages,
|
||||
level,
|
||||
prefix,
|
||||
}: {
|
||||
pages: Cms.ContentItem[];
|
||||
level: number;
|
||||
prefix: string;
|
||||
}) {
|
||||
if (level === 0) {
|
||||
return pages.map((treeNode, index) => (
|
||||
<Node key={index} node={treeNode} level={level} prefix={prefix} />
|
||||
));
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuSub>
|
||||
{pages.map((treeNode, index) => (
|
||||
<Node key={index} node={treeNode} level={level} prefix={prefix} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsNavigation({
|
||||
pages,
|
||||
prefix = '/docs',
|
||||
}: {
|
||||
pages: Cms.ContentItem[];
|
||||
prefix?: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant={'ghost'}
|
||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto'}
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
|
||||
<div className={'lg:hidden'}>
|
||||
<FloatingDocumentationNavigation>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</FloatingDocumentationNavigation>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
app/(marketing)/docs/_components/docs-page-link.tsx
Normal file
45
app/(marketing)/docs/_components/docs-page-link.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { If } from '@kit/ui/if';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
export function DocsPageLink({
|
||||
page,
|
||||
before,
|
||||
after,
|
||||
}: React.PropsWithChildren<{
|
||||
page: {
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
before?: React.ReactNode;
|
||||
after?: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
`ring-muted hover:ring-primary flex w-full items-center space-x-8 rounded-xl p-6 font-medium text-current ring-2 transition-all`,
|
||||
{
|
||||
'justify-start': before,
|
||||
'justify-end self-end': after,
|
||||
},
|
||||
)}
|
||||
href={page.url}
|
||||
>
|
||||
<If condition={before}>{(node) => <>{node}</>}</If>
|
||||
|
||||
<span className={'flex flex-col space-y-1.5'}>
|
||||
<span
|
||||
className={'text-muted-foreground text-xs font-semibold uppercase'}
|
||||
>
|
||||
{before ? `Previous` : ``}
|
||||
{after ? `Next` : ``}
|
||||
</span>
|
||||
|
||||
<span>{page.title}</span>
|
||||
</span>
|
||||
|
||||
<If condition={after}>{(node) => <>{node}</>}</If>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
51
app/(marketing)/docs/_components/docs-table-of-contents.tsx
Normal file
51
app/(marketing)/docs/_components/docs-table-of-contents.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
interface NavItem {
|
||||
text: string;
|
||||
level: number;
|
||||
href: string;
|
||||
children: NavItem[];
|
||||
}
|
||||
|
||||
export function DocsTableOfContents(props: { data: NavItem[] }) {
|
||||
const navData = props.data;
|
||||
|
||||
return (
|
||||
<div className="bg-background sticky inset-y-0 hidden h-svh max-h-full min-w-[14em] border-l p-4 lg:block">
|
||||
<ol
|
||||
role="list"
|
||||
className="relative text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{navData.map((item) => (
|
||||
<li key={item.href} className="group/item relative mt-3 first:mt-0">
|
||||
<a
|
||||
href={item.href}
|
||||
className="block transition-colors **:[font:inherit] hover:text-gray-950 dark:hover:text-white"
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
{item.children && (
|
||||
<ol role="list" className="relative mt-3 pl-4">
|
||||
{item.children.map((child) => (
|
||||
<li
|
||||
key={child.href}
|
||||
className="group/subitem relative mt-3 first:mt-0"
|
||||
>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block transition-colors **:[font:inherit] hover:text-gray-950 dark:hover:text-white"
|
||||
>
|
||||
{child.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
|
||||
export function FloatingDocumentationNavigation(
|
||||
props: React.PropsWithChildren,
|
||||
) {
|
||||
const activePath = usePathname();
|
||||
|
||||
const body = useMemo(() => {
|
||||
return isBrowser() ? document.body : null;
|
||||
}, []);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const enableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = '');
|
||||
|
||||
const disableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = 'hidden');
|
||||
|
||||
// enable/disable body scrolling when the docs are toggled
|
||||
useEffect(() => {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
disableScrolling(body);
|
||||
} else {
|
||||
enableScrolling(body);
|
||||
}
|
||||
}, [isVisible, body]);
|
||||
|
||||
// hide docs when navigating to another page
|
||||
useEffect(() => {
|
||||
setIsVisible(false);
|
||||
}, [activePath]);
|
||||
|
||||
const onClick = () => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<If condition={isVisible}>
|
||||
<div
|
||||
className={
|
||||
'fixed top-0 left-0 z-10 h-screen w-full p-4' +
|
||||
' dark:bg-background flex flex-col space-y-4 overflow-auto bg-white'
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={'fixed right-5 bottom-5 z-10 h-16 w-16 rounded-full'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Menu className={'h-8'} />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
31
app/(marketing)/docs/_lib/server/docs.loader.ts
Normal file
31
app/(marketing)/docs/_lib/server/docs.loader.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
/**
|
||||
* @name getDocs
|
||||
* @description Load the documentation pages.
|
||||
* @param language
|
||||
*/
|
||||
export const getDocs = cache(docsLoader);
|
||||
|
||||
async function docsLoader(language: string | undefined) {
|
||||
const cms = await createCmsClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
try {
|
||||
const data = await cms.getContentItems({
|
||||
collection: 'documentation',
|
||||
language,
|
||||
limit: Infinity,
|
||||
content: false,
|
||||
});
|
||||
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to load documentation pages');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
146
app/(marketing)/docs/_lib/utils.ts
Normal file
146
app/(marketing)/docs/_lib/utils.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Cms } from '@kit/cms';
|
||||
|
||||
interface HeadingNode {
|
||||
text: string;
|
||||
level: number;
|
||||
href: string;
|
||||
children: HeadingNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @name buildDocumentationTree
|
||||
* @description Build a tree structure for the documentation pages.
|
||||
* @param pages
|
||||
*/
|
||||
export function buildDocumentationTree(pages: Cms.ContentItem[]) {
|
||||
const tree: Cms.ContentItem[] = [];
|
||||
|
||||
pages.forEach((page) => {
|
||||
if (page.parentId) {
|
||||
const parent = pages.find((item) => item.slug === page.parentId);
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
|
||||
parent.children.push(page);
|
||||
|
||||
// sort children by order
|
||||
parent.children.sort((a, b) => a.order - b.order);
|
||||
} else {
|
||||
tree.push(page);
|
||||
}
|
||||
});
|
||||
|
||||
return tree.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name extractHeadingsFromJSX
|
||||
* @description Extract headings from JSX. This is used to generate the table of contents for the documentation pages.
|
||||
* @param jsx
|
||||
*/
|
||||
export function extractHeadingsFromJSX(jsx: {
|
||||
props: { children: React.ReactElement[] };
|
||||
}) {
|
||||
const headings: HeadingNode[] = [];
|
||||
let currentH2: HeadingNode | null = null;
|
||||
|
||||
function getTextContent(
|
||||
children: React.ReactElement[] | string | React.ReactElement,
|
||||
): string {
|
||||
try {
|
||||
if (typeof children === 'string') {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children.map((child) => getTextContent(child)).join('');
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
children.props as {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
).children
|
||||
) {
|
||||
return getTextContent(
|
||||
(children.props as { children: React.ReactElement }).children,
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
jsx.props.children.forEach((node) => {
|
||||
if (!node || typeof node !== 'object' || !('type' in node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeType = node.type as string;
|
||||
|
||||
const text = getTextContent(
|
||||
(
|
||||
node.props as {
|
||||
children: React.ReactElement[];
|
||||
}
|
||||
).children,
|
||||
);
|
||||
|
||||
if (nodeType === 'h1') {
|
||||
const slug = generateSlug(text);
|
||||
|
||||
headings.push({
|
||||
text,
|
||||
level: 1,
|
||||
href: `#${slug}`,
|
||||
children: [],
|
||||
});
|
||||
} else if (nodeType === 'h2') {
|
||||
const slug = generateSlug(text);
|
||||
|
||||
currentH2 = {
|
||||
text,
|
||||
level: 2,
|
||||
href: `#${slug}`,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (headings.length > 0) {
|
||||
headings[headings.length - 1]!.children.push(currentH2);
|
||||
} else {
|
||||
headings.push(currentH2);
|
||||
}
|
||||
} else if (nodeType === 'h3' && currentH2) {
|
||||
const slug = generateSlug(text);
|
||||
|
||||
currentH2.children.push({
|
||||
text,
|
||||
level: 3,
|
||||
href: `#${slug}`,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return headings;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function generateSlug(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
27
app/(marketing)/docs/layout.tsx
Normal file
27
app/(marketing)/docs/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
|
||||
// local imports
|
||||
import { DocsNavigation } from './_components/docs-navigation';
|
||||
import { getDocs } from './_lib/server/docs.loader';
|
||||
import { buildDocumentationTree } from './_lib/utils';
|
||||
|
||||
async function DocsLayout({ children }: React.PropsWithChildren) {
|
||||
const { resolvedLanguage } = await createI18nServerInstance();
|
||||
const docs = await getDocs(resolvedLanguage);
|
||||
const tree = buildDocumentationTree(docs);
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={{ '--sidebar-width': '18em' } as React.CSSProperties}
|
||||
className={'h-[calc(100vh-72px)] overflow-y-hidden lg:container'}
|
||||
>
|
||||
<DocsNavigation pages={tree} />
|
||||
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocsLayout;
|
||||
3
app/(marketing)/docs/loading.tsx
Normal file
3
app/(marketing)/docs/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||
|
||||
export default GlobalLoader;
|
||||
39
app/(marketing)/docs/page.tsx
Normal file
39
app/(marketing)/docs/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SitePageHeader } from '../_components/site-page-header';
|
||||
import { DocsCards } from './_components/docs-cards';
|
||||
import { getDocs } from './_lib/server/docs.loader';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('marketing:documentation'),
|
||||
};
|
||||
};
|
||||
|
||||
async function DocsPage() {
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const items = await getDocs(resolvedLanguage);
|
||||
|
||||
// Filter out any docs that have a parentId, as these are children of other docs
|
||||
const cards = items.filter((item) => !item.parentId);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-y-6 xl:gap-y-10'}>
|
||||
<SitePageHeader
|
||||
title={t('marketing:documentation')}
|
||||
subtitle={t('marketing:documentationSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'flex flex-col items-center'}>
|
||||
<div className={'container mx-auto max-w-5xl'}>
|
||||
<DocsCards cards={cards} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(DocsPage);
|
||||
Reference in New Issue
Block a user