Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Built with modern web technologies and Web3 integration, this frontend serves as
### Core Capabilities
- **🏠 Property Discovery**: Browse and search tokenized real estate properties with advanced filtering
- **💰 Wallet Integration**: Connect MetaMask, WalletConnect, and other Web3 wallets seamlessly
- **🔗 Smart Contract Interaction**: Execute property purchases, transfers, and management through intuitive UI
- **� Route-Level Error Handling**: Full-page route fallback UI with retry and home navigation using `RouteErrorBoundary`
- **�🔗 Smart Contract Interaction**: Execute property purchases, transfers, and management through intuitive UI
- **📊 Real-Time Data**: Live property valuations, market trends, and portfolio analytics
- **🔐 Web3 Authentication**: Secure wallet-based authentication with multi-network support
- **� Responsive Design**: Mobile-first design that works perfectly on all devices
Expand Down
1 change: 1 addition & 0 deletions docs/adr/ADR-005-error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class ValidationError extends Error {

- `<RootErrorBoundary>` wraps the entire app and shows a full-page error screen for catastrophic failures
- `<SectionErrorBoundary>` wraps major page sections (property list, wallet panel) and shows inline fallback UI
- `<RouteErrorBoundary>` renders route-level full-screen fallback UI for page-specific failures with retry and home navigation
- `<ComponentErrorBoundary>` wraps individual widgets and renders a compact error state

### Global Error Handler
Expand Down
45 changes: 6 additions & 39 deletions src/components/error/RouteErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Home, RefreshCw, AlertTriangle, Search } from 'lucide-react';
import type { AppError } from '@/types/errors';
import { ErrorCategory } from '@/types/errors';
import { getWalletErrorMessage } from '@/utils/errorHandling';
import { getRouteErrorIconConfig, getRouteErrorTitle } from './routeErrorHelpers';

interface RouteErrorBoundaryProps {
error: AppError;
Expand All @@ -27,51 +28,17 @@ const RouteErrorFallback: React.FC<RouteErrorFallbackProps> = ({
onRetry,
onNavigateHome,
}) => {
const getErrorIcon = () => {
switch (error.category) {
case ErrorCategory.WEB3:
return (
<div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-8 h-8 text-orange-600 dark:text-orange-400" />
</div>
);
case ErrorCategory.NETWORK:
return (
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-8 h-8 text-red-600 dark:text-red-400" />
</div>
);
default:
return (
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-8 h-8 text-gray-600 dark:text-gray-400" />
</div>
);
}
};
const { bgClass, iconClass } = getRouteErrorIconConfig(error.category);

const getErrorTitle = () => {
switch (error.category) {
case ErrorCategory.WEB3:
return 'Wallet Connection Error';
case ErrorCategory.NETWORK:
return 'Network Error';
case ErrorCategory.VALIDATION:
return 'Validation Error';
case ErrorCategory.PERMISSION:
return 'Permission Error';
case ErrorCategory.AUTHENTICATION:
return 'Authentication Error';
default:
return 'Something went wrong';
}
};
const getErrorTitle = () => getRouteErrorTitle(error.category)

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full mx-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8 text-center">
{getErrorIcon()}
<div className={`w-16 h-16 ${bgClass} rounded-full flex items-center justify-center mx-auto mb-4`}>
<AlertTriangle className={`w-8 h-8 ${iconClass}`} />
</div>

<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
{getErrorTitle()}
Expand Down
67 changes: 67 additions & 0 deletions src/components/error/__tests__/RouteErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const pushMock = jest.fn()

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: pushMock,
replace: jest.fn(),
refresh: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
prefetch: jest.fn(),
}),
useSearchParams: () => new URLSearchParams(),
usePathname: () => '/',
}))

import React from 'react'
import userEvent from '@testing-library/user-event'
import { render, screen } from '@testing-library/react'
import { RouteErrorBoundary } from '@/components/error/RouteErrorBoundary'
import { ErrorCategory, ErrorSeverity, type AppError } from '@/types/errors'

