united-tattoo/components/admin/loading-states.tsx

252 lines
7.2 KiB
TypeScript

'use client'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Loader2 } from 'lucide-react'
// Generic loading spinner
export function LoadingSpinner({ size = 'default', className = '' }: {
size?: 'sm' | 'default' | 'lg'
className?: string
}) {
const sizeClasses = {
sm: 'h-4 w-4',
default: 'h-6 w-6',
lg: 'h-8 w-8'
}
return (
<Loader2 className={`animate-spin ${sizeClasses[size]} ${className}`} />
)
}
// Full page loading
export function PageLoading({ message = 'Loading...' }: { message?: string }) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center space-y-4">
<LoadingSpinner size="lg" />
<p className="text-sm text-muted-foreground">{message}</p>
</div>
</div>
)
}
// Dashboard stats loading
export function StatsLoading() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-[100px]" />
<Skeleton className="h-4 w-4" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-[60px] mb-2" />
<Skeleton className="h-3 w-[120px]" />
</CardContent>
</Card>
))}
</div>
)
}
// Table loading
export function TableLoading({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-[150px]" />
<Skeleton className="h-9 w-[100px]" />
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{/* Table header */}
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{Array.from({ length: columns }).map((_, i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</div>
{/* Table rows */}
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={rowIndex} className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{Array.from({ length: columns }).map((_, colIndex) => (
<Skeleton key={colIndex} className="h-4 w-full" />
))}
</div>
))}
</div>
</CardContent>
</Card>
)
}
// Calendar loading
export function CalendarLoading() {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-[200px]" />
<div className="flex gap-2">
<Skeleton className="h-9 w-[120px]" />
<Skeleton className="h-9 w-[100px]" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Calendar header */}
<div className="grid grid-cols-7 gap-2">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</div>
{/* Calendar body */}
{Array.from({ length: 5 }).map((_, weekIndex) => (
<div key={weekIndex} className="grid grid-cols-7 gap-2">
{Array.from({ length: 7 }).map((_, dayIndex) => (
<div key={dayIndex} className="space-y-1">
<Skeleton className="h-6 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
))}
</div>
))}
</div>
</CardContent>
</Card>
)
}
// Form loading
export function FormLoading() {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-[200px]" />
</CardHeader>
<CardContent className="space-y-6">
{/* Form fields */}
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-[100px]" />
<Skeleton className="h-10 w-full" />
</div>
))}
{/* Form actions */}
<div className="flex gap-2 pt-4">
<Skeleton className="h-10 w-[100px]" />
<Skeleton className="h-10 w-[80px]" />
</div>
</CardContent>
</Card>
)
}
// Chart loading
export function ChartLoading({ height = 300 }: { height?: number }) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-[150px]" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-end justify-between" style={{ height: `${height}px` }}>
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton
key={i}
className="w-8"
style={{ height: `${Math.random() * height * 0.8 + height * 0.2}px` }}
/>
))}
</div>
<div className="flex justify-between">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-3 w-8" />
))}
</div>
</div>
</CardContent>
</Card>
)
}
// List loading
export function ListLoading({ items = 5 }: { items?: number }) {
return (
<div className="space-y-3">
{Array.from({ length: items }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-3 w-[150px]" />
</div>
<Skeleton className="h-8 w-[80px]" />
</div>
</CardContent>
</Card>
))}
</div>
)
}
// Image grid loading
export function ImageGridLoading({ count = 12 }: { count?: number }) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="aspect-square w-full rounded-lg" />
<Skeleton className="h-3 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
))}
</div>
)
}
// Button loading state
export function ButtonLoading({
children,
isLoading,
...props
}: {
children: React.ReactNode
isLoading: boolean
[key: string]: any
}) {
return (
<button {...props} disabled={isLoading || props.disabled}>
{isLoading ? (
<div className="flex items-center gap-2">
<LoadingSpinner size="sm" />
<span>Loading...</span>
</div>
) : (
children
)}
</button>
)
}
// Inline loading for small components
export function InlineLoading({ text = 'Loading...' }: { text?: string }) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoadingSpinner size="sm" />
<span>{text}</span>
</div>
)
}