Bon checkpoint + Refactoring

This commit is contained in:
DahoudG
2024-11-08 20:30:23 +00:00
parent 19f6efa995
commit 1e888f41e8
21 changed files with 721 additions and 223 deletions

View File

@@ -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';

View File

@@ -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<List<EventModel>> 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<dynamic> 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<EventModel> events = jsonResponse.map((event) => EventModel.fromJson(event)).toList();
print('[LOG] Conversion JSON -> List<EventModel> 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<EventModel> createEvent(EventModel event) async {
print('Création d\'un nouvel événement avec les données: ${event.toJson()}');

View File

@@ -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<dynamic> participants; // Si participants est une liste simple
final List<dynamic> participants;
final String status;
final int reactionsCount;
final int commentsCount;
@@ -32,25 +32,60 @@ class EventModel {
});
factory EventModel.fromJson(Map<String, dynamic> 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<dynamic> 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<String, dynamic> toJson() {
print('[LOG] Conversion de EventModel en JSON');
return {
'id': id,
'title': title,

View File

@@ -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<void> 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');

View File

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

View File

@@ -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<dynamic> 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<String, dynamic>)).toList();
return friendsJson.map((json) {
_logger.i("[LOG] Conversion JSON -> Friend : $json");
final friend = Friend.fromJson(json as Map<String, dynamic>);
_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<void> 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(

View File

@@ -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<String, dynamic> 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<Object?> get props => [friendId, firstName, lastName, email, imageUrl, status];
List<Object?> get props => [friendId, friendFirstName, friendLastName, email, imageUrl, status];
}

View File

@@ -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<EventScreen> {
body: BlocBuilder<EventBloc, EventState>(
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<EventScreen> {
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<EventScreen> {
},
);
} 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.'));

View File

@@ -45,11 +45,13 @@ class _FriendsScreenState extends State<FriendsScreen> {
/// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis.
void _onScroll() {
final provider = Provider.of<FriendsProvider>(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<FriendsScreen> {
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<FriendsScreen> {
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 ?? '',
);
},
);
},
);
},
),
),

View File

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

View File

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

View File

@@ -65,10 +65,14 @@ class EventBloc extends Bloc<EventEvent, EventState> {
// Gestion du chargement des événements
Future<void> _onLoadEvents(LoadEvents event, Emitter<EventState> 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.'));
}
}

View File

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

View File

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

View File

@@ -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<Widget> children;
@@ -13,25 +15,73 @@ class ExpandableSectionCard extends StatelessWidget {
required this.children,
}) : super(key: key);
@override
_ExpandableSectionCardState createState() => _ExpandableSectionCardState();
}
class _ExpandableSectionCardState extends State<ExpandableSectionCard> with SingleTickerProviderStateMixin {
bool _isExpanded = false;
late AnimationController _controller;
late Animation<double> _iconRotation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_iconRotation = Tween<double>(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,
),
],
),
);
}

View File

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

View File

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

View File

@@ -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<double>(
duration: const Duration(milliseconds: 500),
tween: Tween<double>(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,
),
),
),
);
},
);
}
}

View File

@@ -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<double>(
duration: const Duration(milliseconds: 500),
tween: Tween<double>(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,
);
}
}

View File

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

View File

@@ -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<double>(
duration: const Duration(milliseconds: 600),
tween: Tween<double>(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),
),
),
),
),
],
),
),