describe('RouteErrorBoundary', () => {
const resetError = jest.fn()
const mockError = {
id: 'error-123',
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.HIGH,
message: 'Unable to reach the network',
userMessage: 'Cannot reach network at this time.',
timestamp: new Date(),
context: { endpoint: '/properties' },
isRecoverable: false,
shouldReport: true,
} as AppError

beforeEach(() => {
pushMock.mockClear()
resetError.mockClear()
})

it('renders the expected error UI for a network error', () => {
render(<RouteErrorBoundary error={mockError} routeName="Properties" resetError={resetError} />)

expect(screen.getByRole('heading', { name: /Network Error/i })).toBeInTheDocument()
expect(screen.getByText(/Cannot reach network at this time./i)).toBeInTheDocument()
expect(screen.getByText(/Error in:/i)).toBeInTheDocument()
expect(screen.getByText('Properties')).toBeInTheDocument()
expect(screen.getByText(/Error Details/i)).toBeInTheDocument()
})

it('calls resetError when the retry button is clicked', async () => {
render(<RouteErrorBoundary error={mockError} routeName="Properties" resetError={resetError} />)

await userEvent.click(screen.getByRole('button', { name: /try again/i }))

expect(resetError).toHaveBeenCalledTimes(1)
})

it('navigates home and resets the error when the home button is clicked', async () => {
render(<RouteErrorBoundary error={mockError} routeName="Properties" resetError={resetError} />)

await userEvent.click(screen.getByRole('button', { name: /go home/i }))

expect(pushMock).toHaveBeenCalledWith('/')
expect(resetError).toHaveBeenCalledTimes(1)
})
})
41 changes: 41 additions & 0 deletions src/components/error/__tests__/routeErrorHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ErrorCategory } from '@/types/errors'
import { getRouteErrorIconConfig, getRouteErrorTitle } from '@/components/error/routeErrorHelpers'

describe('routeErrorHelpers', () => {
describe('getRouteErrorTitle', () => {
it('returns the expected title for WEB3 errors', () => {
expect(getRouteErrorTitle(ErrorCategory.WEB3)).toBe('Wallet Connection Error')
})

it('returns the expected title for NETWORK errors', () => {
expect(getRouteErrorTitle(ErrorCategory.NETWORK)).toBe('Network Error')
})

it('returns a fallback title for unknown error categories', () => {
expect(getRouteErrorTitle(ErrorCategory.UNKNOWN)).toBe('Something went wrong')
})
})

describe('getRouteErrorIconConfig', () => {
it('returns orange styling for WEB3 errors', () => {
expect(getRouteErrorIconConfig(ErrorCategory.WEB3)).toEqual({
bgClass: 'bg-orange-100 dark:bg-orange-900/20',
iconClass: 'text-orange-600 dark:text-orange-400',
})
})

it('returns red styling for NETWORK errors', () => {
expect(getRouteErrorIconConfig(ErrorCategory.NETWORK)).toEqual({
bgClass: 'bg-red-100 dark:bg-red-900/20',
iconClass: 'text-red-600 dark:text-red-400',
})
})

it('returns neutral styling for unknown categories', () => {
expect(getRouteErrorIconConfig(ErrorCategory.UNKNOWN)).toEqual({
bgClass: 'bg-gray-100 dark:bg-gray-800',
iconClass: 'text-gray-600 dark:text-gray-400',
})
})
})
})
43 changes: 43 additions & 0 deletions src/components/error/routeErrorHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ErrorCategory } from '@/types/errors'

export interface RouteErrorIconConfig {
bgClass: string
iconClass: string
}

export const getRouteErrorIconConfig = (category: ErrorCategory): RouteErrorIconConfig => {
switch (category) {
case ErrorCategory.WEB3:
return {
bgClass: 'bg-orange-100 dark:bg-orange-900/20',
iconClass: 'text-orange-600 dark:text-orange-400',
}
case ErrorCategory.NETWORK:
return {
bgClass: 'bg-red-100 dark:bg-red-900/20',
iconClass: 'text-red-600 dark:text-red-400',
}
default:
return {
bgClass: 'bg-gray-100 dark:bg-gray-800',
iconClass: 'text-gray-600 dark:text-gray-400',
}
}
}

export const getRouteErrorTitle = (category: ErrorCategory): string => {
switch (category) {
case ErrorCategory.WEB3:
return 'Wallet Connection Error'
case ErrorCategory.NETWORK:
return 'Network Error'
case ErrorCategory.VALIDATION:
return 'Validation Error'
case ErrorCategory.PERMISSION:
return 'Permission Error'
case ErrorCategory.AUTHENTICATION:
return 'Authentication Error'
default:
return 'Something went wrong'
}
}