feat: add table components and admin article list with skeleton loading
Some checks failed
Build and Test / run-test (20.x) (push) Failing after 1m30s

This commit is contained in:
2026-04-13 20:51:20 -03:00
parent 79e6fae0f9
commit c938974d2b
14 changed files with 1054 additions and 34 deletions

View File

@@ -0,0 +1,172 @@
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
import { ArticleListPagination } from '@/ui/components/internal/article-list-pagination';
import { DeleteArticleButton } from '@/ui/components/internal/article/delete-article-button';
import { Button } from '@/ui/components/shadcn/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/ui/components/shadcn/table';
import { FileTextIcon, PencilIcon } from 'lucide-react';
import Link from 'next/link';
type AdminArticleListProps = {
searchParams: Promise<{ page?: string; pageSize?: string }>;
defaultPageSize: number;
};
const formatDate = (date: Date) =>
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(date);
export const AdminArticleList = async ({
searchParams,
defaultPageSize,
}: AdminArticleListProps) => {
const { page: pageParam, pageSize: pageSizeParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const pageSize = Number(pageSizeParam) || defaultPageSize;
const paginationResult = await getArticlesPaginated(page, pageSize);
if (!paginationResult.ok) {
return (
<div className='flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border text-center'>
<FileTextIcon className='size-10 text-muted-foreground/40' />
<p className='text-muted-foreground'>
Failed to load articles. Please try again later.
</p>
</div>
);
}
const {
data: articles,
totalPages,
total,
page: currentPage,
} = paginationResult.value;
if (articles.length === 0) {
return (
<div className='flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border text-center'>
<FileTextIcon className='size-10 text-muted-foreground/40' />
<p className='text-muted-foreground'>No articles yet.</p>
</div>
);
}
return (
<>
<p className='mb-4 text-sm text-muted-foreground'>
{total} article{total === 1 ? '' : 's'} published
</p>
<div className='rounded-lg border border-border'>
<Table>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className='w-16'>Cover</TableHead>
<TableHead className='w-56'>Title</TableHead>
<TableHead>Description</TableHead>
<TableHead className='w-32'>Published</TableHead>
<TableHead className='w-32 text-right'>
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{articles.map((article) => (
<TableRow key={article.externalId}>
<TableCell>
<div className='size-12 shrink-0 overflow-hidden rounded-md bg-muted'>
{article.coverImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={article.coverImageUrl}
alt=''
className='h-full w-full object-cover'
/>
) : (
<div className='flex h-full items-center justify-center'>
<span className='font-mono text-xs font-bold text-muted-foreground/30'>
{'{}'}
</span>
</div>
)}
</div>
</TableCell>
<TableCell className='whitespace-normal'>
<Link
href={`/article/${article.slug}`}
className='font-medium leading-snug hover:text-primary hover:underline underline-offset-4'
>
{article.title}
</Link>
<p className='mt-0.5 text-xs text-muted-foreground'>
/{article.slug}
</p>
</TableCell>
<TableCell className='whitespace-normal'>
<p className='line-clamp-2 text-sm text-muted-foreground'>
{article.description}
</p>
</TableCell>
<TableCell className='text-sm text-muted-foreground'>
<time
dateTime={article.createdAt.toISOString()}
>
{formatDate(article.createdAt)}
</time>
</TableCell>
<TableCell>
<div className='flex items-center justify-end gap-2'>
<Button
asChild
variant='outline'
size='sm'
>
<Link
href={`/admin/article/${article.externalId}`}
>
<PencilIcon className='size-3.5' />
Edit
</Link>
</Button>
<DeleteArticleButton
articleId={article.id}
articleTitle={article.title}
/>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{totalPages > 1 && (
<div className='mt-6 flex items-center justify-between gap-4'>
<p className='text-sm text-muted-foreground'>
Page {currentPage} of {totalPages}
</p>
<ArticleListPagination
currentPage={currentPage}
totalPages={totalPages}
baseUrl='/admin'
/>
</div>
)}
</>
);
};