2025-08-17 13:10:01 +07:00

510 lines
19 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
@RoutePage()
class ErrorPage extends StatefulWidget {
final String? title;
final String? message;
final VoidCallback? onRetry;
final VoidCallback? onBack;
final String? errorCode;
final IconData? errorIcon;
const ErrorPage({
Key? key,
this.title,
this.message,
this.onRetry,
this.onBack,
this.errorCode,
this.errorIcon,
}) : super(key: key);
@override
State<ErrorPage> createState() => _ErrorPageState();
}
class _ErrorPageState extends State<ErrorPage> with TickerProviderStateMixin {
late AnimationController _bounceController;
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _bounceAnimation;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_bounceController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_bounceAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _bounceController, curve: Curves.elasticOut),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0.0, 0.5), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
// Start animations
_bounceController.forward();
Future.delayed(const Duration(milliseconds: 300), () {
_fadeController.forward();
});
Future.delayed(const Duration(milliseconds: 500), () {
_slideController.forward();
});
}
@override
void dispose() {
_bounceController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColor.primary.withOpacity(0.05),
AppColor.background,
AppColor.primaryLight.withOpacity(0.03),
],
),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 60),
// Animated Error Illustration
AnimatedBuilder(
animation: _bounceAnimation,
builder: (context, child) {
return Transform.scale(
scale: _bounceAnimation.value,
child: Stack(
alignment: Alignment.center,
children: [
// Outer glow circle
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColor.error.withOpacity(0.1),
Colors.transparent,
],
),
),
),
// Middle circle with gradient
Container(
width: 140,
height: 140,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColor.error.withOpacity(0.15),
AppColor.warning.withOpacity(0.15),
AppColor.primary.withOpacity(0.1),
],
),
boxShadow: [
BoxShadow(
color: AppColor.error.withOpacity(0.2),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
),
),
// Inner circle with icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: AppColor.primaryGradient,
),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Icon(
widget.errorIcon ?? Icons.sentiment_dissatisfied,
size: 50,
color: AppColor.textWhite,
),
),
// Decorative floating dots
...List.generate(6, (index) {
final radius = 120.0;
return Positioned(
left:
100 +
radius *
0.8 *
(index.isEven ? 1 : -1) *
(index / 6),
top:
100 +
radius *
0.6 *
(index.isOdd ? 1 : -1) *
(index / 6),
child: Container(
width: 8 + (index % 3) * 4,
height: 8 + (index % 3) * 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: [
AppColor.primary,
AppColor.error,
AppColor.warning,
][index % 3].withOpacity(0.6),
),
),
);
}),
],
),
);
},
),
const SizedBox(height: 40),
// Animated Title
FadeTransition(
opacity: _fadeAnimation,
child: Text(
widget.title ?? 'Ups! Ada Yang Salah',
style: AppStyle.h2.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary, // warna solid
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
// Animated Message
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
border: Border.all(
color: AppColor.border.withOpacity(0.5),
),
),
child: Column(
children: [
Text(
widget.message ??
'Sepertinya ada masalah teknis yang tidak terduga. Jangan khawatir, tim kami sedang bekerja keras untuk memperbaikinya!',
style: AppStyle.lg.copyWith(
color: AppColor.textSecondary,
height: 1.6,
),
textAlign: TextAlign.center,
),
if (widget.errorCode != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.error.withOpacity(0.1),
AppColor.warning.withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColor.error.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.code,
size: 16,
color: AppColor.error,
),
const SizedBox(width: 8),
Text(
'Kode Error: ${widget.errorCode}',
style: AppStyle.sm.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
],
),
),
),
),
const SizedBox(height: 50),
// Animated Buttons
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
children: [
// Retry Button with gradient
if (widget.onRetry != null)
Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: ElevatedButton.icon(
onPressed: widget.onRetry,
icon: const Icon(
Icons.refresh_rounded,
color: AppColor.textWhite,
size: 24,
),
label: Text(
'Coba Lagi',
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
const SizedBox(height: 16),
// Back Button with modern design
if (widget.onBack != null)
Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 2,
color: AppColor.primary.withOpacity(0.3),
),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: OutlinedButton.icon(
onPressed: widget.onBack,
icon: Icon(
Icons.arrow_back_ios_rounded,
color: AppColor.primary,
size: 20,
),
label: Text(
'Kembali',
style: AppStyle.xl.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
style: OutlinedButton.styleFrom(
backgroundColor: Colors.transparent,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
],
),
),
),
const SizedBox(height: 40),
// Help text with icon
FadeTransition(
opacity: _fadeAnimation,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.info.withOpacity(0.1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.help_outline_rounded,
size: 18,
color: AppColor.info,
),
const SizedBox(width: 8),
Flexible(
child: Text(
'Butuh bantuan? Hubungi tim support kami',
style: AppStyle.sm.copyWith(
color: AppColor.info,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
),
),
],
),
),
),
),
);
}
}
// Usage Example dengan berbagai variasi
class ErrorPageExamples extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Network Error
ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ErrorPage(
title: 'Koneksi Terputus',
message:
'Sepertinya koneksi internet Anda bermasalah. Periksa jaringan dan coba lagi.',
errorCode: 'NET_404',
errorIcon: Icons.wifi_off_rounded,
onRetry: () => Navigator.pop(context),
onBack: () => Navigator.pop(context),
),
),
),
child: Text('Network Error'),
),
// Server Error
ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ErrorPage(
title: 'Server Sibuk',
message:
'Server sedang mengalami gangguan. Tim kami sedang memperbaikinya.',
errorCode: 'SRV_500',
errorIcon: Icons.dns_rounded,
onRetry: () => Navigator.pop(context),
onBack: () => Navigator.pop(context),
),
),
),
child: Text('Server Error'),
),
],
);
}
}