import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/models/auth_state.dart'; import '../../core/services/auth_controller.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_widgets.dart'; import 'widgets/video_mascot.dart'; class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { final _formKey = GlobalKey(); final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); bool _obscurePass = true; @override void initState() { super.initState(); ref.listenManual>(authControllerProvider, ( prev, next, ) { if (!mounted) return; if (next is AsyncError) { final error = next.error; String msg = 'Ocurrió un error inesperado'; if (error is DioException) { msg = (error.response?.data is Map) ? error.response!.data['detail'] ?? 'Credenciales inválidas' : 'Error de conexión con el servidor'; } else { msg = error.toString(); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: AppTheme.danger, behavior: SnackBarBehavior.floating, ), ); } }); } @override void dispose() { _emailCtrl.dispose(); _passCtrl.dispose(); super.dispose(); } Future _submit() async { if (!(_formKey.currentState?.validate() ?? false)) return; await ref .read(authControllerProvider.notifier) .login(email: _emailCtrl.text.trim(), password: _passCtrl.text); } @override Widget build(BuildContext context) { final loading = ref.watch(authControllerProvider).isLoading; final screenH = MediaQuery.of(context).size.height; return Scaffold( backgroundColor: AppTheme.background, body: Column( children: [ // ── Cabecera verde con mascota ───────────────────────────── _GreenHeader(height: screenH * 0.38), // ── Formulario ───────────────────────────────────────────── Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 28, 24, 24), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AppFormField( label: 'Correo electrónico', hint: 'tu@correo.com', controller: _emailCtrl, keyboardType: TextInputType.emailAddress, validator: (v) => (v == null || v.trim().isEmpty) ? 'Ingresa tu correo' : null, ), const SizedBox(height: 16), AppFormField( label: 'Contraseña', hint: '••••••••', controller: _passCtrl, obscureText: _obscurePass, validator: (v) => (v == null || v.length < 6) ? 'Mínimo 6 caracteres' : null, suffix: IconButton( icon: Icon( _obscurePass ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: AppTheme.textSecondary, ), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () {}, style: TextButton.styleFrom( foregroundColor: AppTheme.primary, padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text( '¿Olvidaste tu contraseña?', style: TextStyle(fontSize: 13), ), ), ), const SizedBox(height: 24), SizedBox( height: 52, child: ElevatedButton( onPressed: loading ? null : _submit, child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: loading ? const SizedBox( key: ValueKey('loading'), width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text( 'Ingresar', key: ValueKey('text'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), const SizedBox(height: 32), Center( child: Wrap( alignment: WrapAlignment.center, children: [ const Text( '¿No tienes cuenta? ', style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), GestureDetector( onTap: () => context.go('/register'), child: const Text( 'Regístrate', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.primary, ), ), ), ], ), ), ], ), ), ), ), ], ), ); } } // ── Cabecera con gradiente verde y mascota ─────────────────────────────────── class _GreenHeader extends StatelessWidget { final double height; const _GreenHeader({required this.height}); @override Widget build(BuildContext context) { return ClipPath( clipper: _WaveClipper(), child: Container( height: height, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [0.0, 0.6, 1.0], colors: [Color(0xFF0A4A38), Color(0xFF0F6E56), Color(0xFF1D9E75)], ), ), child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 8), const VideoMascot(size: 108), const SizedBox(height: 16), const Text( 'RecolectApp', style: TextStyle( fontSize: 30, fontWeight: FontWeight.w800, color: Colors.white, letterSpacing: -0.8, ), ), const SizedBox(height: 4), Text( 'Bienvenido de nuevo', style: TextStyle( fontSize: 14, color: Colors.white.withValues(alpha: 0.82), fontWeight: FontWeight.w400, ), ), const SizedBox(height: 28), ], ), ), ), ), ), ); } } class _WaveClipper extends CustomClipper { @override Path getClip(Size size) { final path = Path(); path.lineTo(0, size.height - 36); path.quadraticBezierTo( size.width * 0.25, size.height, size.width * 0.5, size.height - 18, ); path.quadraticBezierTo( size.width * 0.75, size.height - 36, size.width, size.height - 10, ); path.lineTo(size.width, 0); path.close(); return path; } @override bool shouldReclip(_WaveClipper old) => false; }