Refactoring + Checkpoint

This commit is contained in:
DahoudG
2024-11-17 23:00:18 +00:00
parent 1e888f41e8
commit 77ab8a02a2
56 changed files with 1904 additions and 790 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
);
}
}

View File

@@ -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),
],
),
),

View File

@@ -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,
);
}
}

View File

@@ -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,

View File

@@ -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),

View File

@@ -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),
),
);
},
);
}

View File

@@ -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,
),

View 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,
);
}
}

View File

@@ -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,
);
}
}

View 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
},
),
],
);
}
}

View 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
),
);
}
}

View 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
},
);
}
}

View 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;
},
);
}
}

View 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),
],
),
),
);
}
}

View 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.
);
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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.
}
}

View 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,
);
}
}

View 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.
);
}
}

View File

@@ -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
),
),
],
),
);
}
}

View File

@@ -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.
),
],
),
);
}
}

View File

@@ -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),
),
),
),

View File

@@ -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,
);
}
}

View File

@@ -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),
],
),
),
);
}
}

View File

@@ -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
),
),
),

View File

@@ -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,
);
}
}