diff --git a/docs/EVOLUTIONS_CDC.md b/docs/EVOLUTIONS_CDC.md index cb1c828..8b7a74d 100644 --- a/docs/EVOLUTIONS_CDC.md +++ b/docs/EVOLUTIONS_CDC.md @@ -236,4 +236,22 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante : - Messages d'erreur clairs en cas de : - Email non trouvé - Lien expiré - - Mot de passe non conforme \ No newline at end of file + - Mot de passe non conforme + +## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition) + +### X.1 Recadrage et Redimensionnement des Photos + +#### X.1.1 Fonctionnalités +- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants). +- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final. +- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application. + +#### X.1.2 Solution Technique Envisagée (pour discussion) +- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`. +- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise. + +#### X.1.3 Impact sur l'application +- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`). +- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes. +- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée. \ No newline at end of file diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9ce3b78 --- /dev/null +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + runApp(const PtiPasApp()); +void main() { + runApp(const MyApp()); // Exécution simple +} -final _router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (_, __) => const LoginPage(), - ), - GoRoute( - path: '/legal', - builder: (_, __) => const LegalPage(), - ), - GoRoute( - path: '/privacy', - builder: (_, __) => const PrivacyPage(), - ), - ], -); - -class PtiPasApp extends StatelessWidget { - const PtiPasApp({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { - return MaterialApp.router( + // Pas besoin de Provider.of ici + + return MaterialApp( title: 'P\'titsPas', - routerConfig: _router, - debugShowCheckedModeBanner: false, - theme: ThemeData( - fontFamily: 'Merienda', - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF8AD0C8), - brightness: Brightness.light, + theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut + textTheme: GoogleFonts.meriendaTextTheme( + ThemeData.light().textTheme, ), + // TODO: Définir les couleurs principales si besoin ), + localizationsDelegates: const [ // Configuration pour la localisation + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ // Langues supportées + Locale('fr', 'FR'), // Français + // Locale('en', 'US'), // Anglais, si besoin + ], + locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut + initialRoute: AppRouter.login, + onGenerateRoute: AppRouter.generateRoute, + debugShowCheckedModeBanner: false, ); } } \ No newline at end of file diff --git a/frontend/lib/navigation/app_router.dart b/frontend/lib/navigation/app_router.dart index 50f455e..70fd225 100644 --- a/frontend/lib/navigation/app_router.dart +++ b/frontend/lib/navigation/app_router.dart @@ -1,26 +1,76 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../screens/auth/login_screen.dart'; +import '../screens/auth/register_choice_screen.dart'; +import '../screens/auth/parent_register_step1_screen.dart'; +import '../screens/auth/parent_register_step2_screen.dart'; +import '../screens/auth/parent_register_step3_screen.dart'; import '../screens/home/home_screen.dart'; class AppRouter { static const String login = '/login'; + static const String registerChoice = '/register-choice'; + static const String parentRegisterStep1 = '/parent-register/step1'; + static const String parentRegisterStep2 = '/parent-register/step2'; + static const String parentRegisterStep3 = '/parent-register/step3'; static const String home = '/home'; static Route generateRoute(RouteSettings settings) { + Widget screen; + bool slideTransition = false; + switch (settings.name) { case login: - return MaterialPageRoute(builder: (_) => const LoginScreen()); + screen = const LoginPage(); + break; + case registerChoice: + screen = const RegisterChoiceScreen(); + slideTransition = true; // Activer la transition pour cet écran + break; + case parentRegisterStep1: + screen = const ParentRegisterStep1Screen(); + slideTransition = true; // Activer la transition pour cet écran + break; + case parentRegisterStep2: + screen = const ParentRegisterStep2Screen(); + slideTransition = true; + break; + case parentRegisterStep3: + screen = const ParentRegisterStep3Screen(); + slideTransition = true; + break; case home: - return MaterialPageRoute(builder: (_) => const HomeScreen()); + screen = const HomeScreen(); + break; default: - return MaterialPageRoute( - builder: (_) => Scaffold( - body: Center( - child: Text('Route non définie: ${settings.name}'), - ), + screen = Scaffold( + body: Center( + child: Text('Route non définie : ${settings.name}'), ), ); } + + if (slideTransition) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => screen, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); // Glisse depuis la droite + const end = Offset.zero; + const curve = Curves.easeInOut; // Animation douce + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 400), // Durée de la transition + ); + } else { + // Transition par défaut pour les autres écrans + return MaterialPageRoute(builder: (_) => screen); + } } } \ No newline at end of file diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index d4588ad..d9b82b7 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -197,7 +197,19 @@ class _LoginPageState extends State { const SizedBox(height: 10), // Lien de création de compte Center( - child: Container(), // Suppression du bouton 'Créer un compte' + child: TextButton( + onPressed: () { + Navigator.pushNamed(context, '/register-choice'); + }, + child: Text( + 'Créer un compte', + style: GoogleFonts.merienda( + fontSize: 16, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), ), const SizedBox(height: 20), // Réduit l'espacement en bas ], diff --git a/frontend/lib/screens/auth/parent_register_step1_screen.dart b/frontend/lib/screens/auth/parent_register_step1_screen.dart new file mode 100644 index 0000000..4f9e6d8 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step1_screen.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron + +class ParentRegisterStep1Screen extends StatefulWidget { + const ParentRegisterStep1Screen({super.key}); + + @override + State createState() => _ParentRegisterStep1ScreenState(); +} + +class _ParentRegisterStep1ScreenState extends State { + final _formKey = GlobalKey(); + + // Contrôleurs pour les champs + final _lastNameController = TextEditingController(); + final _firstNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _addressController = TextEditingController(); + final _postalCodeController = TextEditingController(); + final _cityController = TextEditingController(); + + @override + void dispose() { + _lastNameController.dispose(); + _firstNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _addressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Contenu centré + Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Indicateur d'étape (à rendre dynamique) + Text( + 'Étape 1/X', + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 10), + // Texte d'instruction + Text( + 'Merci de renseigner les informations du premier parent :', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + + // Carte jaune contenant le formulaire + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/card_yellow_h.png'), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Votre nom de famille')), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Votre prénom')), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone')), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail')), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe')), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe')), + ], + ), + const SizedBox(height: 20), + _buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Numéro et nom de votre rue'), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal')), + const SizedBox(width: 20), + Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Votre ville')), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + + // Chevron de navigation gauche (Retour) + Positioned( + top: screenSize.height / 2 - 20, // Centré verticalement + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), // Inverse horizontalement + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), // Retour à l'écran de choix + tooltip: 'Retour', + ), + ), + + // Chevron de navigation droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, // Centré verticalement + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + // TODO: Sauvegarder les données du parent 1 + Navigator.pushNamed(context, '/parent-register/step2'); // Naviguer vers l'étape 2 + } + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } + + // Widget pour construire les champs de texte avec le fond personnalisé + Widget _buildTextField( + TextEditingController controller, + String label, { + TextInputType? keyboardType, + bool obscureText = false, + String? hintText, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$label :', + style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87), + ), + const SizedBox(height: 5), + Container( + height: 50, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: TextFormField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black87), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14), + hintText: hintText ?? label, + hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38), + ), + validator: (value) { + // Validation désactivée + return null; + /* + if (value == null || value.isEmpty) { + return 'Ce champ est obligatoire'; + } + // TODO: Ajouter des validations spécifiques (email, téléphone, mot de passe) + if (label == 'Confirmation' && value != _passwordController.text) { + return 'Les mots de passe ne correspondent pas'; + } + return null; + */ + }, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step2_screen.dart b/frontend/lib/screens/auth/parent_register_step2_screen.dart new file mode 100644 index 0000000..a8b44b8 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step2_screen.dart @@ -0,0 +1,374 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron + +class ParentRegisterStep2Screen extends StatefulWidget { + const ParentRegisterStep2Screen({super.key}); + + @override + State createState() => _ParentRegisterStep2ScreenState(); +} + +class _ParentRegisterStep2ScreenState extends State { + final _formKey = GlobalKey(); + + // TODO: Recevoir les infos du parent 1 pour pré-remplir l'adresse + // String? _parent1Address; + // String? _parent1PostalCode; + // String? _parent1City; + + bool _addParent2 = false; // Par défaut, on n'ajoute pas le parent 2 + bool _sameAddressAsParent1 = false; + + // Contrôleurs pour les champs du parent 2 + final _lastNameController = TextEditingController(); + final _firstNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _addressController = TextEditingController(); + final _postalCodeController = TextEditingController(); + final _cityController = TextEditingController(); + + @override + void dispose() { + _lastNameController.dispose(); + _firstNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _addressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + super.dispose(); + } + + // Helper pour activer/désactiver tous les champs sauf l'adresse + bool get _parent2FieldsEnabled => _addParent2; + // Helper pour activer/désactiver les champs d'adresse + bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1; + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Contenu centré + Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Indicateur d'étape + Text( + 'Étape 2/X', // Mettre à jour le numéro d'étape total + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 10), + // Texte d'instruction + Text( + 'Renseignez les informations du deuxième parent (optionnel) :', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + + // Carte bleue contenant le formulaire + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + decoration: const BoxDecoration( // Retour à la décoration + image: DecorationImage( + image: AssetImage('assets/images/card_blue_h.png'), // Utilisation de l'image horizontale + fit: BoxFit.fill, + ), + ), + // Suppression du Stack et Transform.rotate + child: Form( + key: _formKey, + child: SingleChildScrollView( // Le SingleChildScrollView redevient l'enfant direct + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // --- Interrupteurs sur une ligne --- + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Option 1: Ajouter Parent 2 + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.person_add_alt_1, size: 20), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Ajouter Parent 2 ?', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + _buildCustomSwitch( + value: _addParent2, + onChanged: (bool? newValue) { + final bool actualValue = newValue ?? false; + setState(() { + _addParent2 = actualValue; + if (!_addParent2) { + _formKey.currentState?.reset(); + _lastNameController.clear(); + _firstNameController.clear(); + _phoneController.clear(); + _emailController.clear(); + _passwordController.clear(); + _confirmPasswordController.clear(); + _addressController.clear(); + _postalCodeController.clear(); + _cityController.clear(); + _sameAddressAsParent1 = false; + } + }); + }, + ), + ], + ), + ), + const SizedBox(width: 10), + + // Option 2: Même Adresse + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), // Griser l'icône si désactivé + const SizedBox(width: 8), + Flexible( + child: Text( + 'Même Adresse ?', + style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), // Griser le texte si désactivé + overflow: TextOverflow.ellipsis, + ), + ), + _buildCustomSwitch( + value: _sameAddressAsParent1, + onChanged: _addParent2 ? (bool? newValue) { + final bool actualValue = newValue ?? false; + setState(() { + _sameAddressAsParent1 = actualValue; + if (_sameAddressAsParent1) { + _addressController.clear(); + _postalCodeController.clear(); + _cityController.clear(); + // TODO: Pré-remplir + } + }); + } : null, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 25), // Espacement ajusté après les switchs + + // --- Champs du Parent 2 (conditionnels) --- + // Nom & Prénom + Row( + children: [ + Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Nom du deuxième parent', enabled: _parent2FieldsEnabled)), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Prénom du deuxième parent', enabled: _parent2FieldsEnabled)), + ], + ), + const SizedBox(height: 20), + // Téléphone & Email + Row( + children: [ + Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son numéro de téléphone', enabled: _parent2FieldsEnabled)), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son adresse e-mail', enabled: _parent2FieldsEnabled)), + ], + ), + const SizedBox(height: 20), + // Mot de passe + Row( + children: [ + Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled)), + const SizedBox(width: 20), + Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmer son mot de passe', enabled: _parent2FieldsEnabled)), + ], + ), + const SizedBox(height: 20), + + // --- Champs Adresse (conditionnels) --- + _buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled)), + const SizedBox(width: 20), + Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled)), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + + // Chevron de navigation gauche (Retour) + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), // Retour à l'étape 1 + tooltip: 'Retour', + ), + ), + + // Chevron de navigation droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + // Si on n'ajoute pas de parent 2, on passe directement + if (!_addParent2) { + // Naviguer vers l'étape 3 (enfants) + print('Passer à l\'étape 3 (enfants) - Sans Parent 2'); + Navigator.pushNamed(context, '/parent-register/step3'); + return; + } + // Si on ajoute un parent 2 + // Valider seulement si on n'utilise PAS la même adresse + bool isFormValid = true; + // TODO: Remettre la validation quand elle sera prête + /* + if (!_sameAddressAsParent1) { + isFormValid = _formKey.currentState?.validate() ?? false; + } + */ + + if (isFormValid) { + // TODO: Sauvegarder les données du parent 2 + print('Passer à l\'étape 3 (enfants) - Avec Parent 2'); + Navigator.pushNamed(context, '/parent-register/step3'); + } + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } + + // --- NOUVEAU WIDGET --- + // Widget pour construire un switch personnalisé avec images + Widget _buildCustomSwitch({required bool value, required ValueChanged? onChanged}) { + // --- DEBUG --- + print("Building Custom Switch with value: $value"); + // ------------- + const double switchHeight = 25.0; + const double switchWidth = 40.0; + + return InkWell( + onTap: onChanged != null ? () => onChanged(!value) : null, + child: Opacity( + // Griser le switch si désactivé + opacity: onChanged != null ? 1.0 : 0.5, + child: Image.asset( + value ? 'assets/images/switch_on.png' : 'assets/images/switch_off.png', + height: switchHeight, + width: switchWidth, + fit: BoxFit.contain, // Ou BoxFit.fill selon le rendu souhaité + ), + ), + ); + } + + // Widget pour construire les champs de texte (identique à l'étape 1) + Widget _buildTextField( + TextEditingController controller, + String label, { + TextInputType? keyboardType, + bool obscureText = false, + String? hintText, + bool enabled = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$label :', + style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87), + ), + const SizedBox(height: 5), + Container( + height: 50, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: TextFormField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + enabled: enabled, + style: GoogleFonts.merienda(fontSize: 16, color: enabled ? Colors.black87 : Colors.grey), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14), + hintText: hintText ?? label, + hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38), + ), + validator: (value) { + if (!enabled) return null; // Ne pas valider si désactivé + // Le reste de la validation (commentée précédemment) + return null; + /* + if (value == null || value.isEmpty) { + return 'Ce champ est obligatoire'; + } + // TODO: Validations spécifiques + if (label == 'Confirmation' && value != _passwordController.text) { + return 'Les mots de passe ne correspondent pas'; + } + return null; + */ + }, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart new file mode 100644 index 0000000..e1fb315 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -0,0 +1,472 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget +import 'package:image_picker/image_picker.dart'; +// import 'package:image_cropper/image_cropper.dart'; // Supprimé +import 'dart:io' show File, Platform; // Ajout de Platform +import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb + +// TODO: Créer un modèle de données pour l'enfant +// class ChildData { ... } + +class ParentRegisterStep3Screen extends StatefulWidget { + const ParentRegisterStep3Screen({super.key}); + + @override + State createState() => _ParentRegisterStep3ScreenState(); +} + +class _ParentRegisterStep3ScreenState extends State { + // TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs + // List _children = [ChildData()]; // Commencer avec un enfant + final _formKey = GlobalKey(); // Une clé par enfant sera nécessaire si validation complexe + + // Contrôleurs pour le premier enfant (pour l'instant) + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); + final _dobController = TextEditingController(); + bool _photoConsent = false; + bool _multipleBirth = false; + bool _isUnbornChild = false; // Nouvelle variable d'état + // TODO: Ajouter variable pour stocker l'image sélectionnée (par enfant) + // File? _childImage; + + // File? _childImage; // Déjà présent et commenté + // Liste pour stocker les images des enfants (si gestion multi-enfants) + List _childImages = [null]; // Initialiser avec null pour le premier enfant + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _dobController.dispose(); + // TODO: Disposer les contrôleurs de tous les enfants + super.dispose(); + } + + // TODO: Pré-remplir le nom de famille avec celui du parent 1 + @override + void initState() { + super.initState(); + } + + Future _selectDate(BuildContext context) async { + final DateTime now = DateTime.now(); + DateTime initialDatePickerDate = now; + DateTime firstDatePickerDate = DateTime(1980); + DateTime lastDatePickerDate = now; + + if (_isUnbornChild) { + firstDatePickerDate = now; // Ne peut pas être avant aujourd'hui si à naître + lastDatePickerDate = now.add(const Duration(days: 300)); // Environ 10 mois dans le futur + // Si une date de naissance avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage + if (_dobController.text.isNotEmpty) { + try { + // Tenter de parser la date existante + List parts = _dobController.text.split('/'); + DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}"); + if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) { + initialDatePickerDate = parsedDate; + } + } catch (e) { /* Ignorer si le format est incorrect */ } + } + } else { + // Si une date prévisionnelle avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage + if (_dobController.text.isNotEmpty) { + try { + List parts = _dobController.text.split('/'); + DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}"); + if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) { + initialDatePickerDate = parsedDate; + } + } catch (e) { /* Ignorer */ } + } + } + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: initialDatePickerDate, + firstDate: firstDatePickerDate, + lastDate: lastDatePickerDate, + locale: const Locale('fr', 'FR'), + ); + if (picked != null) { + setState(() { + _dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; + }); + } + } + + // Méthode pour sélectionner une image + Future _pickImage(int childIndex) async { + final ImagePicker picker = ImagePicker(); + try { + final XFile? pickedFile = await picker.pickImage( + source: ImageSource.gallery, + imageQuality: 70, + maxWidth: 1024, + maxHeight: 1024, + ); + + if (pickedFile != null) { + // On utilise directement le fichier sélectionné, sans recadrage + setState(() { + if (childIndex < _childImages.length) { + _childImages[childIndex] = File(pickedFile.path); + } else { + print("Erreur: Index d'enfant hors limites pour l'image."); + } + }); + } // Fin de if (pickedFile != null) + + } catch (e) { + print("Erreur lors de la sélection de l'image: $e"); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Contenu centré et scrollable + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), // Ajout de padding vertical + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Indicateur d'étape + Text( + 'Étape 3/X', // Mettre à jour le numéro d'étape total + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 10), + // Texte d'instruction + Text( + 'Merci de renseigner les informations de/vos enfant(s) :', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + + // Zone principale : Cartes enfants + Bouton Ajouter + // Utilisation d'une Row pour placer côte à côte comme sur la maquette + // Il faudra peut-être ajuster pour les petits écrans (Wrap?) + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, // CHANGED: pour centrer verticalement + children: [ + // TODO: Remplacer par une ListView ou Column dynamique basée sur _children + // Pour l'instant, une seule carte + _buildChildCard(context, 0), // Index 0 pour le premier enfant + + const SizedBox(width: 30), + + HoverReliefWidget( + onPressed: () { + print("Ajouter un enfant via HoverReliefWidget"); + // setState(() { _children.add(ChildData()); }); + }, + borderRadius: BorderRadius.circular(15), + child: Image.asset( + 'assets/images/plus.png', + height: 80, + width: 80, + ), + ), + ], + ), + ], + ), + ), + ), + + // Chevrons de navigation (identiques aux étapes précédentes) + // Chevron Gauche (Retour Step 2) + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), // Retour étape 2 + tooltip: 'Retour', + ), + ), + + // Chevron Droit (Suivant Step 4) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + // TODO: Valider les infos enfants et Naviguer vers l'étape 4 + print('Passer à l\'étape 4 (Situation familiale)'); + // Navigator.pushNamed(context, '/parent-register/step4'); + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } + + // Widget pour construire UNE carte enfant + // L'index permettra de lier aux bons contrôleurs et données + Widget _buildChildCard(BuildContext context, int index) { + final File? currentChildImage = (index < _childImages.length) ? _childImages[index] : null; + + // TODO: Déterminer la couleur de base de card_lavander.png et ajuster ces couleurs d'ombre + final Color baseLavandeColor = Colors.purple.shade200; // Placeholder pour la couleur de la carte lavande + final Color initialPhotoShadow = baseLavandeColor.withAlpha(90); + final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130); + + return Container( + width: 300, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/card_lavander.png'), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + HoverReliefWidget( + onPressed: () { + _pickImage(index); + }, + borderRadius: BorderRadius.circular(10), + initialShadowColor: initialPhotoShadow, // Ombre lavande + hoverShadowColor: hoverPhotoShadow, // Ombre lavande au survol + child: SizedBox( + height: 100, + width: 100, + child: Center( + child: Padding( + padding: const EdgeInsets.all(5.0), + child: currentChildImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: kIsWeb // Condition pour le Web + ? Image.network( // Utiliser Image.network pour le Web + currentChildImage.path, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + // Optionnel: Afficher un placeholder ou un message en cas d'erreur de chargement + print("Erreur de chargement de l'image réseau: $error"); + return const Icon(Icons.broken_image, size: 40); + }, + ) + : Image.file( // Utiliser Image.file pour les autres plateformes + currentChildImage, + fit: BoxFit.cover, + ), + ) + : Image.asset( + 'assets/images/photo.png', + fit: BoxFit.contain, + ), + ), + ), + ), + ), + const SizedBox(height: 10), // Espace après la photo + + // Nouveau Switch pour "Enfant à naître ?" + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligner le label à gauche, switch à droite + children: [ + Text( + 'Enfant à naître ?', + style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600), + ), + Switch( + value: _isUnbornChild, + onChanged: (bool newValue) { + setState(() { + _isUnbornChild = newValue; + // Optionnel: Réinitialiser la date si le type change + // _dobController.clear(); + }); + }, + activeColor: Theme.of(context).primaryColor, // Utiliser une couleur de thème + ), + ], + ), + const SizedBox(height: 15), // Espace après le switch + + _buildTextField( + _firstNameController, + 'Prénom', + hintText: _isUnbornChild ? 'Prénom (optionnel)' : 'Prénom de l\'enfant', // HintText ajusté + isRequired: !_isUnbornChild, + ), + const SizedBox(height: 10), + + _buildTextField(_lastNameController, 'Nom', hintText: 'Nom de l\'enfant', enabled: true), + const SizedBox(height: 10), + + _buildTextField( + _dobController, + _isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: () => _selectDate(context), + suffixIcon: Icons.calendar_today, + ), + const SizedBox(height: 20), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCustomCheckbox( + label: 'Consentement photo', + value: _photoConsent, + onChanged: (newValue) { + setState(() => _photoConsent = newValue); + } + ), + const SizedBox(height: 10), + _buildCustomCheckbox( + label: 'Naissance multiple', + value: _multipleBirth, + onChanged: (newValue) { + setState(() => _multipleBirth = newValue); + } + ), + ], + ), + ], + ), + ); + } + + // Widget pour construire une checkbox personnalisée + Widget _buildCustomCheckbox({required String label, required bool value, required ValueChanged onChanged}) { + const double checkboxSize = 20.0; + const double checkmarkSizeFactor = 1.4; // Augmenté pour une coche plus grande + + return GestureDetector( + onTap: () => onChanged(!value), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( // Envelopper le Stack dans un SizedBox pour fixer sa taille + width: checkboxSize, + height: checkboxSize, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Image.asset( + 'assets/images/square.png', + height: checkboxSize, // Taille fixe + width: checkboxSize, // Taille fixe + ), + if (value) + Image.asset( + 'assets/images/coche.png', + height: checkboxSize * checkmarkSizeFactor, + width: checkboxSize * checkmarkSizeFactor, + ), + ], + ), + ), + const SizedBox(width: 10), + Text(label, style: GoogleFonts.merienda(fontSize: 14)), + ], + ), + ); + } + + // Widget pour construire les champs de texte (peut être externalisé) + // Ajout de onTap et suffixIcon pour le DatePicker + Widget _buildTextField( + TextEditingController controller, + String label, { + TextInputType? keyboardType, + bool obscureText = false, + String? hintText, + bool enabled = true, + bool readOnly = false, + VoidCallback? onTap, + IconData? suffixIcon, + bool isRequired = true, // Nouveau paramètre, par défaut à true + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87), + ), + const SizedBox(height: 4), + Container( + height: 45, // Hauteur fixe pour correspondre à l'image de fond + decoration: const BoxDecoration( + image: DecorationImage( // Rétablir input_field_bg.png + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + // Pas de borderRadius ici si l'image de fond les a déjà + ), + child: TextFormField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + enabled: enabled, + readOnly: readOnly, + onTap: onTap, + style: GoogleFonts.merienda(fontSize: 15, color: enabled ? Colors.black87 : Colors.grey), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), // Augmentation du padding vertical + hintText: hintText, + hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black38), + suffixIcon: suffixIcon != null ? Padding( + padding: const EdgeInsets.only(right: 8.0), // Espace pour l'icône + child: Icon(suffixIcon, color: Colors.black54, size: 20), + ) : null, + isDense: true, // Aide à réduire la hauteur par défaut + ), + validator: (value) { + if (!enabled) return null; + if (readOnly) return null; + if (isRequired && (value == null || value.isEmpty)) { // Validation conditionnée par isRequired + return 'Ce champ est obligatoire'; + } + // TODO: Validations spécifiques (à garder si pertinent pour d'autres champs) + return null; + }, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/register_choice_screen.dart b/frontend/lib/screens/auth/register_choice_screen.dart new file mode 100644 index 0000000..7e7d770 --- /dev/null +++ b/frontend/lib/screens/auth/register_choice_screen.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import '../../widgets/hover_relief_widget.dart'; // Import du widget générique + +class RegisterChoiceScreen extends StatelessWidget { + const RegisterChoiceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Bouton Retour (chevron gauche) + Positioned( + top: 40, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), + tooltip: 'Retour', + ), + ), + + // Contenu principal en Row (Gauche / Droite) + Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: Row( + children: [ + // Partie Gauche: Texte d'instruction centré + Expanded( + flex: 1, + child: Center( + child: Text( + 'Veuillez choisir votre\ntype de compte :', + style: GoogleFonts.merienda( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + ), + // Espace entre les deux parties + SizedBox(width: screenSize.width * 0.05), + + // Partie Droite: Carte rose avec les boutons + Expanded( + flex: 1, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow + ), + child: AspectRatio( + aspectRatio: 2 / 3, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/card_rose.png'), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image + _buildChoiceButton( + context: context, + iconPath: 'assets/images/icon_parents.png', + label: 'Parents', + onPressed: () { + Navigator.pushNamed(context, '/parent-register/step1'); + }, + ), + // Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image + _buildChoiceButton( + context: context, + iconPath: 'assets/images/icon_assmat.png', + label: 'Assistante Maternelle', + onPressed: () { + // TODO: Naviguer vers l'écran d'inscription assmat + print('Choix: Assistante Maternelle'); + }, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// Nouvelle méthode helper pour construire les boutons de choix +Widget _buildChoiceButton({ + required BuildContext context, + required String iconPath, + required String label, + required VoidCallback onPressed, +}) { + // TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre + final Color baseRoseColor = Colors.pink.shade300; // Placeholder + final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale + final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + HoverReliefWidget( + onPressed: onPressed, + borderRadius: BorderRadius.circular(15.0), + initialShadowColor: initialShadow, // Ombre rose initiale + hoverShadowColor: hoverShadow, // Ombre rose au survol + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset(iconPath, height: 140), + ), + ), + const SizedBox(height: 15), + Text( + label, + style: GoogleFonts.merienda( + fontSize: 26, + fontWeight: FontWeight.w600, + color: Colors.black.withOpacity(0.85), + ), + textAlign: TextAlign.center, + ), + ], + ); +} + +// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs --- +// class HoverChoiceButton extends StatefulWidget { ... } +// class _HoverChoiceButtonState extends State { ... } \ No newline at end of file diff --git a/frontend/lib/screens/home/home_screen.dart b/frontend/lib/screens/home/home_screen.dart index 4c2237c..9a0c6ba 100644 --- a/frontend/lib/screens/home/home_screen.dart +++ b/frontend/lib/screens/home/home_screen.dart @@ -1,50 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../../theme/theme_provider.dart'; -import '../../theme/app_theme.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); - String _getThemeName(ThemeType type) { - switch (type) { - case ThemeType.defaultTheme: - return "P'titsPas"; - case ThemeType.pastelTheme: - return "Pastel"; - case ThemeType.darkTheme: - return "Sombre"; - } - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Accueil'), - actions: [ - Consumer( - builder: (context, themeProvider, child) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: DropdownButton( - value: themeProvider.currentTheme, - items: ThemeType.values.map((ThemeType type) { - return DropdownMenuItem( - value: type, - child: Text(_getThemeName(type)), - ); - }).toList(), - onChanged: (ThemeType? newValue) { - if (newValue != null) { - themeProvider.setTheme(newValue); - } - }, - ), - ); - }, - ), - ], ), body: const Center( child: Text('Bienvenue sur P\'titsPas !'), diff --git a/frontend/lib/widgets/hover_relief_widget.dart b/frontend/lib/widgets/hover_relief_widget.dart new file mode 100644 index 0000000..cee1f79 --- /dev/null +++ b/frontend/lib/widgets/hover_relief_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +class HoverReliefWidget extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final BorderRadius borderRadius; + final double initialElevation; + final double hoverElevation; + final double scaleFactor; + final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol + final Color initialShadowColor; // Nouveau paramètre + final Color hoverShadowColor; // Nouveau paramètre + + const HoverReliefWidget({ + required this.child, + this.onPressed, + this.borderRadius = const BorderRadius.all(Radius.circular(15.0)), + this.initialElevation = 4.0, + this.hoverElevation = 8.0, + this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent + this.enableHoverEffect = true, // Par défaut, l'effet est activé + this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15) + this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3) + super.key, + }); + + @override + State createState() => _HoverReliefWidgetState(); +} + +class _HoverReliefWidgetState extends State { + bool _isHovering = false; + + @override + Widget build(BuildContext context) { + final bool canHover = widget.enableHoverEffect && widget.onPressed != null; + + final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor); + final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity(); + final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor; + final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation; + + Widget content = AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: transform, + transformAlignment: Alignment.center, + child: Material( + color: Colors.transparent, + elevation: elevation, + shadowColor: shadowColor, + borderRadius: widget.borderRadius, + clipBehavior: Clip.antiAlias, + child: widget.child, + ), + ); + + if (widget.onPressed == null) { + // Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol) + // Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover. + return Material( + color: Colors.transparent, + elevation: widget.initialElevation, // Utilise l'élévation initiale + shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable + borderRadius: widget.borderRadius, + clipBehavior: Clip.antiAlias, + child: widget.child, + ); + } + + return MouseRegion( + onEnter: (_) { + if (widget.enableHoverEffect) setState(() => _isHovering = true); + }, + onExit: (_) { + if (widget.enableHoverEffect) setState(() => _isHovering = false); + }, + cursor: SystemMouseCursors.click, + child: InkWell( + onTap: widget.onPressed, + borderRadius: widget.borderRadius, + hoverColor: Colors.transparent, + splashColor: Colors.grey.withOpacity(0.2), + highlightColor: Colors.grey.withOpacity(0.1), + child: content, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index c700298..9d0bbd5 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -126,6 +126,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -240,6 +245,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" js: dependency: "direct main" description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index ff10e21..54b7cf6 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter provider: ^6.1.1 go_router: ^13.2.5 google_fonts: ^6.1.0 @@ -27,13 +29,7 @@ flutter: uses-material-design: true assets: - - assets/images/logo.png - - assets/images/river_logo_desktop.png - - assets/images/paper2.png - - assets/images/field_email.png - - assets/images/field_password.png - - assets/images/btn_green.png - - assets/images/icon.png + - assets/images/ # Déclarer le dossier entier fonts: - family: Merienda diff --git a/frontend/web/index.html b/frontend/web/index.html index 28a7aac..7a6630a 100644 --- a/frontend/web/index.html +++ b/frontend/web/index.html @@ -18,7 +18,7 @@ - + @@ -27,11 +27,17 @@ - + P'titsPas + + +