/// Widget for displaying user-friendly error messages with retry capability library error_display_widget; import 'package:flutter/material.dart'; import '../../core/error/failures.dart'; import '../design_system/tokens/app_colors.dart'; /// Error display widget that shows failures in a user-friendly way class ErrorDisplayWidget extends StatelessWidget { final Failure failure; final VoidCallback? onRetry; final bool showRetryButton; const ErrorDisplayWidget({ super.key, required this.failure, this.onRetry, this.showRetryButton = true, }); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Error icon Icon( _getErrorIcon(), size: 40, color: _getErrorColor(context), ), const SizedBox(height: 12), // Error title Text( _getErrorTitle(), style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, color: _getErrorColor(context), ), textAlign: TextAlign.center, ), const SizedBox(height: 12), // Error message Text( failure.getUserMessage(), style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), // Retry button (if retryable and callback provided) if (showRetryButton && failure.isRetryable && onRetry != null) ...[ const SizedBox(height: 16), ElevatedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 10, ), ), ), ], ], ), ), ); } /// Get appropriate icon for error type IconData _getErrorIcon() { if (failure is NetworkFailure) { return Icons.wifi_off; } else if (failure is UnauthorizedFailure) { return Icons.lock_outline; } else if (failure is ForbiddenFailure) { return Icons.block; } else if (failure is NotFoundFailure) { return Icons.search_off; } else if (failure is ValidationFailure) { return Icons.error_outline; } else if (failure is ServerFailure) { return Icons.cloud_off; } else if (failure is NotImplementedFailure) { return Icons.construction; } else { return Icons.warning_amber; } } /// Get appropriate color for error type Color _getErrorColor(BuildContext context) { if (failure is NetworkFailure) { return AppColors.warning; } else if (failure is UnauthorizedFailure) { return AppColors.error; } else if (failure is ForbiddenFailure) { return AppColors.warning; } else if (failure is ValidationFailure) { return AppColors.warningUI; } else if (failure is NotImplementedFailure) { return AppColors.info; } else { return Theme.of(context).colorScheme.error; } } /// Get appropriate title for error type String _getErrorTitle() { if (failure is NetworkFailure) { return 'Problème de connexion'; } else if (failure is UnauthorizedFailure) { return 'Session expirée'; } else if (failure is ForbiddenFailure) { return 'Accès refusé'; } else if (failure is NotFoundFailure) { return 'Non trouvé'; } else if (failure is ValidationFailure) { return 'Données invalides'; } else if (failure is ServerFailure) { return 'Erreur serveur'; } else if (failure is NotImplementedFailure) { return 'Bientôt disponible'; } else { return 'Une erreur est survenue'; } } } /// Compact error banner for inline display class ErrorBanner extends StatelessWidget { final Failure failure; final VoidCallback? onRetry; final VoidCallback? onDismiss; const ErrorBanner({ super.key, required this.failure, this.onRetry, this.onDismiss, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: _getErrorColor(context).withOpacity(0.1), border: Border.all( color: _getErrorColor(context), width: 1, ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( _getErrorIcon(), color: _getErrorColor(context), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _getErrorTitle(), style: TextStyle( fontWeight: FontWeight.bold, color: _getErrorColor(context), ), ), const SizedBox(height: 4), Text( failure.getUserMessage(), style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), if (failure.isRetryable && onRetry != null) TextButton( onPressed: onRetry, child: const Text('Réessayer'), ), if (onDismiss != null) IconButton( icon: const Icon(Icons.close, size: 20), onPressed: onDismiss, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ); } IconData _getErrorIcon() { if (failure is NetworkFailure) { return Icons.wifi_off; } else if (failure is UnauthorizedFailure) { return Icons.lock_outline; } else if (failure is ForbiddenFailure) { return Icons.block; } else if (failure is NotFoundFailure) { return Icons.search_off; } else if (failure is ValidationFailure) { return Icons.error_outline; } else if (failure is ServerFailure) { return Icons.cloud_off; } else if (failure is NotImplementedFailure) { return Icons.construction; } else { return Icons.warning_amber; } } Color _getErrorColor(BuildContext context) { if (failure is NetworkFailure) { return AppColors.warning; } else if (failure is UnauthorizedFailure) { return AppColors.error; } else if (failure is ForbiddenFailure) { return AppColors.warning; } else if (failure is ValidationFailure) { return AppColors.warningUI; } else if (failure is NotImplementedFailure) { return AppColors.info; } else { return Theme.of(context).colorScheme.error; } } String _getErrorTitle() { if (failure is NetworkFailure) { return 'Problème de connexion'; } else if (failure is UnauthorizedFailure) { return 'Session expirée'; } else if (failure is ForbiddenFailure) { return 'Accès refusé'; } else if (failure is NotFoundFailure) { return 'Non trouvé'; } else if (failure is ValidationFailure) { return 'Données invalides'; } else if (failure is ServerFailure) { return 'Erreur serveur'; } else if (failure is NotImplementedFailure) { return 'Bientôt disponible'; } else { return 'Erreur'; } } } /// Show error as a SnackBar void showErrorSnackBar( BuildContext context, Failure failure, { VoidCallback? onRetry, }) { final snackBar = SnackBar( content: Row( children: [ Icon( failure is NetworkFailure ? Icons.wifi_off : Icons.error_outline, color: Colors.white, ), const SizedBox(width: 12), Expanded( child: Text(failure.getUserMessage()), ), ], ), backgroundColor: failure is NetworkFailure ? AppColors.warning : AppColors.error, behavior: SnackBarBehavior.floating, action: failure.isRetryable && onRetry != null ? SnackBarAction( label: 'Réessayer', textColor: Colors.white, onPressed: onRetry, ) : null, duration: Duration(seconds: failure.isRetryable ? 6 : 4), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); }