From a56001ae2cf7b595ab2e4ec367170bfe0a2eada1 Mon Sep 17 00:00:00 2001 From: Vetrichelvan Date: Mon, 21 Jul 2025 22:47:24 +0530 Subject: [PATCH 1/2] feat: Refactor error handling and improve UI components with animations --- .taskmaster/tasks/tasks.json | 4 +- lib/app.dart | 65 ++- .../authentication/auth_events.dart | 12 +- .../authentication/auth_events.freezed.dart | 75 +--- .../authentication/auth_state_controller.dart | 3 +- .../authentication/auth_states.freezed.dart | 83 ++-- lib/core/theme/animated_widgets.dart | 263 +++++++++++ lib/core/theme/app_theme.dart | 263 +++++++++++ lib/domain/authentication/auth_failures.dart | 3 +- .../authentication/auth_failures.freezed.dart | 34 +- .../authentication/auth_value_failures.dart | 15 +- .../auth_value_failures.freezed.dart | 75 ++-- lib/domain/authentication/i_auth_facade.dart | 1 - lib/domain/core/errors.dart | 3 +- lib/domain/core/value_object.dart | 4 +- lib/firebase_options.dart | 3 +- lib/screens/home_page.dart | 425 +++++++++++++++++- lib/screens/login_page.dart | 328 ++++++++++---- lib/screens/registration_page.dart | 336 ++++++++++++++ 19 files changed, 1671 insertions(+), 324 deletions(-) create mode 100644 lib/core/theme/animated_widgets.dart create mode 100644 lib/core/theme/app_theme.dart create mode 100644 lib/screens/registration_page.dart diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 029b6f5..9e5dc5e 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -13,7 +13,7 @@ "id": 2, "title": "Modernize the User Interface", "description": "Redesign the login, registration, and home pages to be more modern and visually appealing. Adopt a clean design language like Material You, create a consistent theme, and add animations to enhance the user experience.", - "status": "pending", + "status": "done", "priority": "high", "dependencies": [ 1 @@ -92,7 +92,7 @@ ], "metadata": { "created": "2025-07-21T07:47:36.467Z", - "updated": "2025-07-21T10:19:22.468Z", + "updated": "2025-07-21T13:00:48.148Z", "description": "Tasks for master context" } } diff --git a/lib/app.dart b/lib/app.dart index 74b6a04..1525c92 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,19 +1,74 @@ import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; -import "Screens/login_page.dart"; +import "core/theme/app_theme.dart"; +import "screens/login_page.dart"; class FirebaseAuthenticationDDD extends StatelessWidget { - const FirebaseAuthenticationDDD({Key? key}) : super(key: key); + const FirebaseAuthenticationDDD({super.key}); @override Widget build(BuildContext context) { + // Set system UI overlay style + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + statusBarBrightness: Brightness.light, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.dark, + ), + ); + return MaterialApp( + title: "Firebase Auth DDD", debugShowCheckedModeBanner: kDebugMode, - theme: ThemeData( - useMaterial3: true, - ), + + // Apply our modern Material You theme system + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + + // Improved route transitions + onGenerateRoute: (settings) { + switch (settings.name) { + case "/": + return _createRoute(LoginPage()); + default: + return _createRoute(LoginPage()); + } + }, + home: LoginPage(), ); } + + // Custom page route with smooth transitions + PageRoute _createRoute(Widget page) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(0.0, 0.1); + const end = Offset.zero; + const curve = Curves.easeOutCubic; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + var fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: animation, curve: curve), + ); + + return SlideTransition( + position: animation.drive(tween), + child: FadeTransition( + opacity: fadeAnimation, + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 400), + ); + } } diff --git a/lib/application/authentication/auth_events.dart b/lib/application/authentication/auth_events.dart index 7c6dbfa..15250cb 100644 --- a/lib/application/authentication/auth_events.dart +++ b/lib/application/authentication/auth_events.dart @@ -4,15 +4,11 @@ part "auth_events.freezed.dart"; @freezed class AuthEvents with _$AuthEvents { - const factory AuthEvents.emailChanged({required String? email}) = - EmailChanged; + const factory AuthEvents.emailChanged({required String? email}) = EmailChanged; - const factory AuthEvents.passwordChanged({required String? password}) = - PasswordChanged; + const factory AuthEvents.passwordChanged({required String? password}) = PasswordChanged; - const factory AuthEvents.signUpWithEmailAndPasswordPressed() = - SignUPWithEmailAndPasswordPressed; + const factory AuthEvents.signUpWithEmailAndPasswordPressed() = SignUPWithEmailAndPasswordPressed; - const factory AuthEvents.signInWithEmailAndPasswordPressed() = - SignInWithEmailAndPasswordPressed; + const factory AuthEvents.signInWithEmailAndPasswordPressed() = SignInWithEmailAndPasswordPressed; } diff --git a/lib/application/authentication/auth_events.freezed.dart b/lib/application/authentication/auth_events.freezed.dart index 45e1a77..bef2cbc 100644 --- a/lib/application/authentication/auth_events.freezed.dart +++ b/lib/application/authentication/auth_events.freezed.dart @@ -16,8 +16,7 @@ T _$identity(T value) => value; mixin _$AuthEvents { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is AuthEvents); + return identical(this, other) || (other.runtimeType == runtimeType && other is AuthEvents); } @override @@ -52,10 +51,8 @@ extension AuthEventsPatterns on AuthEvents { TResult maybeMap({ TResult Function(EmailChanged value)? emailChanged, TResult Function(PasswordChanged value)? passwordChanged, - TResult Function(SignUPWithEmailAndPasswordPressed value)? - signUpWithEmailAndPasswordPressed, - TResult Function(SignInWithEmailAndPasswordPressed value)? - signInWithEmailAndPasswordPressed, + TResult Function(SignUPWithEmailAndPasswordPressed value)? signUpWithEmailAndPasswordPressed, + TResult Function(SignInWithEmailAndPasswordPressed value)? signInWithEmailAndPasswordPressed, required TResult orElse(), }) { final _that = this; @@ -64,11 +61,9 @@ extension AuthEventsPatterns on AuthEvents { return emailChanged(_that); case PasswordChanged() when passwordChanged != null: return passwordChanged(_that); - case SignUPWithEmailAndPasswordPressed() - when signUpWithEmailAndPasswordPressed != null: + case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: return signUpWithEmailAndPasswordPressed(_that); - case SignInWithEmailAndPasswordPressed() - when signInWithEmailAndPasswordPressed != null: + case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: return signInWithEmailAndPasswordPressed(_that); case _: return orElse(); @@ -92,10 +87,8 @@ extension AuthEventsPatterns on AuthEvents { TResult map({ required TResult Function(EmailChanged value) emailChanged, required TResult Function(PasswordChanged value) passwordChanged, - required TResult Function(SignUPWithEmailAndPasswordPressed value) - signUpWithEmailAndPasswordPressed, - required TResult Function(SignInWithEmailAndPasswordPressed value) - signInWithEmailAndPasswordPressed, + required TResult Function(SignUPWithEmailAndPasswordPressed value) signUpWithEmailAndPasswordPressed, + required TResult Function(SignInWithEmailAndPasswordPressed value) signInWithEmailAndPasswordPressed, }) { final _that = this; switch (_that) { @@ -128,10 +121,8 @@ extension AuthEventsPatterns on AuthEvents { TResult? mapOrNull({ TResult? Function(EmailChanged value)? emailChanged, TResult? Function(PasswordChanged value)? passwordChanged, - TResult? Function(SignUPWithEmailAndPasswordPressed value)? - signUpWithEmailAndPasswordPressed, - TResult? Function(SignInWithEmailAndPasswordPressed value)? - signInWithEmailAndPasswordPressed, + TResult? Function(SignUPWithEmailAndPasswordPressed value)? signUpWithEmailAndPasswordPressed, + TResult? Function(SignInWithEmailAndPasswordPressed value)? signInWithEmailAndPasswordPressed, }) { final _that = this; switch (_that) { @@ -139,11 +130,9 @@ extension AuthEventsPatterns on AuthEvents { return emailChanged(_that); case PasswordChanged() when passwordChanged != null: return passwordChanged(_that); - case SignUPWithEmailAndPasswordPressed() - when signUpWithEmailAndPasswordPressed != null: + case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: return signUpWithEmailAndPasswordPressed(_that); - case SignInWithEmailAndPasswordPressed() - when signInWithEmailAndPasswordPressed != null: + case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: return signInWithEmailAndPasswordPressed(_that); case _: return null; @@ -176,11 +165,9 @@ extension AuthEventsPatterns on AuthEvents { return emailChanged(_that.email); case PasswordChanged() when passwordChanged != null: return passwordChanged(_that.password); - case SignUPWithEmailAndPasswordPressed() - when signUpWithEmailAndPasswordPressed != null: + case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: return signUpWithEmailAndPasswordPressed(); - case SignInWithEmailAndPasswordPressed() - when signInWithEmailAndPasswordPressed != null: + case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: return signInWithEmailAndPasswordPressed(); case _: return orElse(); @@ -247,11 +234,9 @@ extension AuthEventsPatterns on AuthEvents { return emailChanged(_that.email); case PasswordChanged() when passwordChanged != null: return passwordChanged(_that.password); - case SignUPWithEmailAndPasswordPressed() - when signUpWithEmailAndPasswordPressed != null: + case SignUPWithEmailAndPasswordPressed() when signUpWithEmailAndPasswordPressed != null: return signUpWithEmailAndPasswordPressed(); - case SignInWithEmailAndPasswordPressed() - when signInWithEmailAndPasswordPressed != null: + case SignInWithEmailAndPasswordPressed() when signInWithEmailAndPasswordPressed != null: return signInWithEmailAndPasswordPressed(); case _: return null; @@ -270,8 +255,7 @@ class EmailChanged implements AuthEvents { /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $EmailChangedCopyWith get copyWith => - _$EmailChangedCopyWithImpl(this, _$identity); + $EmailChangedCopyWith get copyWith => _$EmailChangedCopyWithImpl(this, _$identity); @override bool operator ==(Object other) { @@ -291,11 +275,8 @@ class EmailChanged implements AuthEvents { } /// @nodoc -abstract mixin class $EmailChangedCopyWith<$Res> - implements $AuthEventsCopyWith<$Res> { - factory $EmailChangedCopyWith( - EmailChanged value, $Res Function(EmailChanged) _then) = - _$EmailChangedCopyWithImpl; +abstract mixin class $EmailChangedCopyWith<$Res> implements $AuthEventsCopyWith<$Res> { + factory $EmailChangedCopyWith(EmailChanged value, $Res Function(EmailChanged) _then) = _$EmailChangedCopyWithImpl; @useResult $Res call({String? email}); } @@ -341,8 +322,7 @@ class PasswordChanged implements AuthEvents { return identical(this, other) || (other.runtimeType == runtimeType && other is PasswordChanged && - (identical(other.password, password) || - other.password == password)); + (identical(other.password, password) || other.password == password)); } @override @@ -355,18 +335,15 @@ class PasswordChanged implements AuthEvents { } /// @nodoc -abstract mixin class $PasswordChangedCopyWith<$Res> - implements $AuthEventsCopyWith<$Res> { - factory $PasswordChangedCopyWith( - PasswordChanged value, $Res Function(PasswordChanged) _then) = +abstract mixin class $PasswordChangedCopyWith<$Res> implements $AuthEventsCopyWith<$Res> { + factory $PasswordChangedCopyWith(PasswordChanged value, $Res Function(PasswordChanged) _then) = _$PasswordChangedCopyWithImpl; @useResult $Res call({String? password}); } /// @nodoc -class _$PasswordChangedCopyWithImpl<$Res> - implements $PasswordChangedCopyWith<$Res> { +class _$PasswordChangedCopyWithImpl<$Res> implements $PasswordChangedCopyWith<$Res> { _$PasswordChangedCopyWithImpl(this._self, this._then); final PasswordChanged _self; @@ -394,9 +371,7 @@ class SignUPWithEmailAndPasswordPressed implements AuthEvents { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is SignUPWithEmailAndPasswordPressed); + return identical(this, other) || (other.runtimeType == runtimeType && other is SignUPWithEmailAndPasswordPressed); } @override @@ -415,9 +390,7 @@ class SignInWithEmailAndPasswordPressed implements AuthEvents { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is SignInWithEmailAndPasswordPressed); + return identical(this, other) || (other.runtimeType == runtimeType && other is SignInWithEmailAndPasswordPressed); } @override diff --git a/lib/application/authentication/auth_state_controller.dart b/lib/application/authentication/auth_state_controller.dart index bebfd99..c75151c 100644 --- a/lib/application/authentication/auth_state_controller.dart +++ b/lib/application/authentication/auth_state_controller.dart @@ -73,8 +73,7 @@ class AuthStateController extends Notifier { } Future _performAuthAction( - Future> Function( - {required EmailAddress emailAddress, required Password password}) + Future> Function({required EmailAddress emailAddress, required Password password}) forwardCall, ) async { final isEmailValid = state.emailAddress.isValid(); diff --git a/lib/application/authentication/auth_states.freezed.dart b/lib/application/authentication/auth_states.freezed.dart index 2a5a006..3ab7bc3 100644 --- a/lib/application/authentication/auth_states.freezed.dart +++ b/lib/application/authentication/auth_states.freezed.dart @@ -15,38 +15,36 @@ T _$identity(T value) => value; /// @nodoc mixin _$AuthStates { EmailAddress get emailAddress; + Password get password; + bool get isSubmitting; + bool get showError; + Option> get authFailureOrSuccess; /// Create a copy of AuthStates /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $AuthStatesCopyWith get copyWith => - _$AuthStatesCopyWithImpl(this as AuthStates, _$identity); + $AuthStatesCopyWith get copyWith => _$AuthStatesCopyWithImpl(this as AuthStates, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is AuthStates && - (identical(other.emailAddress, emailAddress) || - other.emailAddress == emailAddress) && - (identical(other.password, password) || - other.password == password) && - (identical(other.isSubmitting, isSubmitting) || - other.isSubmitting == isSubmitting) && - (identical(other.showError, showError) || - other.showError == showError) && + (identical(other.emailAddress, emailAddress) || other.emailAddress == emailAddress) && + (identical(other.password, password) || other.password == password) && + (identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting) && + (identical(other.showError, showError) || other.showError == showError) && (identical(other.authFailureOrSuccess, authFailureOrSuccess) || other.authFailureOrSuccess == authFailureOrSuccess)); } @override - int get hashCode => Object.hash(runtimeType, emailAddress, password, - isSubmitting, showError, authFailureOrSuccess); + int get hashCode => Object.hash(runtimeType, emailAddress, password, isSubmitting, showError, authFailureOrSuccess); @override String toString() { @@ -56,9 +54,8 @@ mixin _$AuthStates { /// @nodoc abstract mixin class $AuthStatesCopyWith<$Res> { - factory $AuthStatesCopyWith( - AuthStates value, $Res Function(AuthStates) _then) = - _$AuthStatesCopyWithImpl; + factory $AuthStatesCopyWith(AuthStates value, $Res Function(AuthStates) _then) = _$AuthStatesCopyWithImpl; + @useResult $Res call( {EmailAddress emailAddress, @@ -202,11 +199,7 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult maybeWhen( - TResult Function( - EmailAddress emailAddress, - Password password, - bool isSubmitting, - bool showError, + TResult Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, Option> authFailureOrSuccess)? $default, { required TResult orElse(), @@ -214,8 +207,8 @@ extension AuthStatesPatterns on AuthStates { final _that = this; switch (_that) { case _AuthStates() when $default != null: - return $default(_that.emailAddress, _that.password, _that.isSubmitting, - _that.showError, _that.authFailureOrSuccess); + return $default( + _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); case _: return orElse(); } @@ -236,19 +229,15 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult when( - TResult Function( - EmailAddress emailAddress, - Password password, - bool isSubmitting, - bool showError, + TResult Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, Option> authFailureOrSuccess) $default, ) { final _that = this; switch (_that) { case _AuthStates(): - return $default(_that.emailAddress, _that.password, _that.isSubmitting, - _that.showError, _that.authFailureOrSuccess); + return $default( + _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); } } @@ -266,19 +255,15 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult? whenOrNull( - TResult? Function( - EmailAddress emailAddress, - Password password, - bool isSubmitting, - bool showError, + TResult? Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, Option> authFailureOrSuccess)? $default, ) { final _that = this; switch (_that) { case _AuthStates() when $default != null: - return $default(_that.emailAddress, _that.password, _that.isSubmitting, - _that.showError, _that.authFailureOrSuccess); + return $default( + _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); case _: return null; } @@ -311,29 +296,23 @@ class _AuthStates implements AuthStates { @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - _$AuthStatesCopyWith<_AuthStates> get copyWith => - __$AuthStatesCopyWithImpl<_AuthStates>(this, _$identity); + _$AuthStatesCopyWith<_AuthStates> get copyWith => __$AuthStatesCopyWithImpl<_AuthStates>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _AuthStates && - (identical(other.emailAddress, emailAddress) || - other.emailAddress == emailAddress) && - (identical(other.password, password) || - other.password == password) && - (identical(other.isSubmitting, isSubmitting) || - other.isSubmitting == isSubmitting) && - (identical(other.showError, showError) || - other.showError == showError) && + (identical(other.emailAddress, emailAddress) || other.emailAddress == emailAddress) && + (identical(other.password, password) || other.password == password) && + (identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting) && + (identical(other.showError, showError) || other.showError == showError) && (identical(other.authFailureOrSuccess, authFailureOrSuccess) || other.authFailureOrSuccess == authFailureOrSuccess)); } @override - int get hashCode => Object.hash(runtimeType, emailAddress, password, - isSubmitting, showError, authFailureOrSuccess); + int get hashCode => Object.hash(runtimeType, emailAddress, password, isSubmitting, showError, authFailureOrSuccess); @override String toString() { @@ -342,11 +321,9 @@ class _AuthStates implements AuthStates { } /// @nodoc -abstract mixin class _$AuthStatesCopyWith<$Res> - implements $AuthStatesCopyWith<$Res> { - factory _$AuthStatesCopyWith( - _AuthStates value, $Res Function(_AuthStates) _then) = - __$AuthStatesCopyWithImpl; +abstract mixin class _$AuthStatesCopyWith<$Res> implements $AuthStatesCopyWith<$Res> { + factory _$AuthStatesCopyWith(_AuthStates value, $Res Function(_AuthStates) _then) = __$AuthStatesCopyWithImpl; + @override @useResult $Res call( diff --git a/lib/core/theme/animated_widgets.dart b/lib/core/theme/animated_widgets.dart new file mode 100644 index 0000000..d183beb --- /dev/null +++ b/lib/core/theme/animated_widgets.dart @@ -0,0 +1,263 @@ +import "package:flutter/material.dart"; + +class AnimatedFormField extends StatefulWidget { + final String label; + final String? hint; + final bool obscureText; + final TextInputType keyboardType; + final String? Function(String?)? validator; + final void Function(String)? onChanged; + final IconData? prefixIcon; + final Widget? suffixIcon; + final TextEditingController? controller; + + const AnimatedFormField({ + super.key, + required this.label, + this.hint, + this.obscureText = false, + this.keyboardType = TextInputType.text, + this.validator, + this.onChanged, + this.prefixIcon, + this.suffixIcon, + this.controller, + }); + + @override + State createState() => _AnimatedFormFieldState(); +} + +class _AnimatedFormFieldState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + bool _isFocused = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _scaleAnimation = Tween(begin: 0.95, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), + ); + _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Opacity( + opacity: _opacityAnimation.value, + child: Focus( + onFocusChange: (focused) { + setState(() { + _isFocused = focused; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: Matrix4.identity()..scale(_isFocused ? 1.02 : 1.0), + child: TextFormField( + controller: widget.controller, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + validator: widget.validator, + onChanged: widget.onChanged, + decoration: InputDecoration( + labelText: widget.label, + hintText: widget.hint, + prefixIcon: widget.prefixIcon != null ? Icon(widget.prefixIcon) : null, + suffixIcon: widget.suffixIcon, + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +class AnimatedButton extends StatefulWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final IconData? icon; + final ButtonStyle? style; + + const AnimatedButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.icon, + this.style, + }); + + @override + State createState() => _AnimatedButtonState(); +} + +class _AnimatedButtonState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: ElevatedButton( + onPressed: widget.isLoading ? null : widget.onPressed, + style: widget.style, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: widget.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.icon != null) ...[ + Icon(widget.icon), + const SizedBox(width: 8), + ], + Text(widget.text), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +class SlideInWidget extends StatefulWidget { + final Widget child; + final int delay; + final Offset begin; + final Duration duration; + + const SlideInWidget({ + super.key, + required this.child, + this.delay = 0, + this.begin = const Offset(0, 0.3), + this.duration = const Duration(milliseconds: 600), + }); + + @override + State createState() => _SlideInWidgetState(); +} + +class _SlideInWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _slideAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: widget.duration, + vsync: this, + ); + + _slideAnimation = Tween( + begin: widget.begin, + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + )); + + Future.delayed(Duration(milliseconds: widget.delay), () { + if (mounted) { + _animationController.forward(); + } + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: widget.child, + ), + ); + }, + ); + } +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..07b6c46 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,263 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +class AppTheme { + // Color Seed for Material You + static const Color _primarySeed = Color(0xFF6750A4); + + // Light Theme + static ThemeData get lightTheme { + final ColorScheme lightColorScheme = ColorScheme.fromSeed( + seedColor: _primarySeed, + brightness: Brightness.light, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme, + + // App Bar Theme + appBarTheme: AppBarTheme( + elevation: 0, + scrolledUnderElevation: 1, + backgroundColor: lightColorScheme.surface, + foregroundColor: lightColorScheme.onSurface, + centerTitle: true, + titleTextStyle: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + color: lightColorScheme.onSurface, + ), + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + + // Input Decoration Theme + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: lightColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: lightColorScheme.outline.withValues(alpha: 0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: lightColorScheme.primary, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: lightColorScheme.error, + width: 1, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: lightColorScheme.error, + width: 2, + ), + ), + contentPadding: const EdgeInsets.all(20), + labelStyle: TextStyle( + color: lightColorScheme.onSurfaceVariant, + fontSize: 16, + ), + hintStyle: TextStyle( + color: lightColorScheme.onSurfaceVariant.withValues(alpha: 0.6), + fontSize: 16, + ), + ), + + // Elevated Button Theme + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: lightColorScheme.primary, + foregroundColor: lightColorScheme.onPrimary, + disabledBackgroundColor: lightColorScheme.onSurface.withValues(alpha: 0.12), + disabledForegroundColor: lightColorScheme.onSurface.withValues(alpha: 0.38), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + minimumSize: const Size(0, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: lightColorScheme.primary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Card Theme + cardTheme: CardThemeData( + elevation: 0, + color: lightColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: lightColorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + margin: const EdgeInsets.all(8), + ), + + // Scaffold Background + scaffoldBackgroundColor: lightColorScheme.surface, + ); + } + + // Dark Theme + static ThemeData get darkTheme { + final ColorScheme darkColorScheme = ColorScheme.fromSeed( + seedColor: _primarySeed, + brightness: Brightness.dark, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme, + + // App Bar Theme + appBarTheme: AppBarTheme( + elevation: 0, + scrolledUnderElevation: 1, + backgroundColor: darkColorScheme.surface, + foregroundColor: darkColorScheme.onSurface, + centerTitle: true, + titleTextStyle: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + color: darkColorScheme.onSurface, + ), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + + // Input Decoration Theme + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: darkColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: darkColorScheme.outline.withValues(alpha: 0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: darkColorScheme.primary, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: darkColorScheme.error, + width: 1, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: darkColorScheme.error, + width: 2, + ), + ), + contentPadding: const EdgeInsets.all(20), + labelStyle: TextStyle( + color: darkColorScheme.onSurfaceVariant, + fontSize: 16, + ), + hintStyle: TextStyle( + color: darkColorScheme.onSurfaceVariant.withValues(alpha: 0.6), + fontSize: 16, + ), + ), + + // Elevated Button Theme + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: darkColorScheme.primary, + foregroundColor: darkColorScheme.onPrimary, + disabledBackgroundColor: darkColorScheme.onSurface.withValues(alpha: 0.12), + disabledForegroundColor: darkColorScheme.onSurface.withValues(alpha: 0.38), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + minimumSize: const Size(0, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: darkColorScheme.primary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Card Theme + cardTheme: CardThemeData( + elevation: 0, + color: darkColorScheme.surfaceContainerHigh.withValues(alpha: 0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: darkColorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + margin: const EdgeInsets.all(8), + ), + + // Scaffold Background + scaffoldBackgroundColor: darkColorScheme.surface, + ); + } +} diff --git a/lib/domain/authentication/auth_failures.dart b/lib/domain/authentication/auth_failures.dart index 63d5259..580ef23 100644 --- a/lib/domain/authentication/auth_failures.dart +++ b/lib/domain/authentication/auth_failures.dart @@ -8,6 +8,5 @@ class AuthFailures with _$AuthFailures { const factory AuthFailures.emailAlreadyInUse() = EmailAlreadyInUse; - const factory AuthFailures.invalidEmailAndPasswordCombination() = - InavalidEmailAndPasswordCombination; + const factory AuthFailures.invalidEmailAndPasswordCombination() = InavalidEmailAndPasswordCombination; } diff --git a/lib/domain/authentication/auth_failures.freezed.dart b/lib/domain/authentication/auth_failures.freezed.dart index e23f700..5bb9558 100644 --- a/lib/domain/authentication/auth_failures.freezed.dart +++ b/lib/domain/authentication/auth_failures.freezed.dart @@ -16,8 +16,7 @@ T _$identity(T value) => value; mixin _$AuthFailures { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is AuthFailures); + return identical(this, other) || (other.runtimeType == runtimeType && other is AuthFailures); } @override @@ -52,8 +51,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult maybeMap({ TResult Function(ServerError value)? serverError, TResult Function(EmailAlreadyInUse value)? emailAlreadyInUse, - TResult Function(InavalidEmailAndPasswordCombination value)? - invalidEmailAndPasswordCombination, + TResult Function(InavalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, required TResult orElse(), }) { final _that = this; @@ -62,8 +60,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(_that); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(_that); - case InavalidEmailAndPasswordCombination() - when invalidEmailAndPasswordCombination != null: + case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(_that); case _: return orElse(); @@ -87,8 +84,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult map({ required TResult Function(ServerError value) serverError, required TResult Function(EmailAlreadyInUse value) emailAlreadyInUse, - required TResult Function(InavalidEmailAndPasswordCombination value) - invalidEmailAndPasswordCombination, + required TResult Function(InavalidEmailAndPasswordCombination value) invalidEmailAndPasswordCombination, }) { final _that = this; switch (_that) { @@ -119,8 +115,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult? mapOrNull({ TResult? Function(ServerError value)? serverError, TResult? Function(EmailAlreadyInUse value)? emailAlreadyInUse, - TResult? Function(InavalidEmailAndPasswordCombination value)? - invalidEmailAndPasswordCombination, + TResult? Function(InavalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, }) { final _that = this; switch (_that) { @@ -128,8 +123,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(_that); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(_that); - case InavalidEmailAndPasswordCombination() - when invalidEmailAndPasswordCombination != null: + case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(_that); case _: return null; @@ -161,8 +155,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(); - case InavalidEmailAndPasswordCombination() - when invalidEmailAndPasswordCombination != null: + case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(); case _: return orElse(); @@ -225,8 +218,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(); - case InavalidEmailAndPasswordCombination() - when invalidEmailAndPasswordCombination != null: + case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(); case _: return null; @@ -241,8 +233,7 @@ class ServerError implements AuthFailures { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is ServerError); + return identical(this, other) || (other.runtimeType == runtimeType && other is ServerError); } @override @@ -261,8 +252,7 @@ class EmailAlreadyInUse implements AuthFailures { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is EmailAlreadyInUse); + return identical(this, other) || (other.runtimeType == runtimeType && other is EmailAlreadyInUse); } @override @@ -281,9 +271,7 @@ class InavalidEmailAndPasswordCombination implements AuthFailures { @override bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is InavalidEmailAndPasswordCombination); + return identical(this, other) || (other.runtimeType == runtimeType && other is InavalidEmailAndPasswordCombination); } @override diff --git a/lib/domain/authentication/auth_value_failures.dart b/lib/domain/authentication/auth_value_failures.dart index 6f93036..3914be3 100644 --- a/lib/domain/authentication/auth_value_failures.dart +++ b/lib/domain/authentication/auth_value_failures.dart @@ -5,18 +5,13 @@ part "auth_value_failures.freezed.dart"; @freezed sealed class AuthValueFailures with _$AuthValueFailures { - const factory AuthValueFailures.invalidEmail({required String? failedValue}) = - InvalidEmail; + const factory AuthValueFailures.invalidEmail({required String? failedValue}) = InvalidEmail; - const factory AuthValueFailures.shortPassword( - {required String? failedValue}) = ShortPassword; + const factory AuthValueFailures.shortPassword({required String? failedValue}) = ShortPassword; - const factory AuthValueFailures.noSpecialSymbol( - {required String? failedValue}) = NoSpecialSymbol; + const factory AuthValueFailures.noSpecialSymbol({required String? failedValue}) = NoSpecialSymbol; - const factory AuthValueFailures.noUpperCase({required String? failedValue}) = - NoUpperCase; + const factory AuthValueFailures.noUpperCase({required String? failedValue}) = NoUpperCase; - const factory AuthValueFailures.noNumber({required String? failedValue}) = - NoNumber; + const factory AuthValueFailures.noNumber({required String? failedValue}) = NoNumber; } diff --git a/lib/domain/authentication/auth_value_failures.freezed.dart b/lib/domain/authentication/auth_value_failures.freezed.dart index 9209fcb..f06e4be 100644 --- a/lib/domain/authentication/auth_value_failures.freezed.dart +++ b/lib/domain/authentication/auth_value_failures.freezed.dart @@ -21,16 +21,14 @@ mixin _$AuthValueFailures { @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') $AuthValueFailuresCopyWith> get copyWith => - _$AuthValueFailuresCopyWithImpl>( - this as AuthValueFailures, _$identity); + _$AuthValueFailuresCopyWithImpl>(this as AuthValueFailures, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is AuthValueFailures && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -44,16 +42,15 @@ mixin _$AuthValueFailures { /// @nodoc abstract mixin class $AuthValueFailuresCopyWith { - factory $AuthValueFailuresCopyWith(AuthValueFailures value, - $Res Function(AuthValueFailures) _then) = + factory $AuthValueFailuresCopyWith(AuthValueFailures value, $Res Function(AuthValueFailures) _then) = _$AuthValueFailuresCopyWithImpl; + @useResult $Res call({String? failedValue}); } /// @nodoc -class _$AuthValueFailuresCopyWithImpl - implements $AuthValueFailuresCopyWith { +class _$AuthValueFailuresCopyWithImpl implements $AuthValueFailuresCopyWith { _$AuthValueFailuresCopyWithImpl(this._self, this._then); final AuthValueFailures _self; @@ -321,8 +318,7 @@ class InvalidEmail implements AuthValueFailures { return identical(this, other) || (other.runtimeType == runtimeType && other is InvalidEmail && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -335,10 +331,8 @@ class InvalidEmail implements AuthValueFailures { } /// @nodoc -abstract mixin class $InvalidEmailCopyWith - implements $AuthValueFailuresCopyWith { - factory $InvalidEmailCopyWith( - InvalidEmail value, $Res Function(InvalidEmail) _then) = +abstract mixin class $InvalidEmailCopyWith implements $AuthValueFailuresCopyWith { + factory $InvalidEmailCopyWith(InvalidEmail value, $Res Function(InvalidEmail) _then) = _$InvalidEmailCopyWithImpl; @override @useResult @@ -346,8 +340,7 @@ abstract mixin class $InvalidEmailCopyWith } /// @nodoc -class _$InvalidEmailCopyWithImpl - implements $InvalidEmailCopyWith { +class _$InvalidEmailCopyWithImpl implements $InvalidEmailCopyWith { _$InvalidEmailCopyWithImpl(this._self, this._then); final InvalidEmail _self; @@ -390,8 +383,7 @@ class ShortPassword implements AuthValueFailures { return identical(this, other) || (other.runtimeType == runtimeType && other is ShortPassword && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -404,10 +396,8 @@ class ShortPassword implements AuthValueFailures { } /// @nodoc -abstract mixin class $ShortPasswordCopyWith - implements $AuthValueFailuresCopyWith { - factory $ShortPasswordCopyWith( - ShortPassword value, $Res Function(ShortPassword) _then) = +abstract mixin class $ShortPasswordCopyWith implements $AuthValueFailuresCopyWith { + factory $ShortPasswordCopyWith(ShortPassword value, $Res Function(ShortPassword) _then) = _$ShortPasswordCopyWithImpl; @override @useResult @@ -415,8 +405,7 @@ abstract mixin class $ShortPasswordCopyWith } /// @nodoc -class _$ShortPasswordCopyWithImpl - implements $ShortPasswordCopyWith { +class _$ShortPasswordCopyWithImpl implements $ShortPasswordCopyWith { _$ShortPasswordCopyWithImpl(this._self, this._then); final ShortPassword _self; @@ -459,8 +448,7 @@ class NoSpecialSymbol implements AuthValueFailures { return identical(this, other) || (other.runtimeType == runtimeType && other is NoSpecialSymbol && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -473,10 +461,8 @@ class NoSpecialSymbol implements AuthValueFailures { } /// @nodoc -abstract mixin class $NoSpecialSymbolCopyWith - implements $AuthValueFailuresCopyWith { - factory $NoSpecialSymbolCopyWith( - NoSpecialSymbol value, $Res Function(NoSpecialSymbol) _then) = +abstract mixin class $NoSpecialSymbolCopyWith implements $AuthValueFailuresCopyWith { + factory $NoSpecialSymbolCopyWith(NoSpecialSymbol value, $Res Function(NoSpecialSymbol) _then) = _$NoSpecialSymbolCopyWithImpl; @override @useResult @@ -484,8 +470,7 @@ abstract mixin class $NoSpecialSymbolCopyWith } /// @nodoc -class _$NoSpecialSymbolCopyWithImpl - implements $NoSpecialSymbolCopyWith { +class _$NoSpecialSymbolCopyWithImpl implements $NoSpecialSymbolCopyWith { _$NoSpecialSymbolCopyWithImpl(this._self, this._then); final NoSpecialSymbol _self; @@ -528,8 +513,7 @@ class NoUpperCase implements AuthValueFailures { return identical(this, other) || (other.runtimeType == runtimeType && other is NoUpperCase && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -542,19 +526,15 @@ class NoUpperCase implements AuthValueFailures { } /// @nodoc -abstract mixin class $NoUpperCaseCopyWith - implements $AuthValueFailuresCopyWith { - factory $NoUpperCaseCopyWith( - NoUpperCase value, $Res Function(NoUpperCase) _then) = - _$NoUpperCaseCopyWithImpl; +abstract mixin class $NoUpperCaseCopyWith implements $AuthValueFailuresCopyWith { + factory $NoUpperCaseCopyWith(NoUpperCase value, $Res Function(NoUpperCase) _then) = _$NoUpperCaseCopyWithImpl; @override @useResult $Res call({String? failedValue}); } /// @nodoc -class _$NoUpperCaseCopyWithImpl - implements $NoUpperCaseCopyWith { +class _$NoUpperCaseCopyWithImpl implements $NoUpperCaseCopyWith { _$NoUpperCaseCopyWithImpl(this._self, this._then); final NoUpperCase _self; @@ -589,16 +569,14 @@ class NoNumber implements AuthValueFailures { @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $NoNumberCopyWith> get copyWith => - _$NoNumberCopyWithImpl>(this, _$identity); + $NoNumberCopyWith> get copyWith => _$NoNumberCopyWithImpl>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is NoNumber && - (identical(other.failedValue, failedValue) || - other.failedValue == failedValue)); + (identical(other.failedValue, failedValue) || other.failedValue == failedValue)); } @override @@ -611,11 +589,8 @@ class NoNumber implements AuthValueFailures { } /// @nodoc -abstract mixin class $NoNumberCopyWith - implements $AuthValueFailuresCopyWith { - factory $NoNumberCopyWith( - NoNumber value, $Res Function(NoNumber) _then) = - _$NoNumberCopyWithImpl; +abstract mixin class $NoNumberCopyWith implements $AuthValueFailuresCopyWith { + factory $NoNumberCopyWith(NoNumber value, $Res Function(NoNumber) _then) = _$NoNumberCopyWithImpl; @override @useResult $Res call({String? failedValue}); diff --git a/lib/domain/authentication/i_auth_facade.dart b/lib/domain/authentication/i_auth_facade.dart index f856d0e..0c1a4a9 100644 --- a/lib/domain/authentication/i_auth_facade.dart +++ b/lib/domain/authentication/i_auth_facade.dart @@ -2,7 +2,6 @@ import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.da import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_objects.dart"; import "package:fpdart/fpdart.dart"; - abstract class IAuthFacade { Future> registerWithEmailAndPassword( {required EmailAddress? emailAddress, required Password? password}); diff --git a/lib/domain/core/errors.dart b/lib/domain/core/errors.dart index 7c50470..a0eb6d7 100644 --- a/lib/domain/core/errors.dart +++ b/lib/domain/core/errors.dart @@ -7,7 +7,6 @@ class UnExpectedValueError extends Error { @override String toString() { - return Error.safeToString( - "UnExpectedValueError{authValueFailures: $authValueFailures}"); + return Error.safeToString("UnExpectedValueError{authValueFailures: $authValueFailures}"); } } diff --git a/lib/domain/core/value_object.dart b/lib/domain/core/value_object.dart index f470e74..564da04 100644 --- a/lib/domain/core/value_object.dart +++ b/lib/domain/core/value_object.dart @@ -2,7 +2,6 @@ import "package:firebase_auth_flutter_ddd/domain/authentication/auth_value_failu import "package:flutter/cupertino.dart"; import "package:fpdart/fpdart.dart"; - @immutable abstract class ValueObject { const ValueObject(); @@ -13,8 +12,7 @@ abstract class ValueObject { @override bool operator ==(Object other) => - identical(this, other) || - other is ValueObject && runtimeType == other.runtimeType; + identical(this, other) || other is ValueObject && runtimeType == other.runtimeType; @override int get hashCode => 0; diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index d514ba5..b56f66d 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,8 +1,7 @@ // File generated by FlutterFire CLI. // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members import "package:firebase_core/firebase_core.dart" show FirebaseOptions; -import "package:flutter/foundation.dart" - show defaultTargetPlatform, kIsWeb, TargetPlatform; +import "package:flutter/foundation.dart" show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index 1e323cf..91b7b44 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -1,25 +1,424 @@ +import "package:firebase_auth_flutter_ddd/core/theme/animated_widgets.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State with TickerProviderStateMixin { + late AnimationController _fabAnimationController; + late Animation _fabScaleAnimation; + + @override + void initState() { + super.initState(); + _fabAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fabScaleAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _fabAnimationController, curve: Curves.elasticOut), + ); + + // Start FAB animation after a delay + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) { + _fabAnimationController.forward(); + } + }); + } + + @override + void dispose() { + _fabAnimationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: const Text("Welcome"), - elevation: 5, - centerTitle: false, - titleTextStyle: const TextStyle(fontSize: 25, color: Colors.black), - ), - body: Center( - child: Text( - "Home Page", - style: Theme.of(context).textTheme.displayMedium, + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: CustomScrollView( + slivers: [ + // Modern App Bar with Material You design + SliverAppBar( + expandedHeight: 200.0, + floating: false, + pinned: true, + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Theme.of(context).colorScheme.onSurface, + flexibleSpace: FlexibleSpaceBar( + title: SlideInWidget( + delay: 100, + begin: const Offset(0, 0.5), + child: Text( + "Welcome Home", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.3), + ], + ), + ), + child: SlideInWidget( + delay: 200, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Container( + height: 80, + width: 80, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + spreadRadius: 0, + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Icon( + Icons.home_rounded, + size: 40, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ], + ), + ), + ), + ), + ), + actions: [ + SlideInWidget( + delay: 300, + begin: const Offset(0.5, 0), + child: IconButton( + icon: const Icon(Icons.settings_rounded), + onPressed: () { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Settings coming soon!"), + ), + ); + }, + ), + ), + ], + ), + + // Content + SliverPadding( + padding: const EdgeInsets.all(24), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // Welcome Message + SlideInWidget( + delay: 400, + child: Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.waving_hand_rounded, + color: Theme.of(context).colorScheme.primary, + size: 28, + ), + const SizedBox(width: 12), + Text( + "Good to see you!", + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + "You have successfully authenticated with Firebase. Your secure session is now active.", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 24), + + // Quick Actions + SlideInWidget( + delay: 500, + child: Text( + "Quick Actions", + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + const SizedBox(height: 16), + + // Action Cards Grid + SlideInWidget( + delay: 600, + child: GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 1.2, + children: [ + _buildActionCard( + context, + icon: Icons.person_rounded, + title: "Profile", + subtitle: "Manage account", + color: Theme.of(context).colorScheme.primary, + onTap: () { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Profile page coming soon!")), + ); + }, + ), + _buildActionCard( + context, + icon: Icons.security_rounded, + title: "Security", + subtitle: "Privacy settings", + color: Theme.of(context).colorScheme.secondary, + onTap: () { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Security settings coming soon!")), + ); + }, + ), + _buildActionCard( + context, + icon: Icons.notifications_rounded, + title: "Notifications", + subtitle: "Manage alerts", + color: Theme.of(context).colorScheme.tertiary, + onTap: () { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Notifications coming soon!")), + ); + }, + ), + _buildActionCard( + context, + icon: Icons.help_rounded, + title: "Help", + subtitle: "Support center", + color: Theme.of(context).colorScheme.error, + onTap: () { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Help center coming soon!")), + ); + }, + ), + ], + ), + ), + + const SizedBox(height: 32), + + // Status Card + SlideInWidget( + delay: 700, + child: Card( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon( + Icons.check_circle_rounded, + color: Theme.of(context).colorScheme.primary, + size: 48, + ), + const SizedBox(height: 16), + Text( + "Authentication Status", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + "Connected & Secure", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Text( + "Your Firebase authentication is working perfectly with the Domain-Driven Design architecture.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 100), // Space for FAB + ]), + ), + ), + ], + ), + floatingActionButton: AnimatedBuilder( + animation: _fabAnimationController, + builder: (context, child) { + return Transform.scale( + scale: _fabScaleAnimation.value, + child: FloatingActionButton.extended( + onPressed: () { + HapticFeedback.mediumImpact(); + _showLogoutDialog(context); + }, + icon: const Icon(Icons.logout_rounded), + label: const Text("Sign Out"), + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + ); + }, + ), + ); + } + + Widget _buildActionCard( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: color.withOpacity(0.3), + width: 1, ), ), + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: color, + size: 32, + ), + const SizedBox(height: 12), + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), ), ); } + + void _showLogoutDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Row( + children: [ + Icon( + Icons.logout_rounded, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 12), + const Text("Sign Out"), + ], + ), + content: const Text("Are you sure you want to sign out of your account?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // TODO: Implement actual logout functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Logout functionality coming soon!"), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + child: const Text("Sign Out"), + ), + ], + ); + }, + ); + } } diff --git a/lib/screens/login_page.dart b/lib/screens/login_page.dart index 8207d18..cbba9ac 100644 --- a/lib/screens/login_page.dart +++ b/lib/screens/login_page.dart @@ -1,17 +1,22 @@ +import "package:firebase_auth_flutter_ddd/core/theme/animated_widgets.dart"; import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; import "package:firebase_auth_flutter_ddd/screens/home_page.dart"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "../application/authentication/auth_state_controller.dart"; import "../application/authentication/auth_states.dart"; +import "registration_page.dart"; import "utils/custom_snackbar.dart"; class LoginPage extends HookConsumerWidget { LoginPage({Key? key}) : super(key: key); final formKey = GlobalKey(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -23,128 +28,257 @@ class LoginPage extends HookConsumerWidget { () {}, (either) => either.fold( (failure) { + HapticFeedback.lightImpact(); buildCustomSnackBar( context: context, - flashBackground: Colors.red, - icon: Icons.warning_rounded, + flashBackground: Theme.of(context).colorScheme.error, + icon: Icons.error_outline_rounded, content: Text( - failure.maybeMap( - orElse: () => "", - emailAlreadyInUse: (value) => "User already exists", - serverError: (value) { - return "Server error occurred"; - }, - invalidEmailAndPasswordCombination: (value) { - return "Invalid email or password"; - }), - style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white), + failure.when( + serverError: () => "Server error occurred", + emailAlreadyInUse: () => "User already exists", + invalidEmailAndPasswordCombination: () => "Invalid email or password"), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), )); }, (success) { + HapticFeedback.lightImpact(); // Changed from successImpact to lightImpact buildCustomSnackBar( context: context, - flashBackground: Colors.green, + flashBackground: Theme.of(context).colorScheme.primary, icon: CupertinoIcons.check_mark_circled_solid, content: Text( - "Login successful", - style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white), + "Welcome back! Login successful", + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), )); - Navigator.push( + Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (context) => const HomePage(), + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => const HomePage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 300), )); }, ), ); }); - return SafeArea( - child: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - }, - child: Scaffold( - appBar: AppBar( - title: const Text("Login"), - elevation: 5, - leading: const Icon( - Icons.login_rounded, - size: 25, - ), - titleTextStyle: const TextStyle( - color: Colors.black, - fontSize: 25, - ), - centerTitle: false, - ), - body: SizedBox.expand( + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, // Changed from background to surface + body: SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), child: Form( key: formKey, - child: Center( - child: SingleChildScrollView( - reverse: true, - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 20), - TextFormField( - decoration: const InputDecoration( - labelText: "Email", - border: OutlineInputBorder(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 40), + + // Hero Section with Animation + SlideInWidget( + delay: 100, + child: Column( + children: [ + Container( + height: 120, + width: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Icons.login_rounded, + size: 64, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter an email"; - } - return null; - }, - onChanged: (value) { - formNotifier.emailChanged(value); - }, - ), - const SizedBox(height: 20), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - labelText: "Password", - border: OutlineInputBorder(), + const SizedBox(height: 32), + Text( + "Welcome back", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, // Changed from onBackground + ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a password"; - } - return null; - }, - onChanged: (value) { - formNotifier.passwordChanged(value); + const SizedBox(height: 8), + Text( + "Sign in to your account to continue", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + + const SizedBox(height: 48), + + // Email Field + SlideInWidget( + delay: 200, + child: AnimatedFormField( + label: "Email", + hint: "Enter your email address", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + controller: emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + // Fixed quote usage + return "Please enter a valid email"; + } + return null; + }, + onChanged: (email) { + formNotifier.emailChanged(email); + }, + ), + ), + + const SizedBox(height: 20), + + // Password Field - Simplified without showPassword dependency + SlideInWidget( + delay: 300, + child: AnimatedFormField( + label: "Password", + hint: "Enter your password", + obscureText: true, + // Simplified to always obscure + prefixIcon: Icons.lock_outline, + controller: passwordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 6) { + return "Password must be at least 6 characters"; + } + return null; + }, + onChanged: (password) { + formNotifier.passwordChanged(password); + }, + ), + ), + + const SizedBox(height: 32), + + // Login Button + SlideInWidget( + delay: 400, + child: AnimatedButton( + text: "Sign In", + icon: Icons.arrow_forward_rounded, + isLoading: formStates.isSubmitting, + onPressed: formStates.isSubmitting + ? null + : () { + if (formKey.currentState!.validate()) { + HapticFeedback.lightImpact(); + formNotifier.signInWithEmailAndPassword(); // Fixed method name + } else { + HapticFeedback.lightImpact(); + } + }, + ), + ), + + const SizedBox(height: 24), + + // Forgot Password + SlideInWidget( + delay: 500, + child: Center( + child: TextButton( + onPressed: () { + // TODO: Navigate to forgot password page + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Forgot password feature coming soon!"), + ), + ); }, + child: const Text("Forgot your password?"), ), - const SizedBox(height: 30), - ElevatedButton( - onPressed: formStates.isSubmitting - ? null - : () async { - if (formKey.currentState!.validate()) { - await formNotifier.signInWithEmailAndPassword(); - } - }, - child: formStates.isSubmitting ? const CircularProgressIndicator() : const Text("Sign In"), - ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: formStates.isSubmitting - ? null - : () async { - if (formKey.currentState!.validate()) { - await formNotifier.signUpWithEmailAndPassword(); - } - }, - child: formStates.isSubmitting ? const CircularProgressIndicator() : const Text("Sign Up"), + ), + ), + + const SizedBox(height: 32), + + // Register Section + SlideInWidget( + delay: 600, + child: Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + "Don't have an account?", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => RegistrationPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 300), + ), + ); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text("Create Account"), + ), + ), + ], + ), ), - ], + ), ), - ), + + const SizedBox(height: 24), + ], ), ), ), diff --git a/lib/screens/registration_page.dart b/lib/screens/registration_page.dart new file mode 100644 index 0000000..b32545b --- /dev/null +++ b/lib/screens/registration_page.dart @@ -0,0 +1,336 @@ +import "package:firebase_auth_flutter_ddd/core/theme/animated_widgets.dart"; +import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; + +import "../application/authentication/auth_state_controller.dart"; +import "../application/authentication/auth_states.dart"; +import "login_page.dart"; +import "utils/custom_snackbar.dart"; + +class RegistrationPage extends HookConsumerWidget { + RegistrationPage({super.key}); + + final formKey = GlobalKey(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formStates = ref.watch(authStateControllerProvider); + final formNotifier = ref.watch(authStateControllerProvider.notifier); + + ref.listen(authStateControllerProvider, (previous, next) { + next.authFailureOrSuccess.fold( + () {}, + (either) => either.fold( + (failure) { + HapticFeedback.lightImpact(); + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.error, + icon: Icons.error_outline_rounded, + content: Text( + failure.when( + serverError: () => "Server error occurred", + emailAlreadyInUse: () => "This email is already registered", + invalidEmailAndPasswordCombination: () => "Invalid email or password format"), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), + )); + }, + (success) { + HapticFeedback.lightImpact(); // Fixed from successImpact + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.primary, + icon: CupertinoIcons.check_mark_circled_solid, + content: Text( + "Account created successfully! Welcome aboard!", + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), + )); + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => LoginPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(-1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 300), + )); + }, + ), + ); + }); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 20), + + // Back Button + SlideInWidget( + delay: 50, + begin: const Offset(-0.3, 0), + child: Align( + alignment: Alignment.centerLeft, + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Hero Section with Animation + SlideInWidget( + delay: 100, + child: Column( + children: [ + Container( + height: 120, + width: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person_add_rounded, + size: 64, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(height: 32), + Text( + "Create Account", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + "Join us today and start your journey", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + + const SizedBox(height: 48), + + // Email Field + SlideInWidget( + delay: 200, + child: AnimatedFormField( + label: "Email Address", + hint: "Enter your email address", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + controller: emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + return "Please enter a valid email"; + } + return null; + }, + onChanged: (email) { + formNotifier.emailChanged(email); + }, + ), + ), + + const SizedBox(height: 20), + + // Password Field - Simplified without showPassword dependency + SlideInWidget( + delay: 300, + child: AnimatedFormField( + label: "Password", + hint: "Create a strong password", + obscureText: true, + // Simplified to always obscure + prefixIcon: Icons.lock_outline, + controller: passwordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter a password"; + } + if (value.length < 6) { + return "Password must be at least 6 characters"; + } + return null; + }, + onChanged: (password) { + formNotifier.passwordChanged(password); + }, + ), + ), + + const SizedBox(height: 20), + + // Confirm Password Field - Simplified without showPassword dependency + SlideInWidget( + delay: 400, + child: AnimatedFormField( + label: "Confirm Password", + hint: "Re-enter your password", + obscureText: true, + // Simplified to always obscure + prefixIcon: Icons.lock_outline, + controller: confirmPasswordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Please confirm your password"; + } + if (value != passwordController.text) { + return "Passwords do not match"; + } + return null; + }, + ), + ), + + const SizedBox(height: 32), + + // Terms and Conditions + SlideInWidget( + delay: 500, + child: Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh.withValues(alpha: 0.3), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline_rounded, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + "By creating an account, you agree to our Terms of Service and Privacy Policy.", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 32), + + // Register Button + SlideInWidget( + delay: 600, + child: AnimatedButton( + text: "Create Account", + icon: Icons.person_add_rounded, + isLoading: formStates.isSubmitting, + onPressed: formStates.isSubmitting + ? null + : () { + if (formKey.currentState!.validate()) { + HapticFeedback.lightImpact(); + formNotifier.signUpWithEmailAndPassword(); // Fixed method name + } else { + HapticFeedback.lightImpact(); + } + }, + ), + ), + + const SizedBox(height: 32), + + // Login Section + SlideInWidget( + delay: 700, + child: Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + "Already have an account?", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () { + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => LoginPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(-1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 300), + ), + ); + }, + child: const Text("Sign In Instead"), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ); + } +} From 4a38e4a84d2bc50935e3bbfa74eb03548c96e5dc Mon Sep 17 00:00:00 2001 From: Vetrichelvan Date: Mon, 21 Jul 2025 22:49:18 +0530 Subject: [PATCH 2/2] feat: add scripts to delete merged branches --- scripts/delete | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scripts/delete diff --git a/scripts/delete b/scripts/delete new file mode 100644 index 0000000..e8d846d --- /dev/null +++ b/scripts/delete @@ -0,0 +1,21 @@ +#!/bin/bash + +# Delete all local branches that are merged / deleted branches in remote + +# Fetch and prune remote branches +git fetch --prune + +# Get the default branch name (either main or master) +default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | cut -d '/' -f 2) + +# If default_branch is not found, default to main +if [ -z "$default_branch" ]; then + default_branch="main" +fi + +# Delete local branches that are merged into the default branch +git branch --merged origin/"$default_branch" | grep -v "^\*" | grep -v "^$default_branch$" | xargs -r git branch -d + +# Delete local branches that track deleted remote branches +git branch -vv | grep ': gone]' | awk '{print $1}' | grep -v "^$default_branch$" | xargs -r git branch -D + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy