first commit

This commit is contained in:
DahoudG
2025-08-20 21:00:35 +00:00
commit b2a23bdf89
583 changed files with 243074 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'welcome_screen.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
bool _isLoading = true;
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
_checkAuthenticationStatus();
}
Future<void> _checkAuthenticationStatus() async {
// Simulation de vérification d'authentification
// En production : vérifier le token JWT, SharedPreferences, etc.
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_isLoading = false;
// Pour le moment, toujours false (pas d'utilisateur connecté)
_isAuthenticated = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return _buildLoadingScreen();
}
if (_isAuthenticated) {
// TODO: Retourner vers la navigation principale
return _buildLoadingScreen(); // Temporaire
} else {
return const WelcomeScreen();
}
}
Widget _buildLoadingScreen() {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
],
),
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
);
}
}

View File

@@ -0,0 +1,489 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _emailSent = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: _emailSent ? _buildSuccessView() : _buildFormView(),
),
),
);
},
),
),
);
}
Widget _buildFormView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildInstructions(),
const SizedBox(height: 32),
_buildForm(),
const SizedBox(height: 32),
_buildSendButton(),
const SizedBox(height: 24),
_buildBackToLogin(),
],
);
}
Widget _buildSuccessView() {
return Column(
children: [
const SizedBox(height: 60),
// Icône de succès
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
border: Border.all(
color: AppTheme.successColor.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.mark_email_read_rounded,
size: 60,
color: AppTheme.successColor,
),
),
const SizedBox(height: 32),
// Titre de succès
const Text(
'Email envoyé !',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Message de succès
Text(
'Nous avons envoyé un lien de réinitialisation à :',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_emailController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Instructions
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.infoColor.withOpacity(0.2),
),
),
child: Column(
children: [
const Icon(
Icons.info_outline,
color: AppTheme.infoColor,
size: 24,
),
const SizedBox(height: 12),
const Text(
'Instructions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'1. Vérifiez votre boîte email (et vos spams)\n'
'2. Cliquez sur le lien de réinitialisation\n'
'3. Créez un nouveau mot de passe sécurisé',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
// Boutons d'action
Column(
children: [
LoadingButton(
onPressed: _handleResendEmail,
text: 'Renvoyer l\'email',
width: double.infinity,
height: 48,
backgroundColor: AppTheme.secondaryColor,
),
const SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Retour à la connexion',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.warningColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.lock_reset_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Pas de problème ! Nous allons vous aider à le récupérer.',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildInstructions() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comment ça marche ?',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Saisissez votre email et nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe.',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.4,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
validator: _validateEmail,
onFieldSubmitted: (_) => _handleSendResetEmail(),
autofocus: true,
),
);
}
Widget _buildSendButton() {
return LoadingButton(
onPressed: _handleSendResetEmail,
isLoading: _isLoading,
text: 'Envoyer le lien de réinitialisation',
width: double.infinity,
height: 56,
);
}
Widget _buildBackToLogin() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Vous vous souvenez de votre mot de passe ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
Future<void> _handleSendResetEmail() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'envoi d'email
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Transition vers la vue de succès
setState(() {
_emailSent = true;
_isLoading = false;
});
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'envoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _handleResendEmail() async {
try {
// Simulation de renvoi d'email
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email renvoyé avec succès !'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du renvoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
}

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
import '../widgets/login_form.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion avec interface sophistiquée
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<AuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
LoginHeader(
onAnimationComplete: () {},
),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: LoginForm(
formKey: _formKey,
emailController: _emailController,
passwordController: _passwordController,
obscurePassword: _obscurePassword,
rememberMe: _rememberMe,
isLoading: _isLoading,
onObscureToggle: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
onRememberMeToggle: (value) {
setState(() {
_rememberMe = value;
});
HapticFeedback.selectionClick();
},
onSubmit: _handleLogin,
),
),
),
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
// Connexion réussie - navigation gérée par l'app principal
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
// Erreur de connexion
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
} else if (state.status == AuthStatus.unauthenticated && state.errorMessage != null) {
// Échec de connexion
_handleLoginError(state.errorMessage!);
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) {
_showErrorMessage('Veuillez remplir tous les champs');
_triggerShakeAnimation();
return;
}
// Déclencher la connexion
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<AuthBloc>().add(AuthLoginRequested(loginRequest));
// Feedback haptique
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
// Effacer l'erreur après affichage
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
context.read<AuthBloc>().add(const AuthErrorCleared());
}
});
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
const Text(
'Connexion réussie !',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}

View File

