Files
gridpilot.gg/apps/website/components/shared/state/LoadingWrapper.tsx
2026-01-06 11:05:16 +01:00

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}
/>
);
}