mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 21:47:56 +00:00
8c38955d4e
- shared/lib/error-handler.ts: Substrate error → user-friendly EN/KMR messages
* Maps 30+ blockchain error types (Staking, Identity, Tiki, ValidatorPool, DEX, Governance)
* extractDispatchError() - Parse Substrate DispatchError
* getUserFriendlyError() - Convert to bilingual messages
* handleBlockchainError() - Toast helper with auto language detection
* SUCCESS_MESSAGES - Success templates with {{param}} interpolation
- web/src/components/ErrorBoundary.tsx: Global React error boundary
* Catches unhandled React errors with fallback UI
* Error details with stack trace (developer mode)
* Try Again / Reload Page / Go Home buttons
* RouteErrorBoundary - Smaller boundary for individual routes
* Support email link (info@pezkuwichain.io)
- shared/components/AsyncComponent.tsx: Async data loading patterns
* CardSkeleton / ListItemSkeleton / TableSkeleton - Animated loading states
* LoadingState - Kurdistan green spinner with custom message
* ErrorState - Red alert with retry button
* EmptyState - Empty inbox icon with optional action
* AsyncComponent<T> - Generic wrapper handling Loading/Error/Empty/Success states
- web/src/App.tsx: Wrapped with ErrorBoundary
* All React errors now caught gracefully
* Beautiful fallback UI instead of white screen of death
Production-ready error handling with bilingual support (EN/KMR).
283 lines
7.6 KiB
TypeScript
283 lines
7.6 KiB
TypeScript
// ========================================
|
|
// Async Component Pattern
|
|
// ========================================
|
|
// Standard pattern for loading/error/empty states
|
|
|
|
import React, { ReactNode } from 'react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Loader2, AlertCircle, Inbox, RefreshCw } from 'lucide-react';
|
|
|
|
// ========================================
|
|
// LOADING SKELETON
|
|
// ========================================
|
|
|
|
export const CardSkeleton: React.FC = () => {
|
|
return (
|
|
<Card className="bg-gray-900 border-gray-800 animate-pulse">
|
|
<CardContent className="p-6">
|
|
<div className="h-6 bg-gray-800 rounded w-1/3 mb-4"></div>
|
|
<div className="space-y-3">
|
|
<div className="h-4 bg-gray-800 rounded"></div>
|
|
<div className="h-4 bg-gray-800 rounded w-5/6"></div>
|
|
<div className="h-4 bg-gray-800 rounded w-4/6"></div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export const ListItemSkeleton: React.FC = () => {
|
|
return (
|
|
<div className="flex items-center gap-4 p-4 bg-gray-900 border border-gray-800 rounded-lg animate-pulse">
|
|
<div className="w-12 h-12 bg-gray-800 rounded-full"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-4 bg-gray-800 rounded w-1/4"></div>
|
|
<div className="h-3 bg-gray-800 rounded w-1/2"></div>
|
|
</div>
|
|
<div className="h-8 w-20 bg-gray-800 rounded"></div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const TableSkeleton: React.FC<{ rows?: number }> = ({ rows = 5 }) => {
|
|
return (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: rows }).map((_, i) => (
|
|
<div key={i} className="flex gap-4 p-3 bg-gray-900 border border-gray-800 rounded animate-pulse">
|
|
<div className="h-4 bg-gray-800 rounded flex-1"></div>
|
|
<div className="h-4 bg-gray-800 rounded w-24"></div>
|
|
<div className="h-4 bg-gray-800 rounded w-32"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ========================================
|
|
// LOADING COMPONENT
|
|
// ========================================
|
|
|
|
export const LoadingState: React.FC<{
|
|
message?: string;
|
|
fullScreen?: boolean;
|
|
}> = ({ message = 'Loading...', fullScreen = false }) => {
|
|
const content = (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<Loader2 className="w-12 h-12 text-green-500 animate-spin" />
|
|
<p className="text-gray-400">{message}</p>
|
|
</div>
|
|
);
|
|
|
|
if (fullScreen) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
|
<Card className="bg-gray-900 border-gray-800 p-8">
|
|
<CardContent>{content}</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center justify-center p-12">
|
|
{content}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ========================================
|
|
// ERROR STATE
|
|
// ========================================
|
|
|
|
export const ErrorState: React.FC<{
|
|
message?: string;
|
|
error?: Error | string;
|
|
onRetry?: () => void;
|
|
fullScreen?: boolean;
|
|
}> = ({
|
|
message = 'An error occurred',
|
|
error,
|
|
onRetry,
|
|
fullScreen = false,
|
|
}) => {
|
|
const errorMessage = typeof error === 'string' ? error : error?.message;
|
|
|
|
const content = (
|
|
<Alert className="bg-red-900/20 border-red-500">
|
|
<AlertCircle className="h-5 w-5 text-red-400" />
|
|
<AlertDescription className="text-gray-300">
|
|
<strong className="block mb-2">{message}</strong>
|
|
{errorMessage && (
|
|
<p className="text-sm text-gray-400 mb-4">{errorMessage}</p>
|
|
)}
|
|
{onRetry && (
|
|
<Button
|
|
onClick={onRetry}
|
|
size="sm"
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
Try Again
|
|
</Button>
|
|
)}
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
|
|
if (fullScreen) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
|
<Card className="bg-gray-900 border-gray-800 max-w-md">
|
|
<CardContent className="p-8">{content}</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <div className="p-4">{content}</div>;
|
|
};
|
|
|
|
// ========================================
|
|
// EMPTY STATE
|
|
// ========================================
|
|
|
|
export const EmptyState: React.FC<{
|
|
message?: string;
|
|
description?: string;
|
|
icon?: ReactNode;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
fullScreen?: boolean;
|
|
}> = ({
|
|
message = 'No data found',
|
|
description,
|
|
icon,
|
|
action,
|
|
fullScreen = false,
|
|
}) => {
|
|
const content = (
|
|
<div className="flex flex-col items-center gap-4 text-center">
|
|
{icon || <Inbox className="w-16 h-16 text-gray-600" />}
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white mb-1">{message}</h3>
|
|
{description && <p className="text-sm text-gray-400">{description}</p>}
|
|
</div>
|
|
{action && (
|
|
<Button onClick={action.onClick} className="bg-green-600 hover:bg-green-700">
|
|
{action.label}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
if (fullScreen) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
|
<Card className="bg-gray-900 border-gray-800 p-8">
|
|
<CardContent>{content}</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <div className="p-12">{content}</div>;
|
|
};
|
|
|
|
// ========================================
|
|
// ASYNC COMPONENT WRAPPER
|
|
// ========================================
|
|
|
|
export interface AsyncComponentProps<T> {
|
|
/** Loading state */
|
|
isLoading: boolean;
|
|
/** Error object */
|
|
error?: Error | string | null;
|
|
/** Data */
|
|
data?: T | null;
|
|
/** Children render function */
|
|
children: (data: T) => ReactNode;
|
|
/** Custom loading component */
|
|
LoadingComponent?: React.ComponentType;
|
|
/** Custom error component */
|
|
ErrorComponent?: React.ComponentType<{ error: Error | string; onRetry?: () => void }>;
|
|
/** Custom empty component */
|
|
EmptyComponent?: React.ComponentType;
|
|
/** Retry callback */
|
|
onRetry?: () => void;
|
|
/** Loading message */
|
|
loadingMessage?: string;
|
|
/** Error message */
|
|
errorMessage?: string;
|
|
/** Empty message */
|
|
emptyMessage?: string;
|
|
/** Full screen mode */
|
|
fullScreen?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Standard async component pattern
|
|
* Handles loading, error, empty, and success states
|
|
*
|
|
* @example
|
|
* <AsyncComponent
|
|
* isLoading={loading}
|
|
* error={error}
|
|
* data={courses}
|
|
* onRetry={refetch}
|
|
* >
|
|
* {(courses) => <CourseList courses={courses} />}
|
|
* </AsyncComponent>
|
|
*/
|
|
export function AsyncComponent<T>({
|
|
isLoading,
|
|
error,
|
|
data,
|
|
children,
|
|
LoadingComponent,
|
|
ErrorComponent,
|
|
EmptyComponent,
|
|
onRetry,
|
|
loadingMessage = 'Loading...',
|
|
errorMessage = 'Failed to load data',
|
|
emptyMessage = 'No data available',
|
|
fullScreen = false,
|
|
}: AsyncComponentProps<T>): JSX.Element {
|
|
// Loading state
|
|
if (isLoading) {
|
|
if (LoadingComponent) {
|
|
return <LoadingComponent />;
|
|
}
|
|
return <LoadingState message={loadingMessage} fullScreen={fullScreen} />;
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
if (ErrorComponent) {
|
|
return <ErrorComponent error={error} onRetry={onRetry} />;
|
|
}
|
|
return (
|
|
<ErrorState
|
|
message={errorMessage}
|
|
error={error}
|
|
onRetry={onRetry}
|
|
fullScreen={fullScreen}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Empty state
|
|
if (!data || (Array.isArray(data) && data.length === 0)) {
|
|
if (EmptyComponent) {
|
|
return <EmptyComponent />;
|
|
}
|
|
return <EmptyState message={emptyMessage} fullScreen={fullScreen} />;
|
|
}
|
|
|
|
// Success state - render children with data
|
|
return <>{children(data)}</>;
|
|
}
|