diff --git a/app/lib/presentation/navigation/routers.dart b/app/lib/presentation/navigation/routers.dart index cd18b74..74fef07 100644 --- a/app/lib/presentation/navigation/routers.dart +++ b/app/lib/presentation/navigation/routers.dart @@ -1,7 +1,7 @@ import 'package:app/main/init.dart'; -import 'package:app/presentation/ui/pages/home/home_page.dart'; -import 'package:app/presentation/ui/pages/login/login_page.dart'; -import 'package:app/presentation/ui/pages/sign_up/sign_up_page.dart'; +import 'package:app/presentation/ui/pages/main/home/home_page.dart'; +import 'package:app/presentation/ui/pages/auth/login/login_page.dart'; +import 'package:app/presentation/ui/pages/auth/sign_up/sign_up_page.dart'; import 'package:app/presentation/ui/pages/splash/splash_page.dart'; import 'package:common/core/resource.dart'; import 'package:domain/bloc/auth/auth_cubit.dart'; diff --git a/app/lib/presentation/resources/dim.dart b/app/lib/presentation/resources/dim.dart index a7bcac2..d8fcc70 100644 --- a/app/lib/presentation/resources/dim.dart +++ b/app/lib/presentation/resources/dim.dart @@ -5,4 +5,18 @@ part of 'resources.dart'; class Dimen { static const primaryButtonHeight = 48.0; static const loadingSpinnerSize = 32.0; + static const loadingSpinnerSizeS = 16.0; + + static const double loginFormMaxWidth = 400.0; + + static const spacingXxs = 2.0; + static const spacingXs = 4.0; + static const spacingS = 8.0; + static const spacingM = 16.0; + static const spacingL = 24.0; + static const spacingXl = 32.0; + static const spacingXxl = 40.0; + static const spacingXxxl = 48.0; + + static const double buttonHeightM = 48.0; } diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_all.dart b/app/lib/presentation/resources/locale/generated/intl/messages_all.dart index 83aec8d..f236ec0 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_all.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_all.dart @@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) { /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( - localeName, (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); + localeName, + (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null, + ); if (availableLocale == null) { return new SynchronousFuture(false); } @@ -60,8 +62,11 @@ bool _messagesExistFor(String locale) { } MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { - var actualLocale = - Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + var actualLocale = Intl.verifiedLocale( + locale, + _messagesExistFor, + onFailure: (_) => null, + ); if (actualLocale == null) return null; return _findExact(actualLocale); } diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart index 1d44685..025ec66 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart @@ -22,18 +22,50 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), - "cookiesAcceptCTA": MessageLookupByLibrary.simpleMessage("Accept"), - "cookiesBody": MessageLookupByLibrary.simpleMessage( - "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services."), - "cookiesTitle": - MessageLookupByLibrary.simpleMessage("This website uses cookies"), - "noConnection": MessageLookupByLibrary.simpleMessage("No connection"), - "pleaseTryAgainLaterWeArenworkingToFixTheIssue": - MessageLookupByLibrary.simpleMessage( - "Please try again later, we are\nworking to fix the issue."), - "retry": MessageLookupByLibrary.simpleMessage("Retry"), - "sorryWeDidntFindAnyProduct": MessageLookupByLibrary.simpleMessage( - "Sorry we didn\'t find any product") - }; + "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), + "cookiesAcceptCTA": MessageLookupByLibrary.simpleMessage("Accept"), + "cookiesBody": MessageLookupByLibrary.simpleMessage( + "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.", + ), + "cookiesTitle": MessageLookupByLibrary.simpleMessage( + "This website uses cookies", + ), + "ctaLogin": MessageLookupByLibrary.simpleMessage("Login"), + "errorEmailInvalid": MessageLookupByLibrary.simpleMessage( + "Please enter a valid email address.", + ), + "errorEmailRequired": MessageLookupByLibrary.simpleMessage( + "Email is required.", + ), + "errorPasswordRequired": MessageLookupByLibrary.simpleMessage( + "Password is required.", + ), + "errorPasswordWeak": MessageLookupByLibrary.simpleMessage( + "Password is too weak.", + ), + "labelAgreeToTerms": MessageLookupByLibrary.simpleMessage( + "I agree to the Terms and Conditions", + ), + "labelEmail": MessageLookupByLibrary.simpleMessage("Email"), + "labelPassword": MessageLookupByLibrary.simpleMessage("Password"), + "loginErrorInvalidCredentials": MessageLookupByLibrary.simpleMessage( + "Invalid email or password.", + ), + "noConnection": MessageLookupByLibrary.simpleMessage("No connection"), + "passwordInstructions": MessageLookupByLibrary.simpleMessage( + "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.", + ), + "pleaseTryAgainLaterWeArenworkingToFixTheIssue": + MessageLookupByLibrary.simpleMessage( + "Please try again later, we are\nworking to fix the issue.", + ), + "retry": MessageLookupByLibrary.simpleMessage("Retry"), + "sorryWeDidntFindAnyProduct": MessageLookupByLibrary.simpleMessage( + "Sorry we didn\'t find any product", + ), + "titleLogin": MessageLookupByLibrary.simpleMessage("Login"), + "titleLoginSubtitle": MessageLookupByLibrary.simpleMessage( + "Use your email and password to login to your account.", + ), + }; } diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart index dcaaa12..d295049 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart @@ -22,18 +22,50 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), - "cookiesAcceptCTA": MessageLookupByLibrary.simpleMessage("Accept"), - "cookiesBody": MessageLookupByLibrary.simpleMessage( - "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services."), - "cookiesTitle": - MessageLookupByLibrary.simpleMessage("This website uses cookies"), - "noConnection": MessageLookupByLibrary.simpleMessage("No connection"), - "pleaseTryAgainLaterWeArenworkingToFixTheIssue": - MessageLookupByLibrary.simpleMessage( - "Please try again later, we are\nworking to fix the issue."), - "retry": MessageLookupByLibrary.simpleMessage("Retry"), - "sorryWeDidntFindAnyProduct": MessageLookupByLibrary.simpleMessage( - "Sorry we didn\'t find any product") - }; + "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), + "cookiesAcceptCTA": MessageLookupByLibrary.simpleMessage("Aceptar"), + "cookiesBody": MessageLookupByLibrary.simpleMessage( + "Usamos cookies para personalizar el contenido y los anuncios, ofrecer funciones de redes sociales y analizar nuestro tráfico. También compartimos información sobre el uso de nuestro sitio con nuestros socios de redes sociales, publicidad y análisis, quienes pueden combinarla con otra información que les hayas proporcionado o que hayan recopilado a partir del uso de sus servicios.", + ), + "cookiesTitle": MessageLookupByLibrary.simpleMessage( + "Este sitio web utiliza cookies", + ), + "ctaLogin": MessageLookupByLibrary.simpleMessage("Iniciar sesión"), + "errorEmailInvalid": MessageLookupByLibrary.simpleMessage( + "Por favor ingresa una dirección de correo válida.", + ), + "errorEmailRequired": MessageLookupByLibrary.simpleMessage( + "El correo electrónico es obligatorio.", + ), + "errorPasswordRequired": MessageLookupByLibrary.simpleMessage( + "La contraseña es obligatoria.", + ), + "errorPasswordWeak": MessageLookupByLibrary.simpleMessage( + "La contraseña es demasiado débil.", + ), + "labelAgreeToTerms": MessageLookupByLibrary.simpleMessage( + "Acepto los Términos y Condiciones", + ), + "labelEmail": MessageLookupByLibrary.simpleMessage("Correo electrónico"), + "labelPassword": MessageLookupByLibrary.simpleMessage("Contraseña"), + "loginErrorInvalidCredentials": MessageLookupByLibrary.simpleMessage( + "Correo o contraseña inválidos.", + ), + "noConnection": MessageLookupByLibrary.simpleMessage("Sin conexión"), + "passwordInstructions": MessageLookupByLibrary.simpleMessage( + "Mínimo 8 caracteres: 1 mayúscula, 1 minúscula, 1 número y 1 carácter especial.", + ), + "pleaseTryAgainLaterWeArenworkingToFixTheIssue": + MessageLookupByLibrary.simpleMessage( + "Por favor, inténtalo más tarde,\nestamos trabajando para resolver el problema.", + ), + "retry": MessageLookupByLibrary.simpleMessage("Reintentar"), + "sorryWeDidntFindAnyProduct": MessageLookupByLibrary.simpleMessage( + "Lo sentimos, no hemos encontrado ningún producto", + ), + "titleLogin": MessageLookupByLibrary.simpleMessage("Iniciar sesión"), + "titleLoginSubtitle": MessageLookupByLibrary.simpleMessage( + "Usa tu correo y contraseña para iniciar sesión en tu cuenta.", + ), + }; } diff --git a/app/lib/presentation/resources/locale/generated/l10n.dart b/app/lib/presentation/resources/locale/generated/l10n.dart index 2fc58c7..1a37223 100644 --- a/app/lib/presentation/resources/locale/generated/l10n.dart +++ b/app/lib/presentation/resources/locale/generated/l10n.dart @@ -18,17 +18,20 @@ class S { static S? _current; static S get current { - assert(_current != null, - 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); + assert( + _current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.', + ); return _current!; } static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -41,8 +44,10 @@ class S { static S of(BuildContext context) { final instance = S.maybeOf(context); - assert(instance != null, - 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); + assert( + instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?', + ); return instance!; } @@ -52,12 +57,7 @@ class S { /// `Flutter Target` String get appName { - return Intl.message( - 'Flutter Target', - name: 'appName', - desc: '', - args: [], - ); + return Intl.message('Flutter Target', name: 'appName', desc: '', args: []); } /// `This website uses cookies` @@ -72,12 +72,7 @@ class S { /// `Accept` String get cookiesAcceptCTA { - return Intl.message( - 'Accept', - name: 'cookiesAcceptCTA', - desc: '', - args: [], - ); + return Intl.message('Accept', name: 'cookiesAcceptCTA', desc: '', args: []); } /// `We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.` @@ -102,12 +97,7 @@ class S { /// `Retry` String get retry { - return Intl.message( - 'Retry', - name: 'retry', - desc: '', - args: [], - ); + return Intl.message('Retry', name: 'retry', desc: '', args: []); } /// `Please try again later, we are\nworking to fix the issue.` @@ -129,6 +119,106 @@ class S { args: [], ); } + + /// `Login` + String get ctaLogin { + return Intl.message('Login', name: 'ctaLogin', desc: '', args: []); + } + + /// `Email` + String get labelEmail { + return Intl.message('Email', name: 'labelEmail', desc: '', args: []); + } + + /// `Password` + String get labelPassword { + return Intl.message('Password', name: 'labelPassword', desc: '', args: []); + } + + /// `Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.` + String get passwordInstructions { + return Intl.message( + 'Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.', + name: 'passwordInstructions', + desc: '', + args: [], + ); + } + + /// `I agree to the Terms and Conditions` + String get labelAgreeToTerms { + return Intl.message( + 'I agree to the Terms and Conditions', + name: 'labelAgreeToTerms', + desc: '', + args: [], + ); + } + + /// `Email is required.` + String get errorEmailRequired { + return Intl.message( + 'Email is required.', + name: 'errorEmailRequired', + desc: '', + args: [], + ); + } + + /// `Password is required.` + String get errorPasswordRequired { + return Intl.message( + 'Password is required.', + name: 'errorPasswordRequired', + desc: '', + args: [], + ); + } + + /// `Login` + String get titleLogin { + return Intl.message('Login', name: 'titleLogin', desc: '', args: []); + } + + /// `Use your email and password to login to your account.` + String get titleLoginSubtitle { + return Intl.message( + 'Use your email and password to login to your account.', + name: 'titleLoginSubtitle', + desc: '', + args: [], + ); + } + + /// `Please enter a valid email address.` + String get errorEmailInvalid { + return Intl.message( + 'Please enter a valid email address.', + name: 'errorEmailInvalid', + desc: '', + args: [], + ); + } + + /// `Password is too weak.` + String get errorPasswordWeak { + return Intl.message( + 'Password is too weak.', + name: 'errorPasswordWeak', + desc: '', + args: [], + ); + } + + /// `Invalid email or password.` + String get loginErrorInvalidCredentials { + return Intl.message( + 'Invalid email or password.', + name: 'loginErrorInvalidCredentials', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/app/lib/presentation/resources/locale/intl_en.arb b/app/lib/presentation/resources/locale/intl_en.arb index a372eed..9ec3f6f 100644 --- a/app/lib/presentation/resources/locale/intl_en.arb +++ b/app/lib/presentation/resources/locale/intl_en.arb @@ -1,10 +1,22 @@ { - "appName": "Flutter Target", - "cookiesTitle": "This website uses cookies", - "cookiesAcceptCTA": "Accept", - "cookiesBody": "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.", + "appName": "Flutter Target", + "cookiesTitle": "This website uses cookies", + "cookiesAcceptCTA": "Accept", + "cookiesBody": "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.", "noConnection": "No connection", "retry": "Retry", "pleaseTryAgainLaterWeArenworkingToFixTheIssue": "Please try again later, we are\nworking to fix the issue.", - "sorryWeDidntFindAnyProduct": "Sorry we didn't find any product" -} \ No newline at end of file + "sorryWeDidntFindAnyProduct": "Sorry we didn't find any product", + "ctaLogin": "Login", + "labelEmail": "Email", + "labelPassword": "Password", + "passwordInstructions": "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.", + "labelAgreeToTerms": "I agree to the Terms and Conditions", + "errorEmailRequired": "Email is required.", + "errorPasswordRequired": "Password is required.", + "titleLogin": "Login", + "titleLoginSubtitle": "Use your email and password to login to your account.", + "errorEmailInvalid": "Please enter a valid email address.", + "errorPasswordWeak": "Password is too weak.", + "loginErrorInvalidCredentials": "Invalid email or password." +} diff --git a/app/lib/presentation/resources/locale/intl_es.arb b/app/lib/presentation/resources/locale/intl_es.arb index a372eed..a834509 100644 --- a/app/lib/presentation/resources/locale/intl_es.arb +++ b/app/lib/presentation/resources/locale/intl_es.arb @@ -1,10 +1,22 @@ { - "appName": "Flutter Target", - "cookiesTitle": "This website uses cookies", - "cookiesAcceptCTA": "Accept", - "cookiesBody": "We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.", - "noConnection": "No connection", - "retry": "Retry", - "pleaseTryAgainLaterWeArenworkingToFixTheIssue": "Please try again later, we are\nworking to fix the issue.", - "sorryWeDidntFindAnyProduct": "Sorry we didn't find any product" -} \ No newline at end of file + "appName": "Flutter Target", + "cookiesTitle": "Este sitio web utiliza cookies", + "cookiesAcceptCTA": "Aceptar", + "cookiesBody": "Usamos cookies para personalizar el contenido y los anuncios, ofrecer funciones de redes sociales y analizar nuestro tráfico. También compartimos información sobre el uso de nuestro sitio con nuestros socios de redes sociales, publicidad y análisis, quienes pueden combinarla con otra información que les hayas proporcionado o que hayan recopilado a partir del uso de sus servicios.", + "noConnection": "Sin conexión", + "retry": "Reintentar", + "pleaseTryAgainLaterWeArenworkingToFixTheIssue": "Por favor, inténtalo más tarde,\nestamos trabajando para resolver el problema.", + "sorryWeDidntFindAnyProduct": "Lo sentimos, no hemos encontrado ningún producto", + "ctaLogin": "Iniciar sesión", + "labelEmail": "Correo electrónico", + "labelPassword": "Contraseña", + "passwordInstructions": "Mínimo 8 caracteres: 1 mayúscula, 1 minúscula, 1 número y 1 carácter especial.", + "labelAgreeToTerms": "Acepto los Términos y Condiciones", + "errorEmailRequired": "El correo electrónico es obligatorio.", + "errorPasswordRequired": "La contraseña es obligatoria.", + "titleLogin": "Iniciar sesión", + "titleLoginSubtitle": "Usa tu correo y contraseña para iniciar sesión en tu cuenta.", + "errorEmailInvalid": "Por favor ingresa una dirección de correo válida.", + "errorPasswordWeak": "La contraseña es demasiado débil.", + "loginErrorInvalidCredentials": "Correo o contraseña inválidos." +} diff --git a/app/lib/presentation/resources/resources.dart b/app/lib/presentation/resources/resources.dart index 70093cb..53a1f2c 100644 --- a/app/lib/presentation/resources/resources.dart +++ b/app/lib/presentation/resources/resources.dart @@ -1,15 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:app/presentation/themes/spacing.dart'; import 'package:flutter_svg/flutter_svg.dart'; part 'dim.dart'; part 'images.dart'; - -extension SpacingOnWidget on Widget { - Spacing get spacing => Spacing(); -} - -extension SpacingOnStateWidget on State { - Spacing get spacing => Spacing(); -} diff --git a/app/lib/presentation/themes/local_theme.dart b/app/lib/presentation/themes/local_theme.dart index 3dc89d4..f00b3b1 100644 --- a/app/lib/presentation/themes/local_theme.dart +++ b/app/lib/presentation/themes/local_theme.dart @@ -59,7 +59,10 @@ abstract class LocalTheme { // i.e: elevatedButton elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - disabledBackgroundColor: colors.primary, + disabledBackgroundColor: colors.primary.withAlpha(90), + disabledForegroundColor: colors.onPrimary.withAlpha(90), + disabledIconColor: colors.onPrimary.withAlpha(90), + disabledMouseCursor: SystemMouseCursors.forbidden, backgroundColor: colors.primary.v40, textStyle: buttonText, foregroundColor: colors.onPrimary.v10, @@ -72,6 +75,16 @@ abstract class LocalTheme { ), ), ), + cardTheme: CardThemeData( + color: colors.surface, + shadowColor: colors.onSurface.shadow(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(themeData.borderRadius), + ), + ), + elevation: 10, + ), ); final primaryFont = 'Roboto Regular'; diff --git a/app/lib/presentation/themes/spacing.dart b/app/lib/presentation/themes/spacing.dart deleted file mode 100644 index ccc52b0..0000000 --- a/app/lib/presentation/themes/spacing.dart +++ /dev/null @@ -1,43 +0,0 @@ -class Spacing { - final double spacerSize; - final double xxs; - final double xs; - final double s; - final double m; - final double l; - final double xl; - final double xxl; - final double xxxl; - final double xxxxl; - final double xxxxxl; - - Spacing._({ - required this.spacerSize, - required this.xxs, - required this.xs, - required this.s, - required this.m, - required this.l, - required this.xl, - required this.xxl, - required this.xxxl, - required this.xxxxl, - required this.xxxxxl, - }); - - factory Spacing({double spacerSize = 4.0}) { - return Spacing._( - spacerSize: spacerSize, - xxs: 1 * spacerSize, - xs: 2 * spacerSize, - s: 3 * spacerSize, - m: 4 * spacerSize, - l: 5 * spacerSize, - xl: 6 * spacerSize, - xxl: 7 * spacerSize, - xxxl: 8 * spacerSize, - xxxxl: 9 * spacerSize, - xxxxxl: 10 * spacerSize, - ); - } -} diff --git a/app/lib/presentation/ui/components/primary_button.dart b/app/lib/presentation/ui/components/primary_button.dart new file mode 100644 index 0000000..b8a2eb8 --- /dev/null +++ b/app/lib/presentation/ui/components/primary_button.dart @@ -0,0 +1,55 @@ +import 'package:app/presentation/resources/resources.dart'; +import 'package:flutter/material.dart'; + +class PrimaryButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool isLoading; + final bool isEnabled; + final Widget? leadingIcon; + final Widget? trailingIcon; + + const PrimaryButton({ + super.key, + required this.label, + required this.onPressed, + this.isEnabled = true, + this.isLoading = false, + this.leadingIcon, + this.trailingIcon, + }) : assert( + leadingIcon == null || trailingIcon == null, + 'Only one of leadingIcon or trailingIcon can be provided', + ); + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + style: Theme.of(context).elevatedButtonTheme.style?.copyWith( + minimumSize: WidgetStateProperty.all( + const Size( + double.infinity, + Dimen.buttonHeightM, + ), + ), + ), + onPressed: isLoading || !isEnabled ? null : onPressed, + icon: leadingIcon ?? trailingIcon ?? const SizedBox.shrink(), + iconAlignment: leadingIcon != null + ? IconAlignment.start + : trailingIcon != null + ? IconAlignment.end + : null, + label: isLoading + ? SizedBox( + width: Dimen.loadingSpinnerSizeS, + height: Dimen.loadingSpinnerSizeS, + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.onPrimary, + strokeWidth: 2, + ), + ) + : Text(label), + ); + } +} diff --git a/app/lib/presentation/ui/custom/cookies.dart b/app/lib/presentation/ui/custom/cookies.dart index 320982a..ffc0ecb 100644 --- a/app/lib/presentation/ui/custom/cookies.dart +++ b/app/lib/presentation/ui/custom/cookies.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:common/devices/platform/abstract/platform_info.dart'; import 'package:app/presentation/resources/locale/generated/l10n.dart'; import 'package:app/presentation/resources/resources.dart'; +import 'package:gap/gap.dart'; class Cookies extends StatefulWidget { final Widget child; @@ -57,7 +58,7 @@ class _CookiesState extends State { ), child: Card( child: Container( - padding: EdgeInsets.all(spacing.s), + padding: const EdgeInsets.all(Dimen.spacingS), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -68,13 +69,13 @@ class _CookiesState extends State { style: Theme.of(context).textTheme.headlineMedium, ), - SizedBox(height: spacing.s), + const Gap(Dimen.spacingS), SelectableText( S.of(context).cookiesBody, style: Theme.of(context).textTheme.headlineMedium, ), - SizedBox(height: spacing.s), + const Gap(Dimen.spacingS), Align( alignment: Alignment.centerRight, child: ElevatedButton( diff --git a/app/lib/presentation/ui/custom/environment_selector.dart b/app/lib/presentation/ui/custom/environment_selector.dart index abf77a3..728a24b 100644 --- a/app/lib/presentation/ui/custom/environment_selector.dart +++ b/app/lib/presentation/ui/custom/environment_selector.dart @@ -18,15 +18,17 @@ class EnvironmentSelector extends StatelessWidget { DropdownMenuItem( value: value, child: Padding( - padding: EdgeInsets.symmetric(horizontal: spacing.xs), + padding: const EdgeInsets.symmetric(horizontal: Dimen.spacingXs), child: Text(label, style: textStyle), ), ); @override Widget build(BuildContext context) { - final textStyle = - Theme.of(context).textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.primary.v0); + final textStyle = Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: Theme.of(context).colorScheme.primary.v0); final items = >[ _item(EnvConfig.kDevEnv, 'Development', textStyle!), diff --git a/app/lib/presentation/ui/pages/auth/login/login_form.dart b/app/lib/presentation/ui/pages/auth/login/login_form.dart new file mode 100644 index 0000000..3446a06 --- /dev/null +++ b/app/lib/presentation/ui/pages/auth/login/login_form.dart @@ -0,0 +1,154 @@ +import 'package:app/presentation/resources/locale/generated/l10n.dart'; +import 'package:app/presentation/resources/resources.dart'; +import 'package:app/presentation/ui/components/primary_button.dart'; +import 'package:common/core/resource.dart'; +import 'package:domain/bloc/auth/auth_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:common/validators/form_validator.dart'; + +class LoginForm extends StatefulWidget { + const LoginForm({super.key}); + + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + TextEditingController emailController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + bool agreeToTerms = false; + + final _formKey = GlobalKey(); + + @override + void dispose() { + emailController.dispose(); + passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).titleLogin, + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(Dimen.spacingL), + Text( + S.of(context).titleLoginSubtitle, + style: Theme.of(context).textTheme.titleSmall, + ), + const Gap(Dimen.spacingL), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelEmail, + ), + keyboardType: TextInputType.emailAddress, + controller: emailController, + validator: (value) { + if (!FormValidator.isEmail(value)) { + return S.of(context).errorEmailInvalid; + } + + return null; + }, + ), + const Gap(Dimen.spacingM), + TextFormField( + decoration: InputDecoration( + labelText: S.of(context).labelPassword, + ), + obscureText: true, + controller: passwordController, + validator: (value) { + if (!FormValidator.isStrongPassword(value)) { + return S.of(context).errorPasswordWeak; + } + return null; + }, + ), + const Gap(Dimen.spacingM), + Text( + S.of(context).passwordInstructions, + style: Theme.of(context).textTheme.bodySmall, + ), + const Gap(Dimen.spacingM), + TextButton( + onPressed: () => setState(() { + agreeToTerms = !agreeToTerms; + }), + child: Row( + children: [ + Checkbox( + value: agreeToTerms, + onChanged: (value) => setState(() { + agreeToTerms = value ?? false; + }), + ), + Text( + S.of(context).labelAgreeToTerms, + style: Theme.of(context).textTheme.titleSmall, + ), + const Gap(Dimen.spacingS), + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "This should open the terms and conditions URL."), + ), + ); + }, + icon: const Icon(Icons.info), + ) + ], + ), + ), + const Gap(Dimen.spacingM), + BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (state is RError) ...[ + Text( + S.of(context).loginErrorInvalidCredentials, + style: + Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + const Gap(Dimen.spacingM), + ], + PrimaryButton( + label: S.of(context).ctaLogin, + onPressed: () { + if ((_formKey.currentState?.validate() ?? false) && + agreeToTerms) { + context.read().login( + email: emailController.text, + password: passwordController.text, + ); + } + }, + isEnabled: agreeToTerms, + isLoading: state is RLoading, + trailingIcon: const Icon(Icons.login), + ) + ], + ); + }, + ), + ], + ), + ); + } +} diff --git a/app/lib/presentation/ui/pages/auth/login/login_page.dart b/app/lib/presentation/ui/pages/auth/login/login_page.dart new file mode 100644 index 0000000..0aa30da --- /dev/null +++ b/app/lib/presentation/ui/pages/auth/login/login_page.dart @@ -0,0 +1,26 @@ +import 'package:app/presentation/resources/resources.dart'; +import 'package:app/presentation/ui/pages/auth/login/login_form.dart'; +import 'package:flutter/material.dart'; + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: Dimen.loginFormMaxWidth, + ), + child: const Card( + child: Padding( + padding: EdgeInsets.all(Dimen.spacingM), + child: LoginForm(), + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/presentation/ui/pages/sign_up/sign_up_page.dart b/app/lib/presentation/ui/pages/auth/sign_up/sign_up_page.dart similarity index 100% rename from app/lib/presentation/ui/pages/sign_up/sign_up_page.dart rename to app/lib/presentation/ui/pages/auth/sign_up/sign_up_page.dart diff --git a/app/lib/presentation/ui/pages/sign_up/sign_up_view.dart b/app/lib/presentation/ui/pages/auth/sign_up/sign_up_view.dart similarity index 100% rename from app/lib/presentation/ui/pages/sign_up/sign_up_view.dart rename to app/lib/presentation/ui/pages/auth/sign_up/sign_up_view.dart diff --git a/app/lib/presentation/ui/pages/login/login_page.dart b/app/lib/presentation/ui/pages/login/login_page.dart deleted file mode 100644 index 6ad41be..0000000 --- a/app/lib/presentation/ui/pages/login/login_page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:app/main/init.dart'; -import 'package:app/presentation/navigation/routers.dart'; -import 'package:app/presentation/resources/resources.dart'; -import 'package:app/presentation/ui/custom/app_theme_switch.dart'; -import 'package:app/presentation/ui/custom/loading_screen.dart'; -import 'package:common/core/resource.dart'; -import 'package:domain/bloc/auth/auth_cubit.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../custom/environment_selector.dart'; - -class LoginPage extends StatelessWidget { - const LoginPage({super.key}); - - AuthCubit get _authCubit => getIt(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - Padding( - padding: EdgeInsets.all(spacing.m), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const AppThemeSwitch(), - SizedBox(height: spacing.m), - SizedBox( - width: double.maxFinite, - child: ElevatedButton( - child: const Text('Login'), - onPressed: () { - _authCubit.login('Rootstrap', '12345678'); - }, - ), - ), - SizedBox(height: spacing.xxxl), - if (kDebugMode) ...[ - EnvironmentSelector(), - ], - ], - ), - ), - const _Loading(), - ], - ), - ); - } -} - -class _Loading extends StatelessWidget { - const _Loading(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is! RLoading) { - return const SizedBox.shrink(); - } - - return Container( - color: Colors.black.withAlpha(50), - width: double.maxFinite, - height: double.maxFinite, - child: const LoadingScreen(), - ); - }, - ); - } -} diff --git a/app/lib/presentation/ui/pages/home/home_page.dart b/app/lib/presentation/ui/pages/main/home/home_page.dart similarity index 77% rename from app/lib/presentation/ui/pages/home/home_page.dart rename to app/lib/presentation/ui/pages/main/home/home_page.dart index acd94a0..7b83c5b 100644 --- a/app/lib/presentation/ui/pages/home/home_page.dart +++ b/app/lib/presentation/ui/pages/main/home/home_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:app/presentation/ui/pages/home/home_view.dart'; +import 'package:app/presentation/ui/pages/main/home/home_view.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); diff --git a/app/lib/presentation/ui/pages/home/home_view.dart b/app/lib/presentation/ui/pages/main/home/home_view.dart similarity index 100% rename from app/lib/presentation/ui/pages/home/home_view.dart rename to app/lib/presentation/ui/pages/main/home/home_view.dart diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5b422a4..8f38ac4 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: flutter_web_plugins: sdk: flutter app_links: ^6.4.1 + gap: ^3.0.1 dev_dependencies: bloc_test: ^9.0.2 diff --git a/modules/common/lib/core/resource.dart b/modules/common/lib/core/resource.dart index 1846ced..b97ef96 100644 --- a/modules/common/lib/core/resource.dart +++ b/modules/common/lib/core/resource.dart @@ -26,6 +26,8 @@ class RSuccess extends Resource { class RError extends Resource { RError({super.state = RState.error, super.data, required super.exception}); + + get message => exception?.toString(); } enum RState { diff --git a/modules/common/lib/validators/form_validator.dart b/modules/common/lib/validators/form_validator.dart new file mode 100644 index 0000000..31dedc4 --- /dev/null +++ b/modules/common/lib/validators/form_validator.dart @@ -0,0 +1,21 @@ +class FormValidator { + static bool isEmail(String? value) { + if (value == null || value.isEmpty) { + return false; + } + final emailRegex = RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); + return emailRegex.hasMatch(value); + } + + /// // At least 8 characters, one uppercase, one lowercase, one number and one special character + static bool isStrongPassword(String? value) { + if (value == null || value.isEmpty) { + return false; + } + // At least 8 characters, one uppercase, one lowercase, one number and one special character + final passwordRegex = RegExp( + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$'); + return passwordRegex.hasMatch(value); + } +} diff --git a/modules/domain/lib/bloc/auth/auth_cubit.dart b/modules/domain/lib/bloc/auth/auth_cubit.dart index fd83f16..03d0d5e 100644 --- a/modules/domain/lib/bloc/auth/auth_cubit.dart +++ b/modules/domain/lib/bloc/auth/auth_cubit.dart @@ -8,10 +8,15 @@ class AuthCubit extends BaseCubit { final AuthService _authService; AuthCubit(this._authService) : super(RSuccess(data: AuthStateUnknown())); - Future login(String username, String password) async { + Future login({ + required String email, + required String password, + }) async { isLoading(); - final authResult = - await _authService.logInWithCredentials(username, password); + final authResult = await _authService.logInWithCredentials( + email, + password, + ); authResult ..mapSuccess((_) => isLogin()) diff --git a/modules/domain/lib/services/auth_service.dart b/modules/domain/lib/services/auth_service.dart index 0327d2a..772f150 100644 --- a/modules/domain/lib/services/auth_service.dart +++ b/modules/domain/lib/services/auth_service.dart @@ -7,7 +7,9 @@ class AuthService { AuthService(this._authRepository); Future> logInWithCredentials( - String username, String password) => + String username, + String password, + ) => _authRepository.login(username, password); bool isLoggedIn() => _authRepository.isLoggedIn();