Skip to content

Commit e9ba4c7

Browse files
committed
Add documentation components and GitHub integration
- Introduced several components to enhance documentation: SearchBar, GitHubConnectModal, MobileHeader, DocsContent, and DocsSidebar. - Implemented functions to fetch and parse GitHub repository content, handle user authentication, and display Markdown files with syntax highlighting. Took 21 seconds
1 parent 60aeee9 commit e9ba4c7

File tree

10 files changed

+896
-11
lines changed

10 files changed

+896
-11
lines changed

app/docs/[org]/[repo]/actions.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
'use server'
2+
3+
import { getServerSession } from 'next-auth/next'
4+
import { authOptions } from '@/lib/auth'
5+
import { Octokit } from '@octokit/rest'
6+
import { getGithubAccessToken } from '@/lib/getOauthAccessToken'
7+
8+
export type RepoContent = {
9+
path: string
10+
type: 'file' | 'dir'
11+
name: string
12+
content?: string
13+
children?: RepoContent[]
14+
}
15+
16+
export async function getGithubContent(org: string, repo: string, path: string) {
17+
try {
18+
const session = await getServerSession(authOptions)
19+
if (!session?.user?.id) {
20+
throw new Error('Unauthorized')
21+
}
22+
23+
const githubToken = await getGithubAccessToken(session.user.id)
24+
if (!githubToken) {
25+
throw new Error('No GitHub token available')
26+
}
27+
28+
console.log(`Fetching content for ${org}/${repo}/${path}...`)
29+
const octokit = new Octokit({
30+
auth: githubToken
31+
})
32+
33+
const response = await octokit.repos.getContent({
34+
owner: org,
35+
repo,
36+
path,
37+
}).catch(error => {
38+
console.error(`GitHub API error for ${path}:`, error.message)
39+
throw new Error(`Failed to fetch ${path}: ${error.message}`)
40+
})
41+
42+
if ('content' in response.data && typeof response.data.content === 'string') {
43+
return Buffer.from(response.data.content, 'base64').toString()
44+
} else {
45+
console.error(`Invalid content format for ${path}:`, response.data)
46+
throw new Error(`Invalid content format for ${path}`)
47+
}
48+
} catch (error) {
49+
console.error('GitHub content error:', error)
50+
throw error
51+
}
52+
}
53+
54+
export async function getRepoContents(org: string, repo: string, path: string = ''): Promise<RepoContent[]> {
55+
try {
56+
const session = await getServerSession(authOptions)
57+
if (!session?.user?.id) {
58+
throw new Error('Unauthorized')
59+
}
60+
61+
const githubToken = await getGithubAccessToken(session.user.id)
62+
if (!githubToken) {
63+
throw new Error('No GitHub token available')
64+
}
65+
66+
const octokit = new Octokit({
67+
auth: githubToken
68+
})
69+
70+
const response = await octokit.repos.getContent({
71+
owner: org,
72+
repo,
73+
path,
74+
})
75+
76+
const contents = Array.isArray(response.data) ? response.data : [response.data]
77+
78+
const processedContents: RepoContent[] = await Promise.all(
79+
contents.map(async (item: any) => {
80+
const result: RepoContent = {
81+
path: item.path,
82+
type: item.type,
83+
name: item.name,
84+
}
85+
86+
if (item.type === 'dir') {
87+
result.children = await getRepoContents(org, repo, item.path)
88+
}
89+
90+
return result
91+
})
92+
)
93+
94+
return processedContents
95+
} catch (error) {
96+
console.error('Error fetching repo contents:', error)
97+
return []
98+
}
99+
}
100+
101+
export async function checkGithubAccess() {
102+
const session = await getServerSession(authOptions)
103+
if (!session?.user?.id) {
104+
return { hasAccess: false, error: 'No session' }
105+
}
106+
107+
try {
108+
const githubToken = await getGithubAccessToken(session.user.id)
109+
return {
110+
hasAccess: !!githubToken,
111+
error: githubToken ? null : 'No GitHub token available'
112+
}
113+
} catch (error) {
114+
return {
115+
hasAccess: false,
116+
error: error instanceof Error ? error.message : 'Failed to check GitHub access'
117+
}
118+
}
119+
}
120+
121+
export async function getImageMetadata(org: string, repo: string, path: string) {
122+
try {
123+
const session = await getServerSession(authOptions)
124+
if (!session?.user?.id) {
125+
throw new Error('Unauthorized')
126+
}
127+
128+
const githubToken = await getGithubAccessToken(session.user.id)
129+
if (!githubToken) {
130+
throw new Error('No GitHub token available')
131+
}
132+
133+
const octokit = new Octokit({
134+
auth: githubToken
135+
})
136+
137+
const response = await octokit.repos.getContent({
138+
owner: org,
139+
repo,
140+
path,
141+
})
142+
143+
if ('content' in response.data && typeof response.data.content === 'string') {
144+
return {
145+
url: response.data.download_url,
146+
size: response.data.size,
147+
type: response.data.type,
148+
}
149+
}
150+
151+
throw new Error('Invalid image metadata')
152+
} catch (error) {
153+
console.error('GitHub image metadata error:', error)
154+
throw error
155+
}
156+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use client'
2+
3+
import { useSearchParams } from 'next/navigation'
4+
import { useEffect, useState } from 'react'
5+
import ReactMarkdown from 'react-markdown'
6+
import remarkGfm from 'remark-gfm'
7+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
9+
import { getGithubContent } from '../actions'
10+
import { resolveGitbookPath } from '../lib/resolveGitbookPath'
11+
import matter from 'gray-matter'
12+
import { Github } from 'lucide-react'
13+
import { Button } from '@/components/ui/button'
14+
15+
interface DocsContentProps {
16+
org: string
17+
repo: string
18+
}
19+
20+
function DocHeader({ title, description }: { title: string, description?: string }) {
21+
return (
22+
<div className="mb-8 border-b border-border pb-8">
23+
<h1 className="text-4xl font-bold mb-4">{title}</h1>
24+
{description && (
25+
<p className="text-lg text-muted-foreground">{description}</p>
26+
)}
27+
</div>
28+
)
29+
}
30+
31+
function EditOnGithubButton({ org, repo, path }: { org: string, repo: string, path: string }) {
32+
const githubUrl = `https://github.com/${org}/${repo}/edit/main/${path}`
33+
34+
return (
35+
<Button
36+
variant="outline"
37+
size="sm"
38+
className="flex items-center gap-2 mb-8"
39+
onClick={() => window.open(githubUrl, '_blank')}
40+
>
41+
<Github className="h-4 w-4" />
42+
Edit on GitHub
43+
</Button>
44+
)
45+
}
46+
47+
export default function DocsContent({ org, repo }: DocsContentProps) {
48+
const searchParams = useSearchParams()
49+
const filePath = searchParams.get('file')
50+
const [content, setContent] = useState('')
51+
const [frontmatter, setFrontmatter] = useState<{
52+
title?: string
53+
description?: string
54+
}>({})
55+
const [isLoading, setIsLoading] = useState(false)
56+
const [error, setError] = useState<string | null>(null)
57+
58+
useEffect(() => {
59+
async function fetchContent() {
60+
if (!filePath) return
61+
62+
setIsLoading(true)
63+
setError(null)
64+
65+
try {
66+
const rawContent = await getGithubContent(org, repo, filePath)
67+
// Parse frontmatter
68+
const { data, content } = matter(rawContent)
69+
70+
// Extract title from first heading if not in frontmatter
71+
const titleMatch = content.match(/^#\s+(.+)$/m)
72+
const title = data.title || (titleMatch ? titleMatch[1] : '')
73+
74+
setFrontmatter({
75+
title,
76+
description: data.description,
77+
})
78+
79+
// Remove the first heading since we'll display it in the header
80+
const contentWithoutTitle = content.replace(/^#\s+.+$/m, '')
81+
setContent(contentWithoutTitle)
82+
} catch (err) {
83+
console.error('Error fetching content:', err)
84+
setError(err instanceof Error ? err.message : 'Failed to load content')
85+
} finally {
86+
setIsLoading(false)
87+
}
88+
}
89+
90+
fetchContent()
91+
}, [filePath, org, repo])
92+
93+
if (isLoading) {
94+
return (
95+
<div className="p-8 text-foreground">
96+
<div className="animate-pulse">Loading content...</div>
97+
</div>
98+
)
99+
}
100+
101+
if (error) {
102+
return (
103+
<div className="p-8 text-destructive">
104+
<h2 className="text-lg font-semibold">Error loading content</h2>
105+
<p>{error}</p>
106+
</div>
107+
)
108+
}
109+
110+
if (!filePath) {
111+
return (
112+
<div className="p-8 text-foreground">
113+
<h1 className="text-2xl font-bold">Welcome to the documentation</h1>
114+
<p className="mt-4">Select a file from the sidebar to get started.</p>
115+
</div>
116+
)
117+
}
118+
119+
return (
120+
<div className="p-8 max-w-3xl mx-auto">
121+
{filePath && (
122+
<EditOnGithubButton org={org} repo={repo} path={filePath} />
123+
)}
124+
{frontmatter.title && (
125+
<DocHeader
126+
title={frontmatter.title}
127+
description={frontmatter.description}
128+
/>
129+
)}
130+
<div className="prose prose-invert">
131+
<ReactMarkdown
132+
remarkPlugins={[remarkGfm]}
133+
components={{
134+
code({ node, inline, className, children, ...props }) {
135+
const match = /language-(\w+)/.exec(className || '')
136+
return !inline && match ? (
137+
<SyntaxHighlighter
138+
style={oneDark as any}
139+
language={match[1]}
140+
PreTag="div"
141+
{...props}
142+
>
143+
{String(children).replace(/\n$/, '')}
144+
</SyntaxHighlighter>
145+
) : (
146+
<code className={className} {...props}>
147+
{children}
148+
</code>
149+
)
150+
},
151+
img({ src, alt, ...props }) {
152+
if (!src) return null
153+
const resolvedPath = resolveGitbookPath(src, filePath)
154+
const rawUrl = `https://raw.githubusercontent.com/${org}/${repo}/main/${resolvedPath}`
155+
return (
156+
<img
157+
src={rawUrl}
158+
alt={alt || ''}
159+
className="rounded-lg shadow-md"
160+
loading="lazy"
161+
{...props}
162+
/>
163+
)
164+
}
165+
}}
166+
>
167+
{content}
168+
</ReactMarkdown>
169+
</div>
170+
</div>
171+
)
172+
}

0 commit comments

Comments
 (0)