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), + ], + ), + ), + ), + ), + ), + ); + } +} 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 +