@@ -0,0 +1,478 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/temp_auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion temporaire simplifié
class TempLoginPage extends StatefulWidget {
const TempLoginPage({super.key});
@override
State<TempLoginPage> createState() => _TempLoginPageState();
}
class _TempLoginPageState extends State<TempLoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'admin@unionflow.dev');
final _passwordController = TextEditingController(text: 'admin123');
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<TempAuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
const LoginHeader(),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: _buildLoginForm(),
);
},
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ email
_buildEmailField(),
const SizedBox(height: 20),
// Champ mot de passe
_buildPasswordField(),
const SizedBox(height: 16),
// Options
_buildOptionsRow(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(),
],
),
);
}
Widget _buildEmailField() {
return TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
),
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,
),
),
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 TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
enabled: !_isLoading,
onFieldSubmitted: (_) => _handleLogin(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: Icon(
Icons.lock_outlined,
color: AppTheme.primaryColor,
),
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: AppTheme.primaryColor,
),
),
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,
),
),
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: () {
setState(() {
_rememberMe = !_rememberMe;
});
HapticFeedback.selectionClick();
},
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: _rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: _rememberMe
? const 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,
),
),
],
),
),
),
// Compte de test
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Compte de test',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 4,
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 20),
const SizedBox(width: 8),
const Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<TempAuthBloc>().add(AuthLoginRequested(loginRequest));
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text('Connexion réussie !'),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}

View File

