✨ feat(tutorial): Add common tutorial list
This commit is contained in:
parent
24b86010e6
commit
872252c98c
153
apps/user/app/(main)/(user)/document/document-button.tsx
Normal file
153
apps/user/app/(main)/(user)/document/document-button.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { queryDocumentDetail } from '@/services/user/document';
|
||||
import { Markdown } from '@repo/ui/markdown';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import { useOutsideClick } from '@shadcn/ui/hooks/use-outside-click';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
interface Item {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
export function DocumentButton({ items }: { items: Item[] }) {
|
||||
const [active, setActive] = useState<Item | boolean | null>(null);
|
||||
const id = useId();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data } = useQuery({
|
||||
enabled: !!(active as Item)?.id,
|
||||
queryKey: ['queryDocumentDetail', (active as Item)?.id],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryDocumentDetail({
|
||||
id: (active as Item)?.id,
|
||||
});
|
||||
return data.data?.content;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (active && typeof active === 'object') {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [active]);
|
||||
|
||||
useOutsideClick(ref, () => setActive(null));
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{active && typeof active === 'object' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className='fixed inset-0 z-10 h-full w-full bg-black/20'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{active && typeof active === 'object' ? (
|
||||
<div className='fixed inset-0 z-[100] grid place-items-center'>
|
||||
<motion.button
|
||||
key={`button-${active.title}-${id}`}
|
||||
layout
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.05,
|
||||
},
|
||||
}}
|
||||
className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full'
|
||||
onClick={() => setActive(null)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</motion.button>
|
||||
<motion.div
|
||||
layoutId={`card-${active.id}-${id}`}
|
||||
ref={ref}
|
||||
className='bg-muted flex size-full flex-col overflow-auto p-6 sm:rounded'
|
||||
>
|
||||
<Markdown
|
||||
components={{
|
||||
img: ({ node, className, ...props }) => {
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img {...props} width={800} height={600} className='my-4 h-auto' />
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{data || ''}
|
||||
</Markdown>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<ul className='flex w-full flex-wrap items-start gap-4'>
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
layoutId={`card-${item.id}-${id}`}
|
||||
key={item.id}
|
||||
onClick={() => setActive(item)}
|
||||
className='flex cursor-pointer flex-col rounded-xl'
|
||||
>
|
||||
<Button variant='secondary'>{item.title}</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const CloseIcon = () => {
|
||||
return (
|
||||
<motion.svg
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.05,
|
||||
},
|
||||
}}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className='h-4 w-4 text-black'
|
||||
>
|
||||
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M18 6l-12 12' />
|
||||
<path d='M6 6l12 12' />
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
@ -1,110 +1,92 @@
|
||||
'use client';
|
||||
|
||||
import { ProList } from '@/components/pro-list';
|
||||
import { queryDocumentDetail, queryDocumentList } from '@/services/user/document';
|
||||
import { Markdown } from '@repo/ui/markdown';
|
||||
import { formatDate } from '@repo/ui/utils';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@shadcn/ui/card';
|
||||
import { queryDocumentList } from '@/services/user/document';
|
||||
import { getTutorialList } from '@/utils/tutorial';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { DocumentButton } from './document-button';
|
||||
import { TutorialButton } from './tutorial-button';
|
||||
|
||||
export default function Page() {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('document');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [selected, setSelected] = useState<number>();
|
||||
|
||||
const { data } = useQuery({
|
||||
enabled: !!selected,
|
||||
queryKey: ['queryDocumentDetail', selected],
|
||||
queryKey: ['queryDocumentList'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryDocumentDetail({
|
||||
id: selected!,
|
||||
});
|
||||
return data.data;
|
||||
const response = await queryDocumentList();
|
||||
const list = response.data.data?.list || [];
|
||||
return {
|
||||
tags: Array.from(new Set(list.reduce((acc: string[], item) => acc.concat(item.tags), []))),
|
||||
list,
|
||||
};
|
||||
},
|
||||
});
|
||||
const { tags, list: DocumentList } = data || { tags: [], list: [] };
|
||||
|
||||
const { data: TutorialList } = useQuery({
|
||||
queryKey: ['getTutorialList', locale],
|
||||
queryFn: async () => {
|
||||
const list = await getTutorialList();
|
||||
return list.get(locale);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{selected ? (
|
||||
<Card>
|
||||
<CardHeader className='pb-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Button variant='outline' onClick={() => setSelected(undefined)}>
|
||||
<ChevronLeft className='size-4' />
|
||||
{t('back')}
|
||||
</Button>
|
||||
<CardTitle className='font-medium'>{data?.title}</CardTitle>
|
||||
<CardDescription>{formatDate(data?.updated_at)}</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown>{data?.content || ''}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<ProList<API.DocumentItem, { tag: string }>
|
||||
params={[
|
||||
{
|
||||
key: 'tag',
|
||||
placeholder: t('category'),
|
||||
options: tags.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
request={async (_, filter) => {
|
||||
const response = await queryDocumentList();
|
||||
const list = response.data.data?.list || [];
|
||||
setTags(
|
||||
Array.from(new Set(list.reduce((acc: string[], item) => acc.concat(item.tags), []))),
|
||||
);
|
||||
const filterList = list.filter((item) =>
|
||||
filter.tag ? item.tags.includes(filter.tag) : true,
|
||||
);
|
||||
return {
|
||||
list: filterList,
|
||||
total: filterList.length || 0,
|
||||
};
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-center justify-between gap-2 space-y-0 p-3'>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setSelected(item.id);
|
||||
}}
|
||||
>
|
||||
{t('read')}
|
||||
</Button>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className='p-3 text-sm'>
|
||||
<ul className='grid gap-3 *:flex *:flex-col lg:grid-cols-2'>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('tags')}</span>
|
||||
<span>{item.tags.join(', ')}</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('updatedAt')}</span>
|
||||
<time>{formatDate(item.updated_at)}</time>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className='space-y-4'>
|
||||
{DocumentList?.length > 0 && (
|
||||
<>
|
||||
<h2 className='flex items-center gap-1.5 font-semibold'>{t('document')}</h2>
|
||||
<Tabs defaultValue='all'>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
<TabsTrigger value='all'>{t('all')}</TabsTrigger>
|
||||
{tags?.map((item) => (
|
||||
<TabsTrigger key={item} value={item}>
|
||||
{item}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value='all'>
|
||||
<DocumentButton items={DocumentList} />
|
||||
</TabsContent>
|
||||
{tags?.map((item) => (
|
||||
<TabsContent value={item} key={item}>
|
||||
<DocumentButton
|
||||
items={DocumentList.filter((docs) => (item ? docs.tags.includes(item) : true))}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
{TutorialList && TutorialList?.length > 0 && (
|
||||
<>
|
||||
<h2 className='flex items-center gap-1.5 font-semibold'>{t('tutorial')}</h2>
|
||||
<Tabs defaultValue={TutorialList?.[0]?.title}>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
{TutorialList?.map((tutorial) => (
|
||||
<TabsTrigger key={tutorial.title} value={tutorial.title}>
|
||||
{tutorial.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{TutorialList?.map((tutorial) => (
|
||||
<TabsContent key={tutorial.title} value={tutorial.title}>
|
||||
<TutorialButton
|
||||
key={tutorial.path}
|
||||
items={
|
||||
tutorial.subItems && tutorial.subItems?.length > 0
|
||||
? tutorial.subItems
|
||||
: [tutorial]
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
151
apps/user/app/(main)/(user)/document/tutorial-button.tsx
Normal file
151
apps/user/app/(main)/(user)/document/tutorial-button.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import { getTutorial } from '@/utils/tutorial';
|
||||
import { Markdown } from '@repo/ui/markdown';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import { useOutsideClick } from '@shadcn/ui/hooks/use-outside-click';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
interface Item {
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
export function TutorialButton({ items }: { items: Item[] }) {
|
||||
const [active, setActive] = useState<Item | boolean | null>(null);
|
||||
const id = useId();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data } = useQuery({
|
||||
enabled: !!(active as Item)?.path,
|
||||
queryKey: ['getTutorial', (active as Item)?.path],
|
||||
queryFn: async () => {
|
||||
const markdown = await getTutorial((active as Item)?.path);
|
||||
return markdown;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (active && typeof active === 'object') {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [active]);
|
||||
|
||||
useOutsideClick(ref, () => setActive(null));
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{active && typeof active === 'object' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className='fixed inset-0 z-10 h-full w-full bg-black/20'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{active && typeof active === 'object' ? (
|
||||
<div className='fixed inset-0 z-[100] grid place-items-center'>
|
||||
<motion.button
|
||||
key={`button-${active.title}-${id}`}
|
||||
layout
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.05,
|
||||
},
|
||||
}}
|
||||
className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full'
|
||||
onClick={() => setActive(null)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</motion.button>
|
||||
<motion.div
|
||||
layoutId={`card-${active.title}-${id}`}
|
||||
ref={ref}
|
||||
className='bg-muted flex size-full flex-col overflow-auto p-6 sm:rounded'
|
||||
>
|
||||
<Markdown
|
||||
components={{
|
||||
img: ({ node, className, ...props }) => {
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img {...props} width={800} height={600} className='my-4 h-auto' />
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{data || ''}
|
||||
</Markdown>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<ul className='flex w-full flex-wrap items-start gap-4'>
|
||||
{items.map((item) => (
|
||||
<motion.div
|
||||
layoutId={`card-${item.title}-${id}`}
|
||||
key={item.title}
|
||||
onClick={() => setActive(item)}
|
||||
className='flex cursor-pointer flex-col rounded-xl'
|
||||
>
|
||||
<Button variant='secondary'>{item.title}</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const CloseIcon = () => {
|
||||
return (
|
||||
<motion.svg
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.05,
|
||||
},
|
||||
}}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className='h-4 w-4 text-black'
|
||||
>
|
||||
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M18 6l-12 12' />
|
||||
<path d='M6 6l12 12' />
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
@ -6,11 +6,11 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
return (
|
||||
<SidebarProvider className='container'>
|
||||
<SidebarLeft className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent lg:flex' />
|
||||
<SidebarInset className='relative flex-grow overflow-hidden'>
|
||||
<SidebarInset className='relative p-4'>
|
||||
{/* <Header /> */}
|
||||
<div className='overflow-auto p-4'>{children}</div>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
<SidebarRight className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent lg:flex' />
|
||||
<SidebarRight className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent 2xl:flex' />
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
{
|
||||
"back": "返回",
|
||||
"category": "分类",
|
||||
"read": "阅读",
|
||||
"tags": "分类",
|
||||
"title": "标题",
|
||||
"updatedAt": "更新时间"
|
||||
"all": "全部",
|
||||
"document": "文档",
|
||||
"tutorial": "教程"
|
||||
}
|
||||
|
||||
80
apps/user/utils/tutorial.ts
Normal file
80
apps/user/utils/tutorial.ts
Normal file
@ -0,0 +1,80 @@
|
||||
const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-tutorial@main';
|
||||
|
||||
export async function getTutorial(path: string): Promise<string> {
|
||||
try {
|
||||
const url = `${BASE_URL}/${path}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
const markdown = addPrefixToImageUrls(text, getUrlPrefix(url));
|
||||
return markdown;
|
||||
} catch (error) {
|
||||
console.error('Error fetching the markdown file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
type TutorialItem = {
|
||||
title: string;
|
||||
path: string;
|
||||
subItems?: TutorialItem[];
|
||||
};
|
||||
|
||||
export async function getTutorialList() {
|
||||
return await getTutorial('SUMMARY.md').then((markdown) => {
|
||||
const map = parseTutorialToMap(
|
||||
markdown.replace(/en-us/gi, 'en-US').replace(/zh-cn/gi, 'zh-CN'),
|
||||
);
|
||||
map.forEach((value, key) => {
|
||||
map.set(
|
||||
key,
|
||||
value.filter((item) => item.title !== 'README'),
|
||||
);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
function parseTutorialToMap(markdown: string): Map<string, TutorialItem[]> {
|
||||
const map = new Map<string, TutorialItem[]>();
|
||||
let currentSection = '';
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('## ')) {
|
||||
currentSection = line.replace('## ', '').trim();
|
||||
map.set(currentSection, []);
|
||||
} else if (line.startsWith('* ')) {
|
||||
const [, text, link] = line.match(/\* \[(.*?)\]\((.*?)\)/) || [];
|
||||
if (text && link) {
|
||||
if (!map.has(currentSection)) {
|
||||
map.set(currentSection, []);
|
||||
}
|
||||
map.get(currentSection)!.push({ title: text, path: link });
|
||||
}
|
||||
} else if (line.startsWith(' * ')) {
|
||||
const [, text, link] = line.match(/\* \[(.*?)\]\((.*?)\)/) || [];
|
||||
if (text && link) {
|
||||
const lastItem = map.get(currentSection)?.slice(-1)[0];
|
||||
if (lastItem) {
|
||||
if (!lastItem.subItems) {
|
||||
lastItem.subItems = [];
|
||||
}
|
||||
lastItem.subItems.push({ title: text, path: link });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
function getUrlPrefix(url: string): string {
|
||||
return url.replace(/\/[^/]+\.md$/, '/');
|
||||
}
|
||||
function addPrefixToImageUrls(markdown: string, prefix: string): string {
|
||||
return markdown.replace(/!\[(.*?)\]\((.*?)\)/g, (match, imgAlt, imgUrl) => {
|
||||
return ` ? imgUrl : `${prefix}${imgUrl}`})`;
|
||||
});
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { cn } from '@shadcn/ui/lib/utils';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ReactMarkdown, { Components } from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
@ -20,7 +20,7 @@ interface CodeBlockProps {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function CodeBlock({ className, children, dark, ...props }: CodeBlockProps) {
|
||||
function CodeBlock({ className, children, ...props }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const match = className?.startsWith('language-') ? /language-(\w+)/.exec(className) : null;
|
||||
|
||||
@ -77,10 +77,10 @@ function CodeBlock({ className, children, dark, ...props }: CodeBlockProps) {
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
dark?: false;
|
||||
components?: Components;
|
||||
}
|
||||
|
||||
export function Markdown({ children, dark }: MarkdownProps) {
|
||||
export function Markdown({ children, components }: MarkdownProps) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className='prose dark:prose-invert w-full max-w-[unset] break-words'
|
||||
@ -197,8 +197,9 @@ export function Markdown({ children, dark }: MarkdownProps) {
|
||||
<pre className={cn('overflow-x-auto rounded-b-lg p-0', className)} {...props} />
|
||||
),
|
||||
code(props) {
|
||||
return <CodeBlock {...(props as CodeBlockProps)} dark={dark} />;
|
||||
return <CodeBlock {...(props as CodeBlockProps)} />;
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user