admin/components/paginated-list/PaginatedListCards.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

84 lines
2.7 KiB
TypeScript

'use client'
import type { ReactNode } from 'react'
import type { PaginationListColumnType } from '@/types'
import type { PaginatedListRow } from '@/components/paginated-list/paginatedListUtils'
import AdminState from '@/components/feedback/AdminState'
import { Skeleton } from '@/components/feedback/LoadingState'
interface Props {
columns: PaginationListColumnType[]
data: PaginatedListRow[]
isLoading: boolean
onRenderCell: (item: PaginatedListRow, columnKey: React.Key) => ReactNode
}
const MobileCardSkeleton = () => (
<div className="rounded-2xl border border-secondary-40 bg-white p-4">
<Skeleton className="h-4 w-2/5" />
<div className="mt-4 flex flex-col gap-3">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-4/5" />
<Skeleton className="h-3 w-3/5" />
</div>
</div>
)
export default function PaginatedListCards({ columns, data, isLoading, onRenderCell }: Props) {
if (isLoading && data.length === 0) {
return (
<div
aria-busy="true"
aria-label="در حال دریافت فهرست"
className="grid gap-3 p-3 md:hidden"
role="status"
>
{Array.from({ length: 5 }).map((_, index) => (
<MobileCardSkeleton key={index} />
))}
</div>
)
}
if (data.length === 0) {
return (
<AdminState
className="md:hidden"
title="اطلاعاتی برای نمایش وجود ندارد"
/>
)
}
const detailColumns = columns.filter((column) => column.field !== 'actions')
const actionsColumn = columns.find((column) => column.field === 'actions')
return (
<div className="grid gap-3 p-3 md:hidden">
{data.map((item, index) => (
<article
key={item.id ?? `mobile-row-${index}`}
className="rounded-2xl border border-secondary-40 bg-white p-4 shadow-sm"
>
<dl className="divide-y divide-secondary-40">
{detailColumns.map((column) => (
<div
key={column.field}
className="grid grid-cols-[minmax(5.5rem,0.7fr)_minmax(0,1.3fr)] items-start gap-3 py-3 first:pt-0 last:pb-0"
>
<dt className="text-xs font-medium leading-6 text-secondary-30">{column.label}</dt>
<dd className="min-w-0 break-words text-xs leading-6 text-secondary-20">{onRenderCell(item, column.field)}</dd>
</div>
))}
</dl>
{actionsColumn ? (
<div className="mt-3 flex flex-wrap justify-start gap-1 border-t border-secondary-40 pt-3">
{onRenderCell(item, actionsColumn.field)}
</div>
) : null}
</article>
))}
</div>
)
}