Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
|
|
interface LoadingSkeletonProps {
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Table skeleton — mimics a data table with header + rows.
|
|
*/
|
|
export function TableSkeleton({ rows = 6, columns = 5 }: { rows?: number; columns?: number }) {
|
|
return (
|
|
<div className="w-full space-y-0 border border-border rounded-lg overflow-hidden">
|
|
{/* Header row */}
|
|
<div className="flex gap-4 px-4 py-3 bg-muted border-b border-border">
|
|
{Array.from({ length: columns }).map((_, i) => (
|
|
<Skeleton key={i} className={cn('h-4', i === 0 ? 'w-1/4' : 'flex-1')} />
|
|
))}
|
|
</div>
|
|
{/* Data rows */}
|
|
{Array.from({ length: rows }).map((_, rowIdx) => (
|
|
<div
|
|
key={rowIdx}
|
|
className={cn(
|
|
'flex gap-4 px-4 py-3.5 border-b border-border last:border-0',
|
|
rowIdx % 2 === 0 ? 'bg-background' : 'bg-muted/20',
|
|
)}
|
|
>
|
|
{Array.from({ length: columns }).map((_, colIdx) => (
|
|
<Skeleton
|
|
key={colIdx}
|
|
className={cn('h-4', colIdx === 0 ? 'w-1/4' : 'flex-1')}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Card skeleton — mimics a content card.
|
|
*/
|
|
export function CardSkeleton({ className }: LoadingSkeletonProps) {
|
|
return (
|
|
<div className={cn('border border-border rounded-lg p-5 space-y-3 bg-background', className)}>
|
|
<div className="flex items-center justify-between">
|
|
<Skeleton className="h-5 w-1/3" />
|
|
<Skeleton className="h-5 w-16 rounded-full" />
|
|
</div>
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-4/5" />
|
|
<div className="pt-2 flex gap-2">
|
|
<Skeleton className="h-8 w-20 rounded-md" />
|
|
<Skeleton className="h-8 w-20 rounded-md" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Form skeleton — mimics a form with labeled inputs.
|
|
*/
|
|
export function FormSkeleton({ fields = 4 }: { fields?: number }) {
|
|
return (
|
|
<div className="space-y-5">
|
|
{Array.from({ length: fields }).map((_, i) => (
|
|
<div key={i} className="space-y-1.5">
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-9 w-full rounded-md" />
|
|
</div>
|
|
))}
|
|
<div className="flex gap-3 pt-2">
|
|
<Skeleton className="h-9 w-24 rounded-md" />
|
|
<Skeleton className="h-9 w-20 rounded-md" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Grid skeleton — a responsive card grid.
|
|
*/
|
|
export function GridSkeleton({ cards = 6 }: { cards?: number }) {
|
|
return (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{Array.from({ length: cards }).map((_, i) => (
|
|
<CardSkeleton key={i} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Page-level loading skeleton — header + content area.
|
|
*/
|
|
export function PageSkeleton() {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Page header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1.5">
|
|
<Skeleton className="h-7 w-48" />
|
|
<Skeleton className="h-4 w-72" />
|
|
</div>
|
|
<Skeleton className="h-9 w-28 rounded-md" />
|
|
</div>
|
|
{/* Content */}
|
|
<TableSkeleton />
|
|
</div>
|
|
);
|
|
}
|