feat(auth): Amélioration UI/UX étape 3 inscription enfants
- Corrige le débordement visuel (RenderFlex overflow) dans les cartes enfants.
- Augmente les marges latérales du sélecteur d'enfants pour un meilleur centrage.
- Ajoute un défilement automatique vers la droite lors de l'ajout d'un enfant.
- Intègre une barre de défilement horizontale et un effet de fondu dynamique (fading edges) au sélecteur d'enfants.
- Ajuste le padding vertical dans CustomAppTextField pour un meilleur centrage du hintText.
- Met à jour index.html :
- Utilise le token {{flutter_service_worker_version}}.
- Ajoute la balise meta mobile-web-app-capable.
- Rétablit temporairement loadEntrypoint pour éviter un écran blanc (avertissement de dépréciation en attente de correction).
This commit is contained in:
parent
df56ba11df
commit
42d147c273
@ -6,9 +6,40 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
||||||
import 'dart:io' show File, Platform; // Ajout de Platform
|
import 'dart:io' show File, Platform; // Ajout de Platform
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
||||||
|
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
|
||||||
|
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
|
||||||
|
|
||||||
// TODO: Créer un modèle de données pour l'enfant
|
// Classe de données pour un enfant
|
||||||
// class ChildData { ... }
|
class _ChildFormData {
|
||||||
|
final Key key; // Pour aider Flutter à identifier les widgets dans une liste
|
||||||
|
final TextEditingController firstNameController;
|
||||||
|
final TextEditingController lastNameController;
|
||||||
|
final TextEditingController dobController;
|
||||||
|
bool photoConsent;
|
||||||
|
bool multipleBirth;
|
||||||
|
bool isUnbornChild;
|
||||||
|
File? imageFile;
|
||||||
|
|
||||||
|
_ChildFormData({
|
||||||
|
required this.key,
|
||||||
|
String initialFirstName = '',
|
||||||
|
String initialLastName = '',
|
||||||
|
String initialDob = '',
|
||||||
|
this.photoConsent = false,
|
||||||
|
this.multipleBirth = false,
|
||||||
|
this.isUnbornChild = false,
|
||||||
|
this.imageFile,
|
||||||
|
}) : firstNameController = TextEditingController(text: initialFirstName),
|
||||||
|
lastNameController = TextEditingController(text: initialLastName),
|
||||||
|
dobController = TextEditingController(text: initialDob);
|
||||||
|
|
||||||
|
// Méthode pour disposer les contrôleurs
|
||||||
|
void dispose() {
|
||||||
|
firstNameController.dispose();
|
||||||
|
lastNameController.dispose();
|
||||||
|
dobController.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ParentRegisterStep3Screen extends StatefulWidget {
|
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||||
const ParentRegisterStep3Screen({super.key});
|
const ParentRegisterStep3Screen({super.key});
|
||||||
@ -22,60 +53,127 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
|
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
|
||||||
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
|
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
|
||||||
|
|
||||||
// Contrôleurs pour le premier enfant (pour l'instant)
|
// Liste pour stocker les données de chaque enfant
|
||||||
final _firstNameController = TextEditingController();
|
List<_ChildFormData> _childrenDataList = [];
|
||||||
final _lastNameController = TextEditingController();
|
final ScrollController _scrollController = ScrollController(); // Ajout du ScrollController
|
||||||
final _dobController = TextEditingController();
|
bool _isScrollable = false;
|
||||||
bool _photoConsent = false;
|
bool _showLeftFade = false;
|
||||||
bool _multipleBirth = false;
|
bool _showRightFade = false;
|
||||||
bool _isUnbornChild = false; // Nouvelle variable d'état
|
static const double _fadeExtent = 0.05; // Pourcentage de la vue pour le fondu (5%)
|
||||||
// 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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_addChild();
|
||||||
|
_scrollController.addListener(_scrollListener);
|
||||||
|
// Appel initial pour définir l'état des fondus après le premier layout
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectDate(BuildContext context) async {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Disposer les contrôleurs de tous les enfants
|
||||||
|
for (var childData in _childrenDataList) {
|
||||||
|
childData.dispose();
|
||||||
|
}
|
||||||
|
_scrollController.removeListener(_scrollListener); // Ne pas oublier de retirer le listener
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollListener() {
|
||||||
|
if (!_scrollController.hasClients) return; // S'assurer que le controller est attaché
|
||||||
|
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final newIsScrollable = position.maxScrollExtent > 0.0;
|
||||||
|
// Le fondu à gauche est affiché si on a scrollé plus loin que la moitié de la zone de fondu
|
||||||
|
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
|
||||||
|
// Le fondu à droite est affiché s'il reste à scroller plus que la moitié de la zone de fondu
|
||||||
|
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
|
||||||
|
|
||||||
|
if (newIsScrollable != _isScrollable ||
|
||||||
|
newShowLeftFade != _showLeftFade ||
|
||||||
|
newShowRightFade != _showRightFade) {
|
||||||
|
setState(() {
|
||||||
|
_isScrollable = newIsScrollable;
|
||||||
|
_showLeftFade = newShowLeftFade;
|
||||||
|
_showRightFade = newShowRightFade;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addChild() {
|
||||||
|
String initialLastName = '';
|
||||||
|
if (_childrenDataList.isNotEmpty) {
|
||||||
|
initialLastName = _childrenDataList.first.lastNameController.text;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_childrenDataList.add(_ChildFormData(
|
||||||
|
key: UniqueKey(),
|
||||||
|
initialLastName: initialLastName,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
// S'assurer que le listener est appelé après la mise à jour de l'UI
|
||||||
|
// et faire défiler vers la fin si possible
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_scrollListener(); // Mettre à jour l'état des fondus
|
||||||
|
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour sélectionner une image (devra être adaptée pour l'index)
|
||||||
|
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) {
|
||||||
|
setState(() {
|
||||||
|
if (childIndex < _childrenDataList.length) {
|
||||||
|
_childrenDataList[childIndex].imageFile = File(pickedFile.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} // Fin de if (pickedFile != null)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la sélection de l'image: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate(BuildContext context, int childIndex) async {
|
||||||
|
final _ChildFormData currentChild = _childrenDataList[childIndex];
|
||||||
final DateTime now = DateTime.now();
|
final DateTime now = DateTime.now();
|
||||||
DateTime initialDatePickerDate = now;
|
DateTime initialDatePickerDate = now;
|
||||||
DateTime firstDatePickerDate = DateTime(1980);
|
DateTime firstDatePickerDate = DateTime(1980);
|
||||||
DateTime lastDatePickerDate = now;
|
DateTime lastDatePickerDate = now;
|
||||||
|
|
||||||
if (_isUnbornChild) {
|
if (currentChild.isUnbornChild) {
|
||||||
firstDatePickerDate = now; // Ne peut pas être avant aujourd'hui si à naître
|
firstDatePickerDate = now;
|
||||||
lastDatePickerDate = now.add(const Duration(days: 300)); // Environ 10 mois dans le futur
|
lastDatePickerDate = now.add(const Duration(days: 300));
|
||||||
// Si une date de naissance avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
if (currentChild.dobController.text.isNotEmpty) {
|
||||||
if (_dobController.text.isNotEmpty) {
|
|
||||||
try {
|
try {
|
||||||
// Tenter de parser la date existante
|
List<String> parts = currentChild.dobController.text.split('/');
|
||||||
List<String> parts = _dobController.text.split('/');
|
|
||||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
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)) {
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||||
initialDatePickerDate = parsedDate;
|
initialDatePickerDate = parsedDate;
|
||||||
}
|
}
|
||||||
} catch (e) { /* Ignorer si le format est incorrect */ }
|
} catch (e) { /* Ignorer */ }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Si une date prévisionnelle avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
if (currentChild.dobController.text.isNotEmpty) {
|
||||||
if (_dobController.text.isNotEmpty) {
|
|
||||||
try {
|
try {
|
||||||
List<String> parts = _dobController.text.split('/');
|
List<String> parts = currentChild.dobController.text.split('/');
|
||||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
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)) {
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||||
initialDatePickerDate = parsedDate;
|
initialDatePickerDate = parsedDate;
|
||||||
@ -93,135 +191,145 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
currentChild.dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour sélectionner une image
|
void _removeChild(Key key) {
|
||||||
Future<void> _pickImage(int childIndex) async {
|
setState(() {
|
||||||
final ImagePicker picker = ImagePicker();
|
// Trouver et supprimer l'enfant par sa clé, et s'assurer qu'il en reste au moins un.
|
||||||
try {
|
if (_childrenDataList.length > 1) {
|
||||||
final XFile? pickedFile = await picker.pickImage(
|
_childrenDataList.removeWhere((child) => child.key == key);
|
||||||
source: ImageSource.gallery,
|
}
|
||||||
imageQuality: 70,
|
});
|
||||||
maxWidth: 1024,
|
// S'assurer que le listener est appelé après la mise à jour de l'UI
|
||||||
maxHeight: 1024,
|
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
||||||
);
|
|
||||||
|
|
||||||
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond papier
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||||
'assets/images/paper2.png',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
repeat: ImageRepeat.repeat,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Contenu centré et scrollable
|
|
||||||
Center(
|
Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40.0), // Ajout de padding vertical
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Indicateur d'étape
|
Text('Étape 3/X', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||||
Text(
|
|
||||||
'Étape 3/X', // Mettre à jour le numéro d'étape total
|
|
||||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
// Texte d'instruction
|
|
||||||
Text(
|
Text(
|
||||||
'Merci de renseigner les informations de/vos enfant(s) :',
|
'Merci de renseigner les informations de/vos enfant(s) :',
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
Padding( // Ajout du Padding pour les marges latérales
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 150.0), // Marge de 150px de chaque côté
|
||||||
|
child: SizedBox(
|
||||||
|
height: 500,
|
||||||
|
child: ShaderMask(
|
||||||
|
shaderCallback: (Rect bounds) {
|
||||||
|
// Déterminer les couleurs du gradient en fonction de l'état de défilement
|
||||||
|
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
|
||||||
|
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
|
||||||
|
|
||||||
|
// Si ce n'est pas scrollable du tout, pas de fondu.
|
||||||
|
if (!_isScrollable) {
|
||||||
|
return LinearGradient(
|
||||||
|
colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black],
|
||||||
|
stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],
|
||||||
|
).createShader(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
// Zone principale : Cartes enfants + Bouton Ajouter
|
return LinearGradient(
|
||||||
// Utilisation d'une Row pour placer côte à côte comme sur la maquette
|
begin: Alignment.centerLeft,
|
||||||
// Il faudra peut-être ajuster pour les petits écrans (Wrap?)
|
end: Alignment.centerRight,
|
||||||
Row(
|
colors: <Color>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
leftFade, // Bord gauche
|
||||||
crossAxisAlignment: CrossAxisAlignment.center, // CHANGED: pour centrer verticalement
|
Colors.black, // Devient opaque
|
||||||
children: [
|
Colors.black, // Reste opaque
|
||||||
// TODO: Remplacer par une ListView ou Column dynamique basée sur _children
|
rightFade // Bord droit
|
||||||
// Pour l'instant, une seule carte
|
],
|
||||||
_buildChildCard(context, 0), // Index 0 pour le premier enfant
|
stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], // 5% de fondu sur chaque bord
|
||||||
|
).createShader(bounds);
|
||||||
const SizedBox(width: 30),
|
|
||||||
|
|
||||||
HoverReliefWidget(
|
|
||||||
onPressed: () {
|
|
||||||
print("Ajouter un enfant via HoverReliefWidget");
|
|
||||||
// setState(() { _children.add(ChildData()); });
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(15),
|
blendMode: BlendMode.dstIn,
|
||||||
child: Image.asset(
|
child: Scrollbar( // Ajout du Scrollbar
|
||||||
'assets/images/plus.png',
|
controller: _scrollController, // Utiliser le même contrôleur
|
||||||
height: 80,
|
thumbVisibility: true, // Rendre la thumb toujours visible pour le web si souhaité, ou la laisser adaptative
|
||||||
width: 80,
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
itemCount: _childrenDataList.length + 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index < _childrenDataList.length) {
|
||||||
|
// Carte Enfant
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 20.0), // Espace entre les cartes
|
||||||
|
child: _ChildCardWidget(
|
||||||
|
key: _childrenDataList[index].key, // Passer la clé unique
|
||||||
|
childData: _childrenDataList[index],
|
||||||
|
childIndex: index,
|
||||||
|
onPickImage: () => _pickImage(index),
|
||||||
|
onDateSelect: () => _selectDate(context, index),
|
||||||
|
onTogglePhotoConsent: (newValue) {
|
||||||
|
setState(() => _childrenDataList[index].photoConsent = newValue);
|
||||||
|
},
|
||||||
|
onToggleMultipleBirth: (newValue) {
|
||||||
|
setState(() => _childrenDataList[index].multipleBirth = newValue);
|
||||||
|
},
|
||||||
|
onToggleIsUnborn: (newValue) {
|
||||||
|
setState(() => _childrenDataList[index].isUnbornChild = newValue);
|
||||||
|
},
|
||||||
|
onRemove: () => _removeChild(_childrenDataList[index].key),
|
||||||
|
canBeRemoved: _childrenDataList.length > 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Bouton Ajouter
|
||||||
|
return Center( // Pour centrer le bouton dans l'espace disponible
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
onPressed: _addChild,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20), // Espace optionnel après la liste
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Chevrons de navigation
|
||||||
// Chevrons de navigation (identiques aux étapes précédentes)
|
|
||||||
// Chevron Gauche (Retour Step 2)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: screenSize.height / 2 - 20,
|
top: screenSize.height / 2 - 20,
|
||||||
left: 40,
|
left: 40,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Transform(
|
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||||
alignment: Alignment.center,
|
onPressed: () => Navigator.pop(context),
|
||||||
transform: Matrix4.rotationY(math.pi),
|
|
||||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context), // Retour étape 2
|
|
||||||
tooltip: 'Retour',
|
tooltip: 'Retour',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Chevron Droit (Suivant Step 4)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: screenSize.height / 2 - 20,
|
top: screenSize.height / 2 - 20,
|
||||||
right: 40,
|
right: 40,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Valider les infos enfants et Naviguer vers l'étape 4
|
|
||||||
print('Passer à l\'étape 4 (Situation familiale)');
|
print('Passer à l\'étape 4 (Situation familiale)');
|
||||||
// Navigator.pushNamed(context, '/parent-register/step4');
|
// Navigator.pushNamed(context, '/parent-register/step4');
|
||||||
},
|
},
|
||||||
@ -232,241 +340,138 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Widget pour construire UNE carte enfant
|
// Nouveau Widget pour la carte enfant
|
||||||
// L'index permettra de lier aux bons contrôleurs et données
|
class _ChildCardWidget extends StatelessWidget {
|
||||||
Widget _buildChildCard(BuildContext context, int index) {
|
final _ChildFormData childData;
|
||||||
final File? currentChildImage = (index < _childImages.length) ? _childImages[index] : null;
|
final int childIndex; // Utile pour certains callbacks ou logging
|
||||||
|
final VoidCallback onPickImage;
|
||||||
|
final VoidCallback onDateSelect;
|
||||||
|
final ValueChanged<bool> onTogglePhotoConsent;
|
||||||
|
final ValueChanged<bool> onToggleMultipleBirth;
|
||||||
|
final ValueChanged<bool> onToggleIsUnborn;
|
||||||
|
final VoidCallback onRemove; // Callback pour supprimer la carte
|
||||||
|
final bool canBeRemoved; // Pour afficher/cacher le bouton de suppression
|
||||||
|
|
||||||
// TODO: Déterminer la couleur de base de card_lavander.png et ajuster ces couleurs d'ombre
|
const _ChildCardWidget({
|
||||||
final Color baseLavandeColor = Colors.purple.shade200; // Placeholder pour la couleur de la carte lavande
|
required Key key, // Important pour le ListView.builder
|
||||||
|
required this.childData,
|
||||||
|
required this.childIndex,
|
||||||
|
required this.onPickImage,
|
||||||
|
required this.onDateSelect,
|
||||||
|
required this.onTogglePhotoConsent,
|
||||||
|
required this.onToggleMultipleBirth,
|
||||||
|
required this.onToggleIsUnborn,
|
||||||
|
required this.onRemove,
|
||||||
|
required this.canBeRemoved,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final File? currentChildImage = childData.imageFile;
|
||||||
|
final Color baseLavandeColor = Colors.purple.shade200;
|
||||||
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
|
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
|
||||||
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
|
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 300,
|
width: 300,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: const DecorationImage(
|
image: const DecorationImage(image: AssetImage('assets/images/card_lavander.png'), fit: BoxFit.cover),
|
||||||
image: AssetImage('assets/images/card_lavander.png'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Stack( // Stack pour pouvoir superposer le bouton de suppression
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildCustomCheckbox(
|
HoverReliefWidget(
|
||||||
label: 'Consentement photo',
|
onPressed: onPickImage,
|
||||||
value: _photoConsent,
|
borderRadius: BorderRadius.circular(10),
|
||||||
onChanged: (newValue) {
|
initialShadowColor: initialPhotoShadow,
|
||||||
setState(() => _photoConsent = newValue);
|
hoverShadowColor: hoverPhotoShadow,
|
||||||
}
|
child: SizedBox(
|
||||||
|
height: 100, width: 100,
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: currentChildImage != null
|
||||||
|
? ClipRRect(borderRadius: BorderRadius.circular(10), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
||||||
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildCustomCheckbox(
|
Row(
|
||||||
label: 'Naissance multiple',
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
value: _multipleBirth,
|
children: [
|
||||||
onChanged: (newValue) {
|
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||||
setState(() => _multipleBirth = newValue);
|
Switch(value: childData.isUnbornChild, onChanged: onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
|
||||||
}
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
CustomAppTextField( // Utilisation du nouveau widget
|
||||||
|
controller: childData.firstNameController,
|
||||||
|
label: 'Prénom',
|
||||||
|
hintText: childData.isUnbornChild ? 'Prénom (optionnel)' : 'Prénom de l\'enfant',
|
||||||
|
isRequired: !childData.isUnbornChild,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
CustomAppTextField( // Utilisation du nouveau widget
|
||||||
|
controller: childData.lastNameController,
|
||||||
|
label: 'Nom',
|
||||||
|
hintText: 'Nom de l\'enfant',
|
||||||
|
enabled: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
CustomAppTextField( // Utilisation du nouveau widget
|
||||||
|
controller: childData.dobController,
|
||||||
|
label: childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||||||
|
hintText: 'JJ/MM/AAAA',
|
||||||
|
readOnly: true,
|
||||||
|
onTap: onDateSelect,
|
||||||
|
suffixIcon: Icons.calendar_today,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AppCustomCheckbox( // Utilisation du nouveau widget
|
||||||
|
label: 'Consentement photo',
|
||||||
|
value: childData.photoConsent,
|
||||||
|
onChanged: onTogglePhotoConsent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
AppCustomCheckbox( // Utilisation du nouveau widget
|
||||||
|
label: 'Naissance multiple',
|
||||||
|
value: childData.multipleBirth,
|
||||||
|
onChanged: onToggleMultipleBirth,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
if (canBeRemoved) // Afficher le bouton de suppression conditionnellement
|
||||||
),
|
Positioned(
|
||||||
);
|
top: -5, // Ajuster pour le positionnement visuel
|
||||||
}
|
right: -5, // Ajuster pour le positionnement visuel
|
||||||
|
child: InkWell(
|
||||||
// Widget pour construire une checkbox personnalisée
|
onTap: onRemove,
|
||||||
Widget _buildCustomCheckbox({required String label, required bool value, required ValueChanged<bool> onChanged}) {
|
customBorder: const CircleBorder(), // Pour un effet de clic circulaire
|
||||||
const double checkboxSize = 20.0;
|
child: Container(
|
||||||
const double checkmarkSizeFactor = 1.4; // Augmenté pour une coche plus grande
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
return GestureDetector(
|
color: Colors.red.withOpacity(0.8), // Fond rouge pour le bouton X
|
||||||
onTap: () => onChanged(!value),
|
shape: BoxShape.circle,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
],
|
child: const Icon(Icons.close, color: Colors.white, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
62
frontend/lib/widgets/app_custom_checkbox.dart
Normal file
62
frontend/lib/widgets/app_custom_checkbox.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class AppCustomCheckbox extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool value;
|
||||||
|
final ValueChanged<bool> onChanged;
|
||||||
|
final double checkboxSize;
|
||||||
|
final double checkmarkSizeFactor;
|
||||||
|
|
||||||
|
const AppCustomCheckbox({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
this.checkboxSize = 20.0,
|
||||||
|
this.checkmarkSizeFactor = 1.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onChanged(!value), // Inverse la valeur au clic
|
||||||
|
behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: checkboxSize,
|
||||||
|
height: checkboxSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/square.png',
|
||||||
|
height: checkboxSize,
|
||||||
|
width: checkboxSize,
|
||||||
|
),
|
||||||
|
if (value)
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/coche.png',
|
||||||
|
height: checkboxSize * checkmarkSizeFactor,
|
||||||
|
width: checkboxSize * checkmarkSizeFactor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis, // Gérer le texte long
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
frontend/lib/widgets/custom_app_text_field.dart
Normal file
84
frontend/lib/widgets/custom_app_text_field.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class CustomAppTextField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final String? hintText;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final bool obscureText;
|
||||||
|
final bool enabled;
|
||||||
|
final bool readOnly;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final IconData? suffixIcon;
|
||||||
|
final bool isRequired;
|
||||||
|
final String? Function(String?)? validator; // Permettre un validateur personnalisé
|
||||||
|
|
||||||
|
const CustomAppTextField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
this.hintText,
|
||||||
|
this.keyboardType,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.enabled = true,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.onTap,
|
||||||
|
this.suffixIcon,
|
||||||
|
this.isRequired = true,
|
||||||
|
this.validator,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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.fromLTRB(15, 13, 15, 11),
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black38),
|
||||||
|
suffixIcon: suffixIcon != null ? Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: Icon(suffixIcon, color: Colors.black54, size: 20),
|
||||||
|
) : null,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
validator: validator ?? // Utilise le validateur fourni, ou celui par défaut
|
||||||
|
(value) {
|
||||||
|
if (!enabled) return null;
|
||||||
|
if (readOnly) return null;
|
||||||
|
if (isRequired && (value == null || value.isEmpty)) {
|
||||||
|
return 'Ce champ est obligatoire';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="P'titsPas">
|
<meta name="apple-mobile-web-app-title" content="P'titsPas">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
@ -40,7 +41,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// The value below is injected by flutter build, do not touch.
|
// The value below is injected by flutter build, do not touch.
|
||||||
const serviceWorkerVersion = null;
|
const serviceWorkerVersion = "{{flutter_service_worker_version}}";
|
||||||
</script>
|
</script>
|
||||||
<!-- This script adds the flutter initialization JS code -->
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
<script src="flutter.js" defer></script>
|
<script src="flutter.js" defer></script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user