Skip to content

Commit e420a37

Browse files
committed
feat: new docs page
1 parent 5080113 commit e420a37

File tree

14 files changed

+1603
-362
lines changed

14 files changed

+1603
-362
lines changed

apps/docs/app/docs/[[...slug]]/page.tsx

Lines changed: 26 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { createRelativeLink } from 'fumadocs-ui/mdx';
2-
import {
3-
DocsBody,
4-
DocsDescription,
5-
DocsPage,
6-
DocsTitle,
7-
} from 'fumadocs-ui/page';
1+
import defaultMdxComponents from 'fumadocs-ui/mdx';
2+
import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/page';
83
import { notFound } from 'next/navigation';
9-
import { StructuredData } from '@/components/structured-data';
4+
import { DocsFooter } from '@/components/docs-footer';
105
import { source } from '@/lib/source';
11-
import { getMDXComponents } from '@/mdx-components';
126

137
export default async function Page(props: {
148
params: Promise<{ slug?: string[] }>;
@@ -19,77 +13,31 @@ export default async function Page(props: {
1913
notFound();
2014
}
2115

22-
const MDXContent = page.data.body;
23-
const url = `https://www.databuddy.cc${page.url}`;
24-
25-
// Generate breadcrumbs from slug
26-
const breadcrumbs = [
27-
{ name: 'Home', url: '/' },
28-
{ name: 'Documentation', url: '/docs' },
29-
];
30-
31-
if (params.slug && params.slug.length > 0) {
32-
let currentPath = '/docs';
33-
params.slug.forEach((segment, index) => {
34-
currentPath += `/${segment}`;
35-
if (index < (params.slug?.length ?? 0) - 1) {
36-
breadcrumbs.push({
37-
name: segment.charAt(0).toUpperCase() + segment.slice(1),
38-
url: currentPath,
39-
});
40-
}
41-
});
42-
}
43-
44-
breadcrumbs.push({ name: page.data.title, url: page.url });
45-
46-
const title = `${page.data.title} | Databuddy Documentation`;
47-
const description =
48-
page.data.description ||
49-
`Learn about ${page.data.title} in Databuddy's privacy-first analytics platform. Complete guides and API documentation.`;
50-
51-
const publishedDate = new Date();
52-
const lastModified = page.data.lastModified
53-
? new Date(page.data.lastModified)
54-
: null;
16+
const MDX = page.data.body;
5517

5618
return (
57-
<>
58-
<StructuredData
59-
elements={[
60-
{
61-
type: 'article',
62-
value: {
63-
title,
64-
description,
65-
datePublished: publishedDate.toISOString(),
66-
dateModified: lastModified?.toISOString(),
67-
},
68-
},
69-
]}
70-
page={{
71-
title,
72-
description,
73-
url,
74-
dateModified: lastModified?.toISOString(),
75-
datePublished: publishedDate.toISOString(),
76-
breadcrumbs,
77-
inLanguage: 'en',
78-
}}
79-
/>
80-
<DocsPage full={page.data.full} toc={page.data.toc}>
81-
<DocsTitle>{page.data.title}</DocsTitle>
82-
<DocsDescription>{page.data.description}</DocsDescription>
83-
<DocsBody>
84-
<MDXContent
85-
components={getMDXComponents({
86-
// this allows you to link to other pages with relative file paths
87-
a: createRelativeLink(source, page),
88-
})}
89-
/>
90-
</DocsBody>
91-
</DocsPage>
92-
</>
19+
<DocsPage
20+
editOnGithub={{
21+
owner: 'databuddy-analytics',
22+
repo: 'databuddy',
23+
sha: 'main',
24+
path: `/docs/content/docs/${page.file.path}`,
25+
}}
26+
footer={{
27+
component: <DocsFooter />,
28+
enabled: true,
29+
}}
30+
full={page.data.full}
31+
tableOfContent={{
32+
style: 'clerk',
33+
}}
34+
toc={page.data.toc}
35+
>
36+
<DocsTitle>{page.data.title}</DocsTitle>
37+
<DocsBody>
38+
<MDX components={defaultMdxComponents} />
39+
</DocsBody>
40+
</DocsPage>
9341
);
9442
}
9543

@@ -112,7 +60,6 @@ export async function generateMetadata(props: {
11260
page.data.description ||
11361
`Learn about ${page.data.title} in Databuddy's privacy-first analytics platform. Complete guides and API documentation.`;
11462

115-
// Generate dynamic keywords based on page content and URL
11663
const baseKeywords = [
11764
page.data.title.toLowerCase(),
11865
'databuddy',
@@ -125,7 +72,6 @@ export async function generateMetadata(props: {
12572
'data ownership',
12673
];
12774

128-
// Add context-specific keywords
12975
const contextKeywords = [
13076
...(page.url.includes('integration') || page.url.includes('Integrations')
13177
? ['integration', 'setup guide', 'installation']

apps/docs/app/docs/layout.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
22
import type { ReactNode } from 'react';
33
import { baseOptions } from '@/app/layout.config';
4+
import CustomSidebar from '@/components/custom-sidebar';
5+
import { Navbar } from '@/components/navbar';
46
import { source } from '@/lib/source';
57

68
export default function Layout({ children }: { children: ReactNode }) {
79
return (
8-
<DocsLayout tree={source.pageTree} {...baseOptions}>
10+
<DocsLayout
11+
tree={source.pageTree}
12+
{...baseOptions}
13+
nav={{
14+
enabled: true,
15+
component: <Navbar />,
16+
}}
17+
sidebar={{
18+
enabled: true,
19+
component: <CustomSidebar />,
20+
}}
21+
>
922
{children}
1023
</DocsLayout>
1124
);

apps/docs/app/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export default function Layout({ children }: { children: ReactNode }) {
112112
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
113113
<RootProvider>
114114
<main>{children}</main>
115-
<Toaster closeButton duration={1500} position="top-center" richColors />
115+
<Toaster
116+
closeButton
117+
duration={1500}
118+
position="top-center"
119+
richColors
120+
/>
116121
</RootProvider>
117122
</ThemeProvider>
118123
</body>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client';
2+
3+
import { CaretDownIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
4+
import { useSearchContext } from 'fumadocs-ui/provider';
5+
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
6+
import { usePathname } from 'next/navigation';
7+
import { Suspense, useCallback, useEffect, useState } from 'react';
8+
import { AsideLink } from '@/components/ui/aside-link';
9+
import { Badge } from '@/components/ui/badge';
10+
import { cn } from '@/lib/utils';
11+
import { contents } from './sidebar-content';
12+
13+
export default function CustomSidebar() {
14+
const [currentOpen, setCurrentOpen] = useState<number>(0);
15+
const pathname = usePathname();
16+
const { setOpenSearch } = useSearchContext();
17+
18+
const getDefaultValue = useCallback(() => {
19+
const defaultValue = contents.findIndex((item) =>
20+
item.list.some((listItem) => listItem.href === pathname)
21+
);
22+
return defaultValue === -1 ? 0 : defaultValue;
23+
}, [pathname]);
24+
25+
useEffect(() => {
26+
setCurrentOpen(getDefaultValue());
27+
}, [getDefaultValue]);
28+
29+
const handleSearch = () => {
30+
setOpenSearch(true);
31+
};
32+
33+
return (
34+
<div className="fixed top-16 left-0 z-30 h-[calc(100vh-4rem)]">
35+
<aside className="flex h-full w-[268px] flex-col overflow-y-auto border-border border-t border-r bg-background lg:w-[286px]">
36+
<div className="flex h-full flex-col">
37+
<button
38+
className="flex w-full items-center justify-start gap-3 border-border border-b px-5 py-3 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
39+
onClick={handleSearch}
40+
type="button"
41+
>
42+
<MagnifyingGlassIcon
43+
className="size-4 flex-shrink-0"
44+
weight="duotone"
45+
/>
46+
<span className="text-sm">Search documentation...</span>
47+
</button>
48+
49+
<MotionConfig
50+
transition={{ duration: 0.4, type: 'spring', bounce: 0 }}
51+
>
52+
<div className="flex flex-col">
53+
{contents.map((item, index) => (
54+
<div key={item.title}>
55+
<button
56+
className="flex w-full items-center gap-3 border-border border-b px-5 py-2.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted/50"
57+
onClick={() => {
58+
if (currentOpen === index) {
59+
setCurrentOpen(-1);
60+
} else {
61+
setCurrentOpen(index);
62+
}
63+
}}
64+
type="button"
65+
>
66+
<item.Icon
67+
className="size-5 flex-shrink-0 text-muted-foreground"
68+
weight="duotone"
69+
/>
70+
<span className="flex-1 text-sm">{item.title}</span>
71+
{item.isNew && <NewBadge />}
72+
<motion.div
73+
animate={{ rotate: currentOpen === index ? 180 : 0 }}
74+
className="flex-shrink-0"
75+
>
76+
<CaretDownIcon
77+
className="h-4 w-4 text-muted-foreground"
78+
weight="duotone"
79+
/>
80+
</motion.div>
81+
</button>
82+
<AnimatePresence initial={false}>
83+
{currentOpen === index && (
84+
<motion.div
85+
animate={{ opacity: 1, height: 'auto' }}
86+
className="relative overflow-hidden"
87+
exit={{ opacity: 0, height: 0 }}
88+
initial={{ opacity: 0, height: 0 }}
89+
>
90+
<motion.div className="text-sm">
91+
{item.list.map((listItem) => (
92+
<div key={listItem.title}>
93+
<Suspense fallback={<>Loading...</>}>
94+
{listItem.group ? (
95+
<div className="mx-5 my-1 flex flex-row items-center gap-2">
96+
<p className="bg-gradient-to-tr from-gray-900 to-stone-900 bg-clip-text text-sm text-transparent dark:from-gray-100 dark:to-stone-200">
97+
{listItem.title}
98+
</p>
99+
<div className="h-px flex-grow bg-gradient-to-r from-stone-800/90 to-stone-800/60" />
100+
</div>
101+
) : (
102+
<AsideLink
103+
activeClassName="!bg-muted !text-foreground font-medium"
104+
className="flex items-center gap-3 px-6 py-2 text-muted-foreground text-sm transition-colors hover:bg-muted/50 hover:text-foreground"
105+
href={listItem.href || '#'}
106+
startWith="/docs"
107+
title={listItem.title}
108+
>
109+
<listItem.icon
110+
className="size-5 flex-shrink-0"
111+
weight="duotone"
112+
/>
113+
<span className="flex-1">
114+
{listItem.title}
115+
</span>
116+
{listItem.isNew && <NewBadge />}
117+
</AsideLink>
118+
)}
119+
</Suspense>
120+
</div>
121+
))}
122+
</motion.div>
123+
</motion.div>
124+
)}
125+
</AnimatePresence>
126+
</div>
127+
))}
128+
</div>
129+
</MotionConfig>
130+
</div>
131+
</aside>
132+
</div>
133+
);
134+
}
135+
136+
function NewBadge({ isSelected }: { isSelected?: boolean }) {
137+
return (
138+
<div className="flex w-full items-center justify-end">
139+
<Badge
140+
className={cn(
141+
' !no-underline !decoration-transparent pointer-events-none border-dashed',
142+
isSelected && '!border-solid'
143+
)}
144+
variant={isSelected ? 'default' : 'outline'}
145+
>
146+
New
147+
</Badge>
148+
</div>
149+
);
150+
}

0 commit comments

Comments
 (0)