feat(error): implement comprehensive error handling and loading states
- Add error boundary components with graceful fallbacks - Implement loading states with skeleton components - Create route-level suspense wrapper - Add error page with recovery options - Include error boundary demo for testing Error handling provides resilient user experience with clear feedback and recovery options when components fail. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
423
reactrebuild0825/src/components/loading/Skeleton.tsx
Normal file
423
reactrebuild0825/src/components/loading/Skeleton.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { Card } from '../ui/Card';
|
||||
|
||||
import { LoadingSpinner } from './LoadingSpinner';
|
||||
|
||||
export interface SkeletonProps {
|
||||
className?: string;
|
||||
rounded?: boolean;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export interface SkeletonLayoutProps {
|
||||
loadingText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base skeleton component with glassmorphism styling
|
||||
*/
|
||||
export function BaseSkeleton({
|
||||
className,
|
||||
rounded = true,
|
||||
animate = true
|
||||
}: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-glass-bg border border-glass-border',
|
||||
{
|
||||
'animate-pulse': animate,
|
||||
rounded,
|
||||
'rounded-lg': rounded
|
||||
},
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for text content
|
||||
*/
|
||||
export function TextSkeleton({
|
||||
lines = 1,
|
||||
className,
|
||||
animate = true
|
||||
}: {
|
||||
lines?: number;
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx('space-y-2', className)}>
|
||||
{Array.from({ length: lines }).map((_, index) => (
|
||||
<BaseSkeleton
|
||||
key={index}
|
||||
className={clsx(
|
||||
'h-4',
|
||||
index === lines - 1 ? 'w-3/4' : 'w-full'
|
||||
)}
|
||||
animate={animate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for avatars and circular elements
|
||||
*/
|
||||
export function AvatarSkeleton({
|
||||
size = 'md',
|
||||
className
|
||||
}: {
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
className?: string;
|
||||
}) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-8 h-8',
|
||||
md: 'w-12 h-12',
|
||||
lg: 'w-16 h-16',
|
||||
xl: 'w-24 h-24'
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSkeleton
|
||||
className={clsx('rounded-full', sizeClasses[size], className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for buttons
|
||||
*/
|
||||
export function ButtonSkeleton({
|
||||
size = 'md',
|
||||
className
|
||||
}: {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}) {
|
||||
const sizeClasses = {
|
||||
sm: 'h-8 w-20',
|
||||
md: 'h-10 w-24',
|
||||
lg: 'h-12 w-32'
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSkeleton
|
||||
className={clsx('rounded-lg', sizeClasses[size], className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for cards
|
||||
*/
|
||||
function CardSkeleton({ loadingText, className }: SkeletonLayoutProps) {
|
||||
return (
|
||||
<div className={clsx('space-y-6', className)}>
|
||||
{/* Loading indicator */}
|
||||
{loadingText && (
|
||||
<div className="text-center mb-8">
|
||||
<LoadingSpinner size="md" variant="accent" text={loadingText} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Card skeletons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<Card key={index} className="p-6">
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<AvatarSkeleton size="md" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<BaseSkeleton className="h-5 w-3/4" />
|
||||
<BaseSkeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-2">
|
||||
<BaseSkeleton className="h-4 w-full" />
|
||||
<BaseSkeleton className="h-4 w-5/6" />
|
||||
<BaseSkeleton className="h-4 w-4/6" />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-between items-center pt-4">
|
||||
<BaseSkeleton className="h-4 w-1/4" />
|
||||
<ButtonSkeleton size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for list items
|
||||
*/
|
||||
function ListSkeleton({ loadingText, className }: SkeletonLayoutProps) {
|
||||
return (
|
||||
<div className={clsx('space-y-4', className)}>
|
||||
{/* Loading indicator */}
|
||||
{loadingText && (
|
||||
<div className="text-center mb-8">
|
||||
<LoadingSpinner size="md" variant="accent" text={loadingText} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List items */}
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center space-x-4 p-4 bg-glass-bg border border-glass-border rounded-lg">
|
||||
<AvatarSkeleton size="md" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<BaseSkeleton className="h-5 w-3/4" />
|
||||
<BaseSkeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<ButtonSkeleton size="sm" />
|
||||
<ButtonSkeleton size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for tables
|
||||
*/
|
||||
function TableSkeleton({ loadingText, className }: SkeletonLayoutProps) {
|
||||
return (
|
||||
<div className={clsx('space-y-4', className)}>
|
||||
{/* Loading indicator */}
|
||||
{loadingText && (
|
||||
<div className="text-center mb-8">
|
||||
<LoadingSpinner size="md" variant="accent" text={loadingText} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
{/* Table header */}
|
||||
<div className="bg-glass-bg border-b border-glass-border px-6 py-4">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<BaseSkeleton key={index} className="h-4 w-3/4" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table rows */}
|
||||
<div className="divide-y divide-glass-border">
|
||||
{Array.from({ length: 10 }).map((_, rowIndex) => (
|
||||
<div key={rowIndex} className="px-6 py-4">
|
||||
<div className="grid grid-cols-5 gap-4 items-center">
|
||||
{Array.from({ length: 5 }).map((_, colIndex) => {
|
||||
if (colIndex === 0) {
|
||||
return (
|
||||
<div key={colIndex} className="flex items-center space-x-3">
|
||||
<AvatarSkeleton size="sm" />
|
||||
<BaseSkeleton className="h-4 w-20" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (colIndex === 4) {
|
||||
return (
|
||||
<div key={colIndex} className="flex space-x-2">
|
||||
<ButtonSkeleton size="sm" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <BaseSkeleton key={colIndex} className="h-4 w-16" />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for full page layouts
|
||||
*/
|
||||
function PageSkeleton({ loadingText, className }: SkeletonLayoutProps) {
|
||||
return (
|
||||
<div className={clsx('min-h-screen bg-gradient-to-br from-background-primary to-background-secondary', className)}>
|
||||
{/* Header skeleton */}
|
||||
<div className="bg-glass-bg border-b border-glass-border px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<BaseSkeleton className="h-8 w-32" />
|
||||
<div className="hidden md:flex space-x-6">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<BaseSkeleton key={index} className="h-4 w-16" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<AvatarSkeleton size="sm" />
|
||||
<ButtonSkeleton size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
{/* Loading indicator */}
|
||||
{loadingText && (
|
||||
<div className="text-center mb-12">
|
||||
<LoadingSpinner size="lg" variant="accent" text={loadingText} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page title and actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div className="space-y-2">
|
||||
<BaseSkeleton className="h-8 w-64" />
|
||||
<BaseSkeleton className="h-4 w-96" />
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 flex space-x-3">
|
||||
<ButtonSkeleton size="md" />
|
||||
<ButtonSkeleton size="md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<Card key={index} className="p-6">
|
||||
<div className="space-y-3">
|
||||
<BaseSkeleton className="h-4 w-16" />
|
||||
<BaseSkeleton className="h-8 w-20" />
|
||||
<BaseSkeleton className="h-3 w-24" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Primary content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<BaseSkeleton className="h-6 w-32" />
|
||||
<ButtonSkeleton size="sm" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center space-x-4 p-3 bg-glass-bg rounded border border-glass-border">
|
||||
<AvatarSkeleton size="sm" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<BaseSkeleton className="h-4 w-3/4" />
|
||||
<BaseSkeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
<BaseSkeleton className="h-6 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<BaseSkeleton className="h-5 w-24" />
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<BaseSkeleton className="h-4 w-20" />
|
||||
<BaseSkeleton className="h-4 w-12" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<BaseSkeleton className="h-5 w-28" />
|
||||
<BaseSkeleton className="h-32 w-full" />
|
||||
<ButtonSkeleton size="md" className="w-full" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton for form layouts
|
||||
*/
|
||||
function FormSkeleton({ loadingText, className }: SkeletonLayoutProps) {
|
||||
return (
|
||||
<div className={clsx('space-y-6', className)}>
|
||||
{/* Loading indicator */}
|
||||
{loadingText && (
|
||||
<div className="text-center mb-8">
|
||||
<LoadingSpinner size="md" variant="accent" text={loadingText} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
{/* Form title */}
|
||||
<BaseSkeleton className="h-6 w-48" />
|
||||
|
||||
{/* Form fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<BaseSkeleton className="h-4 w-24" />
|
||||
<BaseSkeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Text area */}
|
||||
<div className="space-y-2">
|
||||
<BaseSkeleton className="h-4 w-32" />
|
||||
<BaseSkeleton className="h-24 w-full" />
|
||||
</div>
|
||||
|
||||
{/* Form actions */}
|
||||
<div className="flex justify-end space-x-3 pt-6 border-t border-glass-border">
|
||||
<ButtonSkeleton size="md" />
|
||||
<ButtonSkeleton size="md" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export all skeleton components
|
||||
export const Skeleton = {
|
||||
Base: BaseSkeleton,
|
||||
Text: TextSkeleton,
|
||||
Avatar: AvatarSkeleton,
|
||||
Button: ButtonSkeleton,
|
||||
Card: CardSkeleton,
|
||||
List: ListSkeleton,
|
||||
Table: TableSkeleton,
|
||||
Page: PageSkeleton,
|
||||
Form: FormSkeleton
|
||||
};
|
||||
|
||||
export default Skeleton;
|
||||
Reference in New Issue
Block a user