diff --git a/shared/components/AsyncComponent.tsx b/shared/components/AsyncComponent.tsx
new file mode 100644
index 00000000..f2ec9816
--- /dev/null
+++ b/shared/components/AsyncComponent.tsx
@@ -0,0 +1,282 @@
+// ========================================
+// 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 (
+
+
+
+
+
+
+ );
+};
+
+export const ListItemSkeleton: React.FC = () => {
+ return (
+
+ );
+};
+
+export const TableSkeleton: React.FC<{ rows?: number }> = ({ rows = 5 }) => {
+ return (
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ ))}
+
+ );
+};
+
+// ========================================
+// LOADING COMPONENT
+// ========================================
+
+export const LoadingState: React.FC<{
+ message?: string;
+ fullScreen?: boolean;
+}> = ({ message = 'Loading...', fullScreen = false }) => {
+ const content = (
+
+ );
+
+ if (fullScreen) {
+ return (
+
+
+ {content}
+
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+};
+
+// ========================================
+// 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 = (
+
+
+
+ {message}
+ {errorMessage && (
+ {errorMessage}
+ )}
+ {onRetry && (
+
+ )}
+
+
+ );
+
+ if (fullScreen) {
+ return (
+
+
+ {content}
+
+
+ );
+ }
+
+ return {content}
;
+};
+
+// ========================================
+// 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 = (
+
+ {icon ||
}
+
+
{message}
+ {description &&
{description}
}
+
+ {action && (
+
+ )}
+
+ );
+
+ if (fullScreen) {
+ return (
+
+
+ {content}
+
+
+ );
+ }
+
+ return {content}
;
+};
+
+// ========================================
+// ASYNC COMPONENT WRAPPER
+// ========================================
+
+export interface AsyncComponentProps {
+ /** 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
+ *
+ * {(courses) => }
+ *
+ */
+export function AsyncComponent({
+ isLoading,
+ error,
+ data,
+ children,
+ LoadingComponent,
+ ErrorComponent,
+ EmptyComponent,
+ onRetry,
+ loadingMessage = 'Loading...',
+ errorMessage = 'Failed to load data',
+ emptyMessage = 'No data available',
+ fullScreen = false,
+}: AsyncComponentProps): JSX.Element {
+ // Loading state
+ if (isLoading) {
+ if (LoadingComponent) {
+ return ;
+ }
+ return ;
+ }
+
+ // Error state
+ if (error) {
+ if (ErrorComponent) {
+ return ;
+ }
+ return (
+
+ );
+ }
+
+ // Empty state
+ if (!data || (Array.isArray(data) && data.length === 0)) {
+ if (EmptyComponent) {
+ return ;
+ }
+ return ;
+ }
+
+ // Success state - render children with data
+ return <>{children(data)}>;
+}
diff --git a/shared/lib/error-handler.ts b/shared/lib/error-handler.ts
new file mode 100644
index 00000000..41d35286
--- /dev/null
+++ b/shared/lib/error-handler.ts
@@ -0,0 +1,373 @@
+// ========================================
+// Error Handler & User-Friendly Messages
+// ========================================
+// Convert blockchain errors to human-readable messages
+
+import type { ApiPromise } from '@polkadot/api';
+import type { DispatchError } from '@polkadot/types/interfaces';
+
+// ========================================
+// ERROR MESSAGE MAPPINGS
+// ========================================
+
+interface ErrorMessage {
+ en: string;
+ kmr: string; // Kurmanji
+}
+
+/**
+ * User-friendly error messages for common blockchain errors
+ * Key format: "palletName.errorName"
+ */
+const ERROR_MESSAGES: Record = {
+ // Staking errors
+ 'staking.InsufficientBond': {
+ en: 'Bond amount too small. Please check minimum staking requirement.',
+ kmr: 'Mîqdara bond zêde piçûk e. Ji kerema xwe mîqdara kêmtirîn kontrol bike.',
+ },
+ 'staking.AlreadyBonded': {
+ en: 'You have already bonded tokens. Use "Bond More" to add additional stake.',
+ kmr: 'We berê token bond kirine. Ji bo zêdekirin "Bond More" bikar bîne.',
+ },
+ 'staking.NotStash': {
+ en: 'This account is not a stash account. Please use your staking controller.',
+ kmr: 'Ev account stash nîne. Ji kerema xwe controller bikar bîne.',
+ },
+ 'staking.NoMoreChunks': {
+ en: 'Too many unbonding chunks. Please wait for previous unbondings to complete.',
+ kmr: 'Zêde chunk unbonding hene. Ji kerema xwe li çavkaniyên berê bisekine.',
+ },
+
+ // Identity KYC errors
+ 'identityKyc.AlreadyApplied': {
+ en: 'You already have a pending citizenship application. Please wait for approval.',
+ kmr: 'We berê serlêdana welatîtiyê heye. Ji kerema xwe li pejirandina bisekine.',
+ },
+ 'identityKyc.AlreadyApproved': {
+ en: 'Your citizenship application is already approved!',
+ kmr: 'Serlêdana welatîtiya we berê hatiye pejirandin!',
+ },
+ 'identityKyc.NotApproved': {
+ en: 'Your KYC is not approved yet. Please complete citizenship application first.',
+ kmr: 'KYC-ya we hîn nehatiye pejirandin. Pêşî serlêdana welatîtiyê temam bike.',
+ },
+ 'identityKyc.IdentityNotSet': {
+ en: 'Please set your identity information first.',
+ kmr: 'Ji kerema xwe pêşî agahdariya nasnameya xwe saz bike.',
+ },
+
+ // Tiki errors
+ 'tiki.RoleAlreadyAssigned': {
+ en: 'This role is already assigned to the user.',
+ kmr: 'Ev rol berê ji bikarhêner re hatiye veqetandin.',
+ },
+ 'tiki.UnauthorizedRoleAssignment': {
+ en: 'You do not have permission to assign this role.',
+ kmr: 'We destûra veqetandina vê rolê nîne.',
+ },
+ 'tiki.RoleNotFound': {
+ en: 'The specified role does not exist.',
+ kmr: 'Rola diyarkirî tune ye.',
+ },
+
+ // ValidatorPool errors
+ 'validatorPool.AlreadyInPool': {
+ en: 'You are already registered in the validator pool.',
+ kmr: 'We berê di pool-a validator de tomar bûyî.',
+ },
+ 'validatorPool.NotInPool': {
+ en: 'You are not registered in the validator pool.',
+ kmr: 'We di pool-a validator de tomar nebûyî.',
+ },
+ 'validatorPool.InsufficientStake': {
+ en: 'Insufficient stake for validator pool. Please increase your stake.',
+ kmr: 'Stake ji bo pool-a validator kêm e. Ji kerema xwe stake-ya xwe zêde bike.',
+ },
+
+ // DEX/AssetConversion errors
+ 'assetConversion.PoolNotFound': {
+ en: 'Liquidity pool not found for this token pair.',
+ kmr: 'Pool-a liquidity ji bo vê cuda-token nehat dîtin.',
+ },
+ 'assetConversion.InsufficientLiquidity': {
+ en: 'Insufficient liquidity in pool. Try a smaller amount.',
+ kmr: 'Liquidity-ya pool-ê kêm e. Mîqdareke piçûktir biceribîne.',
+ },
+ 'assetConversion.SlippageTooHigh': {
+ en: 'Price impact too high. Increase slippage tolerance or reduce amount.',
+ kmr: 'Bandora bihayê zêde mezin e. Toleransa slippage zêde bike an mîqdarê kêm bike.',
+ },
+ 'assetConversion.AmountTooSmall': {
+ en: 'Swap amount too small. Minimum swap amount not met.',
+ kmr: 'Mîqdara swap zêde piçûk e. Mîqdara kêmtirîn nehatiye gihîştin.',
+ },
+
+ // Balance/Asset errors
+ 'balances.InsufficientBalance': {
+ en: 'Insufficient balance. You do not have enough tokens for this transaction.',
+ kmr: 'Balance-ya we kêm e. Ji bo vê transaction token-ên we têr nînin.',
+ },
+ 'balances.ExistentialDeposit': {
+ en: 'Amount is below existential deposit. Account would be reaped.',
+ kmr: 'Mîqdar ji existential deposit kêmtir e. Account dê were jêbirin.',
+ },
+ 'assets.BalanceLow': {
+ en: 'Asset balance too low for this operation.',
+ kmr: 'Balance-ya asset-ê ji bo vê operation zêde kêm e.',
+ },
+ 'assets.NoPermission': {
+ en: 'You do not have permission to perform this operation on this asset.',
+ kmr: 'We destûra vê operation-ê li ser vê asset-ê nîne.',
+ },
+
+ // Governance errors
+ 'referenda.NotOngoing': {
+ en: 'This referendum is not currently active.',
+ kmr: 'Ev referendum niha ne çalak e.',
+ },
+ 'referenda.AlreadyVoted': {
+ en: 'You have already voted on this referendum.',
+ kmr: 'We berê li ser vê referendum-ê deng da.',
+ },
+ 'convictionVoting.NotVoter': {
+ en: 'You are not eligible to vote. Citizenship required.',
+ kmr: 'We mafê dengdanê nîne. Welatîtî pêwîst e.',
+ },
+
+ // Treasury errors
+ 'treasury.InsufficientProposersBalance': {
+ en: 'Insufficient balance to submit treasury proposal. Bond required.',
+ kmr: 'Ji bo pêşniyara treasury-yê balance kêm e. Bond pêwîst e.',
+ },
+
+ // System/General errors
+ 'system.CallFiltered': {
+ en: 'This action is not permitted by the system filters.',
+ kmr: 'Ev çalakî ji hêla fîltireyên sîstemê ve nayê destûrdan.',
+ },
+ 'BadOrigin': {
+ en: 'Unauthorized: You do not have permission for this action.',
+ kmr: 'Destûrnîn: We destûra vê çalakiyê nîne.',
+ },
+ 'Module': {
+ en: 'A blockchain module error occurred. Please try again.',
+ kmr: 'Xeletiya module-ya blockchain-ê qewimî. Ji kerema xwe dîsa biceribîne.',
+ },
+};
+
+// ========================================
+// ERROR EXTRACTION & FORMATTING
+// ========================================
+
+/**
+ * Extract error information from DispatchError
+ */
+export function extractDispatchError(
+ api: ApiPromise,
+ dispatchError: DispatchError
+): {
+ section: string;
+ name: string;
+ docs: string;
+ raw: string;
+} {
+ if (dispatchError.isModule) {
+ const decoded = api.registry.findMetaError(dispatchError.asModule);
+ return {
+ section: decoded.section,
+ name: decoded.name,
+ docs: decoded.docs.join(' ').trim(),
+ raw: `${decoded.section}.${decoded.name}`,
+ };
+ } else {
+ return {
+ section: 'Unknown',
+ name: dispatchError.type,
+ docs: dispatchError.toString(),
+ raw: dispatchError.toString(),
+ };
+ }
+}
+
+/**
+ * Get user-friendly error message
+ * Falls back to blockchain docs if no custom message exists
+ */
+export function getUserFriendlyError(
+ api: ApiPromise,
+ dispatchError: DispatchError,
+ language: 'en' | 'kmr' = 'en'
+): string {
+ const errorInfo = extractDispatchError(api, dispatchError);
+ const errorKey = errorInfo.raw;
+
+ // Check if we have a custom message
+ const customMessage = ERROR_MESSAGES[errorKey];
+ if (customMessage) {
+ return customMessage[language];
+ }
+
+ // Fallback to blockchain documentation
+ if (errorInfo.docs && errorInfo.docs.length > 0) {
+ return errorInfo.docs;
+ }
+
+ // Final fallback
+ return `Transaction failed: ${errorInfo.section}.${errorInfo.name}`;
+}
+
+// ========================================
+// TOAST HELPER
+// ========================================
+
+export interface ToastFunction {
+ (options: {
+ title: string;
+ description: string;
+ variant?: 'default' | 'destructive';
+ }): void;
+}
+
+/**
+ * Handle blockchain error with toast notification
+ * Automatically extracts user-friendly message
+ */
+export function handleBlockchainError(
+ error: any,
+ api: ApiPromise | null,
+ toast: ToastFunction,
+ language: 'en' | 'kmr' = 'en'
+): void {
+ console.error('Blockchain error:', error);
+
+ // If it's a dispatch error from transaction callback
+ if (error?.isModule !== undefined && api) {
+ const userMessage = getUserFriendlyError(api, error, language);
+ toast({
+ title: language === 'en' ? 'Transaction Failed' : 'Transaction Têk Çû',
+ description: userMessage,
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ // If it's a standard error object
+ if (error?.message) {
+ toast({
+ title: language === 'en' ? 'Error' : 'Xeletî',
+ description: error.message,
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ // If it's a string
+ if (typeof error === 'string') {
+ toast({
+ title: language === 'en' ? 'Error' : 'Xeletî',
+ description: error,
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ // Generic fallback
+ toast({
+ title: language === 'en' ? 'Error' : 'Xeletî',
+ description:
+ language === 'en'
+ ? 'An unexpected error occurred. Please try again.'
+ : 'Xeletîyek nediyar qewimî. Ji kerema xwe dîsa biceribîne.',
+ variant: 'destructive',
+ });
+}
+
+// ========================================
+// SUCCESS MESSAGES
+// ========================================
+
+export interface SuccessMessage {
+ en: string;
+ kmr: string;
+}
+
+export const SUCCESS_MESSAGES: Record = {
+ // Staking
+ 'staking.bonded': {
+ en: 'Successfully staked {{amount}} HEZ. Rewards will start in the next era.',
+ kmr: '{{amount}} HEZ bi serkeftî stake kirin. Xelat di era pêşîn de dest pê dike.',
+ },
+ 'staking.unbonded': {
+ en: 'Unbonded {{amount}} HEZ. Withdrawal available in {{days}} days.',
+ kmr: '{{amount}} HEZ unbond kirin. Di {{days}} rojan de derbasdarî dibe.',
+ },
+ 'staking.nominated': {
+ en: 'Successfully nominated {{count}} validators.',
+ kmr: 'Bi serkeftî {{count}} validator nomînekirin.',
+ },
+ 'staking.scoreStarted': {
+ en: 'Staking score tracking started! Your score will accumulate over time.',
+ kmr: 'Şopa staking dest pê kir! Xala we dê bi demê re kom bibe.',
+ },
+
+ // Citizenship
+ 'citizenship.applied': {
+ en: 'Citizenship application submitted successfully! We will review your application.',
+ kmr: 'Serlêdana welatîtiyê bi serkeftî hate şandin! Em ê serlêdana we binirxînin.',
+ },
+
+ // Governance
+ 'governance.voted': {
+ en: 'Your vote has been recorded successfully!',
+ kmr: 'Deng-a we bi serkeftî hate tomarkirin!',
+ },
+ 'governance.proposed': {
+ en: 'Proposal submitted successfully! Voting will begin soon.',
+ kmr: 'Pêşniyar bi serkeftî hate şandin! Dengdan hêdî dest pê dike.',
+ },
+
+ // DEX
+ 'dex.swapped': {
+ en: 'Successfully swapped {{from}} {{fromToken}} for {{to}} {{toToken}}',
+ kmr: 'Bi serkeftî {{from}} {{fromToken}} bo {{to}} {{toToken}} guhertin',
+ },
+ 'dex.liquidityAdded': {
+ en: 'Successfully added liquidity to the pool!',
+ kmr: 'Bi serkeftî liquidity li pool-ê zêde kir!',
+ },
+ 'dex.liquidityRemoved': {
+ en: 'Successfully removed liquidity from the pool!',
+ kmr: 'Bi serkeftî liquidity ji pool-ê derxist!',
+ },
+};
+
+/**
+ * Handle successful blockchain transaction
+ */
+export function handleBlockchainSuccess(
+ messageKey: string,
+ toast: ToastFunction,
+ params: Record = {},
+ language: 'en' | 'kmr' = 'en'
+): void {
+ const template = SUCCESS_MESSAGES[messageKey];
+
+ if (!template) {
+ toast({
+ title: language === 'en' ? 'Success' : 'Serkeft',
+ description: language === 'en' ? 'Transaction successful!' : 'Transaction serkeftî!',
+ });
+ return;
+ }
+
+ // Replace template variables like {{amount}}
+ let message = template[language];
+ Object.entries(params).forEach(([key, value]) => {
+ message = message.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
+ });
+
+ toast({
+ title: language === 'en' ? 'Success' : 'Serkeft',
+ description: message,
+ });
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index c53bb711..884996c1 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -19,61 +19,64 @@ import { AuthProvider } from '@/contexts/AuthContext';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import NotFound from '@/pages/NotFound';
import { Toaster } from '@/components/ui/toaster';
+import { ErrorBoundary } from '@/components/ErrorBoundary';
import './App.css';
import './i18n/config';
function App() {
return (
-
-
-
-
-
-
-
-
- } />
+
+
+
+
+
+
+
+
+
+ } />
- } />
- } />
- } />
- } />
-
-
-
- } />
-
-
-
- } />
-
-
-
- } />
-
-
-
- } />
-
-
-
- } />
- } />
-
-
-
-
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+ } />
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/web/src/components/ErrorBoundary.tsx b/web/src/components/ErrorBoundary.tsx
new file mode 100644
index 00000000..383e6d33
--- /dev/null
+++ b/web/src/components/ErrorBoundary.tsx
@@ -0,0 +1,243 @@
+// ========================================
+// Error Boundary Component
+// ========================================
+// Catches React errors and displays fallback UI
+
+import React, { Component, ErrorInfo, ReactNode } from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Button } from '@/components/ui/button';
+import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+ errorInfo: ErrorInfo | null;
+}
+
+/**
+ * Global Error Boundary
+ * Catches unhandled errors in React component tree
+ *
+ * @example
+ *
+ *
+ *
+ */
+export class ErrorBoundary extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ hasError: false,
+ error: null,
+ errorInfo: null,
+ };
+ }
+
+ static getDerivedStateFromError(error: Error): Partial {
+ // Update state so next render shows fallback UI
+ return {
+ hasError: true,
+ error,
+ };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ // Log error to console
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
+
+ // Update state with error details
+ this.setState({
+ error,
+ errorInfo,
+ });
+
+ // Call custom error handler if provided
+ if (this.props.onError) {
+ this.props.onError(error, errorInfo);
+ }
+
+ // In production, you might want to log to an error reporting service
+ // Example: Sentry.captureException(error);
+ }
+
+ handleReset = (): void => {
+ this.setState({
+ hasError: false,
+ error: null,
+ errorInfo: null,
+ });
+ };
+
+ handleReload = (): void => {
+ window.location.reload();
+ };
+
+ handleGoHome = (): void => {
+ window.location.href = '/';
+ };
+
+ render(): ReactNode {
+ if (this.state.hasError) {
+ // Use custom fallback if provided
+ if (this.props.fallback) {
+ return this.props.fallback;
+ }
+
+ // Default error UI
+ return (
+
+
+
+
+
+
+ Something Went Wrong
+
+ An unexpected error occurred. We apologize for the inconvenience.
+
+ {this.state.error && (
+
+
+ Error Details (for developers)
+
+
+
+
Error:
+
+ {this.state.error.toString()}
+
+
+ {this.state.errorInfo && (
+
+
Component Stack:
+
+ {this.state.errorInfo.componentStack}
+
+
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ If this problem persists, please contact support at{' '}
+
+ info@pezkuwichain.io
+
+
+
+
+
+ );
+ }
+
+ // No error, render children normally
+ return this.props.children;
+ }
+}
+
+// ========================================
+// ROUTE-LEVEL ERROR BOUNDARY
+// ========================================
+
+/**
+ * Smaller error boundary for individual routes
+ * Less intrusive, doesn't take over the whole screen
+ */
+export const RouteErrorBoundary: React.FC<{
+ children: ReactNode;
+ routeName?: string;
+}> = ({ children, routeName = 'this page' }) => {
+ const [hasError, setHasError] = React.useState(false);
+
+ const handleReset = () => {
+ setHasError(false);
+ };
+
+ if (hasError) {
+ return (
+
+
+
+
+ Error loading {routeName}
+ An error occurred while rendering this component.
+
+
+
+
+
+
+ );
+ }
+
+ return (
+ }>
+ {children}
+
+ );
+};
+
+const RouteErrorFallback: React.FC<{ routeName: string; onReset: () => void }> = ({
+ routeName,
+ onReset,
+}) => {
+ return (
+
+
+
+
+ Error loading {routeName}
+ An unexpected error occurred.
+
+
+
+
+
+
+ );
+};