199 lines
5.5 KiB
TypeScript
199 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { LoadingWrapperProps } from '../types/state.types';
|
|
|
|
/**
|
|
* LoadingWrapper Component
|
|
*
|
|
* Provides consistent loading states with multiple variants:
|
|
* - spinner: Traditional loading spinner (default)
|
|
* - skeleton: Skeleton screens for better UX
|
|
* - full-screen: Centered in viewport
|
|
* - inline: Compact inline loading
|
|
* - card: Loading card placeholders
|
|
*
|
|
* All variants are fully accessible with ARIA labels and keyboard support.
|
|
*/
|
|
export function LoadingWrapper({
|
|
variant = 'spinner',
|
|
message = 'Loading...',
|
|
className = '',
|
|
size = 'md',
|
|
skeletonCount = 3,
|
|
cardConfig,
|
|
ariaLabel = 'Loading content',
|
|
}: LoadingWrapperProps) {
|
|
// Size mappings for different variants
|
|
const sizeClasses = {
|
|
sm: {
|
|
spinner: 'w-4 h-4 border-2',
|
|
inline: 'text-xs',
|
|
card: 'h-24',
|
|
},
|
|
md: {
|
|
spinner: 'w-10 h-10 border-2',
|
|
inline: 'text-sm',
|
|
card: 'h-32',
|
|
},
|
|
lg: {
|
|
spinner: 'w-16 h-16 border-4',
|
|
inline: 'text-base',
|
|
card: 'h-40',
|
|
},
|
|
};
|
|
|
|
const spinnerSize = sizeClasses[size].spinner;
|
|
const inlineSize = sizeClasses[size].inline;
|
|
const cardHeight = cardConfig?.height || sizeClasses[size].card;
|
|
|
|
// Render different variants
|
|
switch (variant) {
|
|
case 'spinner':
|
|
return (
|
|
<div
|
|
className={`flex items-center justify-center min-h-[200px] ${className}`}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div
|
|
className={`${spinnerSize} border-primary-blue border-t-transparent rounded-full animate-spin`}
|
|
/>
|
|
<p className="text-gray-400 text-sm">{message}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'skeleton':
|
|
return (
|
|
<div
|
|
className={`space-y-3 ${className}`}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
{Array.from({ length: skeletonCount }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className="w-full bg-iron-gray/40 rounded-lg animate-pulse"
|
|
style={{ height: cardHeight }}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
case 'full-screen':
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 bg-deep-graphite/90 backdrop-blur-sm flex items-center justify-center p-4"
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<div className="text-center max-w-md">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-16 h-16 border-4 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
<p className="text-white text-lg font-medium">{message}</p>
|
|
<p className="text-gray-400 text-sm">This may take a moment...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'inline':
|
|
return (
|
|
<div
|
|
className={`inline-flex items-center gap-2 ${inlineSize} ${className}`}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<div className="w-4 h-4 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
<span className="text-gray-400">{message}</span>
|
|
</div>
|
|
);
|
|
|
|
case 'card':
|
|
const cardCount = cardConfig?.count || 3;
|
|
const cardClassName = cardConfig?.className || '';
|
|
|
|
return (
|
|
<div
|
|
className={`grid gap-4 ${className}`}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
{Array.from({ length: cardCount }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className={`bg-iron-gray/40 rounded-xl overflow-hidden border border-charcoal-outline/50 ${cardClassName}`}
|
|
style={{ height: cardHeight }}
|
|
>
|
|
<div className="h-full w-full flex items-center justify-center">
|
|
<div className="w-8 h-8 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience component for full-screen loading
|
|
*/
|
|
export function FullScreenLoading({ message = 'Loading...', className = '' }: Pick<LoadingWrapperProps, 'message' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="full-screen"
|
|
message={message}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for inline loading
|
|
*/
|
|
export function InlineLoading({ message = 'Loading...', size = 'sm', className = '' }: Pick<LoadingWrapperProps, 'message' | 'size' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="inline"
|
|
message={message}
|
|
size={size}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for skeleton loading
|
|
*/
|
|
export function SkeletonLoading({ skeletonCount = 3, className = '' }: Pick<LoadingWrapperProps, 'skeletonCount' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="skeleton"
|
|
skeletonCount={skeletonCount}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for card loading
|
|
*/
|
|
export function CardLoading({ cardConfig, className = '' }: Pick<LoadingWrapperProps, 'cardConfig' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="card"
|
|
cardConfig={cardConfig}
|
|
className={className}
|
|
/>
|
|
);
|
|
} |