244 lines
5.4 KiB
TypeScript
244 lines
5.4 KiB
TypeScript
import { EmptyState } from '@/ui/EmptyState';
|
|
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
|
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
|
import { ApiError } from '@/lib/api/base/ApiError';
|
|
import { Inbox, List, LucideIcon } from 'lucide-react';
|
|
import React, { ReactNode } from '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<{ viewData: TData }>;
|
|
/** Loading configuration */
|
|
loading?: PageWrapperLoadingConfig;
|
|
/** Error configuration */
|
|
errorConfig?: PageWrapperErrorConfig;
|
|
/** Empty configuration */
|
|
empty?: PageWrapperEmptyConfig;
|
|
/** Children for flexible content rendering */
|
|
children?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function PageWrapper<TData>({
|
|
data,
|
|
isLoading = false,
|
|
error = null,
|
|
retry,
|
|
Template,
|
|
loading,
|
|
errorConfig,
|
|
empty,
|
|
children,
|
|
}: 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 (
|
|
<React.Fragment>
|
|
<LoadingWrapper
|
|
variant="skeleton"
|
|
message={loadingMessage}
|
|
skeletonCount={3}
|
|
/>
|
|
{children}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
// 2. Error State
|
|
if (error) {
|
|
const errorVariant = errorConfig?.variant || 'full-screen';
|
|
|
|
if (errorVariant === 'card') {
|
|
return (
|
|
<React.Fragment>
|
|
<ErrorDisplay
|
|
error={error as ApiError}
|
|
onRetry={retry}
|
|
variant="card"
|
|
/>
|
|
{children}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
// 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 (
|
|
<React.Fragment>
|
|
<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}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
// If no empty config provided but data is empty, show nothing
|
|
return (
|
|
<React.Fragment>
|
|
{children}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
// 4. Success State - Render Template with data
|
|
return (
|
|
<React.Fragment>
|
|
<Template viewData={data} />
|
|
{children}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for list data with automatic empty state handling
|
|
*/
|
|
export function ListPageWrapper<TData extends unknown[]>({
|
|
data,
|
|
isLoading = false,
|
|
error = null,
|
|
retry,
|
|
Template,
|
|
loading,
|
|
errorConfig,
|
|
empty,
|
|
children,
|
|
}: 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}
|
|
>
|
|
{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,
|
|
}: 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}
|
|
>
|
|
{children}
|
|
</PageWrapper>
|
|
);
|
|
}
|