admin/components/feedback/LoadingState.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

173 lines
5.7 KiB
TypeScript

'use client'
import { Skeleton as HeroSkeleton } from '@heroui/react'
import clsx from 'clsx'
import { texts } from '@/texts'
interface SkeletonProps {
className?: string
}
export const Skeleton = ({ className }: SkeletonProps) => (
<HeroSkeleton
aria-hidden
className={clsx('block rounded-consumer-control bg-consumer-surface-muted motion-reduce:animate-none', className)}
/>
)
export const ListSkeleton = ({ count = 4, className }: { count?: number; className?: string }) => (
<div
aria-busy="true"
aria-label={texts.common.loadingList}
className={clsx('flex flex-col gap-3', className ?? 'p-4')}
role="status"
>
{Array.from({ length: count }).map((_, index) => (
<div
key={index}
className="flex items-center gap-4 rounded-consumer-card border border-consumer-border bg-consumer-surface p-4"
>
<Skeleton className="size-12 shrink-0 rounded-2xl" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-2/5" />
<Skeleton className="h-3 w-3/4" />
</div>
<Skeleton className="h-6 w-16 rounded-full" />
</div>
))}
</div>
)
export const DetailSkeleton = () => (
<div
aria-busy="true"
aria-label={texts.common.loadingDetails}
className="flex flex-col gap-4 p-4"
role="status"
>
<Skeleton className="aspect-[3/4] w-full rounded-consumer-modal" />
<div className="rounded-consumer-modal bg-consumer-surface p-4">
<Skeleton className="h-7 w-1/2" />
<Skeleton className="mt-4 h-4 w-full" />
<Skeleton className="mt-2 h-4 w-4/5" />
<div className="mt-6 grid grid-cols-2 gap-3">
<Skeleton className="h-20" />
<Skeleton className="h-20" />
</div>
</div>
</div>
)
const TABLE_SKELETON_CELL_WIDTHS = ['w-28', 'w-40', 'w-24', 'w-32', 'w-20', 'w-36'] as const
interface TableSkeletonProps {
columnCount?: number
rows?: number
showHeader?: boolean
}
export const TableSkeleton = ({ columnCount = 4, rows = 7, showHeader = true }: TableSkeletonProps) => {
const cellCount = Math.max(1, columnCount)
return (
<div
aria-busy="true"
aria-label={texts.common.loadingTable}
className="w-full overflow-hidden bg-surface"
role="status"
>
{showHeader ? (
<div className="flex gap-6 border-b border-border bg-surface-secondary p-4">
{Array.from({ length: cellCount }).map((_, index) => (
<Skeleton
key={index}
className={`h-4 ${TABLE_SKELETON_CELL_WIDTHS[index % TABLE_SKELETON_CELL_WIDTHS.length]}`}
/>
))}
</div>
) : null}
{Array.from({ length: rows }).map((_, rowIndex) => (
<div
key={rowIndex}
className="flex items-center gap-6 border-b border-separator px-4 py-3 last:border-0"
>
{Array.from({ length: cellCount }).map((_, cellIndex) => (
<Skeleton
key={cellIndex}
className="h-4 min-w-0 flex-1"
/>
))}
</div>
))}
</div>
)
}
/** Consumer route fallback with stable page chrome; intentionally contains no spinner. */
export const ConsumerRouteSkeleton = ({ className }: { className?: string }) => (
<section className={clsx('flex min-h-full flex-col bg-consumer-canvas', className)}>
<div
aria-busy="true"
aria-label={texts.common.preparingPanel}
className="flex min-h-full flex-col"
role="status"
>
<header
aria-hidden
className="consumer-deep-header flex min-h-[72px] items-center gap-3 px-4 py-3"
>
<Skeleton className="size-11 shrink-0 rounded-consumer-control" />
<Skeleton className="h-5 w-36 max-w-[60%]" />
</header>
<div
aria-hidden
className="consumer-page-container flex flex-col gap-4 py-4"
>
<div className="rounded-consumer-modal bg-consumer-surface p-4">
<Skeleton className="h-6 w-2/5" />
<Skeleton className="mt-4 h-4 w-full" />
<Skeleton className="mt-2 h-4 w-4/5" />
</div>
<div className="rounded-consumer-modal bg-consumer-surface p-4">
<Skeleton className="h-5 w-1/3" />
<div className="mt-4 flex flex-col gap-3">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="flex items-center gap-3"
>
<Skeleton className="size-10 shrink-0 rounded-2xl" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-2/5" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
)
export const PageLoading = ({ label = texts.common.preparingPage }: { label?: string }) => (
<div
aria-busy="true"
aria-live="polite"
className="flex min-h-full w-full items-center justify-center bg-consumer-canvas px-4 py-16"
role="status"
>
<div className="flex flex-col items-center gap-5">
<div className="relative flex size-16 items-center justify-center rounded-consumer-surface bg-primary">
<span className="absolute inset-0 animate-ping rounded-consumer-surface bg-primary/20 motion-reduce:animate-none" />
<span className="relative size-5 animate-spin rounded-full border-2 border-white/40 border-t-white motion-reduce:animate-none" />
</div>
<div className="text-center">
<p className="text-sm font-bold text-secondary-20">{label}</p>
<p className="mt-1 text-xs text-secondary-30">{texts.common.pleaseWait}</p>
</div>
</div>
</div>
)