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: [36m${settings.name}[0m'),
- ),
+ 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
+
+
+