feat(mdx): implement MdxPreview component with error handling and loading state

This commit is contained in:
cfngc4594 2025-02-21 00:08:21 +08:00
parent 11fa68c4b7
commit 17894b6e96

View File

@ -1,36 +1,105 @@
import { Suspense } from "react"; "use client";
import remarkGfm from "remark-gfm";
import { compileMDX } from "next-mdx-remote/rsc";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
interface ProblemDescriptionProps { import remarkGfm from "remark-gfm";
mdxSource: string; import { useTheme } from "next-themes";
import rehypePretty from "rehype-pretty-code";
import { Skeleton } from "@/components/ui/skeleton";
import { serialize } from "next-mdx-remote/serialize";
import { useCallback, useEffect, useState } from "react";
import { CircleAlert, TriangleAlert } from "lucide-react";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
interface MdxPreviewProps {
source: string;
} }
export async function ProblemDescription({ mdxSource }: ProblemDescriptionProps) { export default function MdxPreview({ source }: MdxPreviewProps) {
try { const { resolvedTheme } = useTheme();
const { content } = await compileMDX({ const [error, setError] = useState<string | null>(null);
source: mdxSource, const [isLoading, setIsLoading] = useState<boolean>(true);
options: { const [mdxSource, setMdxSource] = useState<MDXRemoteSerializeResult | null>(null);
const components = {
// Define your custom components here
// For example:
// Test: ({ name }: { name: string }) => <p>Test Component: {name}</p>,
};
const getMdxSource = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const mdxSource = await serialize(source, {
mdxOptions: { mdxOptions: {
rehypePlugins: [
[
rehypePretty,
{
theme: resolvedTheme === "light" ? "github-light-default" : "github-dark-default",
keepBackground: false,
},
],
],
remarkPlugins: [remarkGfm], remarkPlugins: [remarkGfm],
}, },
}, });
}); setMdxSource(mdxSource);
} catch (error) {
console.error("Failed to serialize Mdx:", error);
setError("Failed to load mdx content.");
} finally {
setIsLoading(false);
}
}, [source, resolvedTheme]);
return ( // Delay the serialize process to the next event loop to avoid flickering
<ScrollArea className="[&>[data-radix-scroll-area-viewport]]:max-h-[calc(100vh-56px)]"> // when copying code to the editor and the MDX preview shrinks.
<Suspense fallback={<Skeleton className="h-full w-full" />}> useEffect(() => {
<div className="markdown-body"> const timeoutId = setTimeout(() => {
{content} getMdxSource(); // Execute serializeMdx in the next event loop
</div> }, 0);
</Suspense>
<ScrollBar orientation="horizontal" /> return () => clearTimeout(timeoutId); // Cleanup timeout on component unmount
</ScrollArea> }, [getMdxSource]);
);
} catch (error) { if (isLoading) {
console.error("Error compiling MDX:", error); return <Skeleton className="h-full w-full rounded-xl" />;
return <Skeleton className="h-full w-full" />;
} }
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="rounded-lg border border-red-500/50 px-4 py-3 text-red-600">
<p className="text-sm">
<CircleAlert className="-mt-0.5 me-3 inline-flex opacity-60" size={16} strokeWidth={2} aria-hidden="true" />
{error}
</p>
</div>
</div>
);
}
if (!source) {
return (
<div className="h-full flex items-center justify-center">
<div className="rounded-lg border border-amber-500/50 px-4 py-3 text-amber-600">
<p className="text-sm">
<TriangleAlert className="-mt-0.5 me-3 inline-flex opacity-60" size={16} strokeWidth={2} aria-hidden="true" />
No content to preview.
</p>
</div>
</div>
);
}
return (
<ScrollArea className="[&>[data-radix-scroll-area-viewport]]:max-h-[calc(100vh-56px)]">
<div className="markdown-body">
<MDXRemote {...mdxSource!} components={components} />
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
);
} }