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 = / l a n g u a g e - ( \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