diff --git a/lib/config/router.dart b/lib/config/router.dart index 77888da..e4ede3c 100644 --- a/lib/config/router.dart +++ b/lib/config/router.dart @@ -62,6 +62,7 @@ class AppRouter { userId: userId, userFirstName: userFirstName, userLastName: userLastName, + profileImageUrl: '', ), ); diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index c4c6a82..d37a0fb 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -28,12 +28,13 @@ class AppColors { static const Color darkTextSecondary = Color(0xFFBDBDBD); static const Color darkCardColor = Color(0xFF2C2C2C); static const Color darkAccentColor = Color(0xFF81C784); - static const Color darkError = Color(0xFFCF6679); + static const Color darkError = Color(0xFFF1012B); static const Color darkIconPrimary = Colors.white; // Icône primaire blanche static const Color darkIconSecondary = Color(0xFFBDBDBD); // Icône secondaire gris clair // Ajout du background personnalisé - static const Color backgroundCustom = Color(0xFF2C2C3E); + static const Color darkbackgroundCustom = Color(0xFF2C2C3E); + static const Color lightbackgroundCustom = Color(0xFFE0F7FA); // Sélection automatique des couleurs en fonction du mode de thème static Color get primary => isDarkMode() ? darkPrimary : lightPrimary; @@ -49,11 +50,11 @@ class AppColors { static Color get errorColor => isDarkMode() ? darkError : lightError; static Color get iconPrimary => isDarkMode() ? darkIconPrimary : lightIconPrimary; static Color get iconSecondary => isDarkMode() ? darkIconSecondary : lightIconSecondary; - static Color get customBackgroundColor => backgroundCustom; + static Color get customBackgroundColor => isDarkMode() ? darkbackgroundCustom : lightbackgroundCustom; /// Méthode utilitaire pour vérifier si le mode sombre est activé. static bool isDarkMode() { final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; - return brightness == Brightness.dark; + return brightness == Brightness.light; } } diff --git a/lib/core/constants/urls.dart b/lib/core/constants/urls.dart index 8a5f9d4..cb2e7d5 100644 --- a/lib/core/constants/urls.dart +++ b/lib/core/constants/urls.dart @@ -1,5 +1,5 @@ class Urls { - static const String baseUrl = 'http://192.168.1.145:8085'; + static const String baseUrl = 'http://192.168.1.16:8080'; // Authentication and Users Endpoints static const String authenticateUser = '$baseUrl/users/authenticate'; diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index cabb77d..1777e0f 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -68,7 +68,7 @@ class AppTheme { color: AppColors.darkPrimary, iconTheme: IconThemeData(color: AppColors.darkOnPrimary), ), - iconTheme: const IconThemeData(color: AppColors.darkTextPrimary), + iconTheme: const IconThemeData(color: AppColors.darkOnPrimary), colorScheme: const ColorScheme.dark( primary: AppColors.darkPrimary, secondary: AppColors.darkSecondary, diff --git a/lib/data/datasources/event_remote_data_source.dart b/lib/data/datasources/event_remote_data_source.dart index 73e86f9..771fb89 100644 --- a/lib/data/datasources/event_remote_data_source.dart +++ b/lib/data/datasources/event_remote_data_source.dart @@ -77,6 +77,8 @@ class EventRemoteDataSource { print(' - Lien: ${event['link']}'); print(' - URL de l\'image: ${event['imageUrl']}'); print(' - Statut: ${event['status']}'); + print(' - prenom du créateur: ${event['creatorFirstName']}'); + print(' - prenom du créateur: ${event['creatorLastName']}'); } // Transformation du JSON en une liste d'objets EventModel @@ -92,7 +94,6 @@ class EventRemoteDataSource { } } - /// Créer un nouvel événement via l'API. Future createEvent(EventModel event) async { print('Création d\'un nouvel événement avec les données: ${event.toJson()}'); @@ -213,7 +214,7 @@ class EventRemoteDataSource { Future closeEvent(String eventId) async { print('Fermeture de l\'événement avec l\'ID: $eventId'); - final response = await client.post( + final response = await client.patch( Uri.parse('${Urls.closeEvent}/$eventId/close'), headers: {'Content-Type': 'application/json'}, ); @@ -223,11 +224,10 @@ class EventRemoteDataSource { if (response.statusCode == 200) { print('Événement fermé avec succès'); } else if (response.statusCode == 400) { - // Si le serveur retourne une erreur 400, vérifiez le corps du message final responseBody = json.decode(response.body); final errorMessage = responseBody['message'] ?? 'Erreur inconnue'; print('Erreur lors de la fermeture de l\'événement: $errorMessage'); - throw ServerExceptionWithMessage(errorMessage); // Utiliser la nouvelle exception ici + throw ServerExceptionWithMessage(errorMessage); } else { print('Erreur lors de la fermeture de l\'événement: ${response.body}'); throw ServerExceptionWithMessage('Une erreur est survenue lors de la fermeture de l\'événement.'); @@ -238,7 +238,7 @@ class EventRemoteDataSource { Future reopenEvent(String eventId) async { print('Réouverture de l\'événement avec l\'ID: $eventId'); - final response = await client.post( + final response = await client.patch( Uri.parse('${Urls.reopenEvent}/$eventId/reopen'), headers: {'Content-Type': 'application/json'}, ); @@ -248,11 +248,13 @@ class EventRemoteDataSource { if (response.statusCode == 200) { print('Événement rouvert avec succès'); } else if (response.statusCode == 400) { - // Si le serveur retourne une erreur 400, vérifiez le corps du message final responseBody = json.decode(response.body); final errorMessage = responseBody['message'] ?? 'Erreur inconnue'; print('Erreur lors de la réouverture de l\'événement: $errorMessage'); throw ServerExceptionWithMessage(errorMessage); + } else if (response.statusCode == 404) { + print('L\'événement n\'a pas été trouvé.'); + throw ServerExceptionWithMessage('L\'événement n\'existe pas ou a déjà été supprimé.'); } else { print('Erreur lors de la réouverture de l\'événement: ${response.body}'); throw ServerExceptionWithMessage('Une erreur est survenue lors de la réouverture de l\'événement.'); diff --git a/lib/data/models/event_model.dart b/lib/data/models/event_model.dart index 78ad226..2fee1fb 100644 --- a/lib/data/models/event_model.dart +++ b/lib/data/models/event_model.dart @@ -8,8 +8,11 @@ class EventModel { final String link; final String? imageUrl; final String creatorEmail; + final String creatorFirstName; // Prénom du créateur + final String creatorLastName; // Nom du créateur + final String profileImageUrl; final List participants; - final String status; + String status; final int reactionsCount; final int commentsCount; final int sharesCount; @@ -24,6 +27,9 @@ class EventModel { required this.link, this.imageUrl, required this.creatorEmail, + required this.creatorFirstName, + required this.creatorLastName, + required this.profileImageUrl, required this.participants, required this.status, required this.reactionsCount, @@ -44,8 +50,11 @@ class EventModel { final String link = json['link'] ?? 'Lien Inconnu'; final String? imageUrl = json['imageUrl']; final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu'; + final String creatorFirstName = json['creatorFirstName']; // Ajout du prénom + final String creatorLastName = json['creatorLastName']; // Ajout du nom + final String profileImageUrl = json['profileImageUrl']; // Ajout du nom final List participants = json['participants'] ?? []; - final String status = json['status'] ?? 'ouvert'; + String status = json['status'] ?? 'ouvert'; final int reactionsCount = json['reactionsCount'] ?? 0; final int commentsCount = json['commentsCount'] ?? 0; final int sharesCount = json['sharesCount'] ?? 0; @@ -60,6 +69,9 @@ class EventModel { print(' - Lien: $link'); print(' - URL de l\'image: ${imageUrl ?? "Aucune"}'); print(' - Email du créateur: $creatorEmail'); + print(' - Prénom du créateur: $creatorFirstName'); + print(' - Nom du créateur: $creatorLastName'); + print(' - Image de profile du créateur: $profileImageUrl'); print(' - Participants: ${participants.length} participants'); print(' - Statut: $status'); print(' - Nombre de réactions: $reactionsCount'); @@ -76,6 +88,9 @@ class EventModel { link: link, imageUrl: imageUrl, creatorEmail: creatorEmail, + creatorFirstName: creatorFirstName, // Ajout du prénom + creatorLastName: creatorLastName, // Ajout du nom + profileImageUrl: profileImageUrl, participants: participants, status: status, reactionsCount: reactionsCount, @@ -96,6 +111,9 @@ class EventModel { 'link': link, 'imageUrl': imageUrl, 'creatorEmail': creatorEmail, + 'creatorFirstName': creatorFirstName, // Ajout du prénom + 'creatorLastName': creatorLastName, // Ajout du nom + 'profileImageUrl': profileImageUrl, 'participants': participants, 'status': status, 'reactionsCount': reactionsCount, diff --git a/lib/data/providers/friends_provider.dart b/lib/data/providers/friends_provider.dart index a16417b..eb85b7f 100644 --- a/lib/data/providers/friends_provider.dart +++ b/lib/data/providers/friends_provider.dart @@ -4,40 +4,40 @@ import '../../domain/entities/friend.dart'; import '../../data/repositories/friends_repository_impl.dart'; /// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis. -/// Il utilise [FriendsRepositoryImpl] pour interagir avec l'API et assure la gestion des états, -/// comme le chargement, la pagination et les erreurs éventuelles. +/// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer +/// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination, +/// du statut des amis et de la gestion des erreurs. class FriendsProvider with ChangeNotifier { final FriendsRepositoryImpl friendsRepository; - final Logger _logger = Logger(); // Logger pour suivre toutes les actions. + final Logger _logger = Logger(); // Utilisation du logger pour une traçabilité complète des actions. - // Liste privée des amis récupérée depuis l'API + // Liste des amis List _friendsList = []; - bool _isLoading = false; // Indique si une opération de chargement est en cours - bool _hasMore = true; // Indique s'il reste des amis à charger - int _currentPage = 0; - final int _friendsPerPage = 10; // Nombre d'amis par page pour la pagination + bool _isLoading = false; // Indicateur de chargement + bool _hasMore = true; // Indicateur de pagination + int _currentPage = 0; // Numéro de la page actuelle pour la pagination + final int _friendsPerPage = 10; // Nombre d'amis à récupérer par page - /// Constructeur de [FriendsProvider] qui requiert une instance de [FriendsRepositoryImpl]. + /// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl]. FriendsProvider({required this.friendsRepository}); - // Getters pour accéder aux états depuis l'interface utilisateur + // Getters pour accéder à l'état actuel des données bool get isLoading => _isLoading; bool get hasMore => _hasMore; List get friendsList => _friendsList; - /// Récupère la liste paginée des amis pour un utilisateur donné. + /// Récupère la liste des amis pour un utilisateur donné avec pagination. /// - /// [userId] : L'identifiant unique de l'utilisateur. - /// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination. + /// [userId] : L'identifiant unique de l'utilisateur connecté. + /// [loadMore] : Si vrai, charge plus d'amis, sinon recharge la liste depuis le début. /// - /// Cette méthode : - /// - Vérifie si un chargement est déjà en cours. - /// - Initialise ou poursuit la pagination. - /// - Exclut l'utilisateur lui-même de la liste. - /// - Gère les erreurs et logue chaque étape pour une traçabilité complète. + /// Cette méthode gère : + /// - La pagination de la liste d'amis. + /// - L'exclusion de l'utilisateur lui-même. + /// - Les erreurs et les logs pour une traçabilité complète. Future fetchFriends(String userId, {bool loadMore = false}) async { if (_isLoading) { - _logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.'); + _logger.w('[LOG] Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.'); return; } @@ -45,7 +45,7 @@ class FriendsProvider with ChangeNotifier { notifyListeners(); _logger.i('[LOG] Début du chargement des amis pour l\'utilisateur $userId.'); - // Réinitialisation uniquement si ce n'est pas un chargement supplémentaire + // Réinitialisation de la pagination si ce n'est pas un chargement supplémentaire if (!loadMore) { _friendsList = []; _currentPage = 0; @@ -57,23 +57,25 @@ class FriendsProvider with ChangeNotifier { _logger.i('[LOG] Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.'); final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage); + // Gestion de l'absence de nouveaux amis if (newFriends.isEmpty) { _hasMore = false; - _logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.'); + _logger.i('[LOG] Plus d\'amis à charger.'); } else { + // Ajout des amis à la liste, en excluant l'utilisateur connecté for (var friend in newFriends) { if (friend.friendId != userId) { _friendsList.add(friend); - _logger.i("[LOG] Ajout de l'ami : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}"); + _logger.i("[LOG] Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}"); } else { - _logger.w("[WARN] Exclusion de l'utilisateur lui-même de la liste d'amis : ${friend.friendId}"); + _logger.w("[WARN] L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}"); } } _currentPage++; - _logger.i('[LOG] Page suivante préparée pour le prochain chargement, page actuelle : $_currentPage'); + _logger.i('[LOG] Préparation de la page suivante : $_currentPage'); } } catch (e) { - _logger.e('[ERROR] Erreur lors de la récupération des amis : $e'); + _logger.e('[ERROR] Erreur lors du chargement des amis : $e'); } finally { _isLoading = false; _logger.i('[LOG] Fin du chargement des amis.'); @@ -81,16 +83,18 @@ class FriendsProvider with ChangeNotifier { } } - /// Supprime un ami dans l'API et met à jour la liste localement. + /// Supprime un ami de la liste locale et de l'API. /// /// [friendId] : Identifiant unique de l'ami à supprimer. /// - /// Loggue chaque étape pour assurer un suivi précis de l'opération. + /// Cette méthode : + /// - Loggue chaque étape. + /// - Enlève l'ami de la liste locale. Future removeFriend(String friendId) async { try { - _logger.i('[LOG] Tentative de suppression de l\'ami avec l\'ID : $friendId'); - await friendsRepository.removeFriend(friendId); - _friendsList.removeWhere((friend) => friend.friendId == friendId); + _logger.i('[LOG] Suppression de l\'ami avec l\'ID : $friendId'); + await friendsRepository.removeFriend(friendId); // Appel API pour supprimer l'ami + _friendsList.removeWhere((friend) => friend.friendId == friendId); // Suppression locale _logger.i('[LOG] Ami supprimé localement avec succès : $friendId'); } catch (e) { _logger.e('[ERROR] Erreur lors de la suppression de l\'ami : $e'); @@ -101,31 +105,31 @@ class FriendsProvider with ChangeNotifier { /// Récupère les détails d'un ami via l'API. /// - /// [userId] : L'identifiant de l'utilisateur connecté. - /// [friendId] : Identifiant unique de l'ami. + /// [userId] : Identifiant de l'utilisateur connecté. + /// [friendId] : Identifiant de l'ami dont on souhaite récupérer les détails. /// - /// Retourne un `Future` contenant les détails ou `null` en cas d'erreur. + /// Retourne un `Future` contenant les détails de l'ami ou `null` en cas d'erreur. Future fetchFriendDetails(String userId, String friendId) async { try { - _logger.i('[LOG] Tentative de récupération des détails de l\'ami avec l\'ID : $friendId'); + _logger.i('[LOG] Récupération des détails de l\'ami avec l\'ID : $friendId'); final friendDetails = await friendsRepository.getFriendDetails(friendId, userId); if (friendDetails != null) { _logger.i('[LOG] Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}'); } else { - _logger.w('[LOG] Détails de l\'ami introuvables pour l\'ID : $friendId'); + _logger.w('[WARN] Détails de l\'ami introuvables pour l\'ID : $friendId'); } return friendDetails; } catch (e) { - _logger.e('[ERROR] Exception lors de la récupération des détails de l\'ami : $e'); + _logger.e('[ERROR] Erreur lors de la récupération des détails de l\'ami : $e'); return null; } } /// Convertit un statut sous forme de chaîne en [FriendStatus]. /// - /// [status] : Le statut sous forme de chaîne. + /// [status] : Le statut sous forme de chaîne (par exemple, 'pending', 'accepted'). /// /// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu. FriendStatus _convertToFriendStatus(String status) { @@ -141,21 +145,21 @@ class FriendsProvider with ChangeNotifier { } } - /// Met à jour le statut d'un ami (par exemple : accepter, bloquer). + /// Met à jour le statut d'un ami (ex. accepter, bloquer). /// - /// [friendId] : Identifiant unique de l'ami. - /// [status] : Nouveau statut pour l'ami sous forme de chaîne de caractères. + /// [friendId] : Identifiant de l'ami dont on souhaite mettre à jour le statut. + /// [status] : Nouveau statut sous forme de chaîne de caractères. /// - /// Loggue l'action, convertit le statut en `FriendStatus`, et met à jour la liste localement. + /// Loggue l'action, met à jour le statut en local et appelle l'API pour mettre à jour le statut. Future updateFriendStatus(String friendId, String status) async { try { - _logger.i('[LOG] Tentative de mise à jour du statut de l\'ami avec l\'ID : $friendId'); + _logger.i('[LOG] Mise à jour du statut de l\'ami avec l\'ID : $friendId'); - // Conversion du `String` en `FriendStatus` pour l'update locale + // Conversion du statut sous forme de chaîne en statut spécifique final friendStatus = _convertToFriendStatus(status); - await friendsRepository.updateFriendStatus(friendId, status); + await friendsRepository.updateFriendStatus(friendId, status); // Mise à jour dans l'API - // Mise à jour locale de la liste pour afficher le changement de statut + // Mise à jour locale de la liste des amis avec le nouveau statut final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId); if (friendIndex != -1) { _friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus); diff --git a/lib/domain/entities/friend.dart b/lib/domain/entities/friend.dart index ca2f040..bccaf96 100644 --- a/lib/domain/entities/friend.dart +++ b/lib/domain/entities/friend.dart @@ -17,6 +17,8 @@ class Friend extends Equatable { final String? email; // Adresse e-mail, optionnelle mais typiquement présente final String? imageUrl; // URL de l'image de profil, optionnelle final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown` + final String? dateAdded; + final String? lastInteraction; /// Logger statique pour suivre toutes les actions et transformations liées à [Friend]. static final Logger _logger = Logger(); @@ -31,6 +33,8 @@ class Friend extends Equatable { this.email, this.imageUrl, this.status = FriendStatus.unknown, + this.dateAdded, + this.lastInteraction, }) { assert(friendId.isNotEmpty, 'friendId ne doit pas être vide'); _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName'); @@ -53,7 +57,7 @@ class Friend extends Equatable { friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu', friendLastName: json['friendLastName'] as String? ?? '', email: json['email'] as String?, - imageUrl: json['imageUrl'] as String?, + imageUrl: json['friendProfileImageUrl'] as String?, status: _parseStatus(json['status'] as String?), ); } @@ -81,7 +85,7 @@ class Friend extends Equatable { 'friendFirstName': friendFirstName, 'friendLastName': friendLastName, 'email': email, - 'imageUrl': imageUrl, + 'friendProfileImageUrl': imageUrl, 'status': status.name, }; _logger.i('[LOG] Conversion Friend -> JSON : $json'); @@ -99,6 +103,8 @@ class Friend extends Equatable { String? email, String? imageUrl, FriendStatus? status, + String? lastInteraction, + String? dateAdded, }) { final newFriend = Friend( friendId: friendId ?? this.friendId, diff --git a/lib/presentation/screens/dialogs/add_event_dialog.dart b/lib/presentation/screens/dialogs/add_event_dialog.dart index 68ecfd3..9cc29e2 100644 --- a/lib/presentation/screens/dialogs/add_event_dialog.dart +++ b/lib/presentation/screens/dialogs/add_event_dialog.dart @@ -1,16 +1,27 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'dart:io'; - -import '../../widgets/category_field.dart'; +import 'dart:io'; // Pour l'usage des fichiers (image) +import '../../widgets/fields/category_field.dart'; // Importation des widgets personnalisés import '../../widgets/date_picker.dart'; -import '../../widgets/description_field.dart'; -import '../../widgets/link_field.dart'; -import '../../widgets/location_field.dart'; +import '../../widgets/fields/description_field.dart'; +import '../../widgets/fields/link_field.dart'; +import '../../widgets/fields/location_field.dart'; import '../../widgets/submit_button.dart'; -import '../../widgets/title_field.dart'; +import '../../widgets/fields/title_field.dart'; import '../../widgets/image_preview_picker.dart'; +import '../../widgets/fields/tags_field.dart'; +import '../../widgets/fields/attendees_field.dart'; +import '../../widgets/fields/organizer_field.dart'; +import '../../widgets/fields/transport_info_field.dart'; +import '../../widgets/fields/accommodation_info_field.dart'; +import '../../widgets/fields/privacy_rules_field.dart'; +import '../../widgets/fields/security_protocol_field.dart'; +import '../../widgets/fields/parking_field.dart'; +import '../../widgets/fields/accessibility_field.dart'; +import '../../widgets/fields/participation_fee_field.dart'; +/// Page pour ajouter un événement +/// Permet à l'utilisateur de remplir un formulaire avec des détails sur l'événement class AddEventPage extends StatefulWidget { final String userId; final String userFirstName; @@ -28,22 +39,37 @@ class AddEventPage extends StatefulWidget { } class _AddEventPageState extends State { - final _formKey = GlobalKey(); // Form key for validation + final _formKey = GlobalKey(); // Clé pour la validation du formulaire + // Variables pour stocker les données de l'événement String _title = ''; String _description = ''; DateTime? _selectedDate; + DateTime? _endDate; String _location = 'Abidjan'; String _category = ''; String _link = ''; - LatLng? _selectedLatLng = const LatLng(5.348722, -3.985038); // Default coordinates - File? _selectedImageFile; // Store the selected image + String _organizer = ''; + List _tags = []; + int _maxParticipants = 0; + LatLng? _selectedLatLng = const LatLng(5.348722, -3.985038); // Coordonnées par défaut + File? _selectedImageFile; // Image sélectionnée + String _status = 'Actif'; + String _organizerEmail = ''; + String _organizerPhone = ''; + int _participationFee = 0; + String _privacyRules = ''; + String _transportInfo = ''; + String _accommodationInfo = ''; + bool _isAccessible = false; + bool _hasParking = false; + String _securityProtocol = ''; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Ajouter un événement'), + title: const Text('Créer un événement'), backgroundColor: const Color(0xFF1E1E2C), leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -63,6 +89,8 @@ class _AddEventPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Section d'informations de base + _buildSectionHeader('Informations de base'), ImagePreviewPicker( onImagePicked: (File? imageFile) { setState(() { @@ -80,39 +108,126 @@ class _AddEventPageState extends State { onDatePicked: (picked) => setState(() { _selectedDate = picked; }), + label: 'Date de début', + ), + const SizedBox(height: 12), + DatePickerField( + selectedDate: _endDate, + onDatePicked: (picked) => setState(() { + _endDate = picked; + }), + label: 'Date de fin', ), const SizedBox(height: 12), LocationField( location: _location, - selectedLatLng: _selectedLatLng, - onLocationPicked: (pickedLocation) => setState(() { - _selectedLatLng = pickedLocation; - _location = '${pickedLocation?.latitude}, ${pickedLocation?.longitude}'; - }), + onLocationPicked: (value) => setState(() => _location = (value ?? 'Abidjan') as String), ), const SizedBox(height: 12), - CategoryField(onSaved: (value) => setState(() => _category = value ?? '')), + CategoryField( + onSaved: (value) => setState(() => _category = value ?? ''), + ), const SizedBox(height: 12), - LinkField(onSaved: (value) => setState(() => _link = value ?? '')), + LinkField( + onSaved: (value) => setState(() => _link = value ?? ''), + ), + const SizedBox(height: 12), + AttendeesField( + onSaved: (value) => setState(() => _maxParticipants = value ?? 0), + ), + const SizedBox(height: 12), + TagsField( + onSaved: (value) => setState(() => _tags = value ?? []), + ), + const SizedBox(height: 12), + OrganizerField( + onSaved: (value) => setState(() => _organizer = value ?? ''), + ), + const SizedBox(height: 12), + TransportInfoField( + onSaved: (value) => setState(() => _transportInfo = value ?? ''), + ), + const SizedBox(height: 12), + AccommodationInfoField( + onSaved: (value) => setState(() => _accommodationInfo = value ?? ''), + ), + const SizedBox(height: 12), + PrivacyRulesField( + onSaved: (value) => setState(() => _privacyRules = value ?? ''), + ), + const SizedBox(height: 12), + SecurityProtocolField( + onSaved: (value) => setState(() => _securityProtocol = value ?? ''), + ), + const SizedBox(height: 12), + ParkingField( + onSaved: (value) => setState(() => _hasParking = (value as bool?) ?? false), + ), + const SizedBox(height: 12), + AccessibilityField( + onSaved: (value) => setState(() => _participationFee = (value as int?) ?? 0), + ), + const SizedBox(height: 12), + ParticipationFeeField( + onSaved: (value) => setState(() => _participationFee = (value as int?) ?? 0), + ), + const SizedBox(height: 12), + SubmitButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + // Log des données de l'événement avant l'envoi + print('Titre de l\'événement : $_title'); + print('Description de l\'événement : $_description'); + print('Date de début : $_selectedDate'); + print('Date de fin : $_endDate'); + print('Lieu : $_location'); + print('Catégorie : $_category'); + print('Lien de l\'événement : $_link'); + print('Organisateur : $_organizer'); + print('Tags : $_tags'); + print('Maximum de participants : $_maxParticipants'); + print('Image sélectionnée : $_selectedImageFile'); + print('Transport : $_transportInfo'); + print('Hébergement : $_accommodationInfo'); + print('Règles de confidentialité : $_privacyRules'); + print('Protocole de sécurité : $_securityProtocol'); + print('Parking disponible : $_hasParking'); + print('Accessibilité : $_isAccessible'); + print('Frais de participation : $_participationFee'); + // Logique d'envoi des données vers le backend... + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Événement créé avec succès !')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Veuillez remplir tous les champs requis')), + ); + } + }, + ), ], ), ), ), ), - // Bouton en bas de l'écran - Padding( - padding: const EdgeInsets.all(16.0), - child: SubmitButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - // Logic to add the event goes here - } - }, - ), - ), ], ), ); } + + // En-tête de section pour mieux organiser les champs + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ); + } } diff --git a/lib/presentation/screens/event/event_card.dart b/lib/presentation/screens/event/event_card.dart index 064f883..65b2cfd 100644 --- a/lib/presentation/screens/event/event_card.dart +++ b/lib/presentation/screens/event/event_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; // Pour la gestion des logs. import '../../../data/models/event_model.dart'; import '../../widgets/event_header.dart'; @@ -7,19 +8,22 @@ import '../../widgets/event_interaction_row.dart'; import '../../widgets/event_status_badge.dart'; import '../../widgets/swipe_background.dart'; -class EventCard extends StatelessWidget { - final EventModel event; - final String userId; - final String userFirstName; - final String userLastName; - final String status; - final VoidCallback onReact; - final VoidCallback onComment; - final VoidCallback onShare; - final VoidCallback onParticipate; - final VoidCallback onCloseEvent; - final VoidCallback onReopenEvent; - final Function onRemoveEvent; +/// Widget représentant une carte d'événement affichant les informations +/// principales de l'événement avec diverses options d'interaction. +class EventCard extends StatefulWidget { + final EventModel event; // Modèle de données pour l'événement. + final String userId; // ID de l'utilisateur affichant l'événement. + final String userFirstName; // Prénom de l'utilisateur. + final String userLastName; // Nom de l'utilisateur. + final String profileImageUrl; // Image de profile + final String status; // Statut de l'événement (ouvert ou fermé). + final VoidCallback onReact; // Callback pour réagir à l'événement. + final VoidCallback onComment; // Callback pour commenter l'événement. + final VoidCallback onShare; // Callback pour partager l'événement. + final VoidCallback onParticipate; // Callback pour participer à l'événement. + final VoidCallback onCloseEvent; // Callback pour fermer l'événement. + final VoidCallback onReopenEvent; // Callback pour rouvrir l'événement. + final Function onRemoveEvent; // Fonction pour supprimer l'événement. const EventCard({ Key? key, @@ -27,6 +31,7 @@ class EventCard extends StatelessWidget { required this.userId, required this.userFirstName, required this.userLastName, + required this.profileImageUrl, required this.status, required this.onReact, required this.onComment, @@ -37,79 +42,142 @@ class EventCard extends StatelessWidget { required this.onRemoveEvent, }) : super(key: key); + @override + _EventCardState createState() => _EventCardState(); +} + +class _EventCardState extends State { + bool _isExpanded = false; // Contrôle si la description est développée. + static const int _descriptionThreshold = 100; // Limite de caractères. + bool _isClosed = false; // Ajout d'une variable pour suivre l'état de l'événement. + final Logger _logger = Logger(); + + @override + void initState() { + super.initState(); + _isClosed = widget.event.status == 'fermé'; // Initialiser l'état selon le statut de l'événement. + } + @override Widget build(BuildContext context) { - final GlobalKey menuKey = GlobalKey(); + _logger.i("Construction de la carte d'événement"); // Log pour la construction du widget. + final GlobalKey menuKey = GlobalKey(); // Clé pour le menu contextuel. + final String descriptionText = widget.event.description; // Description de l'événement. + final bool shouldTruncate = descriptionText.length > _descriptionThreshold; // Détermine si le texte doit être tronqué. return Dismissible( - key: ValueKey(event.id), - direction: event.status == 'fermé' + key: ValueKey(widget.event.id), // Clé unique pour chaque carte d'événement. + direction: widget.event.status == 'fermé' // Direction du glissement basée sur le statut. ? DismissDirection.startToEnd : DismissDirection.endToStart, - onDismissed: (direction) { - if (event.status == 'fermé') { - onReopenEvent(); + onDismissed: (direction) { // Action déclenchée lors d'un glissement. + if (_isClosed) { + _logger.i("Rouverte de l'événement ${widget.event.id}"); + widget.onReopenEvent(); + setState(() { + _isClosed = false; // Mise à jour de l'état local. + }); } else { - onCloseEvent(); - onRemoveEvent(event.id); + _logger.i("Fermeture de l'événement ${widget.event.id}"); + widget.onCloseEvent(); + widget.onRemoveEvent(widget.event.id); // Suppression de l'événement. + setState(() { + _isClosed = true; // Mise à jour de l'état local. + }); } }, - background: SwipeBackground( - color: event.status == 'fermé' ? Colors.green : Colors.red, - icon: event.status == 'fermé' ? Icons.lock_open : Icons.lock, - label: event.status == 'fermé' ? 'Rouvrir' : 'Fermer', + background: SwipeBackground( // Arrière-plan pour les actions de glissement. + color: _isClosed ? Colors.green : Colors.red, + icon: _isClosed ? Icons.lock_open : Icons.lock, + label: _isClosed ? 'Rouvrir' : 'Fermer', ), child: Card( - color: const Color(0xFF2C2C3E), + color: const Color(0xFF2C2C3E), // Couleur de fond de la carte. margin: const EdgeInsets.symmetric(vertical: 10.0), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), // Bordure arrondie. child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(12.0), // Marge intérieure de la carte. child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Affichage de l'en-tête de l'événement. EventHeader( - userFirstName: userFirstName, - userLastName: userLastName, - eventDate: event.startDate, - imageUrl: event.imageUrl, + creatorFirstName: widget.event.creatorFirstName, + creatorLastName: widget.event.creatorLastName, + profileImageUrl: widget.event.profileImageUrl, + eventDate: widget.event.startDate, + imageUrl: widget.event.imageUrl, menuKey: menuKey, menuContext: context, - location: event.location, - onClose: () { }, + location: widget.event.location, + onClose: () { + _logger.i("Menu de fermeture actionné pour l'événement ${widget.event.id}"); + }, ), - const Divider(color: Colors.white24), + const Divider(color: Colors.white24), // Ligne de séparation visuelle. + Row( children: [ - const Spacer(), // Pusher le badge statut à la droite. - EventStatusBadge(status: status), + const Spacer(), // Pousse le badge de statut à droite. + EventStatusBadge(status: widget.status), // Badge de statut. ], ), + Text( - event.title, + widget.event.title, // Titre de l'événement. style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ), - const SizedBox(height: 5), - Text( - event.description, - style: const TextStyle(color: Colors.white70, fontSize: 14), - maxLines: 3, - overflow: TextOverflow.ellipsis, + const SizedBox(height: 5), // Espacement entre le titre et la description. + + GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; // Change l'état d'expansion. + }); + _logger.i("Changement d'état d'expansion pour la description de l'événement ${widget.event.id}"); + }, + child: Text( + _isExpanded || !shouldTruncate + ? descriptionText + : "${descriptionText.substring(0, _descriptionThreshold)}...", + style: const TextStyle(color: Colors.white70, fontSize: 14), + maxLines: _isExpanded ? null : 3, + overflow: _isExpanded ? TextOverflow.visible : TextOverflow.ellipsis, + ), ), - const SizedBox(height: 10), - EventImage(imageUrl: event.imageUrl), - const Divider(color: Colors.white24), + if (shouldTruncate) // Bouton "Afficher plus" si la description est longue. + GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + }); + _logger.i("Affichage de la description complète de l'événement ${widget.event.id}"); + }, + child: Text( + _isExpanded ? "Afficher moins" : "Afficher plus", + style: const TextStyle( + color: Colors.blue, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 10), // Espacement avant l'image. + + EventImage(imageUrl: widget.event.imageUrl), // Affichage de l'image de l'événement. + const Divider(color: Colors.white24), // Nouvelle ligne de séparation. + + // Rangée pour les interactions de l'événement (réagir, commenter, partager). EventInteractionRow( - onReact: onReact, - onComment: onComment, - onShare: onShare, - reactionsCount: event.reactionsCount, - commentsCount: event.commentsCount, - sharesCount: event.sharesCount, + onReact: widget.onReact, + onComment: widget.onComment, + onShare: widget.onShare, + reactionsCount: widget.event.reactionsCount, + commentsCount: widget.event.commentsCount, + sharesCount: widget.event.sharesCount, ), ], ), @@ -118,3 +186,4 @@ class EventCard extends StatelessWidget { ); } } + diff --git a/lib/presentation/screens/event/event_screen.dart b/lib/presentation/screens/event/event_screen.dart index 0f6c7e4..c3d27d9 100644 --- a/lib/presentation/screens/event/event_screen.dart +++ b/lib/presentation/screens/event/event_screen.dart @@ -10,12 +10,14 @@ class EventScreen extends StatefulWidget { final String userId; final String userFirstName; final String userLastName; + final String profileImageUrl; const EventScreen({ Key? key, required this.userId, required this.userFirstName, required this.userLastName, + required this.profileImageUrl, }) : super(key: key); @override @@ -84,6 +86,7 @@ class _EventScreenState extends State { userId: widget.userId, userFirstName: widget.userFirstName, userLastName: widget.userLastName, + profileImageUrl: widget.profileImageUrl, onReact: () => _onReact(event.id), onComment: () => _onComment(event.id), onShare: () => _onShare(event.id), @@ -143,7 +146,7 @@ class _EventScreenState extends State { void _onCloseEvent(String eventId) { print('Fermeture de l\'événement $eventId'); - // Appeler le bloc pour fermer l'événement + // Appeler le bloc pour fermer l'événement sans recharger la liste entière. context.read().add(CloseEvent(eventId)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('L\'événement a été fermé avec succès.')), @@ -152,10 +155,12 @@ class _EventScreenState extends State { void _onReopenEvent(String eventId) { print('Réouverture de l\'événement $eventId'); - // Appeler le bloc pour rouvrir l'événement + // Appeler le bloc pour rouvrir l'événement sans recharger la liste entière. context.read().add(ReopenEvent(eventId)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('L\'événement a été rouvert avec succès.')), ); } + + } diff --git a/lib/presentation/screens/friends/friends_content.dart b/lib/presentation/screens/friends/friends_content.dart deleted file mode 100644 index fe30a54..0000000 --- a/lib/presentation/screens/friends/friends_content.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../widgets/friend_card.dart'; -import '../../widgets/friend_detail_screen.dart'; - -class FriendsContent extends StatelessWidget { - final List> friends = [ - {'name': 'Alice', 'imageUrl': 'https://example.com/image1.jpg'}, - {'name': 'Bob', 'imageUrl': 'https://example.com/image2.jpg'}, - {'name': 'Charlie', 'imageUrl': 'https://example.com/image3.jpg'}, - // Autres amis... - ]; - - @override - Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), - itemCount: friends.length, - itemBuilder: (context, index) { - final friend = friends[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: FriendCard( - name: friend['name']!, - imageUrl: friend['imageUrl']!, - onTap: () => _navigateToFriendDetail(context, friend), - ), - ); - }, - ); - } - - void _navigateToFriendDetail(BuildContext context, Map friend) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => FriendDetailScreen( - name: friend['name']!, - imageUrl: friend['imageUrl']!, - friendId: friend['friendId']!, - ), - )); - } -} diff --git a/lib/presentation/screens/friends/friends_screen.dart b/lib/presentation/screens/friends/friends_screen.dart index a380425..08d2ec7 100644 --- a/lib/presentation/screens/friends/friends_screen.dart +++ b/lib/presentation/screens/friends/friends_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../data/providers/friends_provider.dart'; +import '../../../domain/entities/friend.dart'; import '../../widgets/friend_detail_screen.dart'; -import '../../widgets/friends_circle.dart'; import '../../widgets/search_friends.dart'; /// [FriendsScreen] est l'écran principal permettant d'afficher et de gérer la liste des amis. /// Il inclut des fonctionnalités de pagination, de recherche, et de rafraîchissement manuel de la liste. +/// Ce widget est un [StatefulWidget] afin de pouvoir mettre à jour dynamiquement la liste des amis. class FriendsScreen extends StatefulWidget { final String userId; // Identifiant de l'utilisateur pour récupérer ses amis @@ -28,7 +29,7 @@ class _FriendsScreenState extends State { // Log pour indiquer le début du chargement des amis debugPrint("[LOG] Initialisation de la page : chargement des amis pour l'utilisateur ${widget.userId}"); - // Chargement initial de la liste d'amis + // Chargement initial de la liste d'amis via le fournisseur (Provider) Provider.of(context, listen: false).fetchFriends(widget.userId); } @@ -46,12 +47,12 @@ class _FriendsScreenState extends State { void _onScroll() { final provider = Provider.of(context, listen: false); - // Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt + // Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt. if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !provider.isLoading && provider.hasMore) { debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante."); - provider.fetchFriends(widget.userId, loadMore: true); + provider.fetchFriends(widget.userId, loadMore: true); // Chargement de plus d'amis } } @@ -64,9 +65,11 @@ class _FriendsScreenState extends State { appBar: AppBar( title: const Text('Mes Amis'), actions: [ + // Bouton pour rafraîchir la liste des amis IconButton( icon: const Icon(Icons.refresh), onPressed: () { + // Vérifie si la liste n'est pas en cours de chargement avant d'envoyer une nouvelle requête. if (!friendsProvider.isLoading) { debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); friendsProvider.fetchFriends(widget.userId); @@ -80,13 +83,13 @@ class _FriendsScreenState extends State { body: SafeArea( child: Column( children: [ + // Widget de recherche d'amis en haut de l'écran const Padding( padding: EdgeInsets.all(8.0), - // Widget pour la recherche d'amis child: SearchFriends(), ), Expanded( - // Construction de la liste d'amis basée sur l'état du FriendsProvider + // Construction de la liste d'amis avec un affichage en grille child: Consumer( builder: (context, friendsProvider, child) { // Si le chargement est en cours et qu'il n'y a aucun ami, afficher un indicateur de chargement. @@ -103,40 +106,69 @@ class _FriendsScreenState extends State { ); } - // Affichage de la grille des amis - debugPrint("[LOG] Affichage de la grille des amis (nombre d'amis : ${friendsProvider.friendsList.length})"); - return GridView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 10, + controller: _scrollController, // Utilisation du contrôleur pour la pagination + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // Deux amis par ligne crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 0.8, // Ajuste la taille des cartes ), - itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0), + itemCount: friendsProvider.friendsList.length, itemBuilder: (context, index) { - if (index >= friendsProvider.friendsList.length) { - return const Center(child: CircularProgressIndicator()); - } final friend = friendsProvider.friendsList[index]; - debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}"); - - return FriendsCircle( - friend: friend, - onTap: () { - debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}"); - FriendDetailScreen.open( - context, - friend.friendId, - friend.friendFirstName, - friend.imageUrl ?? '', - ); - }, + // Affichage de chaque ami dans une carte avec une animation + return GestureDetector( + onTap: () => _navigateToFriendDetail(context, friend), // Action au clic sur l'avatar + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + transform: Matrix4.identity() + ..scale(1.05), // Effet de zoom lors du survol + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar( + radius: 50, + backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty + ? (friend.imageUrl!.startsWith('https') // 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. + ), + const SizedBox(height: 10), + Text( + "${friend.friendFirstName} ${friend.friendLastName}", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 5), + Text( + friend.status.name, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 5), + Text( + friend.lastInteraction ?? 'Aucune interaction récente', + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 12, + ), + ), + ], + ), + ), + ), ); }, ); - }, ), ), @@ -145,4 +177,24 @@ class _FriendsScreenState extends State { ), ); } + + /// Navigation vers l'écran de détails de l'ami + /// Permet de voir les informations complètes d'un ami lorsque l'utilisateur clique sur son avatar. + void _navigateToFriendDetail(BuildContext context, Friend friend) { + debugPrint("[LOG] Navigation : Détails de l'ami ${friend.friendFirstName} ${friend.friendLastName}"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FriendDetailScreen( + friendFirstName: friend.friendFirstName, // Prénom de l'ami + friendLastName: friend.friendLastName, // Nom de l'ami + imageUrl: friend.imageUrl ?? '', // URL de l'image de l'ami (ou valeur par défaut) + friendId: friend.friendId, // Identifiant unique de l'ami + status: friend.status, // Statut de l'ami + lastInteraction: friend.lastInteraction ?? 'Aucune', // Dernière interaction (si disponible) + dateAdded: friend.dateAdded ?? 'Inconnu', // Date d'ajout de l'ami (si disponible) + ), + ), + ); + } } diff --git a/lib/presentation/screens/friends/friends_screen_with_provider.dart b/lib/presentation/screens/friends/friends_screen_with_provider.dart index eccd2dc..b7f0d9a 100644 --- a/lib/presentation/screens/friends/friends_screen_with_provider.dart +++ b/lib/presentation/screens/friends/friends_screen_with_provider.dart @@ -8,20 +8,21 @@ import '../../widgets/friend_detail_screen.dart'; import '../../widgets/friends_appbar.dart'; import '../../widgets/search_friends.dart'; +/// Écran d'affichage des amis avec gestion des amis via un provider. class FriendsScreenWithProvider extends StatelessWidget { - final Logger _logger = Logger(); // Logger pour une meilleure traçabilité + final Logger _logger = Logger(); // Logger pour la traçabilité détaillée. @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.black, - appBar: FriendsAppBar(), + backgroundColor: Colors.black, // Fond noir pour une ambiance immersive. + appBar: FriendsAppBar(), // AppBar personnalisé pour l'écran. body: SafeArea( child: Column( children: [ const Padding( padding: EdgeInsets.all(8.0), - child: SearchFriends(), + child: SearchFriends(), // Barre de recherche pour trouver des amis. ), Expanded( child: Consumer( @@ -29,10 +30,10 @@ class FriendsScreenWithProvider extends StatelessWidget { final friends = friendsProvider.friendsList; if (friends.isEmpty) { - _logger.i("[LOG] Aucun ami trouvé"); + _logger.i("[LOG] Aucun ami trouvé."); // Log pour la recherche sans résultat. return const Center( child: Text( - 'Aucun ami trouvé', + 'Aucun ami trouvé', // Message affiché si aucun ami n'est trouvé. style: TextStyle(color: Colors.white70), ), ); @@ -43,6 +44,10 @@ class FriendsScreenWithProvider extends StatelessWidget { itemCount: friends.length, itemBuilder: (context, index) { final friend = friends[index]; + + // Log lorsque chaque ami est affiché + _logger.i("[LOG] Affichage de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); + return Dismissible( key: Key(friend.friendId), background: Container( @@ -53,7 +58,7 @@ class FriendsScreenWithProvider extends StatelessWidget { ), onDismissed: (direction) { _logger.i("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}"); - friendsProvider.removeFriend(friend.friendId); + friendsProvider.removeFriend(friend.friendId); // Suppression de l'ami via le provider. ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")), ); @@ -61,14 +66,16 @@ class FriendsScreenWithProvider extends StatelessWidget { child: FriendExpandingCard( name: friend.friendFirstName ?? 'Ami inconnu', imageUrl: friend.imageUrl ?? '', - description: "Amis depuis ${friend.friendId}", - onTap: () => _navigateToFriendDetail(context, friend), + description: "Amis depuis ${friend.dateAdded ?? 'Inconnu'}\nStatut : ${friend.status ?? 'Inconnu'}", + onTap: () { + _navigateToFriendDetail(context, friend); // Navigation vers les détails de l'ami. + }, onMessageTap: () { _logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); }, onRemoveTap: () { _logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); - friendsProvider.removeFriend(friend.friendId); + friendsProvider.removeFriend(friend.friendId); // Suppression via le provider. }, ), ); @@ -83,13 +90,19 @@ class FriendsScreenWithProvider extends StatelessWidget { ); } + /// Navigue vers l'écran de détails de l'ami. void _navigateToFriendDetail(BuildContext context, Friend friend) { _logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}"); + Navigator.of(context).push(MaterialPageRoute( builder: (context) => FriendDetailScreen( - name: friend.friendFirstName, - imageUrl: friend.imageUrl ?? '', friendId: friend.friendId, + friendFirstName: friend.friendFirstName, + friendLastName: friend.friendLastName, + imageUrl: friend.imageUrl ?? '', + status: friend.status, + lastInteraction: friend.lastInteraction ?? 'Aucune', + dateAdded: friend.dateAdded ?? 'Inconnu', ), )); } diff --git a/lib/presentation/screens/home/home_content.dart b/lib/presentation/screens/home/home_content.dart index a7a05df..32d3f0b 100644 --- a/lib/presentation/screens/home/home_content.dart +++ b/lib/presentation/screens/home/home_content.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../core/constants/colors.dart'; // Importez les couleurs dynamiques +import 'package:provider/provider.dart'; +import '../../../core/constants/colors.dart'; +import '../../../core/theme/theme_provider.dart'; import '../../widgets/friend_suggestions.dart'; import '../../widgets/group_list.dart'; import '../../widgets/popular_activity_list.dart'; @@ -7,45 +9,52 @@ import '../../widgets/recommended_event_list.dart'; import '../../widgets/section_header.dart'; import '../../widgets/story_section.dart'; +/// Écran principal du contenu d'accueil, affichant diverses sections telles que +/// les suggestions d'amis, les activités populaires, les groupes, etc. +/// Les couleurs s'adaptent dynamiquement au thème sélectionné (clair ou sombre). class HomeContentScreen extends StatelessWidget { const HomeContentScreen({super.key}); @override Widget build(BuildContext context) { + // Récupération du fournisseur de thème pour appliquer le mode jour/nuit + final themeProvider = Provider.of(context); + // Obtention des dimensions de l'écran pour adapter la mise en page final size = MediaQuery.of(context).size; + print("Chargement de HomeContentScreen avec le thème actuel"); return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 15.0), // Marges réduites + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 15.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Section de bienvenue - _buildWelcomeCard(), + // Carte de bienvenue avec couleurs dynamiques + _buildWelcomeCard(themeProvider), + const SizedBox(height: 15), // Espacement entre les sections - const SizedBox(height: 15), // Espacement vertical réduit - - // Section "Moments populaires" + // Section des "Moments populaires" _buildCard( context: context, + themeProvider: themeProvider, // Fournit le thème child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionHeader( title: 'Moments populaires', icon: Icons.camera_alt, - textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), // Taille ajustée + textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - const SizedBox(height: 10), // Espace vertical réduit + const SizedBox(height: 10), StorySection(size: size), ], ), ), + const SizedBox(height: 15), - const SizedBox(height: 15), // Espacement réduit - - // Section des événements recommandés + // Section des "Événements recommandés" _buildCard( context: context, + themeProvider: themeProvider, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -54,17 +63,17 @@ class HomeContentScreen extends StatelessWidget { icon: Icons.star, textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - const SizedBox(height: 10), // Espacement réduit + const SizedBox(height: 10), RecommendedEventList(size: size), ], ), ), + const SizedBox(height: 15), - const SizedBox(height: 15), // Espacement réduit - - // Section des activités populaires + // Section des "Activités populaires" _buildCard( context: context, + themeProvider: themeProvider, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -73,17 +82,17 @@ class HomeContentScreen extends StatelessWidget { icon: Icons.local_activity, textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - const SizedBox(height: 10), // Espacement réduit + const SizedBox(height: 10), PopularActivityList(size: size), ], ), ), + const SizedBox(height: 15), - const SizedBox(height: 15), // Espacement réduit - - // Section des groupes sociaux + // Section "Groupes à rejoindre" _buildCard( context: context, + themeProvider: themeProvider, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,17 +101,17 @@ class HomeContentScreen extends StatelessWidget { icon: Icons.group_add, textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - const SizedBox(height: 10), // Espacement réduit + const SizedBox(height: 10), GroupList(size: size), ], ), ), + const SizedBox(height: 15), - const SizedBox(height: 15), // Espacement réduit - - // Section des suggestions d'amis + // Section des "Suggestions d'amis" _buildCard( context: context, + themeProvider: themeProvider, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -111,7 +120,7 @@ class HomeContentScreen extends StatelessWidget { icon: Icons.person_add, textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - const SizedBox(height: 10), // Espacement réduit + const SizedBox(height: 10), FriendSuggestions(size: size), ], ), @@ -121,11 +130,13 @@ class HomeContentScreen extends StatelessWidget { ); } - // Widget pour la carte de bienvenue - Widget _buildWelcomeCard() { + /// Crée la carte de bienvenue, en utilisant les couleurs dynamiques en fonction du thème sélectionné. + /// [themeProvider] fournit l'état actuel du thème pour adapter les couleurs. + Widget _buildWelcomeCard(ThemeProvider themeProvider) { + print("Création de la carte de bienvenue avec le thème actuel"); return Card( elevation: 5, - color: AppColors.surface, // Utilisation de la couleur dynamique pour la surface + color: themeProvider.isDarkMode ? AppColors.darkSurface : AppColors.lightSurface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16.0), @@ -135,26 +146,32 @@ class HomeContentScreen extends StatelessWidget { Text( 'Bienvenue, Dahoud!', style: TextStyle( - color: AppColors.textPrimary, // Texte dynamique - fontSize: 22, // Taille de police réduite - fontWeight: FontWeight.w600, // Poids de police ajusté + color: themeProvider.isDarkMode ? AppColors.darkOnPrimary : AppColors.lightPrimary, + fontSize: 22, + fontWeight: FontWeight.w600, ), ), - Icon(Icons.waving_hand, color: Colors.orange.shade300, size: 24), // Taille de l'icône ajustée + Icon(Icons.waving_hand, color: Colors.orange.shade300, size: 24), ], ), ), ); } - // Widget générique pour créer une carte design avec des espaces optimisés - Widget _buildCard({required BuildContext context, required Widget child}) { + /// Crée une carte générique pour afficher des sections avec un style uniforme. + /// [themeProvider] est utilisé pour ajuster les couleurs de la carte selon le mode jour/nuit. + Widget _buildCard({ + required BuildContext context, + required ThemeProvider themeProvider, + required Widget child, + }) { + print("Création d'une carte de section avec le thème actuel"); return Card( - elevation: 3, // Réduction de l'élévation pour un look plus épuré - color: AppColors.surface, // Utilisation de la couleur dynamique pour la surface - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), // Coins légèrement arrondis + elevation: 3, + color: themeProvider.isDarkMode ? AppColors.darkSurface : AppColors.lightSurface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Padding( - padding: const EdgeInsets.all(12.0), // Padding interne réduit pour un contenu plus compact + padding: const EdgeInsets.all(12.0), child: child, ), ); diff --git a/lib/presentation/screens/home/home_screen.dart b/lib/presentation/screens/home/home_screen.dart index 4906827..abcfba9 100644 --- a/lib/presentation/screens/home/home_screen.dart +++ b/lib/presentation/screens/home/home_screen.dart @@ -1,22 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; // Pour ThemeProvider +import 'package:provider/provider.dart'; import 'package:afterwork/presentation/screens/event/event_screen.dart'; import 'package:afterwork/presentation/screens/profile/profile_screen.dart'; import 'package:afterwork/presentation/screens/social/social_screen.dart'; import 'package:afterwork/presentation/screens/establishments/establishments_screen.dart'; import 'package:afterwork/presentation/screens/home/home_content.dart'; import 'package:afterwork/data/datasources/event_remote_data_source.dart'; -import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; // Écran de notifications +import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; import '../../../core/constants/colors.dart'; import '../../../core/theme/theme_provider.dart'; -import '../friends/friends_screen.dart'; // Écran des amis +import '../friends/friends_screen.dart'; class HomeScreen extends StatefulWidget { final EventRemoteDataSource eventRemoteDataSource; final String userId; final String userFirstName; final String userLastName; - final String userProfileImage; // Image de profil de l'utilisateur + final String userProfileImage; const HomeScreen({ Key? key, @@ -24,7 +24,7 @@ class HomeScreen extends StatefulWidget { required this.userId, required this.userFirstName, required this.userLastName, - required this.userProfileImage, // Passer l'image de profil ici + required this.userProfileImage, }) : super(key: key); @override @@ -37,7 +37,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM @override void initState() { super.initState(); - _tabController = TabController(length: 6, vsync: this); // Ajouter un onglet pour les notifications + _tabController = TabController(length: 6, vsync: this); } @override @@ -47,51 +47,40 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } void _onMenuSelected(BuildContext context, String option) { - switch (option) { - case 'Publier': - print('Publier sélectionné'); - break; - case 'Story': - print('Story sélectionné'); - break; - default: - break; - } + print('$option sélectionné'); // Log pour chaque option } @override Widget build(BuildContext context) { - // Accès au ThemeProvider final themeProvider = Provider.of(context); return Scaffold( + backgroundColor: AppColors.backgroundColor, body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - backgroundColor: AppColors.backgroundColor, floating: true, pinned: true, snap: true, elevation: 2, + backgroundColor: themeProvider.currentTheme.primaryColor, leading: Padding( - padding: const EdgeInsets.all(4.0), // Ajustement du padding + padding: const EdgeInsets.all(4.0), child: Image.asset( 'lib/assets/images/logo.png', - height: 40, // Taille ajustée du logo + height: 40, ), ), actions: [ _buildActionIcon(Icons.add, 'Publier', context), _buildActionIcon(Icons.search, 'Rechercher', context), _buildActionIcon(Icons.message, 'Message', context), - _buildNotificationsIcon(context, 5), // Gérer la logique des notifications ici - - // Bouton pour basculer entre les thèmes + _buildNotificationsIcon(context, 105), Switch( value: themeProvider.isDarkMode, onChanged: (value) { - themeProvider.toggleTheme(); // Changer le thème + themeProvider.toggleTheme(); }, activeColor: AppColors.accentColor, ), @@ -99,23 +88,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM bottom: TabBar( controller: _tabController, indicatorColor: AppColors.lightPrimary, - labelStyle: const TextStyle( - fontSize: 12, // Taille réduite du texte - fontWeight: FontWeight.w500, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, // Taille ajustée pour les onglets non sélectionnés - ), - labelColor: AppColors.lightPrimary, - unselectedLabelColor: AppColors.iconSecondary, - + labelStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + unselectedLabelStyle: const TextStyle(fontSize: 11), + labelColor: themeProvider.isDarkMode ? AppColors.darkOnPrimary : AppColors.lightOnPrimary, + unselectedLabelColor: themeProvider.isDarkMode ? AppColors.darkIconSecondary : AppColors.lightIconSecondary, tabs: [ const Tab(icon: Icon(Icons.home, size: 24), text: 'Accueil'), const Tab(icon: Icon(Icons.event, size: 24), text: 'Événements'), const Tab(icon: Icon(Icons.location_city, size: 24), text: 'Établissements'), const Tab(icon: Icon(Icons.people, size: 24), text: 'Social'), const Tab(icon: Icon(Icons.people_alt_outlined, size: 24), text: 'Ami(e)s'), - _buildProfileTab(), // Onglet profil + _buildProfileTab(), ], ), ), @@ -129,10 +112,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM userId: widget.userId, userFirstName: widget.userFirstName, userLastName: widget.userLastName, + profileImageUrl: widget.userProfileImage, ), const EstablishmentsScreen(), const SocialScreen(), - FriendsScreen(userId: widget.userId), // Correction ici : passer l'userId + FriendsScreen(userId: widget.userId), const ProfileScreen(), ], ), @@ -140,20 +124,19 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - // Widget pour l'affichage de la photo de profil dans l'onglet Tab _buildProfileTab() { return Tab( child: Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: Colors.blue, + color: AppColors.secondary, width: 2.0, ), ), child: CircleAvatar( radius: 16, - backgroundColor: Colors.grey[200], // Couleur de fond par défaut + backgroundColor: AppColors.surface, child: ClipOval( child: FadeInImage.assetNetwork( placeholder: 'lib/assets/images/user_placeholder.png', @@ -169,18 +152,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - // Icône pour les notifications avec un badge Widget _buildNotificationsIcon(BuildContext context, int notificationCount) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Stack( - clipBehavior: Clip.none, // Permet d'afficher le badge en dehors des limites + clipBehavior: Clip.none, children: [ CircleAvatar( backgroundColor: AppColors.surface, radius: 18, child: IconButton( - icon: const Icon(Icons.notifications, color: AppColors.darkOnPrimary, size: 20), + icon: Icon(Icons.notifications, color: AppColors.iconPrimary, size: 20), onPressed: () { Navigator.push( context, @@ -197,21 +179,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM top: -6, child: Container( padding: const EdgeInsets.all(2), - decoration: const BoxDecoration( + decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), - constraints: const BoxConstraints( + constraints: BoxConstraints( minWidth: 18, minHeight: 18, ), child: Text( notificationCount > 99 ? '99+' : '$notificationCount', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), @@ -221,7 +199,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - // Icône d'action générique Widget _buildActionIcon(IconData iconData, String label, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), @@ -229,7 +206,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM backgroundColor: AppColors.surface, radius: 18, child: IconButton( - icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), + icon: Icon(iconData, color: AppColors.iconPrimary, size: 20), onPressed: () { _onMenuSelected(context, label); }, diff --git a/lib/presentation/screens/profile/profile_screen.dart b/lib/presentation/screens/profile/profile_screen.dart index b90bb5d..590c2f2 100644 --- a/lib/presentation/screens/profile/profile_screen.dart +++ b/lib/presentation/screens/profile/profile_screen.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../core/constants/colors.dart'; import '../../../data/providers/user_provider.dart'; -import '../../widgets/account_deletion_card.dart'; +import '../../widgets/cards/account_deletion_card.dart'; +import '../../widgets/cards/statistics_section_card.dart'; +import '../../widgets/cards/support_section_card.dart'; import '../../widgets/custom_list_tile.dart'; -import '../../widgets/edit_options_card.dart'; -import '../../widgets/expandable_section_card.dart'; +import '../../widgets/cards/edit_options_card.dart'; +import '../../widgets/cards/expandable_section_card.dart'; import '../../widgets/profile_header.dart'; -import '../../widgets/statistics_section_card.dart'; -import '../../widgets/support_section_card.dart'; -import '../../widgets/user_info_card.dart'; +import '../../widgets/cards/user_info_card.dart'; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); diff --git a/lib/presentation/state_management/event_bloc.dart b/lib/presentation/state_management/event_bloc.dart index b2ba896..2c1639c 100644 --- a/lib/presentation/state_management/event_bloc.dart +++ b/lib/presentation/state_management/event_bloc.dart @@ -91,25 +91,48 @@ class EventBloc extends Bloc { // Gestion de la fermeture d'un événement Future _onCloseEvent(CloseEvent event, Emitter emit) async { - emit(EventLoading()); + emit(EventLoading()); // Affiche le chargement try { await remoteDataSource.closeEvent(event.eventId); - final events = await remoteDataSource.getAllEvents(); - emit(EventLoaded(events)); + + // Mise à jour de l'événement spécifique dans l'état + if (state is EventLoaded) { + final updatedEvents = List.from((state as EventLoaded).events); + final updatedEvent = updatedEvents.firstWhere((e) => e.id == event.eventId); + updatedEvent.status = 'fermé'; // Modifier l'état de l'événement localement + + // Émettre un nouvel état avec l'événement mis à jour + emit(EventLoaded(updatedEvents)); + print('Événement fermé et mis à jour localement.'); + } } catch (e) { emit(EventError('Erreur lors de la fermeture de l\'événement.')); + print('Erreur lors de la fermeture de l\'événement : $e'); } } - // Gestion de la réouverture d'un événement Future _onReopenEvent(ReopenEvent event, Emitter emit) async { - emit(EventLoading()); + emit(EventLoading()); // Affiche le chargement try { + // Appel au service backend pour réouvrir l'événement await remoteDataSource.reopenEvent(event.eventId); - final events = await remoteDataSource.getAllEvents(); - emit(EventLoaded(events)); + + // Mise à jour de l'événement spécifique dans l'état + if (state is EventLoaded) { + final updatedEvents = List.from((state as EventLoaded).events); + final updatedEvent = updatedEvents.firstWhere((e) => e.id == event.eventId); + + // Mise à jour du statut local de l'événement + updatedEvent.status = 'ouvert'; + + // Émettre un nouvel état avec l'événement mis à jour + emit(EventLoaded(updatedEvents)); + print('Événement réouvert et mis à jour localement.'); + } } catch (e) { + // En cas d'erreur, émettre un état d'erreur emit(EventError('Erreur lors de la réouverture de l\'événement.')); + print('Erreur lors de la réouverture de l\'événement : $e'); } } diff --git a/lib/presentation/widgets/account_deletion_card.dart b/lib/presentation/widgets/cards/account_deletion_card.dart similarity index 97% rename from lib/presentation/widgets/account_deletion_card.dart rename to lib/presentation/widgets/cards/account_deletion_card.dart index 7cfce5d..2a6f8e5 100644 --- a/lib/presentation/widgets/account_deletion_card.dart +++ b/lib/presentation/widgets/cards/account_deletion_card.dart @@ -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. diff --git a/lib/presentation/widgets/edit_options_card.dart b/lib/presentation/widgets/cards/edit_options_card.dart similarity index 98% rename from lib/presentation/widgets/edit_options_card.dart rename to lib/presentation/widgets/cards/edit_options_card.dart index 11287a5..9422b54 100644 --- a/lib/presentation/widgets/edit_options_card.dart +++ b/lib/presentation/widgets/cards/edit_options_card.dart @@ -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. diff --git a/lib/presentation/widgets/expandable_section_card.dart b/lib/presentation/widgets/cards/expandable_section_card.dart similarity index 98% rename from lib/presentation/widgets/expandable_section_card.dart rename to lib/presentation/widgets/cards/expandable_section_card.dart index f55b8e4..5696f22 100644 --- a/lib/presentation/widgets/expandable_section_card.dart +++ b/lib/presentation/widgets/cards/expandable_section_card.dart @@ -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. diff --git a/lib/presentation/widgets/friend_card.dart b/lib/presentation/widgets/cards/friend_card.dart similarity index 100% rename from lib/presentation/widgets/friend_card.dart rename to lib/presentation/widgets/cards/friend_card.dart diff --git a/lib/presentation/widgets/statistics_section_card.dart b/lib/presentation/widgets/cards/statistics_section_card.dart similarity index 96% rename from lib/presentation/widgets/statistics_section_card.dart rename to lib/presentation/widgets/cards/statistics_section_card.dart index bc04d76..372315d 100644 --- a/lib/presentation/widgets/statistics_section_card.dart +++ b/lib/presentation/widgets/cards/statistics_section_card.dart @@ -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. diff --git a/lib/presentation/widgets/support_section_card.dart b/lib/presentation/widgets/cards/support_section_card.dart similarity index 100% rename from lib/presentation/widgets/support_section_card.dart rename to lib/presentation/widgets/cards/support_section_card.dart diff --git a/lib/presentation/widgets/user_info_card.dart b/lib/presentation/widgets/cards/user_info_card.dart similarity index 95% rename from lib/presentation/widgets/user_info_card.dart rename to lib/presentation/widgets/cards/user_info_card.dart index 35f1218..4eb1a0d 100644 --- a/lib/presentation/widgets/user_info_card.dart +++ b/lib/presentation/widgets/cards/user_info_card.dart @@ -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. diff --git a/lib/presentation/widgets/category_field.dart b/lib/presentation/widgets/category_field.dart deleted file mode 100644 index 88b3266..0000000 --- a/lib/presentation/widgets/category_field.dart +++ /dev/null @@ -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 onSaved; - - const CategoryField({Key? key, required this.onSaved}) : super(key: key); - - @override - _CategoryFieldState createState() => _CategoryFieldState(); -} - -class _CategoryFieldState extends State { - String? _selectedCategory; - Map> _categoryMap = {}; // Map pour stocker les catégories et sous-catégories - List> _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 _loadCategories() async { - try { - final String jsonString = await rootBundle.rootBundle.loadString('lib/assets/json/event_categories.json'); - final Map jsonMap = json.decode(jsonString); - - final Map> categoryMap = {}; - jsonMap['categories'].forEach((key, value) { - categoryMap[key] = List.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> _buildDropdownItems() { - List> items = []; - - _categoryMap.forEach((category, subcategories) { - items.add( - DropdownMenuItem( - enabled: false, - child: Text( - category, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white70, - ), - ), - ), - ); - - for (String subcategory in subcategories) { - items.add( - DropdownMenuItem( - 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( - 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, - ); - } - -} diff --git a/lib/presentation/widgets/date_picker.dart b/lib/presentation/widgets/date_picker.dart index 3332e97..c6e37b3 100644 --- a/lib/presentation/widgets/date_picker.dart +++ b/lib/presentation/widgets/date_picker.dart @@ -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), ], ), ), diff --git a/lib/presentation/widgets/description_field.dart b/lib/presentation/widgets/description_field.dart deleted file mode 100644 index c8d95f7..0000000 --- a/lib/presentation/widgets/description_field.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -class DescriptionField extends StatelessWidget { - final FormFieldSetter 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, - ); - } -} diff --git a/lib/presentation/widgets/event_header.dart b/lib/presentation/widgets/event_header.dart index a4c9ebf..86bf283 100644 --- a/lib/presentation/widgets/event_header.dart +++ b/lib/presentation/widgets/event_header.dart @@ -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, diff --git a/lib/presentation/widgets/event_list.dart b/lib/presentation/widgets/event_list.dart index 06af335..03033f8 100644 --- a/lib/presentation/widgets/event_list.dart +++ b/lib/presentation/widgets/event_list.dart @@ -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), diff --git a/lib/presentation/widgets/event_menu.dart b/lib/presentation/widgets/event_menu.dart index 15c5d9f..76c6bc0 100644 --- a/lib/presentation/widgets/event_menu.dart +++ b/lib/presentation/widgets/event_menu.dart @@ -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(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: [ + 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), + ), + ); + }, + ); +} diff --git a/lib/presentation/widgets/event_status_badge.dart b/lib/presentation/widgets/event_status_badge.dart index 05c0a24..1c819a5 100644 --- a/lib/presentation/widgets/event_status_badge.dart +++ b/lib/presentation/widgets/event_status_badge.dart @@ -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, ), diff --git a/lib/presentation/widgets/fields/accessibility_field.dart b/lib/presentation/widgets/fields/accessibility_field.dart new file mode 100644 index 0000000..c1ece22 --- /dev/null +++ b/lib/presentation/widgets/fields/accessibility_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/accommodation_info_field.dart b/lib/presentation/widgets/fields/accommodation_info_field.dart new file mode 100644 index 0000000..02f50f2 --- /dev/null +++ b/lib/presentation/widgets/fields/accommodation_info_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/attendees_field.dart b/lib/presentation/widgets/fields/attendees_field.dart new file mode 100644 index 0000000..3cf989d --- /dev/null +++ b/lib/presentation/widgets/fields/attendees_field.dart @@ -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 + }, + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/fields/category_field.dart b/lib/presentation/widgets/fields/category_field.dart new file mode 100644 index 0000000..68683da --- /dev/null +++ b/lib/presentation/widgets/fields/category_field.dart @@ -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 onSaved; + + // Constructeur de la classe CategoryField + const CategoryField({Key? key, required this.onSaved}) : super(key: key); + + @override + _CategoryFieldState createState() => _CategoryFieldState(); +} + +class _CategoryFieldState extends State { + // 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> _categoryMap = {}; + + // Liste des éléments du menu déroulant + List> _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 _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 jsonMap = json.decode(jsonString); + + // Map pour stocker les catégories et leurs sous-catégories + final Map> categoryMap = {}; + + // Parcours des catégories et ajout des sous-catégories dans le map + jsonMap['categories'].forEach((key, value) { + categoryMap[key] = List.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> _buildDropdownItems() { + List> 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( + 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( + 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( + 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 + ), + ); + } +} diff --git a/lib/presentation/widgets/fields/description_field.dart b/lib/presentation/widgets/fields/description_field.dart new file mode 100644 index 0000000..823907f --- /dev/null +++ b/lib/presentation/widgets/fields/description_field.dart @@ -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 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 + }, + ); + } +} diff --git a/lib/presentation/widgets/fields/link_field.dart b/lib/presentation/widgets/fields/link_field.dart new file mode 100644 index 0000000..c309f5e --- /dev/null +++ b/lib/presentation/widgets/fields/link_field.dart @@ -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 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; + }, + ); + } +} diff --git a/lib/presentation/widgets/fields/location_field.dart b/lib/presentation/widgets/fields/location_field.dart new file mode 100644 index 0000000..f4365e4 --- /dev/null +++ b/lib/presentation/widgets/fields/location_field.dart @@ -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), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/fields/organizer_field.dart b/lib/presentation/widgets/fields/organizer_field.dart new file mode 100644 index 0000000..2c4cc2e --- /dev/null +++ b/lib/presentation/widgets/fields/organizer_field.dart @@ -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. + ); + } +} diff --git a/lib/presentation/widgets/fields/parking_field.dart b/lib/presentation/widgets/fields/parking_field.dart new file mode 100644 index 0000000..3bccf01 --- /dev/null +++ b/lib/presentation/widgets/fields/parking_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/participation_fee_field.dart b/lib/presentation/widgets/fields/participation_fee_field.dart new file mode 100644 index 0000000..010ecf0 --- /dev/null +++ b/lib/presentation/widgets/fields/participation_fee_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/privacy_rules_field.dart b/lib/presentation/widgets/fields/privacy_rules_field.dart new file mode 100644 index 0000000..5b4971a --- /dev/null +++ b/lib/presentation/widgets/fields/privacy_rules_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/security_protocol_field.dart b/lib/presentation/widgets/fields/security_protocol_field.dart new file mode 100644 index 0000000..aba8401 --- /dev/null +++ b/lib/presentation/widgets/fields/security_protocol_field.dart @@ -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, + ); + } +} diff --git a/lib/presentation/widgets/fields/tags_field.dart b/lib/presentation/widgets/fields/tags_field.dart new file mode 100644 index 0000000..97271eb --- /dev/null +++ b/lib/presentation/widgets/fields/tags_field.dart @@ -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) 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 { + final TextEditingController _controller = TextEditingController(); // Contrôleur pour gérer l'entrée de texte. + List _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. + } +} diff --git a/lib/presentation/widgets/fields/title_field.dart b/lib/presentation/widgets/fields/title_field.dart new file mode 100644 index 0000000..45e068c --- /dev/null +++ b/lib/presentation/widgets/fields/title_field.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class TitleField extends StatelessWidget { + final FormFieldSetter 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, + ); + } +} diff --git a/lib/presentation/widgets/fields/transport_info_field.dart b/lib/presentation/widgets/fields/transport_info_field.dart new file mode 100644 index 0000000..f64611e --- /dev/null +++ b/lib/presentation/widgets/fields/transport_info_field.dart @@ -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. + ); + } +} diff --git a/lib/presentation/widgets/friend_detail_screen.dart b/lib/presentation/widgets/friend_detail_screen.dart index 1635219..2e04aed 100644 --- a/lib/presentation/widgets/friend_detail_screen.dart +++ b/lib/presentation/widgets/friend_detail_screen.dart @@ -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 + ), + ), + ], + ), + ); + } } diff --git a/lib/presentation/widgets/friends_circle.dart b/lib/presentation/widgets/friends_circle.dart index 4faa0ab..2da4a5e 100644 --- a/lib/presentation/widgets/friends_circle.dart +++ b/lib/presentation/widgets/friends_circle.dart @@ -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. ), ], ), ); } } - diff --git a/lib/presentation/widgets/image_preview_picker.dart b/lib/presentation/widgets/image_preview_picker.dart index d82307f..616f86e 100644 --- a/lib/presentation/widgets/image_preview_picker.dart +++ b/lib/presentation/widgets/image_preview_picker.dart @@ -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 { File? _selectedImageFile; + /// Méthode pour ouvrir le modal de sélection d'image avec une animation. Future _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( context: context, builder: (BuildContext context) { @@ -26,14 +36,14 @@ class _ImagePreviewPickerState extends State { 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 { }, ); + // 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 { @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 { : const Center( child: Text( 'Cliquez pour ajouter une image', - style: TextStyle(color: Colors.white54), + style: TextStyle(color: Colors.blueGrey), ), ), ), diff --git a/lib/presentation/widgets/link_field.dart b/lib/presentation/widgets/link_field.dart deleted file mode 100644 index 15bb2eb..0000000 --- a/lib/presentation/widgets/link_field.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -class LinkField extends StatelessWidget { - final FormFieldSetter 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, - ); - } -} diff --git a/lib/presentation/widgets/location_field.dart b/lib/presentation/widgets/location_field.dart deleted file mode 100644 index 8f8a270..0000000 --- a/lib/presentation/widgets/location_field.dart +++ /dev/null @@ -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), - ], - ), - ), - ); - } -} diff --git a/lib/presentation/widgets/submit_button.dart b/lib/presentation/widgets/submit_button.dart index 7c4cdeb..53c95ac 100644 --- a/lib/presentation/widgets/submit_button.dart +++ b/lib/presentation/widgets/submit_button.dart @@ -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 ), ), ), diff --git a/lib/presentation/widgets/title_field.dart b/lib/presentation/widgets/title_field.dart deleted file mode 100644 index 3fd5db1..0000000 --- a/lib/presentation/widgets/title_field.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class TitleField extends StatelessWidget { - final FormFieldSetter 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, - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 136701f..341a2e3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -788,10 +788,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bcfa571..4e702cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: encrypt: ^5.0.0 provider: ^6.0.0 flare_flutter: ^3.0.0 - logger: ^1.1.0 + logger: ^1.4.0 flutter_spinkit: ^5.1.0 flutter_vibrate: ^1.3.0 loading_icon_button: ^0.0.6 @@ -33,6 +33,7 @@ dependencies: google_maps_flutter: ^2.9.0 cupertino_icons: ^1.0.8 + # State management with flutter_bloc flutter_bloc: ^8.0.9