From 1e888f41e8fb46070d72f93c2d84d08ae71db774 Mon Sep 17 00:00:00 2001 From: DahoudG Date: Fri, 8 Nov 2024 20:30:23 +0000 Subject: [PATCH] Bon checkpoint + Refactoring --- lib/core/constants/urls.dart | 2 +- .../datasources/event_remote_data_source.dart | 65 +++++++ lib/data/models/event_model.dart | 69 +++++-- lib/data/providers/friends_provider.dart | 21 ++- lib/data/providers/user_provider.dart | 22 ++- .../repositories/friends_repository_impl.dart | 12 +- lib/domain/entities/friend.dart | 28 +-- .../screens/event/event_screen.dart | 5 + .../screens/friends/friends_screen.dart | 30 +-- .../friends/friends_screen_with_provider.dart | 25 +-- .../screens/profile/profile_screen.dart | 1 - .../state_management/event_bloc.dart | 6 +- .../widgets/account_deletion_card.dart | 12 +- .../widgets/edit_options_card.dart | 83 ++++++++- .../widgets/expandable_section_card.dart | 78 ++++++-- lib/presentation/widgets/friends_circle.dart | 11 +- lib/presentation/widgets/profile_header.dart | 171 ++++++++++++------ lib/presentation/widgets/stat_tile.dart | 47 +++-- .../widgets/statistics_section_card.dart | 79 +++++++- .../widgets/support_section_card.dart | 115 +++++++++--- lib/presentation/widgets/user_info_card.dart | 62 +++++-- 21 files changed, 721 insertions(+), 223 deletions(-) diff --git a/lib/core/constants/urls.dart b/lib/core/constants/urls.dart index 8983bf0..8a5f9d4 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.11:8085'; + static const String baseUrl = 'http://192.168.1.145:8085'; // Authentication and Users Endpoints static const String authenticateUser = '$baseUrl/users/authenticate'; diff --git a/lib/data/datasources/event_remote_data_source.dart b/lib/data/datasources/event_remote_data_source.dart index fe4ea06..73e86f9 100644 --- a/lib/data/datasources/event_remote_data_source.dart +++ b/lib/data/datasources/event_remote_data_source.dart @@ -28,6 +28,71 @@ class EventRemoteDataSource { } } + /// Récupérer les événements créés par un utilisateur spécifique et ses amis. + /// Cette méthode envoie une requête POST au serveur pour obtenir la liste des événements créés + /// par l'utilisateur spécifié et ses amis, en utilisant l'identifiant de l'utilisateur. + /// + /// [userId] : L'identifiant de l'utilisateur pour lequel récupérer les événements. + /// Retourne une liste de modèles d'événements [EventModel]. + Future> getEventsCreatedByUserAndFriends(String userId) async { + // Log de début de la méthode pour signaler l'initialisation de la récupération des événements + print('[LOG] Démarrage de la récupération des événements créés par l\'utilisateur ID: $userId et ses amis.'); + + // Construction de l'URL de l'API pour la requête POST + final url = Uri.parse('${Urls.baseUrl}/events/created-by-user-and-friends'); + print('[LOG] URL construite pour la requête: $url'); + + // Création de l'en-tête de la requête, spécifiant que le contenu est en JSON + final headers = {'Content-Type': 'application/json'}; + print('[LOG] En-têtes de la requête: $headers'); + + // Construction du corps de la requête en JSON, incluant l'identifiant de l'utilisateur + final body = jsonEncode({'userId': userId}); + print('[LOG] Corps de la requête JSON: $body'); + + // Envoi de la requête POST au serveur pour récupérer les événements + final response = await client.post(url, headers: headers, body: body); + print('[LOG] Requête POST envoyée au serveur.'); + + // Vérification et log de l'état de la réponse reçue + print('[LOG] Statut de la réponse HTTP: ${response.statusCode}'); + + // Gestion de la réponse en fonction du code de statut + if (response.statusCode == 200) { + // Déchiffrement du JSON reçu si le code de statut est 200 (OK) + final List jsonResponse = json.decode(response.body); + print('[LOG] Réponse JSON complète reçue (taille: ${jsonResponse.length}) :'); + + // Affichage détaillé de chaque événement + for (var i = 0; i < jsonResponse.length; i++) { + final event = jsonResponse[i]; + print('[LOG] Événement $i :'); + print(' - ID: ${event['id']}'); + print(' - Titre: ${event['title']}'); + print(' - Description: ${event['description']}'); + print(' - Date de début: ${event['startDate']}'); + print(' - Date de fin: ${event['endDate']}'); + print(' - Localisation: ${event['location']}'); + print(' - Catégorie: ${event['category']}'); + print(' - Lien: ${event['link']}'); + print(' - URL de l\'image: ${event['imageUrl']}'); + print(' - Statut: ${event['status']}'); + } + + // Transformation du JSON en une liste d'objets EventModel + List events = jsonResponse.map((event) => EventModel.fromJson(event)).toList(); + print('[LOG] Conversion JSON -> List réussie. Nombre d\'événements: ${events.length}'); + + // Retourne la liste d'événements si tout s'est bien passé + return events; + } else { + // Log et gestion de l'erreur en cas de statut HTTP autre que 200 + print('[ERROR] Erreur lors de la récupération des événements: ${response.body}'); + throw ServerException('[ERROR] Échec de récupération des événements créés par l\'utilisateur $userId et ses amis.'); + } + } + + /// 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()}'); diff --git a/lib/data/models/event_model.dart b/lib/data/models/event_model.dart index 1ac70fc..78ad226 100644 --- a/lib/data/models/event_model.dart +++ b/lib/data/models/event_model.dart @@ -2,13 +2,13 @@ class EventModel { final String id; final String title; final String description; - final String startDate; // Utiliser startDate au lieu de date, si c'est ce que l'API retourne + final String startDate; final String location; final String category; final String link; - final String? imageUrl; // Nullable + final String? imageUrl; final String creatorEmail; - final List participants; // Si participants est une liste simple + final List participants; final String status; final int reactionsCount; final int commentsCount; @@ -32,25 +32,60 @@ class EventModel { }); factory EventModel.fromJson(Map json) { + print('[LOG] Création de l\'EventModel depuis JSON'); + + // Utiliser les valeurs par défaut si une clé est absente + final String id = json['id'] ?? 'ID Inconnu'; + final String title = json['title'] ?? 'Titre Inconnu'; + final String description = json['description'] ?? 'Description Inconnue'; + final String startDate = json['startDate'] ?? 'Date de début Inconnue'; + final String location = json['location'] ?? 'Localisation Inconnue'; + final String category = json['category'] ?? 'Catégorie Inconnue'; + final String link = json['link'] ?? 'Lien Inconnu'; + final String? imageUrl = json['imageUrl']; + final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu'; + final List participants = json['participants'] ?? []; + final String status = json['status'] ?? 'ouvert'; + final int reactionsCount = json['reactionsCount'] ?? 0; + final int commentsCount = json['commentsCount'] ?? 0; + final int sharesCount = json['sharesCount'] ?? 0; + + print('[LOG] Champs extraits depuis JSON :'); + print(' - ID: $id'); + print(' - Titre: $title'); + print(' - Description: $description'); + print(' - Date de début: $startDate'); + print(' - Localisation: $location'); + print(' - Catégorie: $category'); + print(' - Lien: $link'); + print(' - URL de l\'image: ${imageUrl ?? "Aucune"}'); + print(' - Email du créateur: $creatorEmail'); + print(' - Participants: ${participants.length} participants'); + print(' - Statut: $status'); + print(' - Nombre de réactions: $reactionsCount'); + print(' - Nombre de commentaires: $commentsCount'); + print(' - Nombre de partages: $sharesCount'); + return EventModel( - id: json['id'], - title: json['title'], - description: json['description'], - startDate: json['startDate'], // Vérifier si c'est bien startDate - location: json['location'], - category: json['category'], - link: json['link'] ?? '', - imageUrl: json['imageUrl'], // Peut être null - creatorEmail: json['creatorEmail'], // Email du créateur - participants: json['participants'] ?? [], // Gérer les participants - status: json['status'] ?? 'ouvert', // Par défaut à "ouvert" si non fourni - reactionsCount: json['reactionsCount'] ?? 0, - commentsCount: json['commentsCount'] ?? 0, - sharesCount: json['sharesCount'] ?? 0, + id: id, + title: title, + description: description, + startDate: startDate, + location: location, + category: category, + link: link, + imageUrl: imageUrl, + creatorEmail: creatorEmail, + participants: participants, + status: status, + reactionsCount: reactionsCount, + commentsCount: commentsCount, + sharesCount: sharesCount, ); } Map toJson() { + print('[LOG] Conversion de EventModel en JSON'); return { 'id': id, 'title': title, diff --git a/lib/data/providers/friends_provider.dart b/lib/data/providers/friends_provider.dart index 2b69c7c..a16417b 100644 --- a/lib/data/providers/friends_provider.dart +++ b/lib/data/providers/friends_provider.dart @@ -30,7 +30,11 @@ class FriendsProvider with ChangeNotifier { /// [userId] : L'identifiant unique de l'utilisateur. /// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination. /// - /// En cas d'erreur, logue l'exception et gère l'état `isLoading`. + /// 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. Future fetchFriends(String userId, {bool loadMore = false}) async { if (_isLoading) { _logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.'); @@ -39,10 +43,10 @@ class FriendsProvider with ChangeNotifier { _isLoading = true; notifyListeners(); - _logger.i('[LOG] Début du chargement des amis.'); + _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 if (!loadMore) { - // Réinitialisation de la liste et de la pagination si ce n'est pas un chargement supplémentaire _friendsList = []; _currentPage = 0; _hasMore = true; @@ -57,9 +61,16 @@ class FriendsProvider with ChangeNotifier { _hasMore = false; _logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.'); } else { - _friendsList.addAll(newFriends); + 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}"); + } else { + _logger.w("[WARN] Exclusion de l'utilisateur lui-même de la liste d'amis : ${friend.friendId}"); + } + } _currentPage++; - _logger.i('[LOG] Amis ajoutés à la liste. Page actuelle : $_currentPage'); + _logger.i('[LOG] Page suivante préparée pour le prochain chargement, page actuelle : $_currentPage'); } } catch (e) { _logger.e('[ERROR] Erreur lors de la récupération des amis : $e'); diff --git a/lib/data/providers/user_provider.dart b/lib/data/providers/user_provider.dart index 807f13f..e8343ee 100644 --- a/lib/data/providers/user_provider.dart +++ b/lib/data/providers/user_provider.dart @@ -17,26 +17,31 @@ class UserProvider with ChangeNotifier { visitedPlacesCount: 0, ); + bool _isEmailDisplayedElsewhere = false; // Ajout de la propriété pour contrôler l'affichage de l'email + /// Getter pour l'objet utilisateur. User get user => _user; + /// Getter pour vérifier si l'email est affiché ailleurs. + bool get isEmailDisplayedElsewhere => _isEmailDisplayedElsewhere; + + /// Méthode pour définir l'état d'affichage de l'email. + void setEmailDisplayedElsewhere(bool value) { + _isEmailDisplayedElsewhere = value; + debugPrint("[LOG] isEmailDisplayedElsewhere mis à jour : $_isEmailDisplayedElsewhere"); + notifyListeners(); + } + /// Méthode pour définir les informations de l'utilisateur. /// Logue les informations fournies et notifie les listeners des changements. - /// - /// [user] : L'objet utilisateur contenant toutes les informations. void setUser(User user) { debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ${user.toString()}"); - _user = user; - debugPrint("[LOG] Informations utilisateur définies : ${_user.toString()}"); - - // Notifie les widgets écoutant ce provider qu'une modification a eu lieu. notifyListeners(); } /// Méthode pour mettre à jour des statistiques de l'utilisateur. - /// Cette méthode met à jour individuellement des attributs spécifiques comme le nombre d'amis ou d'événements. void updateStatistics({ int? eventsCount, int? friendsCount, @@ -59,12 +64,10 @@ class UserProvider with ChangeNotifier { ); debugPrint("[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}"); - notifyListeners(); } /// Méthode pour réinitialiser les informations de l'utilisateur. - /// Les valeurs sont loguées avant et après la réinitialisation. void resetUser() { debugPrint("[LOG] Réinitialisation des informations de l'utilisateur."); debugPrint("[LOG] Valeurs avant réinitialisation : ${_user.toString()}"); @@ -83,7 +86,6 @@ class UserProvider with ChangeNotifier { ); debugPrint("[LOG] Informations utilisateur réinitialisées : ${_user.toString()}"); - notifyListeners(); } } diff --git a/lib/data/repositories/friends_repository_impl.dart b/lib/data/repositories/friends_repository_impl.dart index 87bf3ff..d1b2440 100644 --- a/lib/data/repositories/friends_repository_impl.dart +++ b/lib/data/repositories/friends_repository_impl.dart @@ -33,11 +33,15 @@ class FriendsRepositoryImpl implements FriendsRepository { if (response.statusCode == 200) { _logger.i("[LOG] Liste des amis récupérée avec succès."); - final List friendsJson = json.decode(response.body); - _logger.i("[LOG] Nombre d'amis récupérés : ${friendsJson.length}"); + _logger.i("[LOG] Nombre d'amis récupérés (excluant l'utilisateur lui-même) : ${friendsJson.length}"); - return friendsJson.map((json) => Friend.fromJson(json as Map)).toList(); + return friendsJson.map((json) { + _logger.i("[LOG] Conversion JSON -> Friend : $json"); + final friend = Friend.fromJson(json as Map); + _logger.i("[LOG] Création d'un objet Friend : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}"); + return friend; + }).toList(); } else { _logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}"); return []; @@ -56,7 +60,7 @@ class FriendsRepositoryImpl implements FriendsRepository { @override Future addFriend(Friend friend) async { try { - _logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.firstName} ${friend.lastName}"); + _logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.friendFirstName} ${friend.friendLastName}"); final uri = Uri.parse('${Urls.baseUrl}/friends/send'); final response = await client.post( diff --git a/lib/domain/entities/friend.dart b/lib/domain/entities/friend.dart index d2e4ad8..ca2f040 100644 --- a/lib/domain/entities/friend.dart +++ b/lib/domain/entities/friend.dart @@ -12,8 +12,8 @@ enum FriendStatus { pending, accepted, blocked, unknown } /// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith]. class Friend extends Equatable { final String friendId; // ID unique de l'ami, requis et non-nullable - final String firstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données - final String lastName; // Nom de famille, non-nullable + final String friendFirstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données + final String friendLastName; // Nom de famille, non-nullable 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` @@ -26,14 +26,14 @@ class Friend extends Equatable { /// La validation des valeurs est incluse pour garantir l'intégrité des données. Friend({ required this.friendId, - this.firstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides - this.lastName = '', + this.friendFirstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides + this.friendLastName = '', this.email, this.imageUrl, this.status = FriendStatus.unknown, }) { assert(friendId.isNotEmpty, 'friendId ne doit pas être vide'); - _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $firstName $lastName'); + _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName'); } /// Méthode factory pour créer un objet [Friend] à partir d'un JSON. @@ -50,8 +50,8 @@ class Friend extends Equatable { return Friend( friendId: json['friendId'] as String, - firstName: json['friendFirstName'] as String? ?? 'Ami inconnu', - lastName: json['friendLastName'] as String? ?? '', + friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu', + friendLastName: json['friendLastName'] as String? ?? '', email: json['email'] as String?, imageUrl: json['imageUrl'] as String?, status: _parseStatus(json['status'] as String?), @@ -78,8 +78,8 @@ class Friend extends Equatable { Map toJson() { final json = { 'friendId': friendId, - 'firstName': firstName, - 'lastName': lastName, + 'friendFirstName': friendFirstName, + 'friendLastName': friendLastName, 'email': email, 'imageUrl': imageUrl, 'status': status.name, @@ -94,16 +94,16 @@ class Friend extends Equatable { /// Log chaque copie pour surveiller l'état des données. Friend copyWith({ String? friendId, - String? firstName, - String? lastName, + String? friendFirstName, + String? friendLastName, String? email, String? imageUrl, FriendStatus? status, }) { final newFriend = Friend( friendId: friendId ?? this.friendId, - firstName: firstName ?? this.firstName, - lastName: lastName ?? this.lastName, + friendFirstName: friendFirstName ?? this.friendFirstName, + friendLastName: friendLastName ?? this.friendLastName, email: email ?? this.email, imageUrl: imageUrl ?? this.imageUrl, status: status ?? this.status, @@ -115,5 +115,5 @@ class Friend extends Equatable { /// Propriétés utilisées pour comparer les objets [Friend], /// facilitant l'utilisation dans des listes et des ensembles. @override - List get props => [friendId, firstName, lastName, email, imageUrl, status]; + List get props => [friendId, friendFirstName, friendLastName, email, imageUrl, status]; } diff --git a/lib/presentation/screens/event/event_screen.dart b/lib/presentation/screens/event/event_screen.dart index 86c7a30..0f6c7e4 100644 --- a/lib/presentation/screens/event/event_screen.dart +++ b/lib/presentation/screens/event/event_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../state_management/event_bloc.dart'; import '../dialogs/add_event_dialog.dart'; +/// Écran principal des événements, affichant une liste d'événements. class EventScreen extends StatefulWidget { final String userId; final String userFirstName; @@ -63,9 +64,11 @@ class _EventScreenState extends State { body: BlocBuilder( builder: (context, state) { if (state is EventLoading) { + print('[LOG] Chargement en cours des événements...'); return const Center(child: CircularProgressIndicator()); } else if (state is EventLoaded) { final events = state.events; + print('[LOG] Nombre d\'événements à afficher: ${events.length}'); if (events.isEmpty) { return const Center(child: Text('Aucun événement disponible.')); } @@ -74,6 +77,7 @@ class _EventScreenState extends State { itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; + print('[LOG] Affichage de l\'événement $index : ${event.title}'); return EventCard( key: ValueKey(event.id), event: event, @@ -99,6 +103,7 @@ class _EventScreenState extends State { }, ); } else if (state is EventError) { + print('[ERROR] Message d\'erreur: ${state.message}'); return Center(child: Text('Erreur: ${state.message}')); } return const Center(child: Text('Aucun événement disponible.')); diff --git a/lib/presentation/screens/friends/friends_screen.dart b/lib/presentation/screens/friends/friends_screen.dart index 0c8a871..a380425 100644 --- a/lib/presentation/screens/friends/friends_screen.dart +++ b/lib/presentation/screens/friends/friends_screen.dart @@ -45,11 +45,13 @@ class _FriendsScreenState extends State { /// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis. void _onScroll() { final provider = Provider.of(context, listen: false); - if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && + + // 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"); - // Charger plus d'amis si on atteint la fin de la liste - provider.fetchFriends(widget.userId); + debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante."); + provider.fetchFriends(widget.userId, loadMore: true); } } @@ -65,10 +67,12 @@ class _FriendsScreenState extends State { IconButton( icon: const Icon(Icons.refresh), onPressed: () { - // Log de l'action de rafraîchissement - debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); - // Rafraîchir la liste des amis - friendsProvider.fetchFriends(widget.userId); + if (!friendsProvider.isLoading) { + debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); + friendsProvider.fetchFriends(widget.userId); + } else { + debugPrint("[LOG] Rafraîchissement en cours, action ignorée."); + } }, ), ], @@ -110,27 +114,29 @@ class _FriendsScreenState extends State { mainAxisSpacing: 10, crossAxisSpacing: 10, ), - itemCount: friendsProvider.friendsList.length, + itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0), 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: () { - // Log pour l'action de visualisation des détails d'un ami debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}"); - // Naviguer vers l'écran des détails de l'ami FriendDetailScreen.open( context, friend.friendId, - friend.firstName ?? 'Ami inconnu', + friend.friendFirstName, friend.imageUrl ?? '', ); }, ); }, ); + }, ), ), diff --git a/lib/presentation/screens/friends/friends_screen_with_provider.dart b/lib/presentation/screens/friends/friends_screen_with_provider.dart index e36f650..eccd2dc 100644 --- a/lib/presentation/screens/friends/friends_screen_with_provider.dart +++ b/lib/presentation/screens/friends/friends_screen_with_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; import '../../../assets/animations/friend_expanding_card.dart'; import '../../../data/providers/friends_provider.dart'; @@ -7,10 +8,9 @@ import '../../widgets/friend_detail_screen.dart'; import '../../widgets/friends_appbar.dart'; import '../../widgets/search_friends.dart'; -/// [FriendsScreenWithProvider] est un écran qui affiche la liste des amis. -/// Il utilise le provider [FriendsProvider] pour gérer les états et les données. -/// Chaque action est loguée pour permettre une traçabilité complète. class FriendsScreenWithProvider extends StatelessWidget { + final Logger _logger = Logger(); // Logger pour une meilleure traçabilité + @override Widget build(BuildContext context) { return Scaffold( @@ -29,6 +29,7 @@ class FriendsScreenWithProvider extends StatelessWidget { final friends = friendsProvider.friendsList; if (friends.isEmpty) { + _logger.i("[LOG] Aucun ami trouvé"); return const Center( child: Text( 'Aucun ami trouvé', @@ -51,19 +52,22 @@ class FriendsScreenWithProvider extends StatelessWidget { child: const Icon(Icons.delete, color: Colors.white), ), onDismissed: (direction) { - debugPrint("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}"); + _logger.i("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}"); friendsProvider.removeFriend(friend.friendId); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")), + ); }, child: FriendExpandingCard( - name: friend.firstName ?? 'Ami inconnu', + name: friend.friendFirstName ?? 'Ami inconnu', imageUrl: friend.imageUrl ?? '', description: "Amis depuis ${friend.friendId}", onTap: () => _navigateToFriendDetail(context, friend), onMessageTap: () { - debugPrint("[LOG] Envoi d'un message à l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + _logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); }, onRemoveTap: () { - debugPrint("[LOG] Tentative de suppression de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + _logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); friendsProvider.removeFriend(friend.friendId); }, ), @@ -79,14 +83,13 @@ class FriendsScreenWithProvider extends StatelessWidget { ); } - /// Navigue vers l'écran des détails de l'utilisateur (ami) récupéré via son `friendId`. void _navigateToFriendDetail(BuildContext context, Friend friend) { - debugPrint("[LOG] Navigation vers les détails de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + _logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}"); Navigator.of(context).push(MaterialPageRoute( builder: (context) => FriendDetailScreen( - name: friend.firstName ?? 'Ami inconnu', + name: friend.friendFirstName, imageUrl: friend.imageUrl ?? '', - friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets + friendId: friend.friendId, ), )); } diff --git a/lib/presentation/screens/profile/profile_screen.dart b/lib/presentation/screens/profile/profile_screen.dart index 436fcbe..b90bb5d 100644 --- a/lib/presentation/screens/profile/profile_screen.dart +++ b/lib/presentation/screens/profile/profile_screen.dart @@ -11,7 +11,6 @@ import '../../widgets/statistics_section_card.dart'; import '../../widgets/support_section_card.dart'; import '../../widgets/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 1c9fbc4..b2ba896 100644 --- a/lib/presentation/state_management/event_bloc.dart +++ b/lib/presentation/state_management/event_bloc.dart @@ -65,10 +65,14 @@ class EventBloc extends Bloc { // Gestion du chargement des événements Future _onLoadEvents(LoadEvents event, Emitter emit) async { emit(EventLoading()); + print('[LOG] Début du chargement des événements pour l\'utilisateur ${event.userId}'); + try { - final events = await remoteDataSource.getAllEvents(); + final events = await remoteDataSource.getEventsCreatedByUserAndFriends(event.userId); + print('[LOG] Événements chargés: ${events.length} éléments récupérés.'); emit(EventLoaded(events)); } catch (e) { + print('[ERROR] Erreur lors du chargement des événements: $e'); emit(EventError('Erreur lors du chargement des événements.')); } } diff --git a/lib/presentation/widgets/account_deletion_card.dart b/lib/presentation/widgets/account_deletion_card.dart index abde75b..7cfce5d 100644 --- a/lib/presentation/widgets/account_deletion_card.dart +++ b/lib/presentation/widgets/account_deletion_card.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.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. class AccountDeletionCard extends StatelessWidget { final BuildContext context; @@ -23,6 +25,7 @@ class AccountDeletionCard extends StatelessWidget { ); } + /// Affiche un dialogue de confirmation pour la suppression du compte. void _showDeleteConfirmationDialog() { showDialog( context: context, @@ -39,14 +42,17 @@ class AccountDeletionCard extends StatelessWidget { ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + debugPrint("[LOG] Suppression du compte annulée."); + Navigator.of(context).pop(); + }, child: Text('Annuler', style: TextStyle(color: AppColors.accentColor)), ), TextButton( onPressed: () { - print("[LOG] Suppression du compte confirmée."); + debugPrint("[LOG] Suppression du compte confirmée."); Navigator.of(context).pop(); - // Logique de suppression du compte + // Logique de suppression du compte ici. }, child: const Text( 'Supprimer', diff --git a/lib/presentation/widgets/edit_options_card.dart b/lib/presentation/widgets/edit_options_card.dart index 64796f1..11287a5 100644 --- a/lib/presentation/widgets/edit_options_card.dart +++ b/lib/presentation/widgets/edit_options_card.dart @@ -1,35 +1,98 @@ import 'package:flutter/material.dart'; -import 'custom_list_tile.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. +/// Les interactions sont entièrement loguées pour une traçabilité complète. class EditOptionsCard extends StatelessWidget { const EditOptionsCard({Key? key}) : super(key: key); @override Widget build(BuildContext context) { + debugPrint("[LOG] Initialisation de EditOptionsCard"); + return Card( - color: AppColors.cardColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 2, + color: AppColors.cardColor.withOpacity(0.95), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 4, + shadowColor: AppColors.darkPrimary.withOpacity(0.3), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - CustomListTile( + _buildOption( + context, icon: Icons.edit, label: 'Éditer le profil', - onTap: () => print("[LOG] Édition du profil."), + logMessage: "Édition du profil", + onTap: () => debugPrint("[LOG] Édition du profil activée."), ), - CustomListTile( + _buildDivider(), + _buildOption( + context, icon: Icons.camera_alt, label: 'Changer la photo de profil', - onTap: () => print("[LOG] Changement de la photo de profil."), + logMessage: "Changement de la photo de profil", + onTap: () => + debugPrint("[LOG] Changement de la photo de profil activé."), ), - CustomListTile( + _buildDivider(), + _buildOption( + context, icon: Icons.lock, label: 'Changer le mot de passe', - onTap: () => print("[LOG] Changement du mot de passe."), + logMessage: "Changement du mot de passe", + onTap: () => debugPrint("[LOG] Changement du mot de passe activé."), ), ], ), ); } + + /// Construit chaque option de la carte avec une animation de feedback visuel. + Widget _buildOption( + BuildContext context, { + required IconData icon, + required String label, + required String logMessage, + required VoidCallback onTap, + }) { + return InkWell( + onTap: () { + debugPrint("[LOG] $logMessage"); + onTap(); + }, + splashColor: AppColors.accentColor.withOpacity(0.3), + highlightColor: AppColors.accentColor.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Row( + children: [ + Icon(icon, color: AppColors.accentColor), + const SizedBox(width: 16), + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + const Spacer(), + const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16), + ], + ), + ), + ); + } + + /// Construit un séparateur entre les options pour une meilleure structure visuelle. + Widget _buildDivider() { + return Divider( + color: Colors.white.withOpacity(0.2), + height: 1, + indent: 16, + endIndent: 16, + ); + } } diff --git a/lib/presentation/widgets/expandable_section_card.dart b/lib/presentation/widgets/expandable_section_card.dart index 29fbe0a..f55b8e4 100644 --- a/lib/presentation/widgets/expandable_section_card.dart +++ b/lib/presentation/widgets/expandable_section_card.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import '../../../../core/constants/colors.dart'; -class ExpandableSectionCard extends StatelessWidget { +/// [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. +class ExpandableSectionCard extends StatefulWidget { final String title; final IconData icon; final List children; @@ -13,25 +15,73 @@ class ExpandableSectionCard extends StatelessWidget { required this.children, }) : super(key: key); + @override + _ExpandableSectionCardState createState() => _ExpandableSectionCardState(); +} + +class _ExpandableSectionCardState extends State with SingleTickerProviderStateMixin { + bool _isExpanded = false; + late AnimationController _controller; + late Animation _iconRotation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _iconRotation = Tween(begin: 0, end: 0.5).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _toggleExpansion() { + setState(() { + _isExpanded = !_isExpanded; + _isExpanded ? _controller.forward() : _controller.reverse(); + debugPrint("[LOG] ${_isExpanded ? 'Ouverture' : 'Fermeture'} de l'ExpandableSectionCard : ${widget.title}"); + }); + } + @override Widget build(BuildContext context) { return Card( color: AppColors.cardColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 2, - child: ExpansionTile( - title: Text( - title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, + elevation: 3, + child: Column( + children: [ + ListTile( + leading: Icon(widget.icon, color: AppColors.accentColor), + title: Text( + widget.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + trailing: RotationTransition( + turns: _iconRotation, + child: Icon(Icons.expand_more, color: AppColors.accentColor), + ), + onTap: _toggleExpansion, ), - ), - leading: Icon(icon, color: AppColors.accentColor), - iconColor: AppColors.accentColor, - collapsedIconColor: AppColors.accentColor, - children: children, + // Contenu de l'expansion + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + firstChild: Container(), + secondChild: Column(children: widget.children), + crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + ), + ], ), ); } diff --git a/lib/presentation/widgets/friends_circle.dart b/lib/presentation/widgets/friends_circle.dart index 99bb391..4faa0ab 100644 --- a/lib/presentation/widgets/friends_circle.dart +++ b/lib/presentation/widgets/friends_circle.dart @@ -22,8 +22,8 @@ class FriendsCircle extends StatelessWidget { @override Widget build(BuildContext context) { // Combine firstName et lastName ou utilise "Ami inconnu" par défaut. - String displayName = [friend.firstName, friend.lastName] - .where((namePart) => namePart.isNotEmpty) + String displayName = [friend.friendFirstName, friend.friendLastName] + .where((namePart) => namePart != null && namePart.isNotEmpty) .join(" ") .trim(); @@ -44,8 +44,10 @@ class FriendsCircle extends StatelessWidget { child: CircleAvatar( radius: 40, backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty - ? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide - : AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // 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!) + : 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 onBackgroundImageError: (error, stackTrace) { _logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error'); }, @@ -71,3 +73,4 @@ class FriendsCircle extends StatelessWidget { ); } } + diff --git a/lib/presentation/widgets/profile_header.dart b/lib/presentation/widgets/profile_header.dart index 06993b8..426f82d 100644 --- a/lib/presentation/widgets/profile_header.dart +++ b/lib/presentation/widgets/profile_header.dart @@ -4,8 +4,9 @@ import '../../../../core/constants/colors.dart'; import '../../../../data/providers/user_provider.dart'; import '../../../../domain/entities/user.dart'; -/// [ProfileHeader] est un widget qui affiche l'en-tête du profil utilisateur. -/// Comprend le nom de l'utilisateur, une image de fond, et un bouton de déconnexion avec confirmation. +/// [ProfileHeader] : Un widget d'en-tête de profil utilisateur visuellement amélioré +/// avec un gradient élégant, des animations, et un bouton de déconnexion stylisé. +/// Entièrement logué pour une traçabilité complète. class ProfileHeader extends StatelessWidget { final User user; @@ -13,97 +14,157 @@ class ProfileHeader extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint("[LOG] Initialisation de ProfileHeader pour l'utilisateur : ${user.userFirstName} ${user.userLastName}"); + return SliverAppBar( - expandedHeight: 200.0, + expandedHeight: 250.0, floating: false, pinned: true, + elevation: 0, backgroundColor: AppColors.darkPrimary, - flexibleSpace: FlexibleSpaceBar( - title: Text( + flexibleSpace: _buildFlexibleSpaceBar(user), + actions: [_buildLogoutButton(context)], + ); + } + + /// Construit un FlexibleSpaceBar avec un gradient et des animations. + /// Affiche le nom de l'utilisateur et l'image de profil avec un effet visuel enrichi. + Widget _buildFlexibleSpaceBar(User user) { + debugPrint("[LOG] Construction de FlexibleSpaceBar avec nom et image de profil."); + + return FlexibleSpaceBar( + centerTitle: true, + title: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: BorderRadius.circular(12.0), + ), + child: Text( 'Profil de ${user.userFirstName}', style: TextStyle( color: AppColors.accentColor, - fontSize: 20.0, + fontSize: 18.0, fontWeight: FontWeight.bold, ), ), - background: Image.network( - user.profileImageUrl, + ), + background: _buildProfileImageWithGradient(user.profileImageUrl), + ); + } + + /// Construit l'image de profil avec un overlay en gradient. + /// En cas d'erreur de chargement, affiche une image par défaut. + Widget _buildProfileImageWithGradient(String profileImageUrl) { + debugPrint("[LOG] Chargement de l'image de profil avec overlay de gradient."); + + return Stack( + fit: StackFit.expand, + children: [ + Image.network( + profileImageUrl, fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator(color: AppColors.accentColor)); + }, errorBuilder: (context, error, stackTrace) { - // Log en cas d'erreur de chargement de l'image - print("[ERROR] Erreur lors du chargement de l'image de profil : $error"); - return Image.asset('lib/assets/images/default_avatar.png', fit: BoxFit.cover); + debugPrint("[ERROR] Erreur lors du chargement de l'image de profil : $error"); + return Image.asset( + 'lib/assets/images/default_avatar.png', + fit: BoxFit.cover, + ); }, ), - ), - actions: [ - IconButton( - icon: const Icon(Icons.logout, color: Colors.white), - onPressed: () { - print("[LOG] Bouton de déconnexion cliqué."); // Log du clic du bouton de déconnexion - _showLogoutConfirmationDialog(context); // Affiche le dialogue de confirmation - }, + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.transparent, AppColors.darkPrimary.withOpacity(0.8)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), ), ], ); } + /// Construit un bouton de déconnexion stylisé avec animation. + /// Log chaque interaction pour assurer une traçabilité complète. + Widget _buildLogoutButton(BuildContext context) { + return IconButton( + icon: const Icon(Icons.logout, color: Colors.white), + splashRadius: 20, + onPressed: () { + debugPrint("[LOG] Clic sur le bouton de déconnexion."); + _showLogoutConfirmationDialog(context); + }, + tooltip: 'Déconnexion', + ); + } + /// Affiche une boîte de dialogue de confirmation pour la déconnexion. - /// Log chaque action et résultat dans le terminal. + /// Log chaque action et résultat pour une visibilité dans le terminal. void _showLogoutConfirmationDialog(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { - // Log affichage du dialogue - print("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion."); + debugPrint("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion."); return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), backgroundColor: AppColors.backgroundColor, title: const Text( 'Confirmer la déconnexion', - style: TextStyle(color: Colors.white), + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), content: const Text( 'Voulez-vous vraiment vous déconnecter ?', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: Colors.white70, fontSize: 16), ), actions: [ - // Bouton d'annulation de la déconnexion - TextButton( - onPressed: () { - print("[LOG] Déconnexion annulée par l'utilisateur."); - Navigator.of(context).pop(); // Ferme le dialogue sans déconnecter - }, - child: Text( - 'Annuler', - style: TextStyle(color: AppColors.accentColor), - ), - ), - // Bouton de confirmation de la déconnexion - TextButton( - onPressed: () { - print("[LOG] Déconnexion confirmée."); // Log de la confirmation - Provider.of(context, listen: false).resetUser(); // Réinitialise les infos utilisateur - print("[LOG] Informations utilisateur réinitialisées dans UserProvider."); - - Navigator.of(context).pop(); // Ferme la boîte de dialogue - print("[LOG] Boîte de dialogue de confirmation fermée."); - - Navigator.of(context).pushReplacementNamed('/'); // Redirige vers l'écran de connexion - print("[LOG] Redirection vers l'écran de connexion."); - }, - child: const Text( - 'Déconnexion', - style: TextStyle(color: Colors.redAccent), - ), - ), + _buildCancelButton(context), + _buildConfirmButton(context), ], ); }, ).then((_) { - // Log lorsque le dialogue est fermé pour toute raison (confirmation ou annulation) - print("[LOG] La boîte de dialogue de confirmation de déconnexion est fermée."); + debugPrint("[LOG] Fermeture de la boîte de dialogue de déconnexion."); }); } + + /// Construit le bouton pour annuler la déconnexion avec log. + Widget _buildCancelButton(BuildContext context) { + return TextButton( + onPressed: () { + debugPrint("[LOG] L'utilisateur a annulé la déconnexion."); + Navigator.of(context).pop(); + }, + child: Text( + 'Annuler', + style: TextStyle(color: AppColors.accentColor, fontWeight: FontWeight.bold), + ), + ); + } + + /// Construit le bouton pour confirmer la déconnexion, avec log et réinitialisation des données utilisateur. + Widget _buildConfirmButton(BuildContext context) { + return TextButton( + onPressed: () { + debugPrint("[LOG] L'utilisateur a confirmé la déconnexion."); + + // Réinitialisation des informations de l'utilisateur + Provider.of(context, listen: false).resetUser(); + debugPrint("[LOG] Informations utilisateur réinitialisées dans UserProvider."); + + Navigator.of(context).pop(); + Navigator.of(context).pushReplacementNamed('/'); // Redirection vers l'écran de connexion + debugPrint("[LOG] Redirection vers l'écran de connexion."); + }, + child: const Text( + 'Déconnexion', + style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold), + ), + ); + } } diff --git a/lib/presentation/widgets/stat_tile.dart b/lib/presentation/widgets/stat_tile.dart index 494aabb..e29cf35 100644 --- a/lib/presentation/widgets/stat_tile.dart +++ b/lib/presentation/widgets/stat_tile.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import '../../../../core/constants/colors.dart'; +/// [StatTile] affiche une statistique utilisateur avec une icône, un label et une valeur. +/// Ce composant inclut des animations et une traçabilité des interactions. class StatTile extends StatelessWidget { final IconData icon; final String label; @@ -15,17 +17,40 @@ class StatTile extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( - leading: Icon(icon, color: AppColors.accentColor), - title: Text(label, style: const TextStyle(color: Colors.white)), - trailing: Text( - value, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + debugPrint("[LOG] Initialisation de StatTile pour la statistique : $label"); + + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 500), + tween: Tween(begin: 0.9, end: 1.0), + curve: Curves.easeOutBack, + builder: (context, scale, child) { + return Transform.scale( + scale: scale, + child: ListTile( + leading: Icon( + icon, + color: AppColors.accentColor, + size: 28, + ), + title: Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + trailing: Text( + value, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + ); + }, ); } } diff --git a/lib/presentation/widgets/statistics_section_card.dart b/lib/presentation/widgets/statistics_section_card.dart index f81adca..bc04d76 100644 --- a/lib/presentation/widgets/statistics_section_card.dart +++ b/lib/presentation/widgets/statistics_section_card.dart @@ -3,6 +3,8 @@ 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. class StatisticsSectionCard extends StatelessWidget { final User user; @@ -10,10 +12,13 @@ class StatisticsSectionCard extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint("[LOG] Initialisation de StatisticsSectionCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}"); + return Card( - color: AppColors.cardColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 2, + color: AppColors.cardColor.withOpacity(0.95), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 5, + shadowColor: AppColors.darkPrimary.withOpacity(0.4), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -28,13 +33,73 @@ class StatisticsSectionCard extends StatelessWidget { ), ), const SizedBox(height: 10), - StatTile(icon: Icons.event, label: 'Événements Participés', value: '${user.eventsCount}'), - StatTile(icon: Icons.place, label: 'Établissements Visités', value: '${user.visitedPlacesCount}'), - StatTile(icon: Icons.post_add, label: 'Publications', value: '${user.postsCount}'), - StatTile(icon: Icons.group, label: 'Amis/Followers', value: '${user.friendsCount}'), + // Liste des statistiques avec animations + _buildAnimatedStatTile( + icon: Icons.event, + label: 'Événements Participés', + value: '${user.eventsCount}', + logMessage: "Affichage des événements participés : ${user.eventsCount}", + ), + _buildDivider(), + _buildAnimatedStatTile( + icon: Icons.place, + label: 'Établissements Visités', + value: '${user.visitedPlacesCount}', + logMessage: "Affichage des établissements visités : ${user.visitedPlacesCount}", + ), + _buildDivider(), + _buildAnimatedStatTile( + icon: Icons.post_add, + label: 'Publications', + value: '${user.postsCount}', + logMessage: "Affichage des publications : ${user.postsCount}", + ), + _buildDivider(), + _buildAnimatedStatTile( + icon: Icons.group, + label: 'Amis/Followers', + value: '${user.friendsCount}', + logMessage: "Affichage des amis/followers : ${user.friendsCount}", + ), ], ), ), ); } + + /// Construit chaque `StatTile` avec une animation de transition en fondu et logue chaque statistique. + Widget _buildAnimatedStatTile({ + required IconData icon, + required String label, + required String value, + required String logMessage, + }) { + debugPrint("[LOG] $logMessage"); + + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 500), + tween: Tween(begin: 0, end: 1), + curve: Curves.easeOut, + builder: (context, opacity, child) { + return Opacity( + opacity: opacity, + child: StatTile( + icon: icon, + label: label, + value: value, + ), + ); + }, + ); + } + + /// Construit un séparateur visuel entre chaque statistique. + Widget _buildDivider() { + return Divider( + color: Colors.white.withOpacity(0.2), + height: 1, + indent: 16, + endIndent: 16, + ); + } } diff --git a/lib/presentation/widgets/support_section_card.dart b/lib/presentation/widgets/support_section_card.dart index 064d605..1357ffd 100644 --- a/lib/presentation/widgets/support_section_card.dart +++ b/lib/presentation/widgets/support_section_card.dart @@ -1,46 +1,107 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../../../../core/constants/colors.dart'; -import 'custom_list_tile.dart'; +/// [SupportSectionCard] affiche les options de support et assistance. +/// Inclut des animations, du retour haptique, et des logs détaillés pour chaque action. class SupportSectionCard extends StatelessWidget { const SupportSectionCard({Key? key}) : super(key: key); @override Widget build(BuildContext context) { + debugPrint("[LOG] Initialisation de SupportSectionCard."); + return Card( - color: AppColors.cardColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 2, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: Text( + color: AppColors.cardColor.withOpacity(0.95), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 6, + shadowColor: AppColors.darkPrimary.withOpacity(0.4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( 'Support et Assistance', style: TextStyle( - fontSize: 20, + fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, + letterSpacing: 1.1, ), ), - ), - CustomListTile( - icon: Icons.help, - label: 'Support et Assistance', - onTap: () => print("[LOG] Accès au Support et Assistance."), - ), - CustomListTile( - icon: Icons.article, - label: 'Conditions d\'utilisation', - onTap: () => print("[LOG] Accès aux conditions d'utilisation."), - ), - CustomListTile( - icon: Icons.privacy_tip, - label: 'Politique de confidentialité', - onTap: () => print("[LOG] Accès à la politique de confidentialité."), - ), - ], + const SizedBox(height: 10), + _buildOption( + context, + icon: Icons.help_outline, + label: 'Support et Assistance', + logMessage: "Accès au Support et Assistance.", + ), + _buildDivider(), + _buildOption( + context, + icon: Icons.article_outlined, + label: 'Conditions d\'utilisation', + logMessage: "Accès aux conditions d'utilisation.", + ), + _buildDivider(), + _buildOption( + context, + icon: Icons.privacy_tip_outlined, + label: 'Politique de confidentialité', + logMessage: "Accès à la politique de confidentialité.", + ), + ], + ), ), ); } + + /// Construit chaque option de support avec une animation de feedback visuel. + Widget _buildOption( + BuildContext context, { + required IconData icon, + required String label, + required String logMessage, + }) { + return InkWell( + onTap: () { + HapticFeedback.lightImpact(); // Retour haptique léger + debugPrint("[LOG] $logMessage"); + // Ajout de la navigation ou de l'action ici. + }, + splashColor: AppColors.accentColor.withOpacity(0.3), + highlightColor: AppColors.cardColor.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Icon(icon, color: AppColors.accentColor, size: 28), + const SizedBox(width: 15), + Expanded( + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + const Icon(Icons.chevron_right, color: Colors.white70), + ], + ), + ), + ); + } + + /// Construit un séparateur entre les options pour une meilleure structure visuelle. + Widget _buildDivider() { + return Divider( + color: Colors.white.withOpacity(0.2), + height: 1, + indent: 16, + endIndent: 16, + ); + } } diff --git a/lib/presentation/widgets/user_info_card.dart b/lib/presentation/widgets/user_info_card.dart index 344b4a0..35f1218 100644 --- a/lib/presentation/widgets/user_info_card.dart +++ b/lib/presentation/widgets/user_info_card.dart @@ -1,7 +1,11 @@ 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'; +/// [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. class UserInfoCard extends StatelessWidget { final User user; @@ -9,38 +13,64 @@ class UserInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint("[LOG] Initialisation de UserInfoCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}"); + return Card( - color: AppColors.cardColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 2, + color: AppColors.cardColor.withOpacity(0.9), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 5, + shadowColor: AppColors.darkPrimary.withOpacity(0.4), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - CircleAvatar( - radius: 50, - backgroundImage: NetworkImage(user.profileImageUrl), - backgroundColor: Colors.transparent, + TweenAnimationBuilder( + duration: const Duration(milliseconds: 600), + tween: Tween(begin: 0, end: 1), + curve: Curves.elasticOut, + builder: (context, scale, child) { + return Transform.scale( + scale: scale, + child: CircleAvatar( + radius: 50, + backgroundImage: NetworkImage(user.profileImageUrl), + backgroundColor: Colors.transparent, + onBackgroundImageError: (error, stackTrace) { + debugPrint("[ERROR] Erreur de chargement de l'image de profil : $error"); + }, + child: child, + ), + ); + }, + child: Icon(Icons.person, size: 50, color: Colors.grey.shade300), ), const SizedBox(height: 10), Text( '${user.userFirstName} ${user.userLastName}', - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.accentColor, letterSpacing: 1.2, ), ), const SizedBox(height: 5), - Text( - user.email, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - decoration: TextDecoration.underline, + if (!context.select((UserProvider provider) => provider.isEmailDisplayedElsewhere)) // Afficher seulement si non affiché ailleurs + GestureDetector( + onTap: () { + debugPrint("[LOG] Clic sur l'email de l'utilisateur : ${user.email}"); + }, + child: Text( + user.email, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade300, + decoration: TextDecoration.underline, + decorationColor: AppColors.accentColor.withOpacity(0.5), + ), + ), ), - ), ], ), ),