Refactoring + Checkpoint
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../../core/constants/colors.dart';
|
||||
|
||||
/// [AccountDeletionCard] est un widget permettant à l'utilisateur de supprimer son compte.
|
||||
/// Il affiche une confirmation avant d'effectuer l'action de suppression.
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../../core/constants/colors.dart';
|
||||
|
||||
/// [EditOptionsCard] permet à l'utilisateur d'accéder aux options d'édition du profil,
|
||||
/// incluant la modification du profil, la photo et le mot de passe.
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../../core/constants/colors.dart';
|
||||
|
||||
/// [ExpandableSectionCard] est une carte qui peut s'étendre pour révéler des éléments enfants.
|
||||
/// Ce composant inclut des animations d'extension, des logs pour chaque action et une expérience utilisateur optimisée.
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
import 'stat_tile.dart';
|
||||
import '../../../../../core/constants/colors.dart';
|
||||
import '../../../../../domain/entities/user.dart';
|
||||
import '../stat_tile.dart';
|
||||
|
||||
/// [StatisticsSectionCard] affiche les statistiques principales de l'utilisateur avec des animations.
|
||||
/// Ce composant est optimisé pour une expérience interactive et une traçabilité complète des actions via les logs.
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
import '../../data/providers/user_provider.dart';
|
||||
import '../../../../../core/constants/colors.dart';
|
||||
import '../../../../../domain/entities/user.dart';
|
||||
import '../../../data/providers/user_provider.dart';
|
||||
|
||||
/// [UserInfoCard] affiche les informations essentielles de l'utilisateur de manière concise.
|
||||
/// Conçu pour minimiser les répétitions tout en garantissant une expérience utilisateur fluide.
|
||||
@@ -1,119 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' as rootBundle;
|
||||
|
||||
class CategoryField extends StatefulWidget {
|
||||
final FormFieldSetter<String> onSaved;
|
||||
|
||||
const CategoryField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CategoryFieldState createState() => _CategoryFieldState();
|
||||
}
|
||||
|
||||
class _CategoryFieldState extends State<CategoryField> {
|
||||
String? _selectedCategory;
|
||||
Map<String, List<String>> _categoryMap = {}; // Map pour stocker les catégories et sous-catégories
|
||||
List<DropdownMenuItem<String>> _dropdownItems = []; // Liste pour stocker les éléments de menu déroulant
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCategories(); // Charger les catégories à partir du JSON
|
||||
}
|
||||
|
||||
// Charger les catégories depuis le fichier JSON
|
||||
Future<void> _loadCategories() async {
|
||||
try {
|
||||
final String jsonString = await rootBundle.rootBundle.loadString('lib/assets/json/event_categories.json');
|
||||
final Map<String, dynamic> jsonMap = json.decode(jsonString);
|
||||
|
||||
final Map<String, List<String>> categoryMap = {};
|
||||
jsonMap['categories'].forEach((key, value) {
|
||||
categoryMap[key] = List<String>.from(value);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_categoryMap = categoryMap;
|
||||
_dropdownItems = _buildDropdownItems();
|
||||
});
|
||||
|
||||
// Ajouter un log pour vérifier si les catégories sont bien chargées
|
||||
print("Catégories chargées: $_categoryMap");
|
||||
} catch (e) {
|
||||
print("Erreur lors du chargement des catégories : $e");
|
||||
}
|
||||
}
|
||||
|
||||
// Construire les éléments du menu déroulant avec catégorisation
|
||||
List<DropdownMenuItem<String>> _buildDropdownItems() {
|
||||
List<DropdownMenuItem<String>> items = [];
|
||||
|
||||
_categoryMap.forEach((category, subcategories) {
|
||||
items.add(
|
||||
DropdownMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Text(
|
||||
category,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (String subcategory in subcategories) {
|
||||
items.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: subcategory,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
subcategory,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter un log pour vérifier si les éléments sont bien créés
|
||||
print("Éléments créés pour le menu déroulant: ${items.length}");
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _dropdownItems.isEmpty
|
||||
? CircularProgressIndicator() // Affiche un indicateur de chargement si les éléments ne sont pas encore prêts
|
||||
: DropdownButtonFormField<String>(
|
||||
value: _selectedCategory,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Catégorie',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.1),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.category, color: Colors.white70),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
dropdownColor: const Color(0xFF2C2C3E),
|
||||
iconEnabledColor: Colors.white70,
|
||||
items: _dropdownItems,
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
_selectedCategory = newValue;
|
||||
});
|
||||
},
|
||||
onSaved: widget.onSaved,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,8 +3,14 @@ import 'package:flutter/material.dart';
|
||||
class DatePickerField extends StatelessWidget {
|
||||
final DateTime? selectedDate;
|
||||
final Function(DateTime?) onDatePicked;
|
||||
final String label; // Texte du label
|
||||
|
||||
const DatePickerField({Key? key, this.selectedDate, required this.onDatePicked}) : super(key: key);
|
||||
const DatePickerField({
|
||||
Key? key,
|
||||
this.selectedDate,
|
||||
required this.onDatePicked,
|
||||
this.label = 'Sélectionnez une date', // Label par défaut
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -21,21 +27,36 @@ class DatePickerField extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 18.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.blueGrey.withOpacity(0.1), // Fond plus doux et moderne
|
||||
borderRadius: BorderRadius.circular(12.0), // Coins arrondis plus prononcés
|
||||
border: Border.all(color: Colors.blueGrey.withOpacity(0.5), width: 2.0), // Bordure légère
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedDate == null
|
||||
? 'Sélectionnez une date'
|
||||
? label
|
||||
: '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}',
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
style: const TextStyle(
|
||||
color: Colors.blueGrey, // Couleur du texte adaptée
|
||||
fontSize: 16.0, // Taille de police améliorée
|
||||
fontWeight: FontWeight.w600, // Poids de police plus important pour un meilleur contraste
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
color: Colors.blueGrey, // Couleur de l'icône assortie au texte
|
||||
),
|
||||
const Icon(Icons.calendar_today, color: Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DescriptionField extends StatelessWidget {
|
||||
final FormFieldSetter<String> onSaved;
|
||||
const DescriptionField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.1),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.description, color: Colors.white70),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer une description';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import 'package:afterwork/core/utils/date_formatter.dart';
|
||||
import 'event_menu.dart';
|
||||
|
||||
class EventHeader extends StatelessWidget {
|
||||
final String userFirstName;
|
||||
final String userLastName;
|
||||
final String creatorFirstName;
|
||||
final String creatorLastName;
|
||||
final String profileImageUrl;
|
||||
final String? eventDate;
|
||||
final String? imageUrl;
|
||||
final String location;
|
||||
@@ -14,8 +15,9 @@ class EventHeader extends StatelessWidget {
|
||||
|
||||
const EventHeader({
|
||||
Key? key,
|
||||
required this.userFirstName,
|
||||
required this.userLastName,
|
||||
required this.creatorFirstName,
|
||||
required this.creatorLastName,
|
||||
required this.profileImageUrl,
|
||||
this.eventDate,
|
||||
this.imageUrl,
|
||||
required this.location,
|
||||
@@ -40,9 +42,9 @@ class EventHeader extends StatelessWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.grey.shade800,
|
||||
backgroundImage: imageUrl != null && imageUrl!.isNotEmpty
|
||||
? NetworkImage(imageUrl!)
|
||||
: const AssetImage('lib/assets/images/placeholder.png') as ImageProvider,
|
||||
backgroundImage: profileImageUrl.isNotEmpty
|
||||
? NetworkImage(profileImageUrl)
|
||||
: AssetImage(profileImageUrl) as ImageProvider,
|
||||
radius: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -51,7 +53,7 @@ class EventHeader extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$userFirstName $userLastName',
|
||||
'$creatorFirstName $creatorLastName',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
@@ -91,11 +93,12 @@ class EventHeader extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Ajout des boutons dans le coin supérieur droit
|
||||
// Placement des icônes avec padding pour éviter qu'elles ne soient trop proches du bord
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
right: -5,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
key: menuKey,
|
||||
@@ -105,6 +108,7 @@ class EventHeader extends StatelessWidget {
|
||||
showEventOptions(menuContext, menuKey);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 0), // Espacement entre les icônes
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white54, size: 20),
|
||||
splashRadius: 20,
|
||||
|
||||
@@ -20,6 +20,7 @@ class EventList extends StatelessWidget {
|
||||
userId: 'user_id_here', // Vous pouvez passer l'ID réel de l'utilisateur connecté
|
||||
userFirstName: 'John', // Vous pouvez passer le prénom réel de l'utilisateur
|
||||
userLastName: 'Doe', // Vous pouvez passer le nom réel de l'utilisateur
|
||||
profileImageUrl: 'profileImageUrl',
|
||||
onReact: () => _handleReact(event),
|
||||
onComment: () => _handleComment(event),
|
||||
onShare: () => _handleShare(event),
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:afterwork/core/constants/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/theme/theme_provider.dart';
|
||||
|
||||
void showEventOptions(BuildContext context, GlobalKey key) {
|
||||
final RenderBox renderBox = key.currentContext!.findRenderObject() as RenderBox;
|
||||
// Obtient la position de l'élément pour afficher le menu contextuel
|
||||
final RenderBox renderBox =
|
||||
key.currentContext!.findRenderObject() as RenderBox;
|
||||
final Offset offset = renderBox.localToGlobal(Offset.zero);
|
||||
final RelativeRect position = RelativeRect.fromLTRB(
|
||||
offset.dx,
|
||||
@@ -10,76 +17,150 @@ void showEventOptions(BuildContext context, GlobalKey key) {
|
||||
offset.dy + renderBox.size.height,
|
||||
);
|
||||
|
||||
// Affiche le menu contextuel avec des options personnalisées
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: 'details',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.blue.shade400, size: 18), // Icône plus petite et bleue
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Voir les détails',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade700, // Texte bleu foncé
|
||||
fontWeight: FontWeight.w500, // Poids de police plus fin
|
||||
fontSize: 14, // Taille légèrement réduite
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildElegantMenuItem(
|
||||
icon: Icons.info_outline,
|
||||
label: 'Voir les détails',
|
||||
color: AppColors.primary, // Utilise la couleur primaire dynamique
|
||||
onTap: () {
|
||||
print('Voir les détails');
|
||||
// Log d'action pour suivre l'interaction utilisateur
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, color: Colors.orange.shade400, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Modifier l\'événement',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildElegantMenuItem(
|
||||
icon: Icons.edit,
|
||||
label: 'Modifier l\'événement',
|
||||
color: AppColors.secondary, // Utilise la couleur secondaire dynamique
|
||||
onTap: () {
|
||||
print('Modifier l\'événement');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete_outline, color: Colors.red.shade400, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Supprimer l\'événement',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildElegantMenuItem(
|
||||
icon: Icons.delete_outline,
|
||||
label: 'Supprimer l\'événement',
|
||||
color: AppColors.errorColor, // Utilise la couleur d'erreur dynamique
|
||||
onTap: () {
|
||||
_showDeleteConfirmation(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
elevation: 5.0, // Réduction de l'élévation pour une ombre plus subtile
|
||||
elevation: 12.0, // Niveau d'élévation du menu pour une ombre modérée
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0), // Ajout de bordures arrondies
|
||||
side: BorderSide(color: Colors.grey.shade300), // Bordure fine et douce
|
||||
borderRadius: BorderRadius.circular(20.0), // Coins arrondis pour un look moderne
|
||||
),
|
||||
color: Colors.white, // Fond blanc pur pour un contraste élégant
|
||||
color: AppColors.customBackgroundColor, // Surface dynamique selon le thème
|
||||
).then((value) {
|
||||
// Gérer les actions en fonction de la sélection
|
||||
if (value == 'details') {
|
||||
print('Voir les détails');
|
||||
} else if (value == 'edit') {
|
||||
print('Modifier l\'événement');
|
||||
} else if (value == 'delete') {
|
||||
print('Supprimer l\'événement');
|
||||
if (value != null) {
|
||||
HapticFeedback.lightImpact(); // Retour haptique pour une meilleure UX
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Construction d'un élément de menu stylisé
|
||||
PopupMenuItem _buildElegantMenuItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return PopupMenuItem(
|
||||
value: label,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTap();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppColors.cardColor, // Couleur de fond dynamique du conteneur
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 15), // Icône avec couleur personnalisée
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color, // Couleur de texte dynamique
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context) {
|
||||
// Récupère le thème sans écoute, car la fonction est appelée en dehors de l'arbre de widgets.
|
||||
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
|
||||
|
||||
// Affiche une boîte de dialogue pour confirmer la suppression
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: AppColors.errorColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Supprimer l\'événement',
|
||||
style: TextStyle(
|
||||
color: AppColors.errorColor, // Utilisation de la couleur d'erreur dynamique
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
'Voulez-vous vraiment supprimer cet événement ? Cette action est irréversible.',
|
||||
style: TextStyle(
|
||||
color: themeProvider.isDarkMode ? AppColors.lightOnPrimary : AppColors.darkPrimary, // Texte principal dynamique
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: MaterialStateProperty.all(Colors.grey.shade200),
|
||||
),
|
||||
child: Text('Annuler', style: TextStyle(color: Colors.grey.shade700)),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.delete, color: themeProvider.isDarkMode ? AppColors.darkPrimary : AppColors.lightOnPrimary, size: 18),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.errorColor, // Bouton de suppression en couleur d'erreur
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
textStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
label: Text('Supprimer'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
print('Événement supprimé');
|
||||
// Logique de suppression réelle ici
|
||||
},
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ class EventStatusBadge extends StatelessWidget {
|
||||
Icon(
|
||||
status == 'fermé' ? Icons.lock : Icons.lock_open,
|
||||
color: status == 'fermé' ? Colors.red : Colors.green,
|
||||
size: 16.0,
|
||||
size: 10.0,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
status == 'fermé' ? 'Fermé' : 'Ouvert',
|
||||
style: TextStyle(
|
||||
color: status == 'fermé' ? Colors.red : Colors.green,
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
26
lib/presentation/widgets/fields/accessibility_field.dart
Normal file
26
lib/presentation/widgets/fields/accessibility_field.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AccessibilityField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const AccessibilityField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Accessibilité',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer des informations sur l\'accessibilité';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AccommodationInfoField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const AccommodationInfoField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Informations sur l\'hébergement',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer des informations sur l\'hébergement';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
80
lib/presentation/widgets/fields/attendees_field.dart
Normal file
80
lib/presentation/widgets/fields/attendees_field.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Un champ pour saisir le nombre maximum de participants à un événement.
|
||||
/// Il est conçu pour permettre à l'utilisateur de saisir un nombre entier.
|
||||
class AttendeesField extends StatelessWidget {
|
||||
// Définition de la fonction de rappel pour sauver la valeur saisie.
|
||||
final Function(int) onSaved;
|
||||
|
||||
// Le constructeur prend une fonction de rappel pour sauvegarder la valeur saisie.
|
||||
const AttendeesField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Retourne un widget de type Column pour organiser le texte et le champ de saisie.
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Texte statique indiquant ce que l'utilisateur doit entrer.
|
||||
const Text(
|
||||
'Nombre maximum de participants', // Le texte est en français et indique le champ à remplir.
|
||||
style: TextStyle(
|
||||
color: Colors.blueGrey, // Couleur du texte
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold, // Met en gras pour la visibilité
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8), // Espacement entre le titre et le champ de saisie.
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.number, // Le champ attend un nombre entier.
|
||||
decoration: InputDecoration(
|
||||
hintStyle: const TextStyle(color: Colors.blueGrey),
|
||||
hintText: 'Entrez ici le nombre maximum de participants...', // L'invite pour aider l'utilisateur.
|
||||
filled: true, // Le champ est rempli avec une couleur de fond.
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond du champ avec opacité.
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie
|
||||
borderSide: BorderSide.none, // Pas de bordure par défaut
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, // Bordure de base
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blue, // Bordure en bleu lors du focus
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.group,
|
||||
color: Colors.blueGrey, // Icône assortie
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.blueGrey, // Couleur du texte saisi
|
||||
fontSize: 16.0, // Taille de police
|
||||
fontWeight: FontWeight.w600, // Poids de la police pour la lisibilité
|
||||
),
|
||||
onChanged: (value) {
|
||||
// Lors de chaque modification de texte, on tente de convertir la valeur en entier.
|
||||
int? maxParticipants = int.tryParse(value) ?? 0; // Conversion en entier, avec une valeur par défaut de 0.
|
||||
print('Nombre maximum de participants saisi : $maxParticipants'); // Log pour suivre la valeur saisie.
|
||||
onSaved(maxParticipants); // Appel de la fonction onSaved pour transmettre la valeur au formulaire principal.
|
||||
},
|
||||
validator: (value) {
|
||||
// Validation pour vérifier si la valeur est un nombre valide.
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un nombre de participants';
|
||||
}
|
||||
return null; // La validation est correcte si la valeur est un nombre
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
192
lib/presentation/widgets/fields/category_field.dart
Normal file
192
lib/presentation/widgets/fields/category_field.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' as rootBundle;
|
||||
|
||||
class CategoryField extends StatefulWidget {
|
||||
// Ce callback est utilisé pour enregistrer la valeur sélectionnée dans le formulaire
|
||||
final FormFieldSetter<String> onSaved;
|
||||
|
||||
// Constructeur de la classe CategoryField
|
||||
const CategoryField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CategoryFieldState createState() => _CategoryFieldState();
|
||||
}
|
||||
|
||||
class _CategoryFieldState extends State<CategoryField> {
|
||||
// Variable pour stocker la catégorie sélectionnée par l'utilisateur
|
||||
String? _selectedCategory;
|
||||
|
||||
// Map pour stocker les catégories et leurs sous-catégories
|
||||
Map<String, List<String>> _categoryMap = {};
|
||||
|
||||
// Liste des éléments du menu déroulant
|
||||
List<DropdownMenuItem<String>> _dropdownItems = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Chargement des catégories dès que l'état est initialisé
|
||||
_loadCategories();
|
||||
}
|
||||
|
||||
/// Méthode pour charger les catégories depuis un fichier JSON.
|
||||
/// Cette méthode récupère les catégories et sous-catégories depuis le fichier JSON
|
||||
/// et met à jour l'état du widget.
|
||||
Future<void> _loadCategories() async {
|
||||
try {
|
||||
// Chargement du fichier JSON à partir des ressources
|
||||
final String jsonString = await rootBundle.rootBundle
|
||||
.loadString('lib/assets/json/event_categories.json');
|
||||
|
||||
// Décodage du fichier JSON pour obtenir un Map
|
||||
final Map<String, dynamic> jsonMap = json.decode(jsonString);
|
||||
|
||||
// Map pour stocker les catégories et leurs sous-catégories
|
||||
final Map<String, List<String>> categoryMap = {};
|
||||
|
||||
// Parcours des catégories et ajout des sous-catégories dans le map
|
||||
jsonMap['categories'].forEach((key, value) {
|
||||
categoryMap[key] = List<String>.from(value);
|
||||
});
|
||||
|
||||
// Mise à jour de l'état avec les nouvelles données chargées
|
||||
setState(() {
|
||||
_categoryMap = categoryMap;
|
||||
_dropdownItems =
|
||||
_buildDropdownItems(); // Reconstruction des éléments du menu
|
||||
});
|
||||
|
||||
// Log pour vérifier si les catégories ont bien été chargées
|
||||
debugPrint("Catégories chargées : $_categoryMap");
|
||||
} catch (e) {
|
||||
// Log en cas d'erreur lors du chargement
|
||||
debugPrint("Erreur lors du chargement des catégories : $e");
|
||||
|
||||
// Affichage d'un message d'erreur à l'utilisateur si le chargement échoue
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors du chargement des catégories. Veuillez réessayer plus tard.')));
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode pour construire la liste des éléments du menu déroulant avec les catégories et sous-catégories.
|
||||
/// Cette méthode crée une liste d'éléments DropdownMenuItem pour afficher dans le DropdownButton.
|
||||
List<DropdownMenuItem<String>> _buildDropdownItems() {
|
||||
List<DropdownMenuItem<String>> items = [];
|
||||
|
||||
// Parcours des catégories et ajout des sous-catégories dans le menu déroulant
|
||||
_categoryMap.forEach((category, subcategories) {
|
||||
// Ajouter une catégorie (non sélectionnable) comme en-tête
|
||||
items.add(
|
||||
DropdownMenuItem<String>(
|
||||
enabled: false,
|
||||
// Cette entrée est désactivée pour qu'elle ne soit pas sélectionnée
|
||||
child: Text(
|
||||
category,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Ajouter les sous-catégories associées à cette catégorie
|
||||
for (String subcategory in subcategories) {
|
||||
items.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: subcategory, // Valeur de la sous-catégorie
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
// Indentation pour les sous-catégories
|
||||
child: Text(
|
||||
subcategory,
|
||||
style: const TextStyle(color: Colors.blueGrey),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Log pour vérifier le nombre d'éléments créés pour le menu déroulant
|
||||
debugPrint("Éléments créés pour le menu déroulant : ${items.length}");
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Si la liste des éléments est vide, afficher un indicateur de chargement
|
||||
return _dropdownItems.isEmpty
|
||||
? const Center(
|
||||
child:
|
||||
CircularProgressIndicator()) // Affichage d'un indicateur de chargement pendant le chargement des données
|
||||
: DropdownButtonFormField<String>(
|
||||
value: _selectedCategory,
|
||||
// Valeur sélectionnée par l'utilisateur
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Catégorie',
|
||||
// Libellé du champ
|
||||
labelStyle: const TextStyle(color: Colors.blueGrey),
|
||||
// Style du libellé
|
||||
filled: true,
|
||||
// Remplissage du champ
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1),
|
||||
// Couleur de fond
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey, // Couleur de la bordure par défaut
|
||||
width: 2.0, // Épaisseur de la bordure
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey,
|
||||
// Couleur de la bordure quand non sélectionné
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue, // Bordure quand le champ est sélectionné
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.category,
|
||||
color: Colors.blueGrey), // Icône du champ
|
||||
),
|
||||
style: const TextStyle(color: Colors.blueGrey),
|
||||
// Style du texte sélectionné
|
||||
dropdownColor: const Color(0xFF2C2C3E),
|
||||
// Couleur de fond du menu déroulant
|
||||
iconEnabledColor: Colors.blueGrey,
|
||||
// Couleur de l'icône du menu déroulant
|
||||
items: _dropdownItems,
|
||||
// Liste des éléments du menu déroulant
|
||||
onChanged: (String? newValue) {
|
||||
// Log pour suivre la valeur sélectionnée
|
||||
debugPrint("Nouvelle catégorie sélectionnée : $newValue");
|
||||
|
||||
setState(() {
|
||||
_selectedCategory =
|
||||
newValue; // Mise à jour de la catégorie sélectionnée
|
||||
});
|
||||
},
|
||||
onSaved: widget.onSaved,
|
||||
// Enregistrer la valeur dans le formulaire
|
||||
hint: const Text(
|
||||
'Veuillez choisir une catégorie',
|
||||
// Texte affiché lorsqu'aucune catégorie n'est sélectionnée
|
||||
style: TextStyle(
|
||||
color: Colors.blueGrey), // Style du texte par défaut
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/presentation/widgets/fields/description_field.dart
Normal file
79
lib/presentation/widgets/fields/description_field.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// `DescriptionField` est un champ de texte utilisé pour saisir une description.
|
||||
/// Ce champ fait partie d'un formulaire et est conçu pour accepter plusieurs lignes de texte.
|
||||
/// Il est doté de validations et d'une logique d'enregistrement personnalisée via `onSaved`.
|
||||
///
|
||||
/// Ce widget utilise des icônes et un style personnalisé pour correspondre à l'apparence de l'application.
|
||||
///
|
||||
/// Arguments :
|
||||
/// - `onSaved`: Une fonction callback utilisée pour enregistrer la valeur du champ de texte une fois que le formulaire est soumis.
|
||||
/// ```
|
||||
class DescriptionField extends StatelessWidget {
|
||||
// Callback utilisé pour enregistrer la valeur de la description
|
||||
final FormFieldSetter<String> onSaved;
|
||||
|
||||
// Constructeur du widget DescriptionField
|
||||
const DescriptionField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Log : Construction du champ DescriptionField
|
||||
debugPrint('Construction du champ DescriptionField');
|
||||
|
||||
return TextFormField(
|
||||
// Décoration du champ de texte
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description', // Texte étiquette affiché à l'utilisateur
|
||||
labelStyle: const TextStyle(color: Colors.blueGrey), // Style de l'étiquette
|
||||
filled: true, // Active le fond coloré
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond plus douce et plus subtile
|
||||
hintStyle: const TextStyle(color: Colors.blueGrey),
|
||||
hintText: 'Entrez un la description ici...',
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie améliorée
|
||||
borderSide: BorderSide.none, // Pas de bordure visible
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, // Bordure de base en bleu gris
|
||||
width: 2.0, // Largeur de la bordure
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blue, // Bordure bleue lors du focus
|
||||
width: 2.0, // Épaisseur de la bordure lors du focus
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.description, color: Colors.blueGrey), // Icône de description avant le texte
|
||||
),
|
||||
// Style du texte dans le champ
|
||||
style: const TextStyle(color: Colors.blueGrey, fontSize: 16.0),
|
||||
// Limite le champ à 3 lignes, avec un retour à la ligne automatique
|
||||
maxLines: 3,
|
||||
// Autres configurations du champ
|
||||
textInputAction: TextInputAction.done, // Permet de soumettre avec la touche "Done" du clavier
|
||||
|
||||
// Validation du champ : assure que le champ n'est pas vide
|
||||
validator: (value) {
|
||||
// Log : Validation du champ DescriptionField
|
||||
debugPrint('Validation du champ DescriptionField');
|
||||
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer une description'; // Message d'erreur si la description est vide
|
||||
}
|
||||
return null; // Retourne null si la validation passe
|
||||
},
|
||||
|
||||
// Lors de la soumission du formulaire, enregistre la valeur saisie
|
||||
onSaved: (value) {
|
||||
// Log : Sauvegarde de la valeur de la description
|
||||
debugPrint('Valeur de la description sauvegardée : $value');
|
||||
onSaved(value); // Appel de la fonction onSaved passée en paramètre
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/presentation/widgets/fields/link_field.dart
Normal file
63
lib/presentation/widgets/fields/link_field.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LinkField extends StatelessWidget {
|
||||
// Le callback `onSaved` est utilisé pour enregistrer la valeur du champ lorsque le formulaire est soumis.
|
||||
final FormFieldSetter<String> onSaved;
|
||||
|
||||
// Constructeur de la classe LinkField, qui attend le callback `onSaved`.
|
||||
const LinkField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Création du champ de texte pour le lien
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Lien (optionnel)', // Le texte affiché lorsqu'il n'y a pas de valeur
|
||||
labelStyle: const TextStyle(color: Colors.blueGrey), // Style du texte du label
|
||||
filled: true, // Remplissage du champ
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec une légère opacité
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)), // Bords arrondis du champ
|
||||
borderSide: BorderSide.none, // Pas de bordure visible
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey, // Couleur de la bordure quand non sélectionné
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue, // Bordure quand le champ est sélectionné
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.link, color: Colors.blueGrey), // Icône de lien à gauche
|
||||
hintText: 'Entrez un lien ici...', // Texte d'indication lorsque le champ est vide
|
||||
),
|
||||
style: const TextStyle(color: Colors.blueGrey), // Style du texte saisi par l'utilisateur
|
||||
onSaved: (value) {
|
||||
// Log de la valeur du champ lorsqu'on l'enregistre
|
||||
debugPrint("Lien enregistré : $value");
|
||||
|
||||
// Appel du callback `onSaved` pour enregistrer la valeur dans le formulaire
|
||||
onSaved(value);
|
||||
},
|
||||
keyboardType: TextInputType.url, // Permet à l'utilisateur de saisir une URL
|
||||
validator: (value) {
|
||||
// Si le champ est rempli, on valide que la valeur est bien une URL correcte
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final Uri? uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
// Log en cas d'erreur de validation
|
||||
debugPrint("URL invalide : $value");
|
||||
return 'Veuillez entrer un lien valide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/presentation/widgets/fields/location_field.dart
Normal file
72
lib/presentation/widgets/fields/location_field.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import '../../screens/location/location_picker_Screen.dart';
|
||||
|
||||
/// `LocationField` est un champ de saisie permettant de sélectionner une localisation sur une carte.
|
||||
/// Il utilise la page `LocationPickerScreen` pour permettre à l'utilisateur de choisir un emplacement précis.
|
||||
/// Ce widget est utilisé dans des formulaires et permet d'afficher la localisation sélectionnée.
|
||||
///
|
||||
/// Arguments :
|
||||
/// - `location`: Une chaîne représentant la localisation actuelle à afficher.
|
||||
/// - `selectedLatLng`: Une variable de type `LatLng?` représentant la latitude et la longitude de la localisation sélectionnée.
|
||||
/// - `onLocationPicked`: Un callback pour retourner la localisation choisie par l'utilisateur.
|
||||
///
|
||||
class LocationField extends StatelessWidget {
|
||||
final String location;
|
||||
final LatLng? selectedLatLng;
|
||||
final Function(LatLng?) onLocationPicked;
|
||||
|
||||
const LocationField({Key? key, required this.location, this.selectedLatLng, required this.onLocationPicked})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Log : Construction du champ LocationField
|
||||
debugPrint('Construction du champ LocationField');
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
// Log : L'utilisateur clique pour choisir une localisation
|
||||
debugPrint('Utilisateur clique pour choisir une localisation');
|
||||
|
||||
final LatLng? pickedLocation = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LocationPickerScreen(),
|
||||
),
|
||||
);
|
||||
|
||||
if (pickedLocation != null) {
|
||||
// Log : L'utilisateur a sélectionné une nouvelle localisation
|
||||
debugPrint('Nouvelle localisation sélectionnée : $pickedLocation');
|
||||
onLocationPicked(pickedLocation);
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300), // Animation fluide lors du focus
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey.withOpacity(0.1), // Fond plus visible, subtilement coloré
|
||||
borderRadius: BorderRadius.circular(12.0), // Bordure arrondie améliorée
|
||||
border: Border.all(
|
||||
color: selectedLatLng == null ? Colors.blueGrey.withOpacity(0.5) : Colors.blue, // Bordure change selon l'état
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedLatLng == null
|
||||
? 'Sélectionnez une localisation' // Message par défaut si aucune localisation sélectionnée
|
||||
: 'Localisation: $location', // Affiche la localisation actuelle
|
||||
style: const TextStyle(color: Colors.blueGrey, fontSize: 16.0),
|
||||
),
|
||||
const Icon(Icons.location_on, color: Colors.blueGrey),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/presentation/widgets/fields/organizer_field.dart
Normal file
54
lib/presentation/widgets/fields/organizer_field.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Un champ de saisie pour l'organisateur, utilisé dans un formulaire.
|
||||
class OrganizerField extends StatelessWidget {
|
||||
// Fonction de rappel pour sauvegarder la valeur de l'organisateur.
|
||||
final Function(String?) onSaved;
|
||||
|
||||
// Constructeur qui prend la fonction onSaved pour transmettre l'organisateur au formulaire.
|
||||
const OrganizerField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Organisateur', // Texte d'étiquette pour le champ de saisie.
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.person, // Icône représentant un organisateur (utilisateur).
|
||||
color: Colors.blueGrey, // Couleur de l'icône.
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
|
||||
borderSide: BorderSide.none, // Pas de bordure par défaut.
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, // Bordure colorée en blueGrey.
|
||||
width: 1.5, // Largeur de la bordure.
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blue, // Bordure bleue au focus.
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
filled: true, // Le champ de saisie est rempli de couleur de fond.
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec opacité.
|
||||
),
|
||||
validator: (value) {
|
||||
// Validation pour vérifier que le champ n'est pas vide.
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un organisateur'; // Message d'erreur si vide.
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved, // Fonction qui est appelée pour sauvegarder la valeur saisie.
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/presentation/widgets/fields/parking_field.dart
Normal file
26
lib/presentation/widgets/fields/parking_field.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParkingField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const ParkingField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Informations sur le parking',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer des informations sur le parking';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/presentation/widgets/fields/participation_fee_field.dart
Normal file
27
lib/presentation/widgets/fields/participation_fee_field.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParticipationFeeField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const ParticipationFeeField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Frais de participation',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer les frais de participation';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/presentation/widgets/fields/privacy_rules_field.dart
Normal file
26
lib/presentation/widgets/fields/privacy_rules_field.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrivacyRulesField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const PrivacyRulesField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Règles de confidentialité',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer des règles de confidentialité';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/presentation/widgets/fields/security_protocol_field.dart
Normal file
26
lib/presentation/widgets/fields/security_protocol_field.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SecurityProtocolField extends StatelessWidget {
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const SecurityProtocolField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Protocole de sécurité',
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un protocole de sécurité';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/presentation/widgets/fields/tags_field.dart
Normal file
89
lib/presentation/widgets/fields/tags_field.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Un champ permettant à l'utilisateur de saisir des tags.
|
||||
/// Il permet également d'afficher les tags saisis sous forme de chips (étiquettes).
|
||||
class TagsField extends StatefulWidget {
|
||||
// Fonction de rappel pour sauvegarder la liste des tags saisis.
|
||||
final Function(List<String>) onSaved;
|
||||
|
||||
// Constructeur qui prend la fonction onSaved pour transmettre les tags au formulaire.
|
||||
const TagsField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TagsFieldState createState() => _TagsFieldState(); // Création de l'état pour gérer les tags.
|
||||
}
|
||||
|
||||
class _TagsFieldState extends State<TagsField> {
|
||||
final TextEditingController _controller = TextEditingController(); // Contrôleur pour gérer l'entrée de texte.
|
||||
List<String> _tags = []; // Liste pour stocker les tags saisis.
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // Alignement à gauche pour les éléments.
|
||||
children: [
|
||||
const SizedBox(height: 8), // Espacement entre le titre et le champ de saisie.
|
||||
TextFormField(
|
||||
controller: _controller, // Associe le contrôleur à ce champ de texte.
|
||||
decoration: InputDecoration(
|
||||
hintStyle: const TextStyle(color: Colors.blueGrey),
|
||||
hintText: 'Entrez un les tags ici séparés par des virgules...',
|
||||
labelText: 'Tags', // Texte d'étiquette pour le champ de saisie.
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.tag, // Icône représentant un tag.
|
||||
color: Colors.blueGrey, // Couleur de l'icône.
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
|
||||
borderSide: BorderSide.none, // Pas de bordure par défaut.
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, // Bordure de base.
|
||||
width: 1.5, // Largeur de la bordure.
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blue, // Bordure bleue au focus.
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
filled: true, // Le champ est rempli avec une couleur de fond.
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond du champ de texte avec opacité.
|
||||
),
|
||||
onFieldSubmitted: (value) {
|
||||
print('Tags soumis : $value'); // Log pour suivre ce qui a été saisi avant la soumission.
|
||||
_addTags(value); // Appel à la méthode _addTags pour ajouter les tags.
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8), // Espacement entre le champ de saisie et les chips.
|
||||
Wrap(
|
||||
spacing: 8.0, // Espacement entre les chips.
|
||||
children: _tags.map((tag) => Chip(
|
||||
label: Text(tag), // Texte du tag à afficher.
|
||||
backgroundColor: Colors.blueGrey.withOpacity(0.2), // Couleur de fond des chips.
|
||||
labelStyle: const TextStyle(color: Colors.blueGrey), // Couleur du texte dans les chips.
|
||||
)).toList(), // Génère une liste de Chips pour chaque tag.
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Fonction pour ajouter les tags à la liste.
|
||||
void _addTags(String value) {
|
||||
setState(() {
|
||||
_tags = value.split(',') // Sépare les tags par des virgules.
|
||||
.map((tag) => tag.trim()) // Supprime les espaces autour des tags.
|
||||
.where((tag) => tag.isNotEmpty) // Exclut les tags vides.
|
||||
.toList(); // Crée la liste de tags.
|
||||
});
|
||||
print('Tags ajoutés : $_tags'); // Log pour vérifier la liste de tags ajoutée.
|
||||
widget.onSaved(_tags); // Envoie la liste des tags au formulaire principal.
|
||||
}
|
||||
}
|
||||
54
lib/presentation/widgets/fields/title_field.dart
Normal file
54
lib/presentation/widgets/fields/title_field.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TitleField extends StatelessWidget {
|
||||
final FormFieldSetter<String> onSaved;
|
||||
const TitleField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Titre',
|
||||
labelStyle: const TextStyle(color: Colors.blueGrey), // Couleur du label
|
||||
filled: true,
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Fond plus doux
|
||||
hintStyle: const TextStyle(color: Colors.blueGrey),
|
||||
hintText: 'Entrez un le titre ici...',
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure plus arrondie
|
||||
borderSide: BorderSide.none, // Pas de bordure par défaut
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, // Bordure de base
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blue, // Bordure en bleu lors du focus
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.title,
|
||||
color: Colors.blueGrey, // Icône assortie
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.blueGrey, // Texte en bleu pour un meilleur contraste
|
||||
fontSize: 16.0, // Taille de police améliorée
|
||||
fontWeight: FontWeight.w600, // Poids de la police pour la lisibilité
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un titre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/presentation/widgets/fields/transport_info_field.dart
Normal file
54
lib/presentation/widgets/fields/transport_info_field.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Un champ de saisie pour les informations de transport, utilisé dans un formulaire.
|
||||
class TransportInfoField extends StatelessWidget {
|
||||
// Fonction de rappel pour sauvegarder les informations de transport.
|
||||
final Function(String?) onSaved;
|
||||
|
||||
// Constructeur qui prend la fonction onSaved pour transmettre les informations de transport au formulaire.
|
||||
const TransportInfoField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration( // Suppression du mot-clé 'const'
|
||||
labelText: 'Informations de transport', // Texte d'étiquette pour le champ de saisie.
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.directions_car, // Icône représentant un moyen de transport (voiture).
|
||||
color: Colors.blueGrey, // Couleur de l'icône.
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
|
||||
borderSide: BorderSide.none, // Pas de bordure par défaut.
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey, // Bordure colorée en blueGrey.
|
||||
width: 1.5, // Largeur de la bordure.
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue, // Bordure bleue au focus.
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
filled: true, // Le champ de saisie est rempli de couleur de fond.
|
||||
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec opacité.
|
||||
),
|
||||
validator: (value) {
|
||||
// Validation pour vérifier que le champ n'est pas vide.
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer des informations sur le transport'; // Message d'erreur si vide.
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved, // Fonction qui est appelée pour sauvegarder la valeur saisie.
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../../domain/entities/friend.dart';
|
||||
|
||||
/// [FriendDetailScreen] affiche les détails d'un ami, incluant son nom, son image de profil,
|
||||
/// et une option pour envoyer un message.
|
||||
///
|
||||
/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails.
|
||||
class FriendDetailScreen extends StatelessWidget {
|
||||
final String name; // Nom de l'ami
|
||||
final String friendFirstName; // Nom de l'ami
|
||||
final String friendLastName;
|
||||
final String imageUrl; // URL de l'image de profil de l'ami
|
||||
final String friendId; // ID de l'ami pour des actions futures
|
||||
final Logger _logger = Logger(); // Logger pour suivre les actions dans le terminal
|
||||
final FriendStatus status;
|
||||
final String lastInteraction;
|
||||
final String dateAdded;
|
||||
|
||||
/// Constructeur de la classe [FriendDetailScreen].
|
||||
/// [name], [imageUrl], et [friendId] doivent être fournis.
|
||||
FriendDetailScreen({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.friendFirstName,
|
||||
required this.friendLastName,
|
||||
required this.imageUrl,
|
||||
required this.friendId,
|
||||
required this.status,
|
||||
required this.lastInteraction,
|
||||
required this.dateAdded,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Méthode statique pour lancer l'écran des détails d'un ami.
|
||||
static void open(BuildContext context, String friendId, String name, String imageUrl) {
|
||||
static void open(
|
||||
BuildContext context,
|
||||
String friendId,
|
||||
String friendFirstName,
|
||||
String friendLastName,
|
||||
String imageUrl,
|
||||
FriendStatus status,
|
||||
String lastInteraction,
|
||||
String dateAdded) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => FriendDetailScreen(
|
||||
friendId: friendId,
|
||||
name: name,
|
||||
friendFirstName: friendFirstName,
|
||||
friendLastName: friendLastName,
|
||||
imageUrl: imageUrl,
|
||||
status: status,
|
||||
lastInteraction: lastInteraction,
|
||||
dateAdded: dateAdded,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -36,70 +56,128 @@ class FriendDetailScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.i('[LOG] Affichage des détails de l\'ami : $name (ID: $friendId)');
|
||||
_logger.i('[LOG] Affichage des détails de l\'ami : $friendFirstName (ID: $friendId)');
|
||||
|
||||
// Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide.
|
||||
final imageProvider = imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true
|
||||
final imageProvider =
|
||||
imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true
|
||||
? NetworkImage(imageUrl)
|
||||
: const AssetImage('lib/assets/images/default_avatar.png') as ImageProvider;
|
||||
: const AssetImage('lib/assets/images/default_avatar.png')
|
||||
as ImageProvider;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(name), // Titre de l'écran affichant le nom de l'ami
|
||||
backgroundColor: Colors.grey.shade800,
|
||||
title: Text(friendFirstName),
|
||||
backgroundColor: Colors.teal.shade800, // Couleur de l'app bar
|
||||
elevation: 6, // Ombre sous l'app bar pour plus de profondeur
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0), // Espacement autour du contenu
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Affichage de l'image de l'ami avec animation `Hero`
|
||||
Hero(
|
||||
tag: friendId, // Tag unique pour l'animation Hero basée sur l'ID de l'ami
|
||||
child: CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundImage: imageProvider,
|
||||
backgroundColor: Colors.grey.shade800,
|
||||
onBackgroundImageError: (error, stackTrace) {
|
||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $name (ID: $friendId): $error');
|
||||
},
|
||||
child: imageUrl.isEmpty
|
||||
? const Icon(Icons.person, size: 50, color: Colors.white) // Icône par défaut si aucune image n'est disponible
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16), // Espacement entre l'image et le texte
|
||||
|
||||
// Affichage du nom de l'ami
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, // Taille de la police pour le nom
|
||||
fontWeight: FontWeight.bold, // Texte en gras
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Espacement avant le bouton
|
||||
|
||||
// Bouton pour envoyer un message à l'ami
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_logger.i('[LOG] Envoi d\'un message à $name (ID: $friendId)');
|
||||
// Logique future pour envoyer un message à l'ami
|
||||
},
|
||||
icon: const Icon(Icons.message),
|
||||
label: const Text('Envoyer un message'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.teal, // Couleur de fond du bouton
|
||||
foregroundColor: Colors.white, // Couleur du texte et de l'icône
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Animation Hero pour une transition fluide lors de la navigation
|
||||
Hero(
|
||||
tag: friendId,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: CircleAvatar(
|
||||
radius: 80,
|
||||
backgroundImage: imageProvider,
|
||||
backgroundColor: Colors.grey.shade800,
|
||||
onBackgroundImageError: (error, stackTrace) {
|
||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $friendFirstName (ID: $friendId): $error');
|
||||
},
|
||||
child: imageUrl.isEmpty
|
||||
? const Icon(Icons.person, size: 60, color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Affichage du nom de l'ami avec une meilleure hiérarchie visuelle
|
||||
Text(
|
||||
'$friendFirstName $friendLastName',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
status.name.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: status == FriendStatus.accepted
|
||||
? Colors.green.shade400
|
||||
: status == FriendStatus.pending
|
||||
? Colors.orange.shade400
|
||||
: Colors.red.shade400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Affichage des informations supplémentaires
|
||||
_buildInfoRow('Dernière interaction:', lastInteraction),
|
||||
_buildInfoRow('Date d\'ajout:', dateAdded),
|
||||
|
||||
const SizedBox(height: 30), // Espacement avant le bouton
|
||||
|
||||
// Bouton pour envoyer un message à l'ami avec animation
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_logger.i('[LOG] Envoi d\'un message à $friendFirstName (ID: $friendId)');
|
||||
// Logique future pour envoyer un message à l'ami
|
||||
},
|
||||
icon: const Icon(Icons.message),
|
||||
label: const Text('Envoyer un message'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.teal, // Couleur du bouton
|
||||
foregroundColor: Colors.white, // Couleur du texte et icône
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30), // Coins arrondis
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
elevation: 5, // Ombre pour effet de survol
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget réutilisable pour afficher une ligne d'information avec un texte d'introduction et une valeur.
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white70, // Couleur plus claire pour les valeurs
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,19 @@ import '../../domain/entities/friend.dart';
|
||||
/// [FriendsCircle] est un widget qui affiche un ami sous forme d'avatar circulaire avec son nom.
|
||||
/// L'avatar est cliquable, permettant à l'utilisateur d'accéder aux détails de l'ami
|
||||
/// ou de déclencher d'autres actions liées.
|
||||
///
|
||||
/// Chaque interaction avec le widget sera loguée pour assurer une traçabilité complète.
|
||||
class FriendsCircle extends StatelessWidget {
|
||||
final Friend friend; // Représente l'entité Friend à afficher (nom et image).
|
||||
final VoidCallback onTap; // Fonction callback exécutée lorsque l'utilisateur clique sur l'avatar.
|
||||
final Friend friend; // L'entité Friend à afficher (contenant l'ID, le prénom, le nom, et l'URL de l'image).
|
||||
final VoidCallback onTap; // La fonction callback qui sera exécutée lors du clic sur l'avatar.
|
||||
|
||||
// Logger pour tracer les actions dans le terminal
|
||||
// Initialisation du logger pour tracer les actions dans le terminal.
|
||||
final Logger _logger = Logger();
|
||||
|
||||
/// Constructeur pour [FriendsCircle], prenant en entrée un ami et une fonction de callback.
|
||||
///
|
||||
/// @param friend: l'ami à afficher, comprenant les informations nécessaires (nom, prénom, imageUrl).
|
||||
/// @param onTap: la fonction qui sera exécutée lorsque l'utilisateur clique sur l'avatar.
|
||||
FriendsCircle({
|
||||
Key? key,
|
||||
required this.friend, // L'ami à afficher (doit inclure friendId, name, imageUrl).
|
||||
@@ -21,56 +26,58 @@ class FriendsCircle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut.
|
||||
// 1. Récupère et assemble les prénoms et noms de l'ami, ou définit "Ami inconnu" si ces valeurs sont vides.
|
||||
String displayName = [friend.friendFirstName, friend.friendLastName]
|
||||
.where((namePart) => namePart != null && namePart.isNotEmpty)
|
||||
.join(" ")
|
||||
.trim();
|
||||
.where((namePart) => namePart != null && namePart.isNotEmpty) // Exclut les parties nulles ou vides.
|
||||
.join(" ") // Joint les parties pour obtenir un nom complet.
|
||||
.trim(); // Supprime les espaces superflus.
|
||||
|
||||
if (displayName.isEmpty) {
|
||||
displayName = 'Ami inconnu';
|
||||
displayName = 'Ami inconnu'; // Utilise "Ami inconnu" si le nom complet est vide.
|
||||
}
|
||||
|
||||
// 2. Widget GestureDetector pour détecter les clics sur l'avatar de l'ami.
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 3. Log du clic sur l'avatar pour traçabilité dans le terminal.
|
||||
_logger.i('[LOG] Avatar de ${displayName.trim()} cliqué');
|
||||
onTap(); // Exécute l'action de clic définie par l'utilisateur
|
||||
onTap(); // Exécute la fonction de callback définie lors du clic.
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments de la colonne.
|
||||
mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments dans la colonne.
|
||||
children: [
|
||||
// 4. Animation Hero avec l'ID unique de l'ami pour effectuer une transition fluide.
|
||||
Hero(
|
||||
tag: friend.friendId, // Tag unique pour l'animation Hero basé sur l'ID de l'ami.
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
radius: 40, // Rayon de l'avatar circulaire.
|
||||
// 5. Gestion de l'image de l'avatar. Si une image est fournie, on l'affiche.
|
||||
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
|
||||
? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau
|
||||
? NetworkImage(friend.imageUrl!)
|
||||
: AssetImage(friend.imageUrl!) as ImageProvider) // Utilise AssetImage si c'est une ressource locale
|
||||
: const AssetImage('lib/assets/images/default_avatar.png'), // Utilise AssetImage pour l'avatar par défaut
|
||||
? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau.
|
||||
? NetworkImage(friend.imageUrl!) // Charge l'image depuis une URL réseau.
|
||||
: AssetImage(friend.imageUrl!) as ImageProvider) // Sinon, charge depuis les ressources locales.
|
||||
: const AssetImage('lib/assets/images/default_avatar.png'), // Si aucune image, utilise l'image par défaut.
|
||||
onBackgroundImageError: (error, stackTrace) {
|
||||
// 6. Log d'erreur si l'image de l'avatar ne se charge pas.
|
||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error');
|
||||
},
|
||||
backgroundColor: Colors.grey.shade800, // Fond si l'image ne charge pas.
|
||||
child: friend.imageUrl == null || friend.imageUrl!.isEmpty
|
||||
? const Icon(Icons.person, size: 40, color: Colors.white) // Icône de remplacement si aucune image n'est disponible
|
||||
: null,
|
||||
backgroundColor: Colors.grey.shade800, // Fond si l'image ne se charge pas correctement.
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8), // Ajoute un espace entre l'image et le texte.
|
||||
const SizedBox(height: 8), // 7. Ajoute un espace entre l'avatar et le nom de l'ami.
|
||||
// 8. Affiche le nom de l'ami sous l'avatar, avec une gestion de dépassement du texte.
|
||||
Text(
|
||||
displayName, // Affiche le nom de l'ami sous l'avatar ou une valeur par défaut.
|
||||
displayName, // Affiche le nom de l'ami sous l'avatar ou "Ami inconnu" si vide.
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white, // Couleur du texte.
|
||||
fontSize: 14, // Taille de police.
|
||||
fontWeight: FontWeight.bold, // Met le texte en gras.
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1, // Limite l'affichage à une ligne.
|
||||
overflow: TextOverflow.ellipsis, // Ajoute des points de suspension si le texte dépasse.
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
/// `ImagePreviewPicker` est un widget permettant à l'utilisateur de choisir une image depuis la galerie ou de prendre une photo.
|
||||
/// Ce widget affiche un aperçu de l'image sélectionnée et gère l'interaction pour choisir une nouvelle image.
|
||||
///
|
||||
/// Arguments :
|
||||
/// - `onImagePicked`: Un callback qui renvoie le fichier image sélectionné (ou null si aucune image n'est choisie).
|
||||
class ImagePreviewPicker extends StatefulWidget {
|
||||
final void Function(File?) onImagePicked;
|
||||
|
||||
@@ -14,9 +19,14 @@ class ImagePreviewPicker extends StatefulWidget {
|
||||
class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
|
||||
File? _selectedImageFile;
|
||||
|
||||
/// Méthode pour ouvrir le modal de sélection d'image avec une animation.
|
||||
Future<void> _pickImage() async {
|
||||
// Log : Ouverture du modal de sélection d'image
|
||||
debugPrint('Ouverture du modal de sélection d\'image');
|
||||
|
||||
final ImagePicker picker = ImagePicker();
|
||||
|
||||
// Affichage du modal de sélection d'image
|
||||
final XFile? pickedFile = await showModalBottomSheet<XFile?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
@@ -26,14 +36,14 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Take a Photo'),
|
||||
title: const Text('Prendre une photo'),
|
||||
onTap: () async {
|
||||
Navigator.pop(context, await picker.pickImage(source: ImageSource.camera));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Choose from Gallery'),
|
||||
title: const Text('Choisir depuis la galerie'),
|
||||
onTap: () async {
|
||||
Navigator.pop(context, await picker.pickImage(source: ImageSource.gallery));
|
||||
},
|
||||
@@ -44,10 +54,13 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
|
||||
},
|
||||
);
|
||||
|
||||
// Si un fichier est sélectionné, mettez à jour l'état avec l'image choisie
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
_selectedImageFile = File(pickedFile.path);
|
||||
widget.onImagePicked(_selectedImageFile); // Pass the picked image to the parent
|
||||
widget.onImagePicked(_selectedImageFile); // Passez l'image au parent
|
||||
// Log : Image sélectionnée
|
||||
debugPrint('Image sélectionnée : ${_selectedImageFile?.path}');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -55,23 +68,28 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _pickImage,
|
||||
onTap: _pickImage, // Ouvre le modal lors du clic
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Aperçu de l\'image (16:9)',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
style: TextStyle(color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
border: Border.all(color: Colors.white70, width: 1),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300), // Animation douce lors du changement d'image
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey.withOpacity(0.1), // Fond légèrement opaque
|
||||
borderRadius: BorderRadius.circular(12.0), // Bordures arrondies
|
||||
border: Border.all(
|
||||
color: _selectedImageFile != null ? Colors.blue : Colors.blueGrey,
|
||||
width: 2.0, // Bordure visible autour de l'image
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9, // Maintient l'aspect ratio de l'image
|
||||
child: _selectedImageFile != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
@@ -88,7 +106,7 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
|
||||
: const Center(
|
||||
child: Text(
|
||||
'Cliquez pour ajouter une image',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
style: TextStyle(color: Colors.blueGrey),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LinkField extends StatelessWidget {
|
||||
final FormFieldSetter<String> onSaved;
|
||||
|
||||
const LinkField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Lien (optionnel)',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.1),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.link, color: Colors.white70),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import '../screens/location/location_picker_Screen.dart';
|
||||
|
||||
class LocationField extends StatelessWidget {
|
||||
final String location;
|
||||
final LatLng? selectedLatLng;
|
||||
final Function(LatLng?) onLocationPicked;
|
||||
|
||||
const LocationField({Key? key, required this.location, this.selectedLatLng, required this.onLocationPicked}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final LatLng? pickedLocation = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LocationPickerScreen(),
|
||||
),
|
||||
);
|
||||
if (pickedLocation != null) {
|
||||
onLocationPicked(pickedLocation);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedLatLng == null
|
||||
? 'Sélectionnez une localisation'
|
||||
: 'Localisation: $location',
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
const Icon(Icons.location_on, color: Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Bouton de soumission avec un gradient visuel et des ombres
|
||||
/// Utilisé pour l'envoi d'un formulaire d'événement
|
||||
class SubmitButton extends StatelessWidget {
|
||||
/// Fonction à exécuter lors de l'appui sur le bouton
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const SubmitButton({Key? key, required this.onPressed}) : super(key: key);
|
||||
@@ -8,11 +11,12 @@ class SubmitButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
// Décoration du bouton avec un dégradé de couleurs et une ombre
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF1DBF73), // Start of the gradient
|
||||
Color(0xFF11998E), // End of the gradient
|
||||
Color(0xFF1DBF73), // Dégradé vert clair
|
||||
Color(0xFF11998E), // Dégradé vert foncé
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
@@ -22,18 +26,18 @@ class SubmitButton extends StatelessWidget {
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(2, 4), // Shadow position
|
||||
offset: const Offset(2, 4), // Position de l'ombre
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
onPressed: onPressed, // Appel de la fonction passée en paramètre
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent, // Button background is transparent to show gradient
|
||||
shadowColor: Colors.transparent, // Remove the default shadow
|
||||
backgroundColor: Colors.transparent, // Fond transparent pour voir le dégradé
|
||||
shadowColor: Colors.transparent, // Suppression de l'ombre par défaut
|
||||
padding: const EdgeInsets.symmetric(vertical: 14.0),
|
||||
minimumSize: const Size(double.infinity, 50), // Bigger button size
|
||||
minimumSize: const Size(double.infinity, 50), // Taille du bouton
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
@@ -41,10 +45,10 @@ class SubmitButton extends StatelessWidget {
|
||||
child: const Text(
|
||||
'Créer l\'événement',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16, // Increase font size
|
||||
fontWeight: FontWeight.bold, // Bold text
|
||||
letterSpacing: 1.2, // Spacing between letters
|
||||
color: Colors.white, // Couleur du texte
|
||||
fontSize: 16, // Taille du texte
|
||||
fontWeight: FontWeight.bold, // Texte en gras
|
||||
letterSpacing: 1.2, // Espacement entre les lettres
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TitleField extends StatelessWidget {
|
||||
final FormFieldSetter<String> onSaved;
|
||||
const TitleField({Key? key, required this.onSaved}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Titre',
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.1),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.title, color: Colors.white70),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un titre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user