Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
84 lines
2.7 KiB
TypeScript
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>
|
|
)
|
|
}
|