feat(auth): Amélioration UI et logique inscription parent étape 3

- Ajout du switch "Enfant à naître" et ajustement du champ prénom.

- Amélioration de la gestion de l'affichage des photos (placeholder, kIsWeb).

- Refactorisation des boutons avec HoverReliefWidget.

- Localisation du DatePicker en français.

- Nettoyage de l'intégration (annulée) de image_cropper.

- Mise à jour de EVOLUTIONS_CDC.md.
This commit is contained in:
Julien Martin
2025-05-06 23:44:10 +02:00
parent bbdacd68aa
commit df56ba11df
28 changed files with 1597 additions and 87 deletions
+13 -1
View File
@@ -197,7 +197,19 @@ class _LoginPageState extends State<LoginPage> {
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
],
@@ -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<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
}
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
// 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;
*/
},
),
),
],
);
}
}
@@ -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<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
// 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<bool?>? 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;
*/
},
),
),
],
);
}
}
@@ -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<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
final _formKey = GlobalKey<FormState>(); // 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<File?> _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<void> _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<String> 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<String> 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<void> _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<bool> 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;
},
),
),
],
);
}
}
@@ -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<HoverChoiceButton> { ... }
@@ -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<ThemeProvider>(
builder: (context, themeProvider, child) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<ThemeType>(
value: themeProvider.currentTheme,
items: ThemeType.values.map((ThemeType type) {
return DropdownMenuItem<ThemeType>(
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 !'),