From bf3c380e8e844d9f7c8cdd5dec8850472430fb3f Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:02:03 -0500 Subject: [PATCH] feat: redesign auth layout and login view with ProsApp logo - auth_layout: gradient header (mobile) / split panel (desktop) - background_auth: gradient blue with network logo, tagline and feature list - custom_title: network logo replacing local asset - login_view: consistent UI matching phone_login_view (inputs, buttons, links to register/phone login, brand colors) Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/layouts/auth/auth_layout.dart | 110 +++++----- .../layouts/auth/widgets/background_auth.dart | 58 +++++- lib/ui/layouts/auth/widgets/custom_title.dart | 47 ++--- lib/ui/views/login_view.dart | 191 +++++++++++------- 4 files changed, 237 insertions(+), 169 deletions(-) diff --git a/lib/ui/layouts/auth/auth_layout.dart b/lib/ui/layouts/auth/auth_layout.dart index c0a4b45..f56cc7d 100644 --- a/lib/ui/layouts/auth/auth_layout.dart +++ b/lib/ui/layouts/auth/auth_layout.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; - -import 'package:prosapp_web_app/ui/layouts/auth/widgets/custom_title.dart'; import 'package:prosapp_web_app/ui/layouts/auth/widgets/background_auth.dart'; class AuthLayout extends StatelessWidget { @@ -10,81 +8,83 @@ class AuthLayout extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - return Scaffold( - body: Scrollbar( - child: ListView( - physics: const ClampingScrollPhysics(), - children: [ - size.width > 1000 - ? _DesktopBody(child: child) - : _MobileBody(child: child), - ], - ), - ), + body: size.width > 900 + ? _DesktopBody(child: child) + : _MobileBody(child: child), ); } } class _MobileBody extends StatelessWidget { final Widget child; - - const _MobileBody({super.key, required this.child}); + const _MobileBody({required this.child}); @override Widget build(BuildContext context) { - return Container( - color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 20), - const CustomTitle(), - SizedBox(width: double.infinity, height: 420, child: child), - const SizedBox( - width: double.infinity, - height: 400, - child: BackgroundAuth(), - ) - ], - ), + return Column( + children: [ + // Gradient header with logo + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 36), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF42A4EF), Color(0xFF1565C0)], + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(32), + bottomRight: Radius.circular(32), + ), + ), + child: Column( + children: [ + Image.network( + 'https://prosapp.co/img/logo_prosapp.png', + height: 52, + color: Colors.white, + colorBlendMode: BlendMode.srcATop, + errorBuilder: (_, __, ___) => const Text( + 'ProsApp', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white), + ), + ), + const SizedBox(height: 8), + const Text( + 'Conecta con los mejores profesionales', + style: TextStyle(color: Colors.white70, fontSize: 13), + ), + ], + ), + ), + Expanded(child: SingleChildScrollView(child: child)), + ], ); } } class _DesktopBody extends StatelessWidget { final Widget child; - - const _DesktopBody({super.key, required this.child}); + const _DesktopBody({required this.child}); @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - - return Container( - width: size.width, - height: size.height, - color: Colors.white, - child: Row( - children: [ - const Expanded(child: BackgroundAuth()), - - // View container - Container( - width: 600, - height: double.infinity, + return Row( + children: [ + // Left gradient panel + const SizedBox(width: 420, child: BackgroundAuth()), + // Right form panel + Expanded( + child: Container( + height: size.height, color: Colors.white, - child: Column( - children: [ - const SizedBox(height: 20), - const CustomTitle(), - const SizedBox(height: 50), - Expanded(child: child), - ], - ), + child: Center(child: child), ), - ], - ), + ), + ], ); } } diff --git a/lib/ui/layouts/auth/widgets/background_auth.dart b/lib/ui/layouts/auth/widgets/background_auth.dart index f16c95e..63a9cdb 100644 --- a/lib/ui/layouts/auth/widgets/background_auth.dart +++ b/lib/ui/layouts/auth/widgets/background_auth.dart @@ -6,18 +6,58 @@ class BackgroundAuth extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - color: Colors.blue, - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - child: const Center( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 30), - child: Image( - image: AssetImage('prosapp-logo.png'), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF42A4EF), Color(0xFF1565C0)], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.network( + 'https://prosapp.co/img/logo_prosapp.png', + width: 180, + color: Colors.white, + colorBlendMode: BlendMode.srcATop, + errorBuilder: (_, __, ___) => const Text( + 'ProsApp', + style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white), ), ), - ), + const SizedBox(height: 24), + const Text( + 'Conecta con los mejores\nprofesionales', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 16, height: 1.5), + ), + const SizedBox(height: 48), + _FeatureItem(icon: Icons.verified_user_outlined, text: 'Profesionales verificados'), + const SizedBox(height: 12), + _FeatureItem(icon: Icons.star_outline, text: 'Calificaciones reales'), + const SizedBox(height: 12), + _FeatureItem(icon: Icons.chat_bubble_outline, text: 'Chat directo'), + ], ), ); } } + +class _FeatureItem extends StatelessWidget { + final IconData icon; + final String text; + const _FeatureItem({required this.icon, required this.text}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: Colors.white70, size: 18), + const SizedBox(width: 8), + Text(text, style: const TextStyle(color: Colors.white70, fontSize: 14)), + ], + ); + } +} diff --git a/lib/ui/layouts/auth/widgets/custom_title.dart b/lib/ui/layouts/auth/widgets/custom_title.dart index 2f74022..1ca9628 100644 --- a/lib/ui/layouts/auth/widgets/custom_title.dart +++ b/lib/ui/layouts/auth/widgets/custom_title.dart @@ -1,38 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; class CustomTitle extends StatelessWidget { - const CustomTitle({ - super.key, - }); + const CustomTitle({super.key}); @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 100), // Añade espacio arriba - Image.asset( - 'logo.png', - width: 60, - height: 60, + return Column( + children: [ + const SizedBox(height: 40), + Image.network( + 'https://prosapp.co/img/logo_prosapp.png', + height: 52, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Text( + 'ProsApp', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF42A4EF)), ), - const SizedBox(height: 40), // Mantiene el espacio entre el logo y el texto - FittedBox( - fit: BoxFit.contain, - child: Text( - "Iniciar sesión", - style: GoogleFonts.montserratAlternates( - fontSize: 30, - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ) - ], - ), + ), + const SizedBox(height: 8), + const Text( + 'Conecta con los mejores profesionales', + style: TextStyle(color: Colors.grey, fontSize: 13), + ), + const SizedBox(height: 32), + ], ); } } diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart index d672bbe..ec0ae56 100644 --- a/lib/ui/views/login_view.dart +++ b/lib/ui/views/login_view.dart @@ -1,11 +1,8 @@ +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/login_form_provider.dart'; import 'package:prosapp_web_app/router/router.dart'; -import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; -import 'package:prosapp_web_app/ui/buttons/link_text.dart'; -import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class LoginView extends StatelessWidget { @@ -18,89 +15,111 @@ class LoginView extends StatelessWidget { return ChangeNotifierProvider( create: (_) => LoginFormProvider(), child: Builder(builder: (context) { - final loginFormProvider = - Provider.of(context, listen: false); + final loginFormProvider = Provider.of(context, listen: false); - return Container( - margin: const EdgeInsets.only(top: 100), - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 370), + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 16), child: Form( autovalidateMode: AutovalidateMode.onUserInteraction, key: loginFormProvider.formKey, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text('Iniciar sesión', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + const Text('Ingresa con tu cuenta de email', + style: TextStyle(color: Colors.grey, fontSize: 13)), + const SizedBox(height: 28), + // Email TextFormField( - onFieldSubmitted: (_) => - onFormSubmit(loginFormProvider, authProvider), - validator: (value) { - if (!EmailValidator.validate(value ?? '')) { - return 'Email no válido'; - } - return null; - }, - onChanged: (value) => loginFormProvider.email = value, - decoration: CustomInputs.loginInputDecoration( - hint: 'Ingresa tu correo', - label: 'Email', - icon: Icons.email_outlined, - ), + keyboardType: TextInputType.emailAddress, + onFieldSubmitted: (_) => _submit(loginFormProvider, authProvider), + validator: (v) => EmailValidator.validate(v ?? '') ? null : 'Email no válido', + onChanged: (v) => loginFormProvider.email = v, + decoration: _inputDecoration('Email', 'correo@ejemplo.com', Icons.email_outlined), ), - - const SizedBox(height: 20), + const SizedBox(height: 16), // Password TextFormField( - onFieldSubmitted: (_) => - onFormSubmit(loginFormProvider, authProvider), - validator: (value) { - if (value == null || value.isEmpty) { - return "Ingresa tu contraseña"; - } - - if (value.length < 8) { - return "La contraseña debe tener al menos 8 caracteres"; - } - - return null; // Válido - }, - onChanged: (value) => loginFormProvider.password = value, obscureText: true, - decoration: CustomInputs.loginInputDecoration( - hint: 'Ingresa tu contraseña', - label: 'Contraseña', - icon: Icons.lock_outline, + onFieldSubmitted: (_) => _submit(loginFormProvider, authProvider), + validator: (v) { + if (v == null || v.isEmpty) return 'Ingresa tu contraseña'; + if (v.length < 6) return 'Mínimo 6 caracteres'; + return null; + }, + onChanged: (v) => loginFormProvider.password = v, + decoration: _inputDecoration('Contraseña', '••••••••', Icons.lock_outline), + ), + const SizedBox(height: 24), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => _submit(loginFormProvider, authProvider), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A4EF), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 0, + ), + child: const Text('Ingresar', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ), ), + const SizedBox(height: 24), - const SizedBox(height: 20), + Row(children: const [ + Expanded(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text('o', style: TextStyle(color: Colors.grey))), + Expanded(child: Divider()), + ]), + const SizedBox(height: 16), - CustomOutlinedButton( - onPressed: () => - onFormSubmit(loginFormProvider, authProvider), - text: "Ingresar", - color: Colors.blue, + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => + Navigator.pushReplacementNamed(context, Flurorouter.phoneLoginRoute), + icon: const Icon(Icons.phone_android, color: Color(0xFF42A4EF)), + label: const Text('Ingresar con celular', + style: TextStyle(color: Color(0xFF42A4EF), fontWeight: FontWeight.w600)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + side: const BorderSide(color: Color(0xFF42A4EF)), + ), + ), ), - const SizedBox(height: 20), + const SizedBox(height: 12), - LinkText( - text: "Nueva cuenta", - onPressed: () { - Navigator.pushReplacementNamed( - context, Flurorouter.registerRoute); - }, - ), - const SizedBox(height: 5), - - LinkText( - text: "Entrar con celular", - onPressed: () { - Navigator.pushReplacementNamed( - context, Flurorouter.phoneLoginRoute); - }, + Center( + child: TextButton( + onPressed: () => + Navigator.pushReplacementNamed(context, Flurorouter.registerRoute), + child: RichText( + text: const TextSpan( + style: TextStyle(color: Colors.grey, fontSize: 13), + children: [ + TextSpan(text: '¿No tienes cuenta? '), + TextSpan( + text: 'Regístrate', + style: TextStyle( + color: Color(0xFF42A4EF), fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), ), ], ), @@ -112,14 +131,32 @@ class LoginView extends StatelessWidget { ); } - void onFormSubmit( - LoginFormProvider loginFormProvider, AuthProvider authProvider) async { - final isValid = loginFormProvider.validateForm(); - if (isValid) { - await authProvider.login( - loginFormProvider.email, - loginFormProvider.password, - ); + InputDecoration _inputDecoration(String label, String hint, IconData icon) { + return InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: Icon(icon, color: const Color(0xFF42A4EF)), + filled: true, + fillColor: const Color(0xFFF5F8FF), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFE0E7FF))), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red)), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red, width: 2)), + ); + } + + void _submit(LoginFormProvider form, AuthProvider auth) async { + if (form.validateForm()) { + await auth.login(form.email, form.password); } } }