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
Some checks failed
Build and Test / run-test (20.x) (push) Failing after 1m30s
This commit is contained in:
172
src/ui/components/internal/article/admin-article-list.tsx
Normal file
172
src/ui/components/internal/article/admin-article-list.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user