@@ -0,0 +1,517 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import '../../../navigation/presentation/pages/main_navigation.dart';
import 'forgot_password_screen.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildLoginForm(),
const SizedBox(height: 24),
_buildForgotPassword(),
const SizedBox(height: 32),
_buildLoginButton(),
const SizedBox(height: 24),
_buildDivider(),
const SizedBox(height: 24),
_buildSocialLogin(),
const SizedBox(height: 32),
_buildSignUpLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.groups_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Bienvenue !',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Connectez-vous à votre compte UnionFlow',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Champ Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
validator: _validatePassword,
onFieldSubmitted: (_) => _handleLogin(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Remember me
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Text(
'Se souvenir de moi',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
],
),
);
}
Widget _buildForgotPassword() {
return Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _navigateToForgotPassword(),
child: const Text(
'Mot de passe oublié ?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
);
}
Widget _buildLoginButton() {
return LoadingButton(
onPressed: _handleLogin,
isLoading: _isLoading,
text: 'Se connecter',
width: double.infinity,
height: 56,
);
}
Widget _buildDivider() {
return Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'ou',
style: TextStyle(
color: AppTheme.textHint,
fontSize: 14,
),
),
),
const Expanded(child: Divider()),
],
);
}
Widget _buildSocialLogin() {
return Column(
children: [
// Google Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleGoogleLogin(),
icon: Image.asset(
'assets/icons/google.png',
width: 20,
height: 20,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.g_mobiledata,
color: Colors.red,
size: 20,
),
),
label: const Text('Continuer avec Google'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 12),
// Microsoft Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleMicrosoftLogin(),
icon: const Icon(
Icons.business,
color: Color(0xFF00A4EF),
size: 20,
),
label: const Text('Continuer avec Microsoft'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildSignUpLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Pas encore de compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToRegister(),
child: const Text(
'S\'inscrire',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? 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;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'authentification
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Navigation vers le dashboard
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const MainNavigation(),
transitionDuration: const Duration(milliseconds: 600),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
),
);
},
),
);
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur de connexion: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _handleGoogleLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Google - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _handleMicrosoftLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Microsoft - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _navigateToForgotPassword() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const ForgotPasswordScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,624 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _acceptTerms = false;
bool _acceptNewsletter = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 32),
_buildRegistrationForm(),
const SizedBox(height: 24),
_buildTermsAndConditions(),
const SizedBox(height: 32),
_buildRegisterButton(),
const SizedBox(height: 24),
_buildLoginLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.person_add_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Créer un compte',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Rejoignez UnionFlow et gérez votre association',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildRegistrationForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Nom et Prénom
Row(
children: [
Expanded(
child: CustomTextField(
controller: _firstNameController,
label: 'Prénom',
hintText: 'Jean',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateFirstName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
const SizedBox(width: 16),
Expanded(
child: CustomTextField(
controller: _lastNameController,
label: 'Nom',
hintText: 'Dupont',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateLastName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
],
),
const SizedBox(height: 16),
// Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'jean.dupont@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Minimum 8 caractères',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
validator: _validatePassword,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Confirmer mot de passe
CustomTextField(
controller: _confirmPasswordController,
label: 'Confirmer le mot de passe',
hintText: 'Retapez votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
validator: _validateConfirmPassword,
onFieldSubmitted: (_) => _handleRegister(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
const SizedBox(height: 16),
// Indicateur de force du mot de passe
_buildPasswordStrengthIndicator(),
],
),
);
}
Widget _buildPasswordStrengthIndicator() {
final password = _passwordController.text;
final strength = _calculatePasswordStrength(password);
Color strengthColor;
String strengthText;
if (strength < 0.3) {
strengthColor = AppTheme.errorColor;
strengthText = 'Faible';
} else if (strength < 0.7) {
strengthColor = AppTheme.warningColor;
strengthText = 'Moyen';
} else {
strengthColor = AppTheme.successColor;
strengthText = 'Fort';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Force du mot de passe',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
if (password.isNotEmpty)
Text(
strengthText,
style: TextStyle(
fontSize: 12,
color: strengthColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Container(
height: 4,
decoration: BoxDecoration(
color: AppTheme.borderColor,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: password.isEmpty ? 0 : strength,
child: Container(
decoration: BoxDecoration(
color: strengthColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
}
double _calculatePasswordStrength(String password) {
if (password.isEmpty) return 0.0;
double strength = 0.0;
// Longueur
if (password.length >= 8) strength += 0.25;
if (password.length >= 12) strength += 0.25;
// Contient des minuscules
if (password.contains(RegExp(r'[a-z]'))) strength += 0.15;
// Contient des majuscules
if (password.contains(RegExp(r'[A-Z]'))) strength += 0.15;
// Contient des chiffres
if (password.contains(RegExp(r'[0-9]'))) strength += 0.1;
// Contient des caractères spéciaux
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) strength += 0.1;
return strength.clamp(0.0, 1.0);
}
Widget _buildTermsAndConditions() {
return Column(
children: [
// Accepter les conditions
Row(
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
children: [
const TextSpan(text: 'J\'accepte les '),
TextSpan(
text: 'Conditions d\'utilisation',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' et la '),
TextSpan(
text: 'Politique de confidentialité',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
],
),
),
),
],
),
// Newsletter (optionnel)
Row(
children: [
Checkbox(
value: _acceptNewsletter,
onChanged: (value) {
setState(() {
_acceptNewsletter = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Expanded(
child: Text(
'Je souhaite recevoir des actualités et conseils par email (optionnel)',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
),
],
),
],
);
}
Widget _buildRegisterButton() {
return LoadingButton(
onPressed: _acceptTerms ? _handleRegister : null,
isLoading: _isLoading,
text: 'Créer mon compte',
width: double.infinity,
height: 56,
enabled: _acceptTerms,
);
}
Widget _buildLoginLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Déjà un compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToLogin(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateFirstName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre prénom';
}
if (value.length < 2) {
return 'Le prénom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateLastName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre nom';
}
if (value.length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir un mot de passe';
}
if (value.length < 8) {
return 'Le mot de passe doit contenir au moins 8 caractères';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Le mot de passe doit contenir au moins une majuscule';
}
if (!value.contains(RegExp(r'[a-z]'))) {
return 'Le mot de passe doit contenir au moins une minuscule';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Le mot de passe doit contenir au moins un chiffre';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez confirmer votre mot de passe';
}
if (value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez accepter les conditions d\'utilisation'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'inscription
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Afficher message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Compte créé avec succès ! Vérifiez votre email.'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
// Navigation vers l'écran de connexion
_navigateToLogin();
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du compte: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _navigateToLogin() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,400 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 200));
_fadeController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
const Color(0xFF0D47A1),
],
),
),
child: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header avec logo
Expanded(
flex: 3,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo principal
Container(
width: 140,
height: 140,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(35),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: const Icon(
Icons.groups_rounded,
size: 70,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 32),
// Titre principal
const Text(
'UnionFlow',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.5,
),
),
const SizedBox(height: 16),
// Sous-titre
Text(
'Gestion moderne d\'associations\net de mutuelles',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
height: 1.4,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Points forts
_buildFeatureHighlights(),
],
),
),
),
// Boutons d'action
Expanded(
flex: 2,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Connexion
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () => _navigateToLogin(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryColor,
elevation: 8,
shadowColor: Colors.black.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login, size: 20),
SizedBox(width: 8),
Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 16),
// Bouton Inscription
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: () => _navigateToRegister(),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(
color: Colors.white,
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_add, size: 20),
SizedBox(width: 8),
Text(
'Créer un compte',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 24),
// Lien mode démo
TextButton(
onPressed: () => _navigateToDemo(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility,
size: 16,
color: Colors.white.withOpacity(0.8),
),
const SizedBox(width: 6),
Text(
'Découvrir en mode démo',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
// Footer
Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
Text(
'Version 1.0.0 • Sécurisé et confidentiel',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
'© 2024 Lions Club International',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 10,
),
),
],
),
),
],
),
),
);
},
),
),
),
);
}
Widget _buildFeatureHighlights() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFeatureItem(Icons.security, 'Sécurisé'),
_buildFeatureItem(Icons.analytics, 'Analytique'),
_buildFeatureItem(Icons.cloud_sync, 'Synchronisé'),
],
),
);
}
Widget _buildFeatureItem(IconData icon, String label) {
return Column(
children: [
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
);
}
void _navigateToLogin() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToDemo() {
// TODO: Implémenter la navigation vers le mode démo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Mode démo - En cours de développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}