import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/widgets/buttons/buttons.dart'; /// Formulaire de connexion sophistiqué avec validation class LoginForm extends StatefulWidget { final GlobalKey formKey; final TextEditingController emailController; final TextEditingController passwordController; final bool obscurePassword; final bool rememberMe; final bool isLoading; final VoidCallback onObscureToggle; final ValueChanged onRememberMeToggle; final VoidCallback onSubmit; const LoginForm({ super.key, required this.formKey, required this.emailController, required this.passwordController, required this.obscurePassword, required this.rememberMe, required this.isLoading, required this.onObscureToggle, required this.onRememberMeToggle, required this.onSubmit, }); @override State createState() => _LoginFormState(); } class _LoginFormState extends State with TickerProviderStateMixin { late AnimationController _fieldAnimationController; late List> _fieldAnimations; final FocusNode _emailFocusNode = FocusNode(); final FocusNode _passwordFocusNode = FocusNode(); bool _emailHasFocus = false; bool _passwordHasFocus = false; @override void initState() { super.initState(); _setupAnimations(); _setupFocusListeners(); _startFieldAnimations(); } void _setupAnimations() { _fieldAnimationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _fieldAnimations = List.generate(4, (index) { return Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: _fieldAnimationController, curve: Interval( index * 0.2, (index * 0.2) + 0.6, curve: Curves.easeOut, ), )); }); } void _setupFocusListeners() { _emailFocusNode.addListener(() { setState(() { _emailHasFocus = _emailFocusNode.hasFocus; }); }); _passwordFocusNode.addListener(() { setState(() { _passwordHasFocus = _passwordFocusNode.hasFocus; }); }); } void _startFieldAnimations() { Future.delayed(const Duration(milliseconds: 200), () { if (mounted) { _fieldAnimationController.forward(); } }); } @override void dispose() { _fieldAnimationController.dispose(); _emailFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Form( key: widget.formKey, child: Column( children: [ // Champ email SlideTransition( position: _fieldAnimations[0], child: _buildEmailField(), ), const SizedBox(height: 20), // Champ mot de passe SlideTransition( position: _fieldAnimations[1], child: _buildPasswordField(), ), const SizedBox(height: 16), // Options (Se souvenir de moi, Mot de passe oublié) SlideTransition( position: _fieldAnimations[2], child: _buildOptionsRow(), ), const SizedBox(height: 32), // Bouton de connexion SlideTransition( position: _fieldAnimations[3], child: _buildLoginButton(), ), ], ), ); } Widget _buildEmailField() { return AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: _emailHasFocus ? [ BoxShadow( color: AppTheme.primaryColor.withOpacity(0.2), blurRadius: 12, offset: const Offset(0, 4), ), ] : [], ), child: TextFormField( controller: widget.emailController, focusNode: _emailFocusNode, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, enabled: !widget.isLoading, onFieldSubmitted: (_) { FocusScope.of(context).requestFocus(_passwordFocusNode); }, decoration: InputDecoration( labelText: 'Adresse email', hintText: 'votre.email@exemple.com', prefixIcon: AnimatedContainer( duration: const Duration(milliseconds: 200), child: Icon( Icons.email_outlined, color: _emailHasFocus ? AppTheme.primaryColor : AppTheme.textSecondary, ), ), filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.primaryColor, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.errorColor, width: 2, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.errorColor, width: 2, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre email'; } if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { return 'Format d\'email invalide'; } return null; }, ), ); } Widget _buildPasswordField() { return AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: _passwordHasFocus ? [ BoxShadow( color: AppTheme.primaryColor.withOpacity(0.2), blurRadius: 12, offset: const Offset(0, 4), ), ] : [], ), child: TextFormField( controller: widget.passwordController, focusNode: _passwordFocusNode, obscureText: widget.obscurePassword, textInputAction: TextInputAction.done, enabled: !widget.isLoading, onFieldSubmitted: (_) => widget.onSubmit(), decoration: InputDecoration( labelText: 'Mot de passe', hintText: 'Saisissez votre mot de passe', prefixIcon: AnimatedContainer( duration: const Duration(milliseconds: 200), child: Icon( Icons.lock_outlined, color: _passwordHasFocus ? AppTheme.primaryColor : AppTheme.textSecondary, ), ), suffixIcon: IconButton( onPressed: widget.onObscureToggle, icon: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: Icon( widget.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, key: ValueKey(widget.obscurePassword), color: _passwordHasFocus ? AppTheme.primaryColor : AppTheme.textSecondary, ), ), ), filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.primaryColor, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.errorColor, width: 2, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: AppTheme.errorColor, width: 2, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre mot de passe'; } if (value.length < 6) { return 'Le mot de passe doit contenir au moins 6 caractères'; } return null; }, ), ); } Widget _buildOptionsRow() { return Row( children: [ // Se souvenir de moi Expanded( child: GestureDetector( onTap: () => widget.onRememberMeToggle(!widget.rememberMe), child: Row( children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), width: 20, height: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all( color: widget.rememberMe ? AppTheme.primaryColor : AppTheme.textSecondary, width: 2, ), color: widget.rememberMe ? AppTheme.primaryColor : Colors.transparent, ), child: widget.rememberMe ? Icon( Icons.check, size: 14, color: Colors.white, ) : null, ), const SizedBox(width: 8), Text( 'Se souvenir de moi', style: TextStyle( fontSize: 14, color: AppTheme.textSecondary, fontWeight: FontWeight.w500, ), ), ], ), ), ), // Mot de passe oublié TextButton( onPressed: widget.isLoading ? null : () { HapticFeedback.selectionClick(); _showForgotPasswordDialog(); }, child: Text( 'Mot de passe oublié ?', style: TextStyle( fontSize: 14, color: AppTheme.primaryColor, fontWeight: FontWeight.w600, ), ), ), ], ); } Widget _buildLoginButton() { return SizedBox( width: double.infinity, height: 56, child: widget.isLoading ? QuickButtons.primary( text: '', onPressed: () {}, loading: true, ) : QuickButtons.primary( text: 'Se connecter', icon: Icons.login, onPressed: widget.onSubmit, size: ButtonSize.large, ), ); } void _showForgotPasswordDialog() { showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon( Icons.help_outline, color: AppTheme.primaryColor, ), const SizedBox(width: 12), const Text('Mot de passe oublié'), ], ), content: const Text( 'Pour récupérer votre mot de passe, veuillez contacter votre administrateur ou utiliser la fonction de récupération sur l\'interface web.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( 'Compris', style: TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.w600, ), ), ), ], ), ); } }