page wrapper
This commit is contained in:
276
apps/website/components/shared/state/PageWrapper.tsx
Normal file
276
apps/website/components/shared/state/PageWrapper.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user