feat: add loading skeleton for article content using Suspense
This commit is contained in:
@@ -2,6 +2,7 @@ import { getArticleBySlug } from '@/lib/feature/article/article.external';
|
|||||||
import { ArrowLeftIcon, CalendarIcon, ClockIcon } from 'lucide-react';
|
import { ArrowLeftIcon, CalendarIcon, ClockIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
@@ -22,8 +23,48 @@ function formatDate(date: Date): string {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticlePage = async ({ params }: ArticlePageProps) => {
|
const ArticleContentSkeleton = () => (
|
||||||
const { slug } = await params;
|
<article className='w-full'>
|
||||||
|
<div className='h-64 w-full animate-pulse bg-muted md:h-96' />
|
||||||
|
|
||||||
|
<div className='container mx-auto max-w-3xl px-4'>
|
||||||
|
<div className='-mt-16 relative z-10'>
|
||||||
|
<div className='mb-6 h-4 w-24 animate-pulse rounded bg-muted' />
|
||||||
|
|
||||||
|
<header className='mb-10'>
|
||||||
|
<div className='mb-4 space-y-2'>
|
||||||
|
<div className='h-8 w-full animate-pulse rounded bg-muted' />
|
||||||
|
<div className='h-8 w-3/4 animate-pulse rounded bg-muted' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-5 space-y-2'>
|
||||||
|
<div className='h-5 w-full animate-pulse rounded bg-muted' />
|
||||||
|
<div className='h-5 w-2/3 animate-pulse rounded bg-muted' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<div className='h-3 w-28 animate-pulse rounded bg-muted' />
|
||||||
|
<div className='h-3 w-16 animate-pulse rounded bg-muted' />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<hr className='mb-10 border-border' />
|
||||||
|
|
||||||
|
<div className='space-y-3 pb-16'>
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className='space-y-2'>
|
||||||
|
<div className='h-4 w-full animate-pulse rounded bg-muted' />
|
||||||
|
<div className='h-4 w-full animate-pulse rounded bg-muted' />
|
||||||
|
<div className='h-4 w-5/6 animate-pulse rounded bg-muted' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArticleContent = async ({ slug }: { slug: string }) => {
|
||||||
const article = await getArticleBySlug(slug);
|
const article = await getArticleBySlug(slug);
|
||||||
|
|
||||||
if (!article) notFound();
|
if (!article) notFound();
|
||||||
@@ -96,4 +137,14 @@ const ArticlePage = async ({ params }: ArticlePageProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ArticlePage = async ({ params }: ArticlePageProps) => {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ArticleContentSkeleton />}>
|
||||||
|
<ArticleContent slug={slug} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ArticlePage;
|
export default ArticlePage;
|
||||||
|
|||||||
Reference in New Issue
Block a user