Files
gridpilot.gg/apps/website/components/shared/state/PageWrapper.tsx
2026-01-07 12:40:52 +01:00

276 lines
6.2 KiB
TypeScript

import React, { ReactNode } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { LoadingWrapper } from './LoadingWrapper';
import { ErrorDisplay } from './ErrorDisplay';
import { EmptyState } from './EmptyState';
import { Inbox, List, LucideIcon } from 'lucide-react';
// ==================== PAGEWRAPPER TYPES ====================
export interface PageWrapperLoadingConfig {
variant?: 'skeleton' | 'full-screen';
message?: string;
}
export interface PageWrapperErrorConfig {
variant?: 'full-screen' | 'card';
card?: {
title?: string;
description?: string;
};
}
export interface PageWrapperEmptyConfig {
icon?: LucideIcon;
title?: string;
description?: string;
action?: {
label: string;
onClick: () => void;
};
}
export interface PageWrapperProps<TData> {
/** Data to be rendered */
data: TData | undefined;
/** Loading state (default: false) */
isLoading?: boolean;
/** Error state (default: null) */
error?: Error | null;
/** Retry function for errors */
retry?: () => void;
/** Template component that receives the data */
Template: React.ComponentType<{ data: TData }>;
/** Loading configuration */
loading?: PageWrapperLoadingConfig;
/** Error configuration */
errorConfig?: PageWrapperErrorConfig;
/** Empty configuration */
empty?: PageWrapperEmptyConfig;
/** Children for flexible content rendering */
children?: ReactNode;
/** Additional CSS classes */
className?: string;
}
/**
* PageWrapper Component
*
* A comprehensive wrapper component that handles all page states:
* - Loading states (skeleton or full-screen)
* - Error states (full-screen or card)
* - Empty states (with icon, title, description, and action)
* - Success state (renders Template component with data)
* - Flexible children support for custom content
*
* Usage Example:
* ```typescript
* <PageWrapper
* data={data}
* isLoading={isLoading}
* error={error}
* retry={retry}
* Template={MyTemplateComponent}
* loading={{ variant: 'skeleton', message: 'Loading...' }}
* error={{ variant: 'full-screen' }}
* empty={{
* icon: Trophy,
* title: 'No data found',
* description: 'Try refreshing the page',
* action: { label: 'Refresh', onClick: retry }
* }}
* >
* <AdditionalContent />
* </PageWrapper>
* ```
*/
export function PageWrapper<TData>({
data,
isLoading = false,
error = null,
retry,
Template,
loading,
errorConfig,
empty,
children,
className = '',
}: PageWrapperProps<TData>) {
// Priority order: Loading > Error > Empty > Success
// 1. Loading State
if (isLoading) {
const loadingVariant = loading?.variant || 'skeleton';
const loadingMessage = loading?.message || 'Loading...';
if (loadingVariant === 'full-screen') {
return (
<LoadingWrapper
variant="full-screen"
message={loadingMessage}
/>
);
}
// Default to skeleton
return (
<div className={className}>
<LoadingWrapper
variant="skeleton"
message={loadingMessage}
skeletonCount={3}
/>
{children}
</div>
);
}
// 2. Error State
if (error) {
const errorVariant = errorConfig?.variant || 'full-screen';
if (errorVariant === 'card') {
const cardTitle = errorConfig?.card?.title || 'Error';
const cardDescription = errorConfig?.card?.description || 'Something went wrong';
return (
<div className={className}>
<ErrorDisplay
error={error as ApiError}
onRetry={retry}
variant="card"
/>
{children}
</div>
);
}
// Default to full-screen
return (
<ErrorDisplay
error={error as ApiError}
onRetry={retry}
variant="full-screen"
/>
);
}
// 3. Empty State
if (!data || (Array.isArray(data) && data.length === 0)) {
if (empty) {
const Icon = empty.icon;
const hasAction = empty.action && retry;
return (
<div className={className}>
<EmptyState
icon={Icon || Inbox}
title={empty.title || 'No data available'}
description={empty.description}
action={hasAction ? {
label: empty.action!.label,
onClick: empty.action!.onClick,
} : undefined}
variant="default"
/>
{children}
</div>
);
}
// If no empty config provided but data is empty, show nothing
return (
<div className={className}>
{children}
</div>
);
}
// 4. Success State - Render Template with data
return (
<div className={className}>
<Template data={data} />
{children}
</div>
);
}
/**
* Convenience component for list data with automatic empty state handling
*/
export function ListPageWrapper<TData extends any[]>({
data,
isLoading = false,
error = null,
retry,
Template,
loading,
errorConfig,
empty,
children,
className = '',
}: PageWrapperProps<TData>) {
const listEmpty = empty || {
icon: List,
title: 'No items found',
description: 'This list is currently empty',
};
return (
<PageWrapper
data={data}
isLoading={isLoading}
error={error}
retry={retry}
Template={Template}
loading={loading}
errorConfig={errorConfig}
empty={listEmpty}
className={className}
>
{children}
</PageWrapper>
);
}
/**
* Convenience component for detail pages with enhanced error handling
*/
export function DetailPageWrapper<TData>({
data,
isLoading = false,
error = null,
retry,
Template,
loading,
errorConfig,
empty,
children,
className = '',
onBack,
onRefresh,
}: PageWrapperProps<TData> & {
onBack?: () => void;
onRefresh?: () => void;
}) {
// Create enhanced error config with additional actions
const enhancedErrorConfig: PageWrapperErrorConfig = {
...errorConfig,
};
return (
<PageWrapper
data={data}
isLoading={isLoading}
error={error}
retry={retry}
Template={Template}
loading={loading}
errorConfig={enhancedErrorConfig}
empty={empty}
className={className}
>
{children}
</PageWrapper>
);
}