website refactor
This commit is contained in:
@@ -1,390 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StateContainerProps, StateContainerConfig } from './types';
|
||||
import { LoadingWrapper } from './LoadingWrapper';
|
||||
import { ErrorDisplay } from './ErrorDisplay';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { Inbox, AlertCircle, Grid, List } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* StateContainer Component
|
||||
*
|
||||
* Combined wrapper that automatically handles all states (loading, error, empty, success)
|
||||
* based on the provided data and state values.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic state detection and rendering
|
||||
* - Customizable configuration for each state
|
||||
* - Custom render functions for advanced use cases
|
||||
* - Consistent behavior across all pages
|
||||
*
|
||||
* Usage Example:
|
||||
* ```typescript
|
||||
* <StateContainer
|
||||
* data={data}
|
||||
* isLoading={isLoading}
|
||||
* error={error}
|
||||
* retry={retry}
|
||||
* config={{
|
||||
* 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 }
|
||||
* }
|
||||
* }}
|
||||
* >
|
||||
* {(content) => <MyContent data={content} />}
|
||||
* </StateContainer>
|
||||
* ```
|
||||
*/
|
||||
export function StateContainer<T>({
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
retry,
|
||||
children,
|
||||
config,
|
||||
className = '',
|
||||
showEmpty = true,
|
||||
isEmpty,
|
||||
}: StateContainerProps<T>) {
|
||||
// Determine if data is empty
|
||||
const isDataEmpty = (data: T | null | undefined): boolean => {
|
||||
if (data === null || data === undefined) return true;
|
||||
if (isEmpty) return isEmpty(data);
|
||||
|
||||
// Default empty checks
|
||||
if (Array.isArray(data)) return data.length === 0;
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
return Object.keys(data).length === 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Priority order: Loading > Error > Empty > Success
|
||||
if (isLoading) {
|
||||
const loadingConfig = config?.loading || {};
|
||||
|
||||
// Custom render
|
||||
if (config?.customRender?.loading) {
|
||||
return <>{config.customRender.loading()}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<LoadingWrapper
|
||||
variant={loadingConfig.variant || 'spinner'}
|
||||
message={loadingConfig.message || 'Loading...'}
|
||||
size={loadingConfig.size || 'md'}
|
||||
skeletonCount={loadingConfig.skeletonCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorConfig = config?.error || {};
|
||||
|
||||
// Custom render
|
||||
if (config?.customRender?.error) {
|
||||
return <>{config.customRender.error(error)}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ErrorDisplay
|
||||
error={error}
|
||||
onRetry={retry}
|
||||
variant={errorConfig.variant || 'full-screen'}
|
||||
actions={errorConfig.actions}
|
||||
showRetry={errorConfig.showRetry}
|
||||
showNavigation={errorConfig.showNavigation}
|
||||
hideTechnicalDetails={errorConfig.hideTechnicalDetails}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showEmpty && isDataEmpty(data)) {
|
||||
const emptyConfig = config?.empty;
|
||||
|
||||
// Custom render
|
||||
if (config?.customRender?.empty) {
|
||||
return <>{config.customRender.empty()}</>;
|
||||
}
|
||||
|
||||
// If no empty config provided, show nothing (or could show default empty state)
|
||||
if (!emptyConfig) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<EmptyState
|
||||
icon={Inbox}
|
||||
title="No data available"
|
||||
description="There is nothing to display here"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<EmptyState
|
||||
icon={emptyConfig.icon}
|
||||
title={emptyConfig.title || 'No data available'}
|
||||
description={emptyConfig.description}
|
||||
action={emptyConfig.action}
|
||||
variant="default"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Success state - render children with data
|
||||
if (data === null || data === undefined) {
|
||||
// This shouldn't happen if we've handled all cases above, but as a fallback
|
||||
return (
|
||||
<div className={className}>
|
||||
<EmptyState
|
||||
icon={AlertCircle}
|
||||
title="Unexpected state"
|
||||
description="No data available but no error or loading state"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, data is guaranteed to be non-null and non-undefined
|
||||
return <>{children(data as T)}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListStateContainer - Specialized for list data
|
||||
* Automatically handles empty arrays with appropriate messaging
|
||||
*/
|
||||
export function ListStateContainer<T>({
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
retry,
|
||||
children,
|
||||
config,
|
||||
className = '',
|
||||
emptyConfig,
|
||||
}: StateContainerProps<T[]> & {
|
||||
emptyConfig?: {
|
||||
icon: any;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const listConfig: StateContainerConfig<T[]> = {
|
||||
...config,
|
||||
empty: emptyConfig || {
|
||||
icon: List,
|
||||
title: 'No items found',
|
||||
description: 'This list is currently empty',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<StateContainer
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={retry}
|
||||
config={listConfig}
|
||||
className={className}
|
||||
isEmpty={(arr) => !arr || arr.length === 0}
|
||||
>
|
||||
{children}
|
||||
</StateContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DetailStateContainer - Specialized for detail pages
|
||||
* Includes back/refresh functionality
|
||||
*/
|
||||
export function DetailStateContainer<T>({
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
retry,
|
||||
children,
|
||||
config,
|
||||
className = '',
|
||||
onBack,
|
||||
onRefresh,
|
||||
}: StateContainerProps<T> & {
|
||||
onBack?: () => void;
|
||||
onRefresh?: () => void;
|
||||
}) {
|
||||
const detailConfig: StateContainerConfig<T> = {
|
||||
...config,
|
||||
error: {
|
||||
...config?.error,
|
||||
actions: [
|
||||
...(config?.error?.actions || []),
|
||||
...(onBack ? [{ label: 'Go Back', onClick: onBack, variant: 'secondary' as const }] : []),
|
||||
...(onRefresh ? [{ label: 'Refresh', onClick: onRefresh, variant: 'primary' as const }] : []),
|
||||
],
|
||||
showNavigation: config?.error?.showNavigation ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<StateContainer
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={retry}
|
||||
config={detailConfig}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</StateContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* PageStateContainer - Full page state management
|
||||
* Wraps content in proper page structure
|
||||
*/
|
||||
export function PageStateContainer<T>({
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
retry,
|
||||
children,
|
||||
config,
|
||||
title,
|
||||
description,
|
||||
}: StateContainerProps<T> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const pageConfig: StateContainerConfig<T> = {
|
||||
loading: {
|
||||
variant: 'full-screen',
|
||||
message: title ? `Loading ${title}...` : 'Loading...',
|
||||
...config?.loading,
|
||||
},
|
||||
error: {
|
||||
variant: 'full-screen',
|
||||
...config?.error,
|
||||
},
|
||||
empty: config?.empty,
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <StateContainer data={data} isLoading={isLoading} error={error} retry={retry} config={pageConfig}>
|
||||
{children}
|
||||
</StateContainer>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <StateContainer data={data} isLoading={isLoading} error={error} retry={retry} config={pageConfig}>
|
||||
{children}
|
||||
</StateContainer>;
|
||||
}
|
||||
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
if (config?.empty) {
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-12">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{title && (
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-gray-400">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<StateContainer data={data} isLoading={isLoading} error={error} retry={retry} config={pageConfig}>
|
||||
{children}
|
||||
</StateContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{title && (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-gray-400">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<StateContainer data={data} isLoading={isLoading} error={error} retry={retry} config={pageConfig}>
|
||||
{children}
|
||||
</StateContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GridStateContainer - Specialized for grid layouts
|
||||
* Handles card-based empty states
|
||||
*/
|
||||
export function GridStateContainer<T>({
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
retry,
|
||||
children,
|
||||
config,
|
||||
className = '',
|
||||
emptyConfig,
|
||||
}: StateContainerProps<T[]> & {
|
||||
emptyConfig?: {
|
||||
icon: any;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const gridConfig: StateContainerConfig<T[]> = {
|
||||
loading: {
|
||||
variant: 'card',
|
||||
...config?.loading,
|
||||
},
|
||||
...config,
|
||||
empty: emptyConfig || {
|
||||
icon: Grid,
|
||||
title: 'No items to display',
|
||||
description: 'Try adjusting your filters or search',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<StateContainer
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={retry}
|
||||
config={gridConfig}
|
||||
className={className}
|
||||
isEmpty={(arr) => !arr || arr.length === 0}
|
||||
>
|
||||
{children}
|
||||
</StateContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user