feat: add reusable loading, error, and not-found widgets

LoadingErrorStates widgets:
- FullScreenLoading: Full-screen loading spinner for initial loads
- FullScreenError: Full-screen error with title, message, and retry
- InlineLoading: Inline loading indicator for pagination
- InlineError: Inline error with retry for pagination
- NotFoundError: User-friendly not-found screen with back navigation

ErrorMessages utility:
- Centralized error message transformation
- Converts technical errors to user-friendly messages
- Handles network errors, timeouts, HTTP status codes
- Provides consistent error UX across the app

All widgets follow Material Design patterns and maintain
consistent styling with AppColors theme.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+240
lib
+27
lib/utils/error_messages.dart
···
···
+
/// Utility class for transforming technical error messages into user-friendly ones
+
class ErrorMessages {
+
/// Transform technical error messages into user-friendly ones
+
static String getUserFriendly(String error) {
+
final lowerError = error.toLowerCase();
+
+
if (lowerError.contains('socketexception') ||
+
lowerError.contains('network') ||
+
lowerError.contains('connection refused')) {
+
return 'Please check your internet connection';
+
} else if (lowerError.contains('timeoutexception') ||
+
lowerError.contains('timeout')) {
+
return 'Request timed out. Please try again';
+
} else if (lowerError.contains('401') ||
+
lowerError.contains('unauthorized')) {
+
return 'Authentication failed. Please sign in again';
+
} else if (lowerError.contains('404') || lowerError.contains('not found')) {
+
return 'Content not found';
+
} else if (lowerError.contains('500') ||
+
lowerError.contains('internal server')) {
+
return 'Server error. Please try again later';
+
}
+
+
// Fallback to generic message for unknown errors
+
return 'Something went wrong. Please try again';
+
}
+
}
+213
lib/widgets/loading_error_states.dart
···
···
+
import 'package:flutter/material.dart';
+
+
import '../constants/app_colors.dart';
+
+
/// Full-screen loading indicator
+
class FullScreenLoading extends StatelessWidget {
+
const FullScreenLoading({super.key});
+
+
@override
+
Widget build(BuildContext context) {
+
return const Center(
+
child: CircularProgressIndicator(color: AppColors.primary),
+
);
+
}
+
}
+
+
/// Full-screen error state with retry button
+
class FullScreenError extends StatelessWidget {
+
const FullScreenError({
+
required this.message,
+
required this.onRetry,
+
this.title = 'Failed to load',
+
super.key,
+
});
+
+
final String title;
+
final String message;
+
final VoidCallback onRetry;
+
+
@override
+
Widget build(BuildContext context) {
+
return Center(
+
child: Padding(
+
padding: const EdgeInsets.all(24),
+
child: Column(
+
mainAxisAlignment: MainAxisAlignment.center,
+
children: [
+
const Icon(
+
Icons.error_outline,
+
size: 64,
+
color: AppColors.primary,
+
),
+
const SizedBox(height: 16),
+
Text(
+
title,
+
style: const TextStyle(
+
fontSize: 20,
+
color: AppColors.textPrimary,
+
fontWeight: FontWeight.bold,
+
),
+
),
+
const SizedBox(height: 8),
+
Text(
+
message,
+
style: const TextStyle(
+
fontSize: 14,
+
color: AppColors.textSecondary,
+
),
+
textAlign: TextAlign.center,
+
),
+
const SizedBox(height: 24),
+
ElevatedButton(
+
onPressed: onRetry,
+
style: ElevatedButton.styleFrom(
+
backgroundColor: AppColors.primary,
+
),
+
child: const Text('Retry'),
+
),
+
],
+
),
+
),
+
);
+
}
+
}
+
+
/// Inline loading indicator (for pagination)
+
class InlineLoading extends StatelessWidget {
+
const InlineLoading({super.key});
+
+
@override
+
Widget build(BuildContext context) {
+
return const Center(
+
child: Padding(
+
padding: EdgeInsets.all(16),
+
child: CircularProgressIndicator(
+
color: AppColors.primary,
+
),
+
),
+
);
+
}
+
}
+
+
/// Inline error state with retry button (for pagination)
+
class InlineError extends StatelessWidget {
+
const InlineError({
+
required this.message,
+
required this.onRetry,
+
super.key,
+
});
+
+
final String message;
+
final VoidCallback onRetry;
+
+
@override
+
Widget build(BuildContext context) {
+
return Container(
+
margin: const EdgeInsets.all(16),
+
padding: const EdgeInsets.all(16),
+
decoration: BoxDecoration(
+
color: AppColors.background,
+
borderRadius: BorderRadius.circular(8),
+
border: Border.all(color: AppColors.primary),
+
),
+
child: Column(
+
children: [
+
const Icon(
+
Icons.error_outline,
+
color: AppColors.primary,
+
size: 32,
+
),
+
const SizedBox(height: 8),
+
Text(
+
message,
+
style: const TextStyle(
+
color: AppColors.textSecondary,
+
fontSize: 14,
+
),
+
textAlign: TextAlign.center,
+
),
+
const SizedBox(height: 12),
+
TextButton(
+
onPressed: onRetry,
+
style: TextButton.styleFrom(
+
foregroundColor: AppColors.primary,
+
),
+
child: const Text('Retry'),
+
),
+
],
+
),
+
);
+
}
+
}
+
+
/// Full-screen not found error state with back button
+
///
+
/// Used when a resource cannot be found (e.g., missing post in route extras).
+
/// Provides a user-friendly message and navigation option.
+
class NotFoundError extends StatelessWidget {
+
const NotFoundError({
+
required this.title,
+
required this.message,
+
required this.onBackPressed,
+
super.key,
+
});
+
+
final String title;
+
final String message;
+
final VoidCallback onBackPressed;
+
+
@override
+
Widget build(BuildContext context) {
+
return Scaffold(
+
backgroundColor: AppColors.background,
+
appBar: AppBar(
+
backgroundColor: AppColors.background,
+
foregroundColor: AppColors.textPrimary,
+
title: const Text('Not Found'),
+
elevation: 0,
+
),
+
body: Center(
+
child: Padding(
+
padding: const EdgeInsets.all(24),
+
child: Column(
+
mainAxisAlignment: MainAxisAlignment.center,
+
children: [
+
const Icon(
+
Icons.search_off,
+
size: 64,
+
color: AppColors.primary,
+
),
+
const SizedBox(height: 16),
+
Text(
+
title,
+
style: const TextStyle(
+
fontSize: 20,
+
color: AppColors.textPrimary,
+
fontWeight: FontWeight.bold,
+
),
+
),
+
const SizedBox(height: 8),
+
Text(
+
message,
+
style: const TextStyle(
+
fontSize: 14,
+
color: AppColors.textSecondary,
+
),
+
textAlign: TextAlign.center,
+
),
+
const SizedBox(height: 24),
+
ElevatedButton(
+
onPressed: onBackPressed,
+
style: ElevatedButton.styleFrom(
+
backgroundColor: AppColors.primary,
+
),
+
child: const Text('Go Back'),
+
),
+
],
+
),
+
),
+
),
+
);
+
}
+
}