Refactoring + Checkpoint

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

View File

@@ -62,6 +62,7 @@ class AppRouter {
userId: userId, userId: userId,
userFirstName: userFirstName, userFirstName: userFirstName,
userLastName: userLastName, userLastName: userLastName,
profileImageUrl: '',
), ),
); );

View File

@@ -28,12 +28,13 @@ class AppColors {
static const Color darkTextSecondary = Color(0xFFBDBDBD); static const Color darkTextSecondary = Color(0xFFBDBDBD);
static const Color darkCardColor = Color(0xFF2C2C2C); static const Color darkCardColor = Color(0xFF2C2C2C);
static const Color darkAccentColor = Color(0xFF81C784); static const Color darkAccentColor = Color(0xFF81C784);
static const Color darkError = Color(0xFFCF6679); static const Color darkError = Color(0xFFF1012B);
static const Color darkIconPrimary = Colors.white; // Icône primaire blanche static const Color darkIconPrimary = Colors.white; // Icône primaire blanche
static const Color darkIconSecondary = Color(0xFFBDBDBD); // Icône secondaire gris clair static const Color darkIconSecondary = Color(0xFFBDBDBD); // Icône secondaire gris clair
// Ajout du background personnalisé // Ajout du background personnalisé
static const Color backgroundCustom = Color(0xFF2C2C3E); static const Color darkbackgroundCustom = Color(0xFF2C2C3E);
static const Color lightbackgroundCustom = Color(0xFFE0F7FA);
// Sélection automatique des couleurs en fonction du mode de thème // Sélection automatique des couleurs en fonction du mode de thème
static Color get primary => isDarkMode() ? darkPrimary : lightPrimary; static Color get primary => isDarkMode() ? darkPrimary : lightPrimary;
@@ -49,11 +50,11 @@ class AppColors {
static Color get errorColor => isDarkMode() ? darkError : lightError; static Color get errorColor => isDarkMode() ? darkError : lightError;
static Color get iconPrimary => isDarkMode() ? darkIconPrimary : lightIconPrimary; static Color get iconPrimary => isDarkMode() ? darkIconPrimary : lightIconPrimary;
static Color get iconSecondary => isDarkMode() ? darkIconSecondary : lightIconSecondary; static Color get iconSecondary => isDarkMode() ? darkIconSecondary : lightIconSecondary;
static Color get customBackgroundColor => backgroundCustom; static Color get customBackgroundColor => isDarkMode() ? darkbackgroundCustom : lightbackgroundCustom;
/// Méthode utilitaire pour vérifier si le mode sombre est activé. /// Méthode utilitaire pour vérifier si le mode sombre est activé.
static bool isDarkMode() { static bool isDarkMode() {
final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
return brightness == Brightness.dark; return brightness == Brightness.light;
} }
} }

View File

@@ -1,5 +1,5 @@
class Urls { class Urls {
static const String baseUrl = 'http://192.168.1.145:8085'; static const String baseUrl = 'http://192.168.1.16:8080';
// Authentication and Users Endpoints // Authentication and Users Endpoints
static const String authenticateUser = '$baseUrl/users/authenticate'; static const String authenticateUser = '$baseUrl/users/authenticate';

View File

@@ -68,7 +68,7 @@ class AppTheme {
color: AppColors.darkPrimary, color: AppColors.darkPrimary,
iconTheme: IconThemeData(color: AppColors.darkOnPrimary), iconTheme: IconThemeData(color: AppColors.darkOnPrimary),
), ),
iconTheme: const IconThemeData(color: AppColors.darkTextPrimary), iconTheme: const IconThemeData(color: AppColors.darkOnPrimary),
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: AppColors.darkPrimary, primary: AppColors.darkPrimary,
secondary: AppColors.darkSecondary, secondary: AppColors.darkSecondary,

View File

@@ -77,6 +77,8 @@ class EventRemoteDataSource {
print(' - Lien: ${event['link']}'); print(' - Lien: ${event['link']}');
print(' - URL de l\'image: ${event['imageUrl']}'); print(' - URL de l\'image: ${event['imageUrl']}');
print(' - Statut: ${event['status']}'); print(' - Statut: ${event['status']}');
print(' - prenom du créateur: ${event['creatorFirstName']}');
print(' - prenom du créateur: ${event['creatorLastName']}');
} }
// Transformation du JSON en une liste d'objets EventModel // Transformation du JSON en une liste d'objets EventModel
@@ -92,7 +94,6 @@ class EventRemoteDataSource {
} }
} }
/// Créer un nouvel événement via l'API. /// Créer un nouvel événement via l'API.
Future<EventModel> createEvent(EventModel event) async { Future<EventModel> createEvent(EventModel event) async {
print('Création d\'un nouvel événement avec les données: ${event.toJson()}'); print('Création d\'un nouvel événement avec les données: ${event.toJson()}');
@@ -213,7 +214,7 @@ class EventRemoteDataSource {
Future<void> closeEvent(String eventId) async { Future<void> closeEvent(String eventId) async {
print('Fermeture de l\'événement avec l\'ID: $eventId'); print('Fermeture de l\'événement avec l\'ID: $eventId');
final response = await client.post( final response = await client.patch(
Uri.parse('${Urls.closeEvent}/$eventId/close'), Uri.parse('${Urls.closeEvent}/$eventId/close'),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
@@ -223,11 +224,10 @@ class EventRemoteDataSource {
if (response.statusCode == 200) { if (response.statusCode == 200) {
print('Événement fermé avec succès'); print('Événement fermé avec succès');
} else if (response.statusCode == 400) { } else if (response.statusCode == 400) {
// Si le serveur retourne une erreur 400, vérifiez le corps du message
final responseBody = json.decode(response.body); final responseBody = json.decode(response.body);
final errorMessage = responseBody['message'] ?? 'Erreur inconnue'; final errorMessage = responseBody['message'] ?? 'Erreur inconnue';
print('Erreur lors de la fermeture de l\'événement: $errorMessage'); print('Erreur lors de la fermeture de l\'événement: $errorMessage');
throw ServerExceptionWithMessage(errorMessage); // Utiliser la nouvelle exception ici throw ServerExceptionWithMessage(errorMessage);
} else { } else {
print('Erreur lors de la fermeture de l\'événement: ${response.body}'); print('Erreur lors de la fermeture de l\'événement: ${response.body}');
throw ServerExceptionWithMessage('Une erreur est survenue lors de la fermeture de l\'événement.'); throw ServerExceptionWithMessage('Une erreur est survenue lors de la fermeture de l\'événement.');
@@ -238,7 +238,7 @@ class EventRemoteDataSource {
Future<void> reopenEvent(String eventId) async { Future<void> reopenEvent(String eventId) async {
print('Réouverture de l\'événement avec l\'ID: $eventId'); print('Réouverture de l\'événement avec l\'ID: $eventId');
final response = await client.post( final response = await client.patch(
Uri.parse('${Urls.reopenEvent}/$eventId/reopen'), Uri.parse('${Urls.reopenEvent}/$eventId/reopen'),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
@@ -248,11 +248,13 @@ class EventRemoteDataSource {
if (response.statusCode == 200) { if (response.statusCode == 200) {
print('Événement rouvert avec succès'); print('Événement rouvert avec succès');
} else if (response.statusCode == 400) { } else if (response.statusCode == 400) {
// Si le serveur retourne une erreur 400, vérifiez le corps du message
final responseBody = json.decode(response.body); final responseBody = json.decode(response.body);
final errorMessage = responseBody['message'] ?? 'Erreur inconnue'; final errorMessage = responseBody['message'] ?? 'Erreur inconnue';
print('Erreur lors de la réouverture de l\'événement: $errorMessage'); print('Erreur lors de la réouverture de l\'événement: $errorMessage');
throw ServerExceptionWithMessage(errorMessage); throw ServerExceptionWithMessage(errorMessage);
} else if (response.statusCode == 404) {
print('L\'événement n\'a pas été trouvé.');
throw ServerExceptionWithMessage('L\'événement n\'existe pas ou a déjà été supprimé.');
} else { } else {
print('Erreur lors de la réouverture de l\'événement: ${response.body}'); print('Erreur lors de la réouverture de l\'événement: ${response.body}');
throw ServerExceptionWithMessage('Une erreur est survenue lors de la réouverture de l\'événement.'); throw ServerExceptionWithMessage('Une erreur est survenue lors de la réouverture de l\'événement.');

View File

@@ -8,8 +8,11 @@ class EventModel {
final String link; final String link;
final String? imageUrl; final String? imageUrl;
final String creatorEmail; final String creatorEmail;
final String creatorFirstName; // Prénom du créateur
final String creatorLastName; // Nom du créateur
final String profileImageUrl;
final List<dynamic> participants; final List<dynamic> participants;
final String status; String status;
final int reactionsCount; final int reactionsCount;
final int commentsCount; final int commentsCount;
final int sharesCount; final int sharesCount;
@@ -24,6 +27,9 @@ class EventModel {
required this.link, required this.link,
this.imageUrl, this.imageUrl,
required this.creatorEmail, required this.creatorEmail,
required this.creatorFirstName,
required this.creatorLastName,
required this.profileImageUrl,
required this.participants, required this.participants,
required this.status, required this.status,
required this.reactionsCount, required this.reactionsCount,
@@ -44,8 +50,11 @@ class EventModel {
final String link = json['link'] ?? 'Lien Inconnu'; final String link = json['link'] ?? 'Lien Inconnu';
final String? imageUrl = json['imageUrl']; final String? imageUrl = json['imageUrl'];
final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu'; final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu';
final String creatorFirstName = json['creatorFirstName']; // Ajout du prénom
final String creatorLastName = json['creatorLastName']; // Ajout du nom
final String profileImageUrl = json['profileImageUrl']; // Ajout du nom
final List<dynamic> participants = json['participants'] ?? []; final List<dynamic> participants = json['participants'] ?? [];
final String status = json['status'] ?? 'ouvert'; String status = json['status'] ?? 'ouvert';
final int reactionsCount = json['reactionsCount'] ?? 0; final int reactionsCount = json['reactionsCount'] ?? 0;
final int commentsCount = json['commentsCount'] ?? 0; final int commentsCount = json['commentsCount'] ?? 0;
final int sharesCount = json['sharesCount'] ?? 0; final int sharesCount = json['sharesCount'] ?? 0;
@@ -60,6 +69,9 @@ class EventModel {
print(' - Lien: $link'); print(' - Lien: $link');
print(' - URL de l\'image: ${imageUrl ?? "Aucune"}'); print(' - URL de l\'image: ${imageUrl ?? "Aucune"}');
print(' - Email du créateur: $creatorEmail'); print(' - Email du créateur: $creatorEmail');
print(' - Prénom du créateur: $creatorFirstName');
print(' - Nom du créateur: $creatorLastName');
print(' - Image de profile du créateur: $profileImageUrl');
print(' - Participants: ${participants.length} participants'); print(' - Participants: ${participants.length} participants');
print(' - Statut: $status'); print(' - Statut: $status');
print(' - Nombre de réactions: $reactionsCount'); print(' - Nombre de réactions: $reactionsCount');
@@ -76,6 +88,9 @@ class EventModel {
link: link, link: link,
imageUrl: imageUrl, imageUrl: imageUrl,
creatorEmail: creatorEmail, creatorEmail: creatorEmail,
creatorFirstName: creatorFirstName, // Ajout du prénom
creatorLastName: creatorLastName, // Ajout du nom
profileImageUrl: profileImageUrl,
participants: participants, participants: participants,
status: status, status: status,
reactionsCount: reactionsCount, reactionsCount: reactionsCount,
@@ -96,6 +111,9 @@ class EventModel {
'link': link, 'link': link,
'imageUrl': imageUrl, 'imageUrl': imageUrl,
'creatorEmail': creatorEmail, 'creatorEmail': creatorEmail,
'creatorFirstName': creatorFirstName, // Ajout du prénom
'creatorLastName': creatorLastName, // Ajout du nom
'profileImageUrl': profileImageUrl,
'participants': participants, 'participants': participants,
'status': status, 'status': status,
'reactionsCount': reactionsCount, 'reactionsCount': reactionsCount,

View File

@@ -4,40 +4,40 @@ import '../../domain/entities/friend.dart';
import '../../data/repositories/friends_repository_impl.dart'; import '../../data/repositories/friends_repository_impl.dart';
/// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis. /// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis.
/// Il utilise [FriendsRepositoryImpl] pour interagir avec l'API et assure la gestion des états, /// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer
/// comme le chargement, la pagination et les erreurs éventuelles. /// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination,
/// du statut des amis et de la gestion des erreurs.
class FriendsProvider with ChangeNotifier { class FriendsProvider with ChangeNotifier {
final FriendsRepositoryImpl friendsRepository; final FriendsRepositoryImpl friendsRepository;
final Logger _logger = Logger(); // Logger pour suivre toutes les actions. final Logger _logger = Logger(); // Utilisation du logger pour une traçabilité complète des actions.
// Liste privée des amis récupérée depuis l'API // Liste des amis
List<Friend> _friendsList = []; List<Friend> _friendsList = [];
bool _isLoading = false; // Indique si une opération de chargement est en cours bool _isLoading = false; // Indicateur de chargement
bool _hasMore = true; // Indique s'il reste des amis à charger bool _hasMore = true; // Indicateur de pagination
int _currentPage = 0; int _currentPage = 0; // Numéro de la page actuelle pour la pagination
final int _friendsPerPage = 10; // Nombre d'amis par page pour la pagination final int _friendsPerPage = 10; // Nombre d'amis à récupérer par page
/// Constructeur de [FriendsProvider] qui requiert une instance de [FriendsRepositoryImpl]. /// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl].
FriendsProvider({required this.friendsRepository}); FriendsProvider({required this.friendsRepository});
// Getters pour accéder aux états depuis l'interface utilisateur // Getters pour accéder à l'état actuel des données
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
bool get hasMore => _hasMore; bool get hasMore => _hasMore;
List<Friend> get friendsList => _friendsList; List<Friend> get friendsList => _friendsList;
/// Récupère la liste paginée des amis pour un utilisateur donné. /// Récupère la liste des amis pour un utilisateur donné avec pagination.
/// ///
/// [userId] : L'identifiant unique de l'utilisateur. /// [userId] : L'identifiant unique de l'utilisateur connecté.
/// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination. /// [loadMore] : Si vrai, charge plus d'amis, sinon recharge la liste depuis le début.
/// ///
/// Cette méthode : /// Cette méthode gère :
/// - Vérifie si un chargement est déjà en cours. /// - La pagination de la liste d'amis.
/// - Initialise ou poursuit la pagination. /// - L'exclusion de l'utilisateur lui-même.
/// - Exclut l'utilisateur lui-même de la liste. /// - Les erreurs et les logs pour une traçabilité complète.
/// - Gère les erreurs et logue chaque étape pour une traçabilité complète.
Future<void> fetchFriends(String userId, {bool loadMore = false}) async { Future<void> fetchFriends(String userId, {bool loadMore = false}) async {
if (_isLoading) { if (_isLoading) {
_logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.'); _logger.w('[LOG] Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.');
return; return;
} }
@@ -45,7 +45,7 @@ class FriendsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
_logger.i('[LOG] Début du chargement des amis pour l\'utilisateur $userId.'); _logger.i('[LOG] Début du chargement des amis pour l\'utilisateur $userId.');
// Réinitialisation uniquement si ce n'est pas un chargement supplémentaire // Réinitialisation de la pagination si ce n'est pas un chargement supplémentaire
if (!loadMore) { if (!loadMore) {
_friendsList = []; _friendsList = [];
_currentPage = 0; _currentPage = 0;
@@ -57,23 +57,25 @@ class FriendsProvider with ChangeNotifier {
_logger.i('[LOG] Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.'); _logger.i('[LOG] Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.');
final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage); final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage);
// Gestion de l'absence de nouveaux amis
if (newFriends.isEmpty) { if (newFriends.isEmpty) {
_hasMore = false; _hasMore = false;
_logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.'); _logger.i('[LOG] Plus d\'amis à charger.');
} else { } else {
// Ajout des amis à la liste, en excluant l'utilisateur connecté
for (var friend in newFriends) { for (var friend in newFriends) {
if (friend.friendId != userId) { if (friend.friendId != userId) {
_friendsList.add(friend); _friendsList.add(friend);
_logger.i("[LOG] Ajout de l'ami : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}"); _logger.i("[LOG] Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}");
} else { } else {
_logger.w("[WARN] Exclusion de l'utilisateur lui-même de la liste d'amis : ${friend.friendId}"); _logger.w("[WARN] L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}");
} }
} }
_currentPage++; _currentPage++;
_logger.i('[LOG] Page suivante préparée pour le prochain chargement, page actuelle : $_currentPage'); _logger.i('[LOG] Préparation de la page suivante : $_currentPage');
} }
} catch (e) { } catch (e) {
_logger.e('[ERROR] Erreur lors de la récupération des amis : $e'); _logger.e('[ERROR] Erreur lors du chargement des amis : $e');
} finally { } finally {
_isLoading = false; _isLoading = false;
_logger.i('[LOG] Fin du chargement des amis.'); _logger.i('[LOG] Fin du chargement des amis.');
@@ -81,16 +83,18 @@ class FriendsProvider with ChangeNotifier {
} }
} }
/// Supprime un ami dans l'API et met à jour la liste localement. /// Supprime un ami de la liste locale et de l'API.
/// ///
/// [friendId] : Identifiant unique de l'ami à supprimer. /// [friendId] : Identifiant unique de l'ami à supprimer.
/// ///
/// Loggue chaque étape pour assurer un suivi précis de l'opération. /// Cette méthode :
/// - Loggue chaque étape.
/// - Enlève l'ami de la liste locale.
Future<void> removeFriend(String friendId) async { Future<void> removeFriend(String friendId) async {
try { try {
_logger.i('[LOG] Tentative de suppression de l\'ami avec l\'ID : $friendId'); _logger.i('[LOG] Suppression de l\'ami avec l\'ID : $friendId');
await friendsRepository.removeFriend(friendId); await friendsRepository.removeFriend(friendId); // Appel API pour supprimer l'ami
_friendsList.removeWhere((friend) => friend.friendId == friendId); _friendsList.removeWhere((friend) => friend.friendId == friendId); // Suppression locale
_logger.i('[LOG] Ami supprimé localement avec succès : $friendId'); _logger.i('[LOG] Ami supprimé localement avec succès : $friendId');
} catch (e) { } catch (e) {
_logger.e('[ERROR] Erreur lors de la suppression de l\'ami : $e'); _logger.e('[ERROR] Erreur lors de la suppression de l\'ami : $e');
@@ -101,31 +105,31 @@ class FriendsProvider with ChangeNotifier {
/// Récupère les détails d'un ami via l'API. /// Récupère les détails d'un ami via l'API.
/// ///
/// [userId] : L'identifiant de l'utilisateur connecté. /// [userId] : Identifiant de l'utilisateur connecté.
/// [friendId] : Identifiant unique de l'ami. /// [friendId] : Identifiant de l'ami dont on souhaite récupérer les détails.
/// ///
/// Retourne un `Future<Friend?>` contenant les détails ou `null` en cas d'erreur. /// Retourne un `Future<Friend?>` contenant les détails de l'ami ou `null` en cas d'erreur.
Future<Friend?> fetchFriendDetails(String userId, String friendId) async { Future<Friend?> fetchFriendDetails(String userId, String friendId) async {
try { try {
_logger.i('[LOG] Tentative de récupération des détails de l\'ami avec l\'ID : $friendId'); _logger.i('[LOG] Récupération des détails de l\'ami avec l\'ID : $friendId');
final friendDetails = await friendsRepository.getFriendDetails(friendId, userId); final friendDetails = await friendsRepository.getFriendDetails(friendId, userId);
if (friendDetails != null) { if (friendDetails != null) {
_logger.i('[LOG] Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}'); _logger.i('[LOG] Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}');
} else { } else {
_logger.w('[LOG] Détails de l\'ami introuvables pour l\'ID : $friendId'); _logger.w('[WARN] Détails de l\'ami introuvables pour l\'ID : $friendId');
} }
return friendDetails; return friendDetails;
} catch (e) { } catch (e) {
_logger.e('[ERROR] Exception lors de la récupération des détails de l\'ami : $e'); _logger.e('[ERROR] Erreur lors de la récupération des détails de l\'ami : $e');
return null; return null;
} }
} }
/// Convertit un statut sous forme de chaîne en [FriendStatus]. /// Convertit un statut sous forme de chaîne en [FriendStatus].
/// ///
/// [status] : Le statut sous forme de chaîne. /// [status] : Le statut sous forme de chaîne (par exemple, 'pending', 'accepted').
/// ///
/// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu. /// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu.
FriendStatus _convertToFriendStatus(String status) { FriendStatus _convertToFriendStatus(String status) {
@@ -141,21 +145,21 @@ class FriendsProvider with ChangeNotifier {
} }
} }
/// Met à jour le statut d'un ami (par exemple : accepter, bloquer). /// Met à jour le statut d'un ami (ex. accepter, bloquer).
/// ///
/// [friendId] : Identifiant unique de l'ami. /// [friendId] : Identifiant de l'ami dont on souhaite mettre à jour le statut.
/// [status] : Nouveau statut pour l'ami sous forme de chaîne de caractères. /// [status] : Nouveau statut sous forme de chaîne de caractères.
/// ///
/// Loggue l'action, convertit le statut en `FriendStatus`, et met à jour la liste localement. /// Loggue l'action, met à jour le statut en local et appelle l'API pour mettre à jour le statut.
Future<void> updateFriendStatus(String friendId, String status) async { Future<void> updateFriendStatus(String friendId, String status) async {
try { try {
_logger.i('[LOG] Tentative de mise à jour du statut de l\'ami avec l\'ID : $friendId'); _logger.i('[LOG] Mise à jour du statut de l\'ami avec l\'ID : $friendId');
// Conversion du `String` en `FriendStatus` pour l'update locale // Conversion du statut sous forme de chaîne en statut spécifique
final friendStatus = _convertToFriendStatus(status); final friendStatus = _convertToFriendStatus(status);
await friendsRepository.updateFriendStatus(friendId, status); await friendsRepository.updateFriendStatus(friendId, status); // Mise à jour dans l'API
// Mise à jour locale de la liste pour afficher le changement de statut // Mise à jour locale de la liste des amis avec le nouveau statut
final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId); final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId);
if (friendIndex != -1) { if (friendIndex != -1) {
_friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus); _friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus);

View File

@@ -17,6 +17,8 @@ class Friend extends Equatable {
final String? email; // Adresse e-mail, optionnelle mais typiquement présente final String? email; // Adresse e-mail, optionnelle mais typiquement présente
final String? imageUrl; // URL de l'image de profil, optionnelle final String? imageUrl; // URL de l'image de profil, optionnelle
final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown` final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown`
final String? dateAdded;
final String? lastInteraction;
/// Logger statique pour suivre toutes les actions et transformations liées à [Friend]. /// Logger statique pour suivre toutes les actions et transformations liées à [Friend].
static final Logger _logger = Logger(); static final Logger _logger = Logger();
@@ -31,6 +33,8 @@ class Friend extends Equatable {
this.email, this.email,
this.imageUrl, this.imageUrl,
this.status = FriendStatus.unknown, this.status = FriendStatus.unknown,
this.dateAdded,
this.lastInteraction,
}) { }) {
assert(friendId.isNotEmpty, 'friendId ne doit pas être vide'); assert(friendId.isNotEmpty, 'friendId ne doit pas être vide');
_logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName'); _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName');
@@ -53,7 +57,7 @@ class Friend extends Equatable {
friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu', friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu',
friendLastName: json['friendLastName'] as String? ?? '', friendLastName: json['friendLastName'] as String? ?? '',
email: json['email'] as String?, email: json['email'] as String?,
imageUrl: json['imageUrl'] as String?, imageUrl: json['friendProfileImageUrl'] as String?,
status: _parseStatus(json['status'] as String?), status: _parseStatus(json['status'] as String?),
); );
} }
@@ -81,7 +85,7 @@ class Friend extends Equatable {
'friendFirstName': friendFirstName, 'friendFirstName': friendFirstName,
'friendLastName': friendLastName, 'friendLastName': friendLastName,
'email': email, 'email': email,
'imageUrl': imageUrl, 'friendProfileImageUrl': imageUrl,
'status': status.name, 'status': status.name,
}; };
_logger.i('[LOG] Conversion Friend -> JSON : $json'); _logger.i('[LOG] Conversion Friend -> JSON : $json');
@@ -99,6 +103,8 @@ class Friend extends Equatable {
String? email, String? email,
String? imageUrl, String? imageUrl,
FriendStatus? status, FriendStatus? status,
String? lastInteraction,
String? dateAdded,
}) { }) {
final newFriend = Friend( final newFriend = Friend(
friendId: friendId ?? this.friendId, friendId: friendId ?? this.friendId,

View File

@@ -1,16 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:io'; import 'dart:io'; // Pour l'usage des fichiers (image)
import '../../widgets/fields/category_field.dart'; // Importation des widgets personnalisés
import '../../widgets/category_field.dart';
import '../../widgets/date_picker.dart'; import '../../widgets/date_picker.dart';
import '../../widgets/description_field.dart'; import '../../widgets/fields/description_field.dart';
import '../../widgets/link_field.dart'; import '../../widgets/fields/link_field.dart';
import '../../widgets/location_field.dart'; import '../../widgets/fields/location_field.dart';
import '../../widgets/submit_button.dart'; import '../../widgets/submit_button.dart';
import '../../widgets/title_field.dart'; import '../../widgets/fields/title_field.dart';
import '../../widgets/image_preview_picker.dart'; import '../../widgets/image_preview_picker.dart';
import '../../widgets/fields/tags_field.dart';
import '../../widgets/fields/attendees_field.dart';
import '../../widgets/fields/organizer_field.dart';
import '../../widgets/fields/transport_info_field.dart';
import '../../widgets/fields/accommodation_info_field.dart';
import '../../widgets/fields/privacy_rules_field.dart';
import '../../widgets/fields/security_protocol_field.dart';
import '../../widgets/fields/parking_field.dart';
import '../../widgets/fields/accessibility_field.dart';
import '../../widgets/fields/participation_fee_field.dart';
/// Page pour ajouter un événement
/// Permet à l'utilisateur de remplir un formulaire avec des détails sur l'événement
class AddEventPage extends StatefulWidget { class AddEventPage extends StatefulWidget {
final String userId; final String userId;
final String userFirstName; final String userFirstName;
@@ -28,22 +39,37 @@ class AddEventPage extends StatefulWidget {
} }
class _AddEventPageState extends State<AddEventPage> { class _AddEventPageState extends State<AddEventPage> {
final _formKey = GlobalKey<FormState>(); // Form key for validation final _formKey = GlobalKey<FormState>(); // Clé pour la validation du formulaire
// Variables pour stocker les données de l'événement
String _title = ''; String _title = '';
String _description = ''; String _description = '';
DateTime? _selectedDate; DateTime? _selectedDate;
DateTime? _endDate;
String _location = 'Abidjan'; String _location = 'Abidjan';
String _category = ''; String _category = '';
String _link = ''; String _link = '';
LatLng? _selectedLatLng = const LatLng(5.348722, -3.985038); // Default coordinates String _organizer = '';
File? _selectedImageFile; // Store the selected image List<String> _tags = [];
int _maxParticipants = 0;
LatLng? _selectedLatLng = const LatLng(5.348722, -3.985038); // Coordonnées par défaut
File? _selectedImageFile; // Image sélectionnée
String _status = 'Actif';
String _organizerEmail = '';
String _organizerPhone = '';
int _participationFee = 0;
String _privacyRules = '';
String _transportInfo = '';
String _accommodationInfo = '';
bool _isAccessible = false;
bool _hasParking = false;
String _securityProtocol = '';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Ajouter un événement'), title: const Text('Créer un événement'),
backgroundColor: const Color(0xFF1E1E2C), backgroundColor: const Color(0xFF1E1E2C),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
@@ -63,6 +89,8 @@ class _AddEventPageState extends State<AddEventPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Section d'informations de base
_buildSectionHeader('Informations de base'),
ImagePreviewPicker( ImagePreviewPicker(
onImagePicked: (File? imageFile) { onImagePicked: (File? imageFile) {
setState(() { setState(() {
@@ -80,39 +108,126 @@ class _AddEventPageState extends State<AddEventPage> {
onDatePicked: (picked) => setState(() { onDatePicked: (picked) => setState(() {
_selectedDate = picked; _selectedDate = picked;
}), }),
label: 'Date de début',
),
const SizedBox(height: 12),
DatePickerField(
selectedDate: _endDate,
onDatePicked: (picked) => setState(() {
_endDate = picked;
}),
label: 'Date de fin',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
LocationField( LocationField(
location: _location, location: _location,
selectedLatLng: _selectedLatLng, onLocationPicked: (value) => setState(() => _location = (value ?? 'Abidjan') as String),
onLocationPicked: (pickedLocation) => setState(() {
_selectedLatLng = pickedLocation;
_location = '${pickedLocation?.latitude}, ${pickedLocation?.longitude}';
}),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
CategoryField(onSaved: (value) => setState(() => _category = value ?? '')), CategoryField(
onSaved: (value) => setState(() => _category = value ?? ''),
),
const SizedBox(height: 12), const SizedBox(height: 12),
LinkField(onSaved: (value) => setState(() => _link = value ?? '')), LinkField(
onSaved: (value) => setState(() => _link = value ?? ''),
),
const SizedBox(height: 12),
AttendeesField(
onSaved: (value) => setState(() => _maxParticipants = value ?? 0),
),
const SizedBox(height: 12),
TagsField(
onSaved: (value) => setState(() => _tags = value ?? []),
),
const SizedBox(height: 12),
OrganizerField(
onSaved: (value) => setState(() => _organizer = value ?? ''),
),
const SizedBox(height: 12),
TransportInfoField(
onSaved: (value) => setState(() => _transportInfo = value ?? ''),
),
const SizedBox(height: 12),
AccommodationInfoField(
onSaved: (value) => setState(() => _accommodationInfo = value ?? ''),
),
const SizedBox(height: 12),
PrivacyRulesField(
onSaved: (value) => setState(() => _privacyRules = value ?? ''),
),
const SizedBox(height: 12),
SecurityProtocolField(
onSaved: (value) => setState(() => _securityProtocol = value ?? ''),
),
const SizedBox(height: 12),
ParkingField(
onSaved: (value) => setState(() => _hasParking = (value as bool?) ?? false),
),
const SizedBox(height: 12),
AccessibilityField(
onSaved: (value) => setState(() => _participationFee = (value as int?) ?? 0),
),
const SizedBox(height: 12),
ParticipationFeeField(
onSaved: (value) => setState(() => _participationFee = (value as int?) ?? 0),
),
const SizedBox(height: 12),
SubmitButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// Log des données de l'événement avant l'envoi
print('Titre de l\'événement : $_title');
print('Description de l\'événement : $_description');
print('Date de début : $_selectedDate');
print('Date de fin : $_endDate');
print('Lieu : $_location');
print('Catégorie : $_category');
print('Lien de l\'événement : $_link');
print('Organisateur : $_organizer');
print('Tags : $_tags');
print('Maximum de participants : $_maxParticipants');
print('Image sélectionnée : $_selectedImageFile');
print('Transport : $_transportInfo');
print('Hébergement : $_accommodationInfo');
print('Règles de confidentialité : $_privacyRules');
print('Protocole de sécurité : $_securityProtocol');
print('Parking disponible : $_hasParking');
print('Accessibilité : $_isAccessible');
print('Frais de participation : $_participationFee');
// Logique d'envoi des données vers le backend...
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Événement créé avec succès !')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez remplir tous les champs requis')),
);
}
},
),
], ],
), ),
), ),
), ),
), ),
// Bouton en bas de l'écran
Padding(
padding: const EdgeInsets.all(16.0),
child: SubmitButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// Logic to add the event goes here
}
},
),
),
], ],
), ),
); );
} }
// En-tête de section pour mieux organiser les champs
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/logger.dart'; // Pour la gestion des logs.
import '../../../data/models/event_model.dart'; import '../../../data/models/event_model.dart';
import '../../widgets/event_header.dart'; import '../../widgets/event_header.dart';
@@ -7,19 +8,22 @@ import '../../widgets/event_interaction_row.dart';
import '../../widgets/event_status_badge.dart'; import '../../widgets/event_status_badge.dart';
import '../../widgets/swipe_background.dart'; import '../../widgets/swipe_background.dart';
class EventCard extends StatelessWidget { /// Widget représentant une carte d'événement affichant les informations
final EventModel event; /// principales de l'événement avec diverses options d'interaction.
final String userId; class EventCard extends StatefulWidget {
final String userFirstName; final EventModel event; // Modèle de données pour l'événement.
final String userLastName; final String userId; // ID de l'utilisateur affichant l'événement.
final String status; final String userFirstName; // Prénom de l'utilisateur.
final VoidCallback onReact; final String userLastName; // Nom de l'utilisateur.
final VoidCallback onComment; final String profileImageUrl; // Image de profile
final VoidCallback onShare; final String status; // Statut de l'événement (ouvert ou fermé).
final VoidCallback onParticipate; final VoidCallback onReact; // Callback pour réagir à l'événement.
final VoidCallback onCloseEvent; final VoidCallback onComment; // Callback pour commenter l'événement.
final VoidCallback onReopenEvent; final VoidCallback onShare; // Callback pour partager l'événement.
final Function onRemoveEvent; final VoidCallback onParticipate; // Callback pour participer à l'événement.
final VoidCallback onCloseEvent; // Callback pour fermer l'événement.
final VoidCallback onReopenEvent; // Callback pour rouvrir l'événement.
final Function onRemoveEvent; // Fonction pour supprimer l'événement.
const EventCard({ const EventCard({
Key? key, Key? key,
@@ -27,6 +31,7 @@ class EventCard extends StatelessWidget {
required this.userId, required this.userId,
required this.userFirstName, required this.userFirstName,
required this.userLastName, required this.userLastName,
required this.profileImageUrl,
required this.status, required this.status,
required this.onReact, required this.onReact,
required this.onComment, required this.onComment,
@@ -37,79 +42,142 @@ class EventCard extends StatelessWidget {
required this.onRemoveEvent, required this.onRemoveEvent,
}) : super(key: key); }) : super(key: key);
@override
_EventCardState createState() => _EventCardState();
}
class _EventCardState extends State<EventCard> {
bool _isExpanded = false; // Contrôle si la description est développée.
static const int _descriptionThreshold = 100; // Limite de caractères.
bool _isClosed = false; // Ajout d'une variable pour suivre l'état de l'événement.
final Logger _logger = Logger();
@override
void initState() {
super.initState();
_isClosed = widget.event.status == 'fermé'; // Initialiser l'état selon le statut de l'événement.
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final GlobalKey menuKey = GlobalKey(); _logger.i("Construction de la carte d'événement"); // Log pour la construction du widget.
final GlobalKey menuKey = GlobalKey(); // Clé pour le menu contextuel.
final String descriptionText = widget.event.description; // Description de l'événement.
final bool shouldTruncate = descriptionText.length > _descriptionThreshold; // Détermine si le texte doit être tronqué.
return Dismissible( return Dismissible(
key: ValueKey(event.id), key: ValueKey(widget.event.id), // Clé unique pour chaque carte d'événement.
direction: event.status == 'fermé' direction: widget.event.status == 'fermé' // Direction du glissement basée sur le statut.
? DismissDirection.startToEnd ? DismissDirection.startToEnd
: DismissDirection.endToStart, : DismissDirection.endToStart,
onDismissed: (direction) { onDismissed: (direction) { // Action déclenchée lors d'un glissement.
if (event.status == 'fermé') { if (_isClosed) {
onReopenEvent(); _logger.i("Rouverte de l'événement ${widget.event.id}");
widget.onReopenEvent();
setState(() {
_isClosed = false; // Mise à jour de l'état local.
});
} else { } else {
onCloseEvent(); _logger.i("Fermeture de l'événement ${widget.event.id}");
onRemoveEvent(event.id); widget.onCloseEvent();
widget.onRemoveEvent(widget.event.id); // Suppression de l'événement.
setState(() {
_isClosed = true; // Mise à jour de l'état local.
});
} }
}, },
background: SwipeBackground( background: SwipeBackground( // Arrière-plan pour les actions de glissement.
color: event.status == 'fermé' ? Colors.green : Colors.red, color: _isClosed ? Colors.green : Colors.red,
icon: event.status == 'fermé' ? Icons.lock_open : Icons.lock, icon: _isClosed ? Icons.lock_open : Icons.lock,
label: event.status == 'fermé' ? 'Rouvrir' : 'Fermer', label: _isClosed ? 'Rouvrir' : 'Fermer',
), ),
child: Card( child: Card(
color: const Color(0xFF2C2C3E), color: const Color(0xFF2C2C3E), // Couleur de fond de la carte.
margin: const EdgeInsets.symmetric(vertical: 10.0), margin: const EdgeInsets.symmetric(vertical: 10.0),
shape: shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), // Bordure arrondie.
RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0), // Marge intérieure de la carte.
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Affichage de l'en-tête de l'événement.
EventHeader( EventHeader(
userFirstName: userFirstName, creatorFirstName: widget.event.creatorFirstName,
userLastName: userLastName, creatorLastName: widget.event.creatorLastName,
eventDate: event.startDate, profileImageUrl: widget.event.profileImageUrl,
imageUrl: event.imageUrl, eventDate: widget.event.startDate,
imageUrl: widget.event.imageUrl,
menuKey: menuKey, menuKey: menuKey,
menuContext: context, menuContext: context,
location: event.location, location: widget.event.location,
onClose: () { }, onClose: () {
_logger.i("Menu de fermeture actionné pour l'événement ${widget.event.id}");
},
), ),
const Divider(color: Colors.white24), const Divider(color: Colors.white24), // Ligne de séparation visuelle.
Row( Row(
children: [ children: [
const Spacer(), // Pusher le badge statut à la droite. const Spacer(), // Pousse le badge de statut à droite.
EventStatusBadge(status: status), EventStatusBadge(status: widget.status), // Badge de statut.
], ],
), ),
Text( Text(
event.title, widget.event.title, // Titre de l'événement.
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 5), const SizedBox(height: 5), // Espacement entre le titre et la description.
Text(
event.description, GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded; // Change l'état d'expansion.
});
_logger.i("Changement d'état d'expansion pour la description de l'événement ${widget.event.id}");
},
child: Text(
_isExpanded || !shouldTruncate
? descriptionText
: "${descriptionText.substring(0, _descriptionThreshold)}...",
style: const TextStyle(color: Colors.white70, fontSize: 14), style: const TextStyle(color: Colors.white70, fontSize: 14),
maxLines: 3, maxLines: _isExpanded ? null : 3,
overflow: TextOverflow.ellipsis, overflow: _isExpanded ? TextOverflow.visible : TextOverflow.ellipsis,
), ),
const SizedBox(height: 10), ),
EventImage(imageUrl: event.imageUrl), if (shouldTruncate) // Bouton "Afficher plus" si la description est longue.
const Divider(color: Colors.white24), GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
_logger.i("Affichage de la description complète de l'événement ${widget.event.id}");
},
child: Text(
_isExpanded ? "Afficher moins" : "Afficher plus",
style: const TextStyle(
color: Colors.blue,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10), // Espacement avant l'image.
EventImage(imageUrl: widget.event.imageUrl), // Affichage de l'image de l'événement.
const Divider(color: Colors.white24), // Nouvelle ligne de séparation.
// Rangée pour les interactions de l'événement (réagir, commenter, partager).
EventInteractionRow( EventInteractionRow(
onReact: onReact, onReact: widget.onReact,
onComment: onComment, onComment: widget.onComment,
onShare: onShare, onShare: widget.onShare,
reactionsCount: event.reactionsCount, reactionsCount: widget.event.reactionsCount,
commentsCount: event.commentsCount, commentsCount: widget.event.commentsCount,
sharesCount: event.sharesCount, sharesCount: widget.event.sharesCount,
), ),
], ],
), ),
@@ -118,3 +186,4 @@ class EventCard extends StatelessWidget {
); );
} }
} }

View File

@@ -10,12 +10,14 @@ class EventScreen extends StatefulWidget {
final String userId; final String userId;
final String userFirstName; final String userFirstName;
final String userLastName; final String userLastName;
final String profileImageUrl;
const EventScreen({ const EventScreen({
Key? key, Key? key,
required this.userId, required this.userId,
required this.userFirstName, required this.userFirstName,
required this.userLastName, required this.userLastName,
required this.profileImageUrl,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -84,6 +86,7 @@ class _EventScreenState extends State<EventScreen> {
userId: widget.userId, userId: widget.userId,
userFirstName: widget.userFirstName, userFirstName: widget.userFirstName,
userLastName: widget.userLastName, userLastName: widget.userLastName,
profileImageUrl: widget.profileImageUrl,
onReact: () => _onReact(event.id), onReact: () => _onReact(event.id),
onComment: () => _onComment(event.id), onComment: () => _onComment(event.id),
onShare: () => _onShare(event.id), onShare: () => _onShare(event.id),
@@ -143,7 +146,7 @@ class _EventScreenState extends State<EventScreen> {
void _onCloseEvent(String eventId) { void _onCloseEvent(String eventId) {
print('Fermeture de l\'événement $eventId'); print('Fermeture de l\'événement $eventId');
// Appeler le bloc pour fermer l'événement // Appeler le bloc pour fermer l'événement sans recharger la liste entière.
context.read<EventBloc>().add(CloseEvent(eventId)); context.read<EventBloc>().add(CloseEvent(eventId));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('L\'événement a été fermé avec succès.')), const SnackBar(content: Text('L\'événement a été fermé avec succès.')),
@@ -152,10 +155,12 @@ class _EventScreenState extends State<EventScreen> {
void _onReopenEvent(String eventId) { void _onReopenEvent(String eventId) {
print('Réouverture de l\'événement $eventId'); print('Réouverture de l\'événement $eventId');
// Appeler le bloc pour rouvrir l'événement // Appeler le bloc pour rouvrir l'événement sans recharger la liste entière.
context.read<EventBloc>().add(ReopenEvent(eventId)); context.read<EventBloc>().add(ReopenEvent(eventId));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('L\'événement a été rouvert avec succès.')), const SnackBar(content: Text('L\'événement a été rouvert avec succès.')),
); );
} }
} }

View File

@@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import '../../widgets/friend_card.dart';
import '../../widgets/friend_detail_screen.dart';
class FriendsContent extends StatelessWidget {
final List<Map<String, String>> friends = [
{'name': 'Alice', 'imageUrl': 'https://example.com/image1.jpg'},
{'name': 'Bob', 'imageUrl': 'https://example.com/image2.jpg'},
{'name': 'Charlie', 'imageUrl': 'https://example.com/image3.jpg'},
// Autres amis...
];
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: FriendCard(
name: friend['name']!,
imageUrl: friend['imageUrl']!,
onTap: () => _navigateToFriendDetail(context, friend),
),
);
},
);
}
void _navigateToFriendDetail(BuildContext context, Map<String, String> friend) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => FriendDetailScreen(
name: friend['name']!,
imageUrl: friend['imageUrl']!,
friendId: friend['friendId']!,
),
));
}
}

View File

@@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../data/providers/friends_provider.dart'; import '../../../data/providers/friends_provider.dart';
import '../../../domain/entities/friend.dart';
import '../../widgets/friend_detail_screen.dart'; import '../../widgets/friend_detail_screen.dart';
import '../../widgets/friends_circle.dart';
import '../../widgets/search_friends.dart'; import '../../widgets/search_friends.dart';
/// [FriendsScreen] est l'écran principal permettant d'afficher et de gérer la liste des amis. /// [FriendsScreen] est l'écran principal permettant d'afficher et de gérer la liste des amis.
/// Il inclut des fonctionnalités de pagination, de recherche, et de rafraîchissement manuel de la liste. /// Il inclut des fonctionnalités de pagination, de recherche, et de rafraîchissement manuel de la liste.
/// Ce widget est un [StatefulWidget] afin de pouvoir mettre à jour dynamiquement la liste des amis.
class FriendsScreen extends StatefulWidget { class FriendsScreen extends StatefulWidget {
final String userId; // Identifiant de l'utilisateur pour récupérer ses amis final String userId; // Identifiant de l'utilisateur pour récupérer ses amis
@@ -28,7 +29,7 @@ class _FriendsScreenState extends State<FriendsScreen> {
// Log pour indiquer le début du chargement des amis // Log pour indiquer le début du chargement des amis
debugPrint("[LOG] Initialisation de la page : chargement des amis pour l'utilisateur ${widget.userId}"); debugPrint("[LOG] Initialisation de la page : chargement des amis pour l'utilisateur ${widget.userId}");
// Chargement initial de la liste d'amis // Chargement initial de la liste d'amis via le fournisseur (Provider)
Provider.of<FriendsProvider>(context, listen: false).fetchFriends(widget.userId); Provider.of<FriendsProvider>(context, listen: false).fetchFriends(widget.userId);
} }
@@ -46,12 +47,12 @@ class _FriendsScreenState extends State<FriendsScreen> {
void _onScroll() { void _onScroll() {
final provider = Provider.of<FriendsProvider>(context, listen: false); final provider = Provider.of<FriendsProvider>(context, listen: false);
// Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt // Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt.
if (_scrollController.position.pixels >= if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 && _scrollController.position.maxScrollExtent - 200 &&
!provider.isLoading && provider.hasMore) { !provider.isLoading && provider.hasMore) {
debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante."); debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante.");
provider.fetchFriends(widget.userId, loadMore: true); provider.fetchFriends(widget.userId, loadMore: true); // Chargement de plus d'amis
} }
} }
@@ -64,9 +65,11 @@ class _FriendsScreenState extends State<FriendsScreen> {
appBar: AppBar( appBar: AppBar(
title: const Text('Mes Amis'), title: const Text('Mes Amis'),
actions: [ actions: [
// Bouton pour rafraîchir la liste des amis
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
onPressed: () { onPressed: () {
// Vérifie si la liste n'est pas en cours de chargement avant d'envoyer une nouvelle requête.
if (!friendsProvider.isLoading) { if (!friendsProvider.isLoading) {
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
friendsProvider.fetchFriends(widget.userId); friendsProvider.fetchFriends(widget.userId);
@@ -80,13 +83,13 @@ class _FriendsScreenState extends State<FriendsScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
// Widget de recherche d'amis en haut de l'écran
const Padding( const Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
// Widget pour la recherche d'amis
child: SearchFriends(), child: SearchFriends(),
), ),
Expanded( Expanded(
// Construction de la liste d'amis basée sur l'état du FriendsProvider // Construction de la liste d'amis avec un affichage en grille
child: Consumer<FriendsProvider>( child: Consumer<FriendsProvider>(
builder: (context, friendsProvider, child) { builder: (context, friendsProvider, child) {
// Si le chargement est en cours et qu'il n'y a aucun ami, afficher un indicateur de chargement. // Si le chargement est en cours et qu'il n'y a aucun ami, afficher un indicateur de chargement.
@@ -103,41 +106,70 @@ class _FriendsScreenState extends State<FriendsScreen> {
); );
} }
// Affichage de la grille des amis
debugPrint("[LOG] Affichage de la grille des amis (nombre d'amis : ${friendsProvider.friendsList.length})");
return GridView.builder( return GridView.builder(
controller: _scrollController, controller: _scrollController, // Utilisation du contrôleur pour la pagination
padding: const EdgeInsets.all(16), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // Deux amis par ligne
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10, crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.8, // Ajuste la taille des cartes
), ),
itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0), itemCount: friendsProvider.friendsList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= friendsProvider.friendsList.length) {
return const Center(child: CircularProgressIndicator());
}
final friend = friendsProvider.friendsList[index]; final friend = friendsProvider.friendsList[index];
debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}"); // Affichage de chaque ami dans une carte avec une animation
return GestureDetector(
return FriendsCircle( onTap: () => _navigateToFriendDetail(context, friend), // Action au clic sur l'avatar
friend: friend, child: AnimatedContainer(
onTap: () { duration: const Duration(milliseconds: 300),
debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}"); curve: Curves.easeInOut,
FriendDetailScreen.open( transform: Matrix4.identity()
context, ..scale(1.05), // Effet de zoom lors du survol
friend.friendId, child: Card(
friend.friendFirstName, elevation: 6,
friend.imageUrl ?? '', shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 50,
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
? (friend.imageUrl!.startsWith('https') // Vérifie si l'image est une URL réseau.
? NetworkImage(friend.imageUrl!) // Charge l'image depuis une URL réseau.
: AssetImage(friend.imageUrl!) as ImageProvider) // Sinon, charge depuis les ressources locales.
: const AssetImage('lib/assets/images/default_avatar.png'), // Si aucune image, utilise l'image par défaut.
),
const SizedBox(height: 10),
Text(
"${friend.friendFirstName} ${friend.friendLastName}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 5),
Text(
friend.status.name,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 5),
Text(
friend.lastInteraction ?? 'Aucune interaction récente',
style: const TextStyle(
fontStyle: FontStyle.italic,
fontSize: 12,
),
),
],
),
),
),
); );
}, },
); );
}, },
);
},
), ),
), ),
], ],
@@ -145,4 +177,24 @@ class _FriendsScreenState extends State<FriendsScreen> {
), ),
); );
} }
/// Navigation vers l'écran de détails de l'ami
/// Permet de voir les informations complètes d'un ami lorsque l'utilisateur clique sur son avatar.
void _navigateToFriendDetail(BuildContext context, Friend friend) {
debugPrint("[LOG] Navigation : Détails de l'ami ${friend.friendFirstName} ${friend.friendLastName}");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FriendDetailScreen(
friendFirstName: friend.friendFirstName, // Prénom de l'ami
friendLastName: friend.friendLastName, // Nom de l'ami
imageUrl: friend.imageUrl ?? '', // URL de l'image de l'ami (ou valeur par défaut)
friendId: friend.friendId, // Identifiant unique de l'ami
status: friend.status, // Statut de l'ami
lastInteraction: friend.lastInteraction ?? 'Aucune', // Dernière interaction (si disponible)
dateAdded: friend.dateAdded ?? 'Inconnu', // Date d'ajout de l'ami (si disponible)
),
),
);
}
} }

View File

@@ -8,20 +8,21 @@ import '../../widgets/friend_detail_screen.dart';
import '../../widgets/friends_appbar.dart'; import '../../widgets/friends_appbar.dart';
import '../../widgets/search_friends.dart'; import '../../widgets/search_friends.dart';
/// Écran d'affichage des amis avec gestion des amis via un provider.
class FriendsScreenWithProvider extends StatelessWidget { class FriendsScreenWithProvider extends StatelessWidget {
final Logger _logger = Logger(); // Logger pour une meilleure traçabilité final Logger _logger = Logger(); // Logger pour la traçabilité détaillée.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black, // Fond noir pour une ambiance immersive.
appBar: FriendsAppBar(), appBar: FriendsAppBar(), // AppBar personnalisé pour l'écran.
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
const Padding( const Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: SearchFriends(), child: SearchFriends(), // Barre de recherche pour trouver des amis.
), ),
Expanded( Expanded(
child: Consumer<FriendsProvider>( child: Consumer<FriendsProvider>(
@@ -29,10 +30,10 @@ class FriendsScreenWithProvider extends StatelessWidget {
final friends = friendsProvider.friendsList; final friends = friendsProvider.friendsList;
if (friends.isEmpty) { if (friends.isEmpty) {
_logger.i("[LOG] Aucun ami trouvé"); _logger.i("[LOG] Aucun ami trouvé."); // Log pour la recherche sans résultat.
return const Center( return const Center(
child: Text( child: Text(
'Aucun ami trouvé', 'Aucun ami trouvé', // Message affiché si aucun ami n'est trouvé.
style: TextStyle(color: Colors.white70), style: TextStyle(color: Colors.white70),
), ),
); );
@@ -43,6 +44,10 @@ class FriendsScreenWithProvider extends StatelessWidget {
itemCount: friends.length, itemCount: friends.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final friend = friends[index]; final friend = friends[index];
// Log lorsque chaque ami est affiché
_logger.i("[LOG] Affichage de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
return Dismissible( return Dismissible(
key: Key(friend.friendId), key: Key(friend.friendId),
background: Container( background: Container(
@@ -53,7 +58,7 @@ class FriendsScreenWithProvider extends StatelessWidget {
), ),
onDismissed: (direction) { onDismissed: (direction) {
_logger.i("[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); friendsProvider.removeFriend(friend.friendId); // Suppression de l'ami via le provider.
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")), SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")),
); );
@@ -61,14 +66,16 @@ class FriendsScreenWithProvider extends StatelessWidget {
child: FriendExpandingCard( child: FriendExpandingCard(
name: friend.friendFirstName ?? 'Ami inconnu', name: friend.friendFirstName ?? 'Ami inconnu',
imageUrl: friend.imageUrl ?? '', imageUrl: friend.imageUrl ?? '',
description: "Amis depuis ${friend.friendId}", description: "Amis depuis ${friend.dateAdded ?? 'Inconnu'}\nStatut : ${friend.status ?? 'Inconnu'}",
onTap: () => _navigateToFriendDetail(context, friend), onTap: () {
_navigateToFriendDetail(context, friend); // Navigation vers les détails de l'ami.
},
onMessageTap: () { onMessageTap: () {
_logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); _logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
}, },
onRemoveTap: () { onRemoveTap: () {
_logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}"); _logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
friendsProvider.removeFriend(friend.friendId); friendsProvider.removeFriend(friend.friendId); // Suppression via le provider.
}, },
), ),
); );
@@ -83,13 +90,19 @@ class FriendsScreenWithProvider extends StatelessWidget {
); );
} }
/// Navigue vers l'écran de détails de l'ami.
void _navigateToFriendDetail(BuildContext context, Friend friend) { void _navigateToFriendDetail(BuildContext context, Friend friend) {
_logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}"); _logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}");
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => FriendDetailScreen( builder: (context) => FriendDetailScreen(
name: friend.friendFirstName,
imageUrl: friend.imageUrl ?? '',
friendId: friend.friendId, friendId: friend.friendId,
friendFirstName: friend.friendFirstName,
friendLastName: friend.friendLastName,
imageUrl: friend.imageUrl ?? '',
status: friend.status,
lastInteraction: friend.lastInteraction ?? 'Aucune',
dateAdded: friend.dateAdded ?? 'Inconnu',
), ),
)); ));
} }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart'; // Importez les couleurs dynamiques import 'package:provider/provider.dart';
import '../../../core/constants/colors.dart';
import '../../../core/theme/theme_provider.dart';
import '../../widgets/friend_suggestions.dart'; import '../../widgets/friend_suggestions.dart';
import '../../widgets/group_list.dart'; import '../../widgets/group_list.dart';
import '../../widgets/popular_activity_list.dart'; import '../../widgets/popular_activity_list.dart';
@@ -7,45 +9,52 @@ import '../../widgets/recommended_event_list.dart';
import '../../widgets/section_header.dart'; import '../../widgets/section_header.dart';
import '../../widgets/story_section.dart'; import '../../widgets/story_section.dart';
/// Écran principal du contenu d'accueil, affichant diverses sections telles que
/// les suggestions d'amis, les activités populaires, les groupes, etc.
/// Les couleurs s'adaptent dynamiquement au thème sélectionné (clair ou sombre).
class HomeContentScreen extends StatelessWidget { class HomeContentScreen extends StatelessWidget {
const HomeContentScreen({super.key}); const HomeContentScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Récupération du fournisseur de thème pour appliquer le mode jour/nuit
final themeProvider = Provider.of<ThemeProvider>(context);
// Obtention des dimensions de l'écran pour adapter la mise en page
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
print("Chargement de HomeContentScreen avec le thème actuel");
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 15.0), // Marges réduites padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 15.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Section de bienvenue // Carte de bienvenue avec couleurs dynamiques
_buildWelcomeCard(), _buildWelcomeCard(themeProvider),
const SizedBox(height: 15), // Espacement entre les sections
const SizedBox(height: 15), // Espacement vertical réduit // Section des "Moments populaires"
// Section "Moments populaires"
_buildCard( _buildCard(
context: context, context: context,
themeProvider: themeProvider, // Fournit le thème
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SectionHeader( const SectionHeader(
title: 'Moments populaires', title: 'Moments populaires',
icon: Icons.camera_alt, icon: Icons.camera_alt,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), // Taille ajustée textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), // Espace vertical réduit const SizedBox(height: 10),
StorySection(size: size), StorySection(size: size),
], ],
), ),
), ),
const SizedBox(height: 15),
const SizedBox(height: 15), // Espacement réduit // Section des "Événements recommandés"
// Section des événements recommandés
_buildCard( _buildCard(
context: context, context: context,
themeProvider: themeProvider,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -54,17 +63,17 @@ class HomeContentScreen extends StatelessWidget {
icon: Icons.star, icon: Icons.star,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), // Espacement réduit const SizedBox(height: 10),
RecommendedEventList(size: size), RecommendedEventList(size: size),
], ],
), ),
), ),
const SizedBox(height: 15),
const SizedBox(height: 15), // Espacement réduit // Section des "Activités populaires"
// Section des activités populaires
_buildCard( _buildCard(
context: context, context: context,
themeProvider: themeProvider,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -73,17 +82,17 @@ class HomeContentScreen extends StatelessWidget {
icon: Icons.local_activity, icon: Icons.local_activity,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), // Espacement réduit const SizedBox(height: 10),
PopularActivityList(size: size), PopularActivityList(size: size),
], ],
), ),
), ),
const SizedBox(height: 15),
const SizedBox(height: 15), // Espacement réduit // Section "Groupes à rejoindre"
// Section des groupes sociaux
_buildCard( _buildCard(
context: context, context: context,
themeProvider: themeProvider,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -92,17 +101,17 @@ class HomeContentScreen extends StatelessWidget {
icon: Icons.group_add, icon: Icons.group_add,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), // Espacement réduit const SizedBox(height: 10),
GroupList(size: size), GroupList(size: size),
], ],
), ),
), ),
const SizedBox(height: 15),
const SizedBox(height: 15), // Espacement réduit // Section des "Suggestions d'amis"
// Section des suggestions d'amis
_buildCard( _buildCard(
context: context, context: context,
themeProvider: themeProvider,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -111,7 +120,7 @@ class HomeContentScreen extends StatelessWidget {
icon: Icons.person_add, icon: Icons.person_add,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 10), // Espacement réduit const SizedBox(height: 10),
FriendSuggestions(size: size), FriendSuggestions(size: size),
], ],
), ),
@@ -121,11 +130,13 @@ class HomeContentScreen extends StatelessWidget {
); );
} }
// Widget pour la carte de bienvenue /// Crée la carte de bienvenue, en utilisant les couleurs dynamiques en fonction du thème sélectionné.
Widget _buildWelcomeCard() { /// [themeProvider] fournit l'état actuel du thème pour adapter les couleurs.
Widget _buildWelcomeCard(ThemeProvider themeProvider) {
print("Création de la carte de bienvenue avec le thème actuel");
return Card( return Card(
elevation: 5, elevation: 5,
color: AppColors.surface, // Utilisation de la couleur dynamique pour la surface color: themeProvider.isDarkMode ? AppColors.darkSurface : AppColors.lightSurface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -135,26 +146,32 @@ class HomeContentScreen extends StatelessWidget {
Text( Text(
'Bienvenue, Dahoud!', 'Bienvenue, Dahoud!',
style: TextStyle( style: TextStyle(
color: AppColors.textPrimary, // Texte dynamique color: themeProvider.isDarkMode ? AppColors.darkOnPrimary : AppColors.lightPrimary,
fontSize: 22, // Taille de police réduite fontSize: 22,
fontWeight: FontWeight.w600, // Poids de police ajusté fontWeight: FontWeight.w600,
), ),
), ),
Icon(Icons.waving_hand, color: Colors.orange.shade300, size: 24), // Taille de l'icône ajustée Icon(Icons.waving_hand, color: Colors.orange.shade300, size: 24),
], ],
), ),
), ),
); );
} }
// Widget générique pour créer une carte design avec des espaces optimisés /// Crée une carte générique pour afficher des sections avec un style uniforme.
Widget _buildCard({required BuildContext context, required Widget child}) { /// [themeProvider] est utilisé pour ajuster les couleurs de la carte selon le mode jour/nuit.
Widget _buildCard({
required BuildContext context,
required ThemeProvider themeProvider,
required Widget child,
}) {
print("Création d'une carte de section avec le thème actuel");
return Card( return Card(
elevation: 3, // Réduction de l'élévation pour un look plus épuré elevation: 3,
color: AppColors.surface, // Utilisation de la couleur dynamique pour la surface color: themeProvider.isDarkMode ? AppColors.darkSurface : AppColors.lightSurface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), // Coins légèrement arrondis shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), // Padding interne réduit pour un contenu plus compact padding: const EdgeInsets.all(12.0),
child: child, child: child,
), ),
); );

View File

@@ -1,22 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // Pour ThemeProvider import 'package:provider/provider.dart';
import 'package:afterwork/presentation/screens/event/event_screen.dart'; import 'package:afterwork/presentation/screens/event/event_screen.dart';
import 'package:afterwork/presentation/screens/profile/profile_screen.dart'; import 'package:afterwork/presentation/screens/profile/profile_screen.dart';
import 'package:afterwork/presentation/screens/social/social_screen.dart'; import 'package:afterwork/presentation/screens/social/social_screen.dart';
import 'package:afterwork/presentation/screens/establishments/establishments_screen.dart'; import 'package:afterwork/presentation/screens/establishments/establishments_screen.dart';
import 'package:afterwork/presentation/screens/home/home_content.dart'; import 'package:afterwork/presentation/screens/home/home_content.dart';
import 'package:afterwork/data/datasources/event_remote_data_source.dart'; import 'package:afterwork/data/datasources/event_remote_data_source.dart';
import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; // Écran de notifications import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart';
import '../../../core/constants/colors.dart'; import '../../../core/constants/colors.dart';
import '../../../core/theme/theme_provider.dart'; import '../../../core/theme/theme_provider.dart';
import '../friends/friends_screen.dart'; // Écran des amis import '../friends/friends_screen.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
final EventRemoteDataSource eventRemoteDataSource; final EventRemoteDataSource eventRemoteDataSource;
final String userId; final String userId;
final String userFirstName; final String userFirstName;
final String userLastName; final String userLastName;
final String userProfileImage; // Image de profil de l'utilisateur final String userProfileImage;
const HomeScreen({ const HomeScreen({
Key? key, Key? key,
@@ -24,7 +24,7 @@ class HomeScreen extends StatefulWidget {
required this.userId, required this.userId,
required this.userFirstName, required this.userFirstName,
required this.userLastName, required this.userLastName,
required this.userProfileImage, // Passer l'image de profil ici required this.userProfileImage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -37,7 +37,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 6, vsync: this); // Ajouter un onglet pour les notifications _tabController = TabController(length: 6, vsync: this);
} }
@override @override
@@ -47,51 +47,40 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
} }
void _onMenuSelected(BuildContext context, String option) { void _onMenuSelected(BuildContext context, String option) {
switch (option) { print('$option sélectionné'); // Log pour chaque option
case 'Publier':
print('Publier sélectionné');
break;
case 'Story':
print('Story sélectionné');
break;
default:
break;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Accès au ThemeProvider
final themeProvider = Provider.of<ThemeProvider>(context); final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
SliverAppBar( SliverAppBar(
backgroundColor: AppColors.backgroundColor,
floating: true, floating: true,
pinned: true, pinned: true,
snap: true, snap: true,
elevation: 2, elevation: 2,
backgroundColor: themeProvider.currentTheme.primaryColor,
leading: Padding( leading: Padding(
padding: const EdgeInsets.all(4.0), // Ajustement du padding padding: const EdgeInsets.all(4.0),
child: Image.asset( child: Image.asset(
'lib/assets/images/logo.png', 'lib/assets/images/logo.png',
height: 40, // Taille ajustée du logo height: 40,
), ),
), ),
actions: [ actions: [
_buildActionIcon(Icons.add, 'Publier', context), _buildActionIcon(Icons.add, 'Publier', context),
_buildActionIcon(Icons.search, 'Rechercher', context), _buildActionIcon(Icons.search, 'Rechercher', context),
_buildActionIcon(Icons.message, 'Message', context), _buildActionIcon(Icons.message, 'Message', context),
_buildNotificationsIcon(context, 5), // Gérer la logique des notifications ici _buildNotificationsIcon(context, 105),
// Bouton pour basculer entre les thèmes
Switch( Switch(
value: themeProvider.isDarkMode, value: themeProvider.isDarkMode,
onChanged: (value) { onChanged: (value) {
themeProvider.toggleTheme(); // Changer le thème themeProvider.toggleTheme();
}, },
activeColor: AppColors.accentColor, activeColor: AppColors.accentColor,
), ),
@@ -99,23 +88,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
indicatorColor: AppColors.lightPrimary, indicatorColor: AppColors.lightPrimary,
labelStyle: const TextStyle( labelStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
fontSize: 12, // Taille réduite du texte unselectedLabelStyle: const TextStyle(fontSize: 11),
fontWeight: FontWeight.w500, labelColor: themeProvider.isDarkMode ? AppColors.darkOnPrimary : AppColors.lightOnPrimary,
), unselectedLabelColor: themeProvider.isDarkMode ? AppColors.darkIconSecondary : AppColors.lightIconSecondary,
unselectedLabelStyle: const TextStyle(
fontSize: 11, // Taille ajustée pour les onglets non sélectionnés
),
labelColor: AppColors.lightPrimary,
unselectedLabelColor: AppColors.iconSecondary,
tabs: [ tabs: [
const Tab(icon: Icon(Icons.home, size: 24), text: 'Accueil'), const Tab(icon: Icon(Icons.home, size: 24), text: 'Accueil'),
const Tab(icon: Icon(Icons.event, size: 24), text: 'Événements'), const Tab(icon: Icon(Icons.event, size: 24), text: 'Événements'),
const Tab(icon: Icon(Icons.location_city, size: 24), text: 'Établissements'), const Tab(icon: Icon(Icons.location_city, size: 24), text: 'Établissements'),
const Tab(icon: Icon(Icons.people, size: 24), text: 'Social'), const Tab(icon: Icon(Icons.people, size: 24), text: 'Social'),
const Tab(icon: Icon(Icons.people_alt_outlined, size: 24), text: 'Ami(e)s'), const Tab(icon: Icon(Icons.people_alt_outlined, size: 24), text: 'Ami(e)s'),
_buildProfileTab(), // Onglet profil _buildProfileTab(),
], ],
), ),
), ),
@@ -129,10 +112,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
userId: widget.userId, userId: widget.userId,
userFirstName: widget.userFirstName, userFirstName: widget.userFirstName,
userLastName: widget.userLastName, userLastName: widget.userLastName,
profileImageUrl: widget.userProfileImage,
), ),
const EstablishmentsScreen(), const EstablishmentsScreen(),
const SocialScreen(), const SocialScreen(),
FriendsScreen(userId: widget.userId), // Correction ici : passer l'userId FriendsScreen(userId: widget.userId),
const ProfileScreen(), const ProfileScreen(),
], ],
), ),
@@ -140,20 +124,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
); );
} }
// Widget pour l'affichage de la photo de profil dans l'onglet
Tab _buildProfileTab() { Tab _buildProfileTab() {
return Tab( return Tab(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: Colors.blue, color: AppColors.secondary,
width: 2.0, width: 2.0,
), ),
), ),
child: CircleAvatar( child: CircleAvatar(
radius: 16, radius: 16,
backgroundColor: Colors.grey[200], // Couleur de fond par défaut backgroundColor: AppColors.surface,
child: ClipOval( child: ClipOval(
child: FadeInImage.assetNetwork( child: FadeInImage.assetNetwork(
placeholder: 'lib/assets/images/user_placeholder.png', placeholder: 'lib/assets/images/user_placeholder.png',
@@ -169,18 +152,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
); );
} }
// Icône pour les notifications avec un badge
Widget _buildNotificationsIcon(BuildContext context, int notificationCount) { Widget _buildNotificationsIcon(BuildContext context, int notificationCount) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0), padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Stack( child: Stack(
clipBehavior: Clip.none, // Permet d'afficher le badge en dehors des limites clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: AppColors.surface, backgroundColor: AppColors.surface,
radius: 18, radius: 18,
child: IconButton( child: IconButton(
icon: const Icon(Icons.notifications, color: AppColors.darkOnPrimary, size: 20), icon: Icon(Icons.notifications, color: AppColors.iconPrimary, size: 20),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@@ -197,21 +179,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
top: -6, top: -6,
child: Container( child: Container(
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.red, color: Colors.red,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
constraints: const BoxConstraints( constraints: BoxConstraints(
minWidth: 18, minWidth: 18,
minHeight: 18, minHeight: 18,
), ),
child: Text( child: Text(
notificationCount > 99 ? '99+' : '$notificationCount', notificationCount > 99 ? '99+' : '$notificationCount',
style: const TextStyle( style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@@ -221,7 +199,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
); );
} }
// Icône d'action générique
Widget _buildActionIcon(IconData iconData, String label, BuildContext context) { Widget _buildActionIcon(IconData iconData, String label, BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0), padding: const EdgeInsets.symmetric(horizontal: 6.0),
@@ -229,7 +206,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
backgroundColor: AppColors.surface, backgroundColor: AppColors.surface,
radius: 18, radius: 18,
child: IconButton( child: IconButton(
icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), icon: Icon(iconData, color: AppColors.iconPrimary, size: 20),
onPressed: () { onPressed: () {
_onMenuSelected(context, label); _onMenuSelected(context, label);
}, },

View File

@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../core/constants/colors.dart'; import '../../../core/constants/colors.dart';
import '../../../data/providers/user_provider.dart'; import '../../../data/providers/user_provider.dart';
import '../../widgets/account_deletion_card.dart'; import '../../widgets/cards/account_deletion_card.dart';
import '../../widgets/cards/statistics_section_card.dart';
import '../../widgets/cards/support_section_card.dart';
import '../../widgets/custom_list_tile.dart'; import '../../widgets/custom_list_tile.dart';
import '../../widgets/edit_options_card.dart'; import '../../widgets/cards/edit_options_card.dart';
import '../../widgets/expandable_section_card.dart'; import '../../widgets/cards/expandable_section_card.dart';
import '../../widgets/profile_header.dart'; import '../../widgets/profile_header.dart';
import '../../widgets/statistics_section_card.dart'; import '../../widgets/cards/user_info_card.dart';
import '../../widgets/support_section_card.dart';
import '../../widgets/user_info_card.dart';
class ProfileScreen extends StatelessWidget { class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key}); const ProfileScreen({super.key});

View File

@@ -91,25 +91,48 @@ class EventBloc extends Bloc<EventEvent, EventState> {
// Gestion de la fermeture d'un événement // Gestion de la fermeture d'un événement
Future<void> _onCloseEvent(CloseEvent event, Emitter<EventState> emit) async { Future<void> _onCloseEvent(CloseEvent event, Emitter<EventState> emit) async {
emit(EventLoading()); emit(EventLoading()); // Affiche le chargement
try { try {
await remoteDataSource.closeEvent(event.eventId); await remoteDataSource.closeEvent(event.eventId);
final events = await remoteDataSource.getAllEvents();
emit(EventLoaded(events)); // Mise à jour de l'événement spécifique dans l'état
if (state is EventLoaded) {
final updatedEvents = List<EventModel>.from((state as EventLoaded).events);
final updatedEvent = updatedEvents.firstWhere((e) => e.id == event.eventId);
updatedEvent.status = 'fermé'; // Modifier l'état de l'événement localement
// Émettre un nouvel état avec l'événement mis à jour
emit(EventLoaded(updatedEvents));
print('Événement fermé et mis à jour localement.');
}
} catch (e) { } catch (e) {
emit(EventError('Erreur lors de la fermeture de l\'événement.')); emit(EventError('Erreur lors de la fermeture de l\'événement.'));
print('Erreur lors de la fermeture de l\'événement : $e');
} }
} }
// Gestion de la réouverture d'un événement
Future<void> _onReopenEvent(ReopenEvent event, Emitter<EventState> emit) async { Future<void> _onReopenEvent(ReopenEvent event, Emitter<EventState> emit) async {
emit(EventLoading()); emit(EventLoading()); // Affiche le chargement
try { try {
// Appel au service backend pour réouvrir l'événement
await remoteDataSource.reopenEvent(event.eventId); await remoteDataSource.reopenEvent(event.eventId);
final events = await remoteDataSource.getAllEvents();
emit(EventLoaded(events)); // Mise à jour de l'événement spécifique dans l'état
if (state is EventLoaded) {
final updatedEvents = List<EventModel>.from((state as EventLoaded).events);
final updatedEvent = updatedEvents.firstWhere((e) => e.id == event.eventId);
// Mise à jour du statut local de l'événement
updatedEvent.status = 'ouvert';
// Émettre un nouvel état avec l'événement mis à jour
emit(EventLoaded(updatedEvents));
print('Événement réouvert et mis à jour localement.');
}
} catch (e) { } catch (e) {
// En cas d'erreur, émettre un état d'erreur
emit(EventError('Erreur lors de la réouverture de l\'événement.')); emit(EventError('Erreur lors de la réouverture de l\'événement.'));
print('Erreur lors de la réouverture de l\'événement : $e');
} }
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../../core/constants/colors.dart';
/// [AccountDeletionCard] est un widget permettant à l'utilisateur de supprimer son compte. /// [AccountDeletionCard] est un widget permettant à l'utilisateur de supprimer son compte.
/// Il affiche une confirmation avant d'effectuer l'action de suppression. /// Il affiche une confirmation avant d'effectuer l'action de suppression.

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../../core/constants/colors.dart';
/// [EditOptionsCard] permet à l'utilisateur d'accéder aux options d'édition du profil, /// [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. /// incluant la modification du profil, la photo et le mot de passe.

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../../core/constants/colors.dart';
/// [ExpandableSectionCard] est une carte qui peut s'étendre pour révéler des éléments enfants. /// [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. /// Ce composant inclut des animations d'extension, des logs pour chaque action et une expérience utilisateur optimisée.

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../../core/constants/colors.dart';
import '../../../../domain/entities/user.dart'; import '../../../../../domain/entities/user.dart';
import 'stat_tile.dart'; import '../stat_tile.dart';
/// [StatisticsSectionCard] affiche les statistiques principales de l'utilisateur avec des animations. /// [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. /// Ce composant est optimisé pour une expérience interactive et une traçabilité complète des actions via les logs.

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/constants/colors.dart'; import '../../../../../core/constants/colors.dart';
import '../../../../domain/entities/user.dart'; import '../../../../../domain/entities/user.dart';
import '../../data/providers/user_provider.dart'; import '../../../data/providers/user_provider.dart';
/// [UserInfoCard] affiche les informations essentielles de l'utilisateur de manière concise. /// [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. /// Conçu pour minimiser les répétitions tout en garantissant une expérience utilisateur fluide.

View File

@@ -1,119 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' as rootBundle;
class CategoryField extends StatefulWidget {
final FormFieldSetter<String> onSaved;
const CategoryField({Key? key, required this.onSaved}) : super(key: key);
@override
_CategoryFieldState createState() => _CategoryFieldState();
}
class _CategoryFieldState extends State<CategoryField> {
String? _selectedCategory;
Map<String, List<String>> _categoryMap = {}; // Map pour stocker les catégories et sous-catégories
List<DropdownMenuItem<String>> _dropdownItems = []; // Liste pour stocker les éléments de menu déroulant
@override
void initState() {
super.initState();
_loadCategories(); // Charger les catégories à partir du JSON
}
// Charger les catégories depuis le fichier JSON
Future<void> _loadCategories() async {
try {
final String jsonString = await rootBundle.rootBundle.loadString('lib/assets/json/event_categories.json');
final Map<String, dynamic> jsonMap = json.decode(jsonString);
final Map<String, List<String>> categoryMap = {};
jsonMap['categories'].forEach((key, value) {
categoryMap[key] = List<String>.from(value);
});
setState(() {
_categoryMap = categoryMap;
_dropdownItems = _buildDropdownItems();
});
// Ajouter un log pour vérifier si les catégories sont bien chargées
print("Catégories chargées: $_categoryMap");
} catch (e) {
print("Erreur lors du chargement des catégories : $e");
}
}
// Construire les éléments du menu déroulant avec catégorisation
List<DropdownMenuItem<String>> _buildDropdownItems() {
List<DropdownMenuItem<String>> items = [];
_categoryMap.forEach((category, subcategories) {
items.add(
DropdownMenuItem<String>(
enabled: false,
child: Text(
category,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white70,
),
),
),
);
for (String subcategory in subcategories) {
items.add(
DropdownMenuItem<String>(
value: subcategory,
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
subcategory,
style: const TextStyle(color: Colors.white),
),
),
),
);
}
});
// Ajouter un log pour vérifier si les éléments sont bien créés
print("Éléments créés pour le menu déroulant: ${items.length}");
return items;
}
@override
Widget build(BuildContext context) {
return _dropdownItems.isEmpty
? CircularProgressIndicator() // Affiche un indicateur de chargement si les éléments ne sont pas encore prêts
: DropdownButtonFormField<String>(
value: _selectedCategory,
decoration: InputDecoration(
labelText: 'Catégorie',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.category, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
dropdownColor: const Color(0xFF2C2C3E),
iconEnabledColor: Colors.white70,
items: _dropdownItems,
onChanged: (String? newValue) {
setState(() {
_selectedCategory = newValue;
});
},
onSaved: widget.onSaved,
);
}
}

View File

@@ -3,8 +3,14 @@ import 'package:flutter/material.dart';
class DatePickerField extends StatelessWidget { class DatePickerField extends StatelessWidget {
final DateTime? selectedDate; final DateTime? selectedDate;
final Function(DateTime?) onDatePicked; final Function(DateTime?) onDatePicked;
final String label; // Texte du label
const DatePickerField({Key? key, this.selectedDate, required this.onDatePicked}) : super(key: key); const DatePickerField({
Key? key,
this.selectedDate,
required this.onDatePicked,
this.label = 'Sélectionnez une date', // Label par défaut
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -21,21 +27,36 @@ class DatePickerField extends StatelessWidget {
} }
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 18.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1), color: Colors.blueGrey.withOpacity(0.1), // Fond plus doux et moderne
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(12.0), // Coins arrondis plus prononcés
border: Border.all(color: Colors.blueGrey.withOpacity(0.5), width: 2.0), // Bordure légère
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(0, 4),
blurRadius: 8,
),
],
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
selectedDate == null selectedDate == null
? 'Sélectionnez une date' ? label
: '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}', : '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}',
style: const TextStyle(color: Colors.white70), style: const TextStyle(
color: Colors.blueGrey, // Couleur du texte adaptée
fontSize: 16.0, // Taille de police améliorée
fontWeight: FontWeight.w600, // Poids de police plus important pour un meilleur contraste
),
),
Icon(
Icons.calendar_today,
color: Colors.blueGrey, // Couleur de l'icône assortie au texte
), ),
const Icon(Icons.calendar_today, color: Colors.white70),
], ],
), ),
), ),

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
class DescriptionField extends StatelessWidget {
final FormFieldSetter<String> onSaved;
const DescriptionField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Description',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.description, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
maxLines: 3,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une description';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -3,8 +3,9 @@ import 'package:afterwork/core/utils/date_formatter.dart';
import 'event_menu.dart'; import 'event_menu.dart';
class EventHeader extends StatelessWidget { class EventHeader extends StatelessWidget {
final String userFirstName; final String creatorFirstName;
final String userLastName; final String creatorLastName;
final String profileImageUrl;
final String? eventDate; final String? eventDate;
final String? imageUrl; final String? imageUrl;
final String location; final String location;
@@ -14,8 +15,9 @@ class EventHeader extends StatelessWidget {
const EventHeader({ const EventHeader({
Key? key, Key? key,
required this.userFirstName, required this.creatorFirstName,
required this.userLastName, required this.creatorLastName,
required this.profileImageUrl,
this.eventDate, this.eventDate,
this.imageUrl, this.imageUrl,
required this.location, required this.location,
@@ -40,9 +42,9 @@ class EventHeader extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.grey.shade800, backgroundColor: Colors.grey.shade800,
backgroundImage: imageUrl != null && imageUrl!.isNotEmpty backgroundImage: profileImageUrl.isNotEmpty
? NetworkImage(imageUrl!) ? NetworkImage(profileImageUrl)
: const AssetImage('lib/assets/images/placeholder.png') as ImageProvider, : AssetImage(profileImageUrl) as ImageProvider,
radius: 22, radius: 22,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -51,7 +53,7 @@ class EventHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'$userFirstName $userLastName', '$creatorFirstName $creatorLastName',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14, fontSize: 14,
@@ -91,11 +93,12 @@ class EventHeader extends StatelessWidget {
), ),
], ],
), ),
// Ajout des boutons dans le coin supérieur droit // Placement des icônes avec padding pour éviter qu'elles ne soient trop proches du bord
Positioned( Positioned(
top: 0, top: 0,
right: 0, right: -5,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
key: menuKey, key: menuKey,
@@ -105,6 +108,7 @@ class EventHeader extends StatelessWidget {
showEventOptions(menuContext, menuKey); showEventOptions(menuContext, menuKey);
}, },
), ),
const SizedBox(width: 0), // Espacement entre les icônes
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 20), icon: const Icon(Icons.close, color: Colors.white54, size: 20),
splashRadius: 20, splashRadius: 20,

View File

@@ -20,6 +20,7 @@ class EventList extends StatelessWidget {
userId: 'user_id_here', // Vous pouvez passer l'ID réel de l'utilisateur connecté userId: 'user_id_here', // Vous pouvez passer l'ID réel de l'utilisateur connecté
userFirstName: 'John', // Vous pouvez passer le prénom réel de l'utilisateur userFirstName: 'John', // Vous pouvez passer le prénom réel de l'utilisateur
userLastName: 'Doe', // Vous pouvez passer le nom réel de l'utilisateur userLastName: 'Doe', // Vous pouvez passer le nom réel de l'utilisateur
profileImageUrl: 'profileImageUrl',
onReact: () => _handleReact(event), onReact: () => _handleReact(event),
onComment: () => _handleComment(event), onComment: () => _handleComment(event),
onShare: () => _handleShare(event), onShare: () => _handleShare(event),

View File

@@ -1,7 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:afterwork/core/constants/colors.dart';
import 'package:provider/provider.dart';
import '../../core/theme/theme_provider.dart';
void showEventOptions(BuildContext context, GlobalKey key) { void showEventOptions(BuildContext context, GlobalKey key) {
final RenderBox renderBox = key.currentContext!.findRenderObject() as RenderBox; // Obtient la position de l'élément pour afficher le menu contextuel
final RenderBox renderBox =
key.currentContext!.findRenderObject() as RenderBox;
final Offset offset = renderBox.localToGlobal(Offset.zero); final Offset offset = renderBox.localToGlobal(Offset.zero);
final RelativeRect position = RelativeRect.fromLTRB( final RelativeRect position = RelativeRect.fromLTRB(
offset.dx, offset.dx,
@@ -10,76 +17,150 @@ void showEventOptions(BuildContext context, GlobalKey key) {
offset.dy + renderBox.size.height, offset.dy + renderBox.size.height,
); );
// Affiche le menu contextuel avec des options personnalisées
showMenu( showMenu(
context: context, context: context,
position: position, position: position,
items: [ items: [
PopupMenuItem( _buildElegantMenuItem(
value: 'details', icon: Icons.info_outline,
child: Row( label: 'Voir les détails',
children: [ color: AppColors.primary, // Utilise la couleur primaire dynamique
Icon(Icons.info_outline, color: Colors.blue.shade400, size: 18), // Icône plus petite et bleue onTap: () {
const SizedBox(width: 10),
Text(
'Voir les détails',
style: TextStyle(
color: Colors.blue.shade700, // Texte bleu foncé
fontWeight: FontWeight.w500, // Poids de police plus fin
fontSize: 14, // Taille légèrement réduite
),
),
],
),
),
PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, color: Colors.orange.shade400, size: 18),
const SizedBox(width: 10),
Text(
'Modifier l\'événement',
style: TextStyle(
color: Colors.orange.shade700,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline, color: Colors.red.shade400, size: 18),
const SizedBox(width: 10),
Text(
'Supprimer l\'événement',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
],
elevation: 5.0, // Réduction de l'élévation pour une ombre plus subtile
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0), // Ajout de bordures arrondies
side: BorderSide(color: Colors.grey.shade300), // Bordure fine et douce
),
color: Colors.white, // Fond blanc pur pour un contraste élégant
).then((value) {
// Gérer les actions en fonction de la sélection
if (value == 'details') {
print('Voir les détails'); print('Voir les détails');
} else if (value == 'edit') { // Log d'action pour suivre l'interaction utilisateur
},
),
_buildElegantMenuItem(
icon: Icons.edit,
label: 'Modifier l\'événement',
color: AppColors.secondary, // Utilise la couleur secondaire dynamique
onTap: () {
print('Modifier l\'événement'); print('Modifier l\'événement');
} else if (value == 'delete') { },
print('Supprimer l\'événement'); ),
_buildElegantMenuItem(
icon: Icons.delete_outline,
label: 'Supprimer l\'événement',
color: AppColors.errorColor, // Utilise la couleur d'erreur dynamique
onTap: () {
_showDeleteConfirmation(context);
},
),
],
elevation: 12.0, // Niveau d'élévation du menu pour une ombre modérée
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0), // Coins arrondis pour un look moderne
),
color: AppColors.customBackgroundColor, // Surface dynamique selon le thème
).then((value) {
if (value != null) {
HapticFeedback.lightImpact(); // Retour haptique pour une meilleure UX
} }
}); });
} }
// Construction d'un élément de menu stylisé
PopupMenuItem _buildElegantMenuItem({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return PopupMenuItem(
value: label,
child: GestureDetector(
onTap: () {
onTap();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.cardColor, // Couleur de fond dynamique du conteneur
),
child: Row(
children: [
Icon(icon, color: color, size: 15), // Icône avec couleur personnalisée
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: TextStyle(
color: color, // Couleur de texte dynamique
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
),
),
);
}
void _showDeleteConfirmation(BuildContext context) {
// Récupère le thème sans écoute, car la fonction est appelée en dehors de l'arbre de widgets.
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
// Affiche une boîte de dialogue pour confirmer la suppression
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(Icons.warning_amber_rounded, color: AppColors.errorColor),
const SizedBox(width: 8),
Text(
'Supprimer l\'événement',
style: TextStyle(
color: AppColors.errorColor, // Utilisation de la couleur d'erreur dynamique
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
'Voulez-vous vraiment supprimer cet événement ? Cette action est irréversible.',
style: TextStyle(
color: themeProvider.isDarkMode ? AppColors.lightOnPrimary : AppColors.darkPrimary, // Texte principal dynamique
fontSize: 15,
),
),
actions: <Widget>[
TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.all(Colors.grey.shade200),
),
child: Text('Annuler', style: TextStyle(color: Colors.grey.shade700)),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton.icon(
icon: Icon(Icons.delete, color: themeProvider.isDarkMode ? AppColors.darkPrimary : AppColors.lightOnPrimary, size: 18),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.errorColor, // Bouton de suppression en couleur d'erreur
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
label: Text('Supprimer'),
onPressed: () {
Navigator.of(context).pop();
print('Événement supprimé');
// Logique de suppression réelle ici
},
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
);
},
);
}

View File

@@ -23,14 +23,14 @@ class EventStatusBadge extends StatelessWidget {
Icon( Icon(
status == 'fermé' ? Icons.lock : Icons.lock_open, status == 'fermé' ? Icons.lock : Icons.lock_open,
color: status == 'fermé' ? Colors.red : Colors.green, color: status == 'fermé' ? Colors.red : Colors.green,
size: 16.0, size: 10.0,
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
status == 'fermé' ? 'Fermé' : 'Ouvert', status == 'fermé' ? 'Fermé' : 'Ouvert',
style: TextStyle( style: TextStyle(
color: status == 'fermé' ? Colors.red : Colors.green, color: status == 'fermé' ? Colors.red : Colors.green,
fontSize: 12, fontSize: 10,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class AccessibilityField extends StatelessWidget {
final Function(String?) onSaved;
const AccessibilityField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Accessibilité',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer des informations sur l\'accessibilité';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class AccommodationInfoField extends StatelessWidget {
final Function(String?) onSaved;
const AccommodationInfoField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Informations sur l\'hébergement',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer des informations sur l\'hébergement';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
/// Un champ pour saisir le nombre maximum de participants à un événement.
/// Il est conçu pour permettre à l'utilisateur de saisir un nombre entier.
class AttendeesField extends StatelessWidget {
// Définition de la fonction de rappel pour sauver la valeur saisie.
final Function(int) onSaved;
// Le constructeur prend une fonction de rappel pour sauvegarder la valeur saisie.
const AttendeesField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
// Retourne un widget de type Column pour organiser le texte et le champ de saisie.
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Texte statique indiquant ce que l'utilisateur doit entrer.
const Text(
'Nombre maximum de participants', // Le texte est en français et indique le champ à remplir.
style: TextStyle(
color: Colors.blueGrey, // Couleur du texte
fontSize: 16,
fontWeight: FontWeight.bold, // Met en gras pour la visibilité
),
),
const SizedBox(height: 8), // Espacement entre le titre et le champ de saisie.
TextFormField(
keyboardType: TextInputType.number, // Le champ attend un nombre entier.
decoration: InputDecoration(
hintStyle: const TextStyle(color: Colors.blueGrey),
hintText: 'Entrez ici le nombre maximum de participants...', // L'invite pour aider l'utilisateur.
filled: true, // Le champ est rempli avec une couleur de fond.
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond du champ avec opacité.
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie
borderSide: BorderSide.none, // Pas de bordure par défaut
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blueGrey, // Bordure de base
width: 1.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blue, // Bordure en bleu lors du focus
width: 2.0,
),
),
prefixIcon: const Icon(
Icons.group,
color: Colors.blueGrey, // Icône assortie
),
),
style: const TextStyle(
color: Colors.blueGrey, // Couleur du texte saisi
fontSize: 16.0, // Taille de police
fontWeight: FontWeight.w600, // Poids de la police pour la lisibilité
),
onChanged: (value) {
// Lors de chaque modification de texte, on tente de convertir la valeur en entier.
int? maxParticipants = int.tryParse(value) ?? 0; // Conversion en entier, avec une valeur par défaut de 0.
print('Nombre maximum de participants saisi : $maxParticipants'); // Log pour suivre la valeur saisie.
onSaved(maxParticipants); // Appel de la fonction onSaved pour transmettre la valeur au formulaire principal.
},
validator: (value) {
// Validation pour vérifier si la valeur est un nombre valide.
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nombre de participants';
}
return null; // La validation est correcte si la valeur est un nombre
},
),
],
);
}
}

View File

@@ -0,0 +1,192 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' as rootBundle;
class CategoryField extends StatefulWidget {
// Ce callback est utilisé pour enregistrer la valeur sélectionnée dans le formulaire
final FormFieldSetter<String> onSaved;
// Constructeur de la classe CategoryField
const CategoryField({Key? key, required this.onSaved}) : super(key: key);
@override
_CategoryFieldState createState() => _CategoryFieldState();
}
class _CategoryFieldState extends State<CategoryField> {
// Variable pour stocker la catégorie sélectionnée par l'utilisateur
String? _selectedCategory;
// Map pour stocker les catégories et leurs sous-catégories
Map<String, List<String>> _categoryMap = {};
// Liste des éléments du menu déroulant
List<DropdownMenuItem<String>> _dropdownItems = [];
@override
void initState() {
super.initState();
// Chargement des catégories dès que l'état est initialisé
_loadCategories();
}
/// Méthode pour charger les catégories depuis un fichier JSON.
/// Cette méthode récupère les catégories et sous-catégories depuis le fichier JSON
/// et met à jour l'état du widget.
Future<void> _loadCategories() async {
try {
// Chargement du fichier JSON à partir des ressources
final String jsonString = await rootBundle.rootBundle
.loadString('lib/assets/json/event_categories.json');
// Décodage du fichier JSON pour obtenir un Map
final Map<String, dynamic> jsonMap = json.decode(jsonString);
// Map pour stocker les catégories et leurs sous-catégories
final Map<String, List<String>> categoryMap = {};
// Parcours des catégories et ajout des sous-catégories dans le map
jsonMap['categories'].forEach((key, value) {
categoryMap[key] = List<String>.from(value);
});
// Mise à jour de l'état avec les nouvelles données chargées
setState(() {
_categoryMap = categoryMap;
_dropdownItems =
_buildDropdownItems(); // Reconstruction des éléments du menu
});
// Log pour vérifier si les catégories ont bien été chargées
debugPrint("Catégories chargées : $_categoryMap");
} catch (e) {
// Log en cas d'erreur lors du chargement
debugPrint("Erreur lors du chargement des catégories : $e");
// Affichage d'un message d'erreur à l'utilisateur si le chargement échoue
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'Erreur lors du chargement des catégories. Veuillez réessayer plus tard.')));
}
}
/// Méthode pour construire la liste des éléments du menu déroulant avec les catégories et sous-catégories.
/// Cette méthode crée une liste d'éléments DropdownMenuItem pour afficher dans le DropdownButton.
List<DropdownMenuItem<String>> _buildDropdownItems() {
List<DropdownMenuItem<String>> items = [];
// Parcours des catégories et ajout des sous-catégories dans le menu déroulant
_categoryMap.forEach((category, subcategories) {
// Ajouter une catégorie (non sélectionnable) comme en-tête
items.add(
DropdownMenuItem<String>(
enabled: false,
// Cette entrée est désactivée pour qu'elle ne soit pas sélectionnée
child: Text(
category,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blueGrey,
),
),
),
);
// Ajouter les sous-catégories associées à cette catégorie
for (String subcategory in subcategories) {
items.add(
DropdownMenuItem<String>(
value: subcategory, // Valeur de la sous-catégorie
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
// Indentation pour les sous-catégories
child: Text(
subcategory,
style: const TextStyle(color: Colors.blueGrey),
),
),
),
);
}
});
// Log pour vérifier le nombre d'éléments créés pour le menu déroulant
debugPrint("Éléments créés pour le menu déroulant : ${items.length}");
return items;
}
@override
Widget build(BuildContext context) {
// Si la liste des éléments est vide, afficher un indicateur de chargement
return _dropdownItems.isEmpty
? const Center(
child:
CircularProgressIndicator()) // Affichage d'un indicateur de chargement pendant le chargement des données
: DropdownButtonFormField<String>(
value: _selectedCategory,
// Valeur sélectionnée par l'utilisateur
decoration: InputDecoration(
labelText: 'Catégorie',
// Libellé du champ
labelStyle: const TextStyle(color: Colors.blueGrey),
// Style du libellé
filled: true,
// Remplissage du champ
fillColor: Colors.blueGrey.withOpacity(0.1),
// Couleur de fond
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.blueGrey, // Couleur de la bordure par défaut
width: 2.0, // Épaisseur de la bordure
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.blueGrey,
// Couleur de la bordure quand non sélectionné
width: 2.0,
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.blue, // Bordure quand le champ est sélectionné
width: 2.0,
),
),
prefixIcon: const Icon(Icons.category,
color: Colors.blueGrey), // Icône du champ
),
style: const TextStyle(color: Colors.blueGrey),
// Style du texte sélectionné
dropdownColor: const Color(0xFF2C2C3E),
// Couleur de fond du menu déroulant
iconEnabledColor: Colors.blueGrey,
// Couleur de l'icône du menu déroulant
items: _dropdownItems,
// Liste des éléments du menu déroulant
onChanged: (String? newValue) {
// Log pour suivre la valeur sélectionnée
debugPrint("Nouvelle catégorie sélectionnée : $newValue");
setState(() {
_selectedCategory =
newValue; // Mise à jour de la catégorie sélectionnée
});
},
onSaved: widget.onSaved,
// Enregistrer la valeur dans le formulaire
hint: const Text(
'Veuillez choisir une catégorie',
// Texte affiché lorsqu'aucune catégorie n'est sélectionnée
style: TextStyle(
color: Colors.blueGrey), // Style du texte par défaut
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
/// `DescriptionField` est un champ de texte utilisé pour saisir une description.
/// Ce champ fait partie d'un formulaire et est conçu pour accepter plusieurs lignes de texte.
/// Il est doté de validations et d'une logique d'enregistrement personnalisée via `onSaved`.
///
/// Ce widget utilise des icônes et un style personnalisé pour correspondre à l'apparence de l'application.
///
/// Arguments :
/// - `onSaved`: Une fonction callback utilisée pour enregistrer la valeur du champ de texte une fois que le formulaire est soumis.
/// ```
class DescriptionField extends StatelessWidget {
// Callback utilisé pour enregistrer la valeur de la description
final FormFieldSetter<String> onSaved;
// Constructeur du widget DescriptionField
const DescriptionField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
// Log : Construction du champ DescriptionField
debugPrint('Construction du champ DescriptionField');
return TextFormField(
// Décoration du champ de texte
decoration: InputDecoration(
labelText: 'Description', // Texte étiquette affiché à l'utilisateur
labelStyle: const TextStyle(color: Colors.blueGrey), // Style de l'étiquette
filled: true, // Active le fond coloré
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond plus douce et plus subtile
hintStyle: const TextStyle(color: Colors.blueGrey),
hintText: 'Entrez un la description ici...',
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie améliorée
borderSide: BorderSide.none, // Pas de bordure visible
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blueGrey, // Bordure de base en bleu gris
width: 2.0, // Largeur de la bordure
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blue, // Bordure bleue lors du focus
width: 2.0, // Épaisseur de la bordure lors du focus
),
),
prefixIcon: const Icon(Icons.description, color: Colors.blueGrey), // Icône de description avant le texte
),
// Style du texte dans le champ
style: const TextStyle(color: Colors.blueGrey, fontSize: 16.0),
// Limite le champ à 3 lignes, avec un retour à la ligne automatique
maxLines: 3,
// Autres configurations du champ
textInputAction: TextInputAction.done, // Permet de soumettre avec la touche "Done" du clavier
// Validation du champ : assure que le champ n'est pas vide
validator: (value) {
// Log : Validation du champ DescriptionField
debugPrint('Validation du champ DescriptionField');
if (value == null || value.isEmpty) {
return 'Veuillez entrer une description'; // Message d'erreur si la description est vide
}
return null; // Retourne null si la validation passe
},
// Lors de la soumission du formulaire, enregistre la valeur saisie
onSaved: (value) {
// Log : Sauvegarde de la valeur de la description
debugPrint('Valeur de la description sauvegardée : $value');
onSaved(value); // Appel de la fonction onSaved passée en paramètre
},
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class LinkField extends StatelessWidget {
// Le callback `onSaved` est utilisé pour enregistrer la valeur du champ lorsque le formulaire est soumis.
final FormFieldSetter<String> onSaved;
// Constructeur de la classe LinkField, qui attend le callback `onSaved`.
const LinkField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
// Création du champ de texte pour le lien
return TextFormField(
decoration: InputDecoration(
labelText: 'Lien (optionnel)', // Le texte affiché lorsqu'il n'y a pas de valeur
labelStyle: const TextStyle(color: Colors.blueGrey), // Style du texte du label
filled: true, // Remplissage du champ
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec une légère opacité
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)), // Bords arrondis du champ
borderSide: BorderSide.none, // Pas de bordure visible
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.blueGrey, // Couleur de la bordure quand non sélectionné
width: 2.0,
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.blue, // Bordure quand le champ est sélectionné
width: 2.0,
),
),
prefixIcon: const Icon(Icons.link, color: Colors.blueGrey), // Icône de lien à gauche
hintText: 'Entrez un lien ici...', // Texte d'indication lorsque le champ est vide
),
style: const TextStyle(color: Colors.blueGrey), // Style du texte saisi par l'utilisateur
onSaved: (value) {
// Log de la valeur du champ lorsqu'on l'enregistre
debugPrint("Lien enregistré : $value");
// Appel du callback `onSaved` pour enregistrer la valeur dans le formulaire
onSaved(value);
},
keyboardType: TextInputType.url, // Permet à l'utilisateur de saisir une URL
validator: (value) {
// Si le champ est rempli, on valide que la valeur est bien une URL correcte
if (value != null && value.isNotEmpty) {
final Uri? uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
// Log en cas d'erreur de validation
debugPrint("URL invalide : $value");
return 'Veuillez entrer un lien valide';
}
}
return null;
},
);
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../screens/location/location_picker_Screen.dart';
/// `LocationField` est un champ de saisie permettant de sélectionner une localisation sur une carte.
/// Il utilise la page `LocationPickerScreen` pour permettre à l'utilisateur de choisir un emplacement précis.
/// Ce widget est utilisé dans des formulaires et permet d'afficher la localisation sélectionnée.
///
/// Arguments :
/// - `location`: Une chaîne représentant la localisation actuelle à afficher.
/// - `selectedLatLng`: Une variable de type `LatLng?` représentant la latitude et la longitude de la localisation sélectionnée.
/// - `onLocationPicked`: Un callback pour retourner la localisation choisie par l'utilisateur.
///
class LocationField extends StatelessWidget {
final String location;
final LatLng? selectedLatLng;
final Function(LatLng?) onLocationPicked;
const LocationField({Key? key, required this.location, this.selectedLatLng, required this.onLocationPicked})
: super(key: key);
@override
Widget build(BuildContext context) {
// Log : Construction du champ LocationField
debugPrint('Construction du champ LocationField');
return GestureDetector(
onTap: () async {
// Log : L'utilisateur clique pour choisir une localisation
debugPrint('Utilisateur clique pour choisir une localisation');
final LatLng? pickedLocation = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LocationPickerScreen(),
),
);
if (pickedLocation != null) {
// Log : L'utilisateur a sélectionné une nouvelle localisation
debugPrint('Nouvelle localisation sélectionnée : $pickedLocation');
onLocationPicked(pickedLocation);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300), // Animation fluide lors du focus
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.blueGrey.withOpacity(0.1), // Fond plus visible, subtilement coloré
borderRadius: BorderRadius.circular(12.0), // Bordure arrondie améliorée
border: Border.all(
color: selectedLatLng == null ? Colors.blueGrey.withOpacity(0.5) : Colors.blue, // Bordure change selon l'état
width: 2.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedLatLng == null
? 'Sélectionnez une localisation' // Message par défaut si aucune localisation sélectionnée
: 'Localisation: $location', // Affiche la localisation actuelle
style: const TextStyle(color: Colors.blueGrey, fontSize: 16.0),
),
const Icon(Icons.location_on, color: Colors.blueGrey),
],
),
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
/// Un champ de saisie pour l'organisateur, utilisé dans un formulaire.
class OrganizerField extends StatelessWidget {
// Fonction de rappel pour sauvegarder la valeur de l'organisateur.
final Function(String?) onSaved;
// Constructeur qui prend la fonction onSaved pour transmettre l'organisateur au formulaire.
const OrganizerField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Organisateur', // Texte d'étiquette pour le champ de saisie.
labelStyle: const TextStyle(
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
),
prefixIcon: const Icon(
Icons.person, // Icône représentant un organisateur (utilisateur).
color: Colors.blueGrey, // Couleur de l'icône.
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
borderSide: BorderSide.none, // Pas de bordure par défaut.
),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blueGrey, // Bordure colorée en blueGrey.
width: 1.5, // Largeur de la bordure.
),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blue, // Bordure bleue au focus.
width: 2.0,
),
),
filled: true, // Le champ de saisie est rempli de couleur de fond.
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec opacité.
),
validator: (value) {
// Validation pour vérifier que le champ n'est pas vide.
if (value == null || value.isEmpty) {
return 'Veuillez entrer un organisateur'; // Message d'erreur si vide.
}
return null;
},
onSaved: onSaved, // Fonction qui est appelée pour sauvegarder la valeur saisie.
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class ParkingField extends StatelessWidget {
final Function(String?) onSaved;
const ParkingField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Informations sur le parking',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer des informations sur le parking';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class ParticipationFeeField extends StatelessWidget {
final Function(String?) onSaved;
const ParticipationFeeField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Frais de participation',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer les frais de participation';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class PrivacyRulesField extends StatelessWidget {
final Function(String?) onSaved;
const PrivacyRulesField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Règles de confidentialité',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer des règles de confidentialité';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class SecurityProtocolField extends StatelessWidget {
final Function(String?) onSaved;
const SecurityProtocolField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: const InputDecoration(
labelText: 'Protocole de sécurité',
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un protocole de sécurité';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
/// Un champ permettant à l'utilisateur de saisir des tags.
/// Il permet également d'afficher les tags saisis sous forme de chips (étiquettes).
class TagsField extends StatefulWidget {
// Fonction de rappel pour sauvegarder la liste des tags saisis.
final Function(List<String>) onSaved;
// Constructeur qui prend la fonction onSaved pour transmettre les tags au formulaire.
const TagsField({Key? key, required this.onSaved}) : super(key: key);
@override
_TagsFieldState createState() => _TagsFieldState(); // Création de l'état pour gérer les tags.
}
class _TagsFieldState extends State<TagsField> {
final TextEditingController _controller = TextEditingController(); // Contrôleur pour gérer l'entrée de texte.
List<String> _tags = []; // Liste pour stocker les tags saisis.
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, // Alignement à gauche pour les éléments.
children: [
const SizedBox(height: 8), // Espacement entre le titre et le champ de saisie.
TextFormField(
controller: _controller, // Associe le contrôleur à ce champ de texte.
decoration: InputDecoration(
hintStyle: const TextStyle(color: Colors.blueGrey),
hintText: 'Entrez un les tags ici séparés par des virgules...',
labelText: 'Tags', // Texte d'étiquette pour le champ de saisie.
labelStyle: const TextStyle(
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
),
prefixIcon: const Icon(
Icons.tag, // Icône représentant un tag.
color: Colors.blueGrey, // Couleur de l'icône.
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
borderSide: BorderSide.none, // Pas de bordure par défaut.
),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blueGrey, // Bordure de base.
width: 1.5, // Largeur de la bordure.
),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blue, // Bordure bleue au focus.
width: 2.0,
),
),
filled: true, // Le champ est rempli avec une couleur de fond.
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond du champ de texte avec opacité.
),
onFieldSubmitted: (value) {
print('Tags soumis : $value'); // Log pour suivre ce qui a été saisi avant la soumission.
_addTags(value); // Appel à la méthode _addTags pour ajouter les tags.
},
),
const SizedBox(height: 8), // Espacement entre le champ de saisie et les chips.
Wrap(
spacing: 8.0, // Espacement entre les chips.
children: _tags.map((tag) => Chip(
label: Text(tag), // Texte du tag à afficher.
backgroundColor: Colors.blueGrey.withOpacity(0.2), // Couleur de fond des chips.
labelStyle: const TextStyle(color: Colors.blueGrey), // Couleur du texte dans les chips.
)).toList(), // Génère une liste de Chips pour chaque tag.
),
],
);
}
// Fonction pour ajouter les tags à la liste.
void _addTags(String value) {
setState(() {
_tags = value.split(',') // Sépare les tags par des virgules.
.map((tag) => tag.trim()) // Supprime les espaces autour des tags.
.where((tag) => tag.isNotEmpty) // Exclut les tags vides.
.toList(); // Crée la liste de tags.
});
print('Tags ajoutés : $_tags'); // Log pour vérifier la liste de tags ajoutée.
widget.onSaved(_tags); // Envoie la liste des tags au formulaire principal.
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class TitleField extends StatelessWidget {
final FormFieldSetter<String> onSaved;
const TitleField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Titre',
labelStyle: const TextStyle(color: Colors.blueGrey), // Couleur du label
filled: true,
fillColor: Colors.blueGrey.withOpacity(0.1), // Fond plus doux
hintStyle: const TextStyle(color: Colors.blueGrey),
hintText: 'Entrez un le titre ici...',
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure plus arrondie
borderSide: BorderSide.none, // Pas de bordure par défaut
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blueGrey, // Bordure de base
width: 1.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: const BorderSide(
color: Colors.blue, // Bordure en bleu lors du focus
width: 2.0,
),
),
prefixIcon: const Icon(
Icons.title,
color: Colors.blueGrey, // Icône assortie
),
),
style: const TextStyle(
color: Colors.blueGrey, // Texte en bleu pour un meilleur contraste
fontSize: 16.0, // Taille de police améliorée
fontWeight: FontWeight.w600, // Poids de la police pour la lisibilité
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un titre';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
/// Un champ de saisie pour les informations de transport, utilisé dans un formulaire.
class TransportInfoField extends StatelessWidget {
// Fonction de rappel pour sauvegarder les informations de transport.
final Function(String?) onSaved;
// Constructeur qui prend la fonction onSaved pour transmettre les informations de transport au formulaire.
const TransportInfoField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration( // Suppression du mot-clé 'const'
labelText: 'Informations de transport', // Texte d'étiquette pour le champ de saisie.
labelStyle: const TextStyle(
color: Colors.blueGrey, // Couleur de l'étiquette en blueGrey.
),
prefixIcon: const Icon(
Icons.directions_car, // Icône représentant un moyen de transport (voiture).
color: Colors.blueGrey, // Couleur de l'icône.
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)), // Bordure arrondie.
borderSide: BorderSide.none, // Pas de bordure par défaut.
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: BorderSide(
color: Colors.blueGrey, // Bordure colorée en blueGrey.
width: 1.5, // Largeur de la bordure.
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
borderSide: BorderSide(
color: Colors.blue, // Bordure bleue au focus.
width: 2.0,
),
),
filled: true, // Le champ de saisie est rempli de couleur de fond.
fillColor: Colors.blueGrey.withOpacity(0.1), // Couleur de fond avec opacité.
),
validator: (value) {
// Validation pour vérifier que le champ n'est pas vide.
if (value == null || value.isEmpty) {
return 'Veuillez entrer des informations sur le transport'; // Message d'erreur si vide.
}
return null;
},
onSaved: onSaved, // Fonction qui est appelée pour sauvegarder la valeur saisie.
);
}
}

View File

@@ -1,34 +1,54 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import '../../domain/entities/friend.dart';
/// [FriendDetailScreen] affiche les détails d'un ami, incluant son nom, son image de profil, /// [FriendDetailScreen] affiche les détails d'un ami, incluant son nom, son image de profil,
/// et une option pour envoyer un message. /// et une option pour envoyer un message.
///
/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails. /// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails.
class FriendDetailScreen extends StatelessWidget { class FriendDetailScreen extends StatelessWidget {
final String name; // Nom de l'ami final String friendFirstName; // Nom de l'ami
final String friendLastName;
final String imageUrl; // URL de l'image de profil de l'ami final String imageUrl; // URL de l'image de profil de l'ami
final String friendId; // ID de l'ami pour des actions futures final String friendId; // ID de l'ami pour des actions futures
final Logger _logger = Logger(); // Logger pour suivre les actions dans le terminal final Logger _logger = Logger(); // Logger pour suivre les actions dans le terminal
final FriendStatus status;
final String lastInteraction;
final String dateAdded;
/// Constructeur de la classe [FriendDetailScreen]. /// Constructeur de la classe [FriendDetailScreen].
/// [name], [imageUrl], et [friendId] doivent être fournis.
FriendDetailScreen({ FriendDetailScreen({
Key? key, Key? key,
required this.name, required this.friendFirstName,
required this.friendLastName,
required this.imageUrl, required this.imageUrl,
required this.friendId, required this.friendId,
required this.status,
required this.lastInteraction,
required this.dateAdded,
}) : super(key: key); }) : super(key: key);
/// Méthode statique pour lancer l'écran des détails d'un ami. /// Méthode statique pour lancer l'écran des détails d'un ami.
static void open(BuildContext context, String friendId, String name, String imageUrl) { static void open(
BuildContext context,
String friendId,
String friendFirstName,
String friendLastName,
String imageUrl,
FriendStatus status,
String lastInteraction,
String dateAdded) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => FriendDetailScreen( builder: (_) => FriendDetailScreen(
friendId: friendId, friendId: friendId,
name: name, friendFirstName: friendFirstName,
friendLastName: friendLastName,
imageUrl: imageUrl, imageUrl: imageUrl,
status: status,
lastInteraction: lastInteraction,
dateAdded: dateAdded,
), ),
), ),
); );
@@ -36,70 +56,128 @@ class FriendDetailScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_logger.i('[LOG] Affichage des détails de l\'ami : $name (ID: $friendId)'); _logger.i('[LOG] Affichage des détails de l\'ami : $friendFirstName (ID: $friendId)');
// Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide. // Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide.
final imageProvider = imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true final imageProvider =
imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true
? NetworkImage(imageUrl) ? NetworkImage(imageUrl)
: const AssetImage('lib/assets/images/default_avatar.png') as ImageProvider; : const AssetImage('lib/assets/images/default_avatar.png')
as ImageProvider;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(name), // Titre de l'écran affichant le nom de l'ami title: Text(friendFirstName),
backgroundColor: Colors.grey.shade800, backgroundColor: Colors.teal.shade800, // Couleur de l'app bar
elevation: 6, // Ombre sous l'app bar pour plus de profondeur
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), // Espacement autour du contenu padding: const EdgeInsets.all(16.0), // Espacement autour du contenu
child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Affichage de l'image de l'ami avec animation `Hero` // Animation Hero pour une transition fluide lors de la navigation
Hero( Hero(
tag: friendId, // Tag unique pour l'animation Hero basée sur l'ID de l'ami tag: friendId,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: CircleAvatar( child: CircleAvatar(
radius: 50, radius: 80,
backgroundImage: imageProvider, backgroundImage: imageProvider,
backgroundColor: Colors.grey.shade800, backgroundColor: Colors.grey.shade800,
onBackgroundImageError: (error, stackTrace) { onBackgroundImageError: (error, stackTrace) {
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $name (ID: $friendId): $error'); _logger.e('[ERROR] Erreur lors du chargement de l\'image pour $friendFirstName (ID: $friendId): $error');
}, },
child: imageUrl.isEmpty child: imageUrl.isEmpty
? const Icon(Icons.person, size: 50, color: Colors.white) // Icône par défaut si aucune image n'est disponible ? const Icon(Icons.person, size: 60, color: Colors.white)
: null, : null,
), ),
), ),
const SizedBox(height: 16), // Espacement entre l'image et le texte ),
const SizedBox(height: 16),
// Affichage du nom de l'ami // Affichage du nom de l'ami avec une meilleure hiérarchie visuelle
Text( Text(
name, '$friendFirstName $friendLastName',
style: const TextStyle( style: const TextStyle(
fontSize: 24, // Taille de la police pour le nom fontSize: 28,
fontWeight: FontWeight.bold, // Texte en gras fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
), ),
), ),
const SizedBox(height: 20), // Espacement avant le bouton const SizedBox(height: 8),
Text(
status.name.toUpperCase(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: status == FriendStatus.accepted
? Colors.green.shade400
: status == FriendStatus.pending
? Colors.orange.shade400
: Colors.red.shade400,
),
),
const SizedBox(height: 20),
// Bouton pour envoyer un message à l'ami // Affichage des informations supplémentaires
_buildInfoRow('Dernière interaction:', lastInteraction),
_buildInfoRow('Date d\'ajout:', dateAdded),
const SizedBox(height: 30), // Espacement avant le bouton
// Bouton pour envoyer un message à l'ami avec animation
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
_logger.i('[LOG] Envoi d\'un message à $name (ID: $friendId)'); _logger.i('[LOG] Envoi d\'un message à $friendFirstName (ID: $friendId)');
// Logique future pour envoyer un message à l'ami // Logique future pour envoyer un message à l'ami
}, },
icon: const Icon(Icons.message), icon: const Icon(Icons.message),
label: const Text('Envoyer un message'), label: const Text('Envoyer un message'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal, // Couleur de fond du bouton backgroundColor: Colors.teal, // Couleur du bouton
foregroundColor: Colors.white, // Couleur du texte et de l'icône foregroundColor: Colors.white, // Couleur du texte et icône
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(30), // Coins arrondis
), ),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
elevation: 5, // Ombre pour effet de survol
), ),
), ),
], ],
), ),
), ),
),
);
}
/// Widget réutilisable pour afficher une ligne d'information avec un texte d'introduction et une valeur.
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(width: 8),
Text(
value,
style: const TextStyle(
fontSize: 16,
color: Colors.white70, // Couleur plus claire pour les valeurs
),
),
],
),
); );
} }
} }

View File

@@ -5,14 +5,19 @@ import '../../domain/entities/friend.dart';
/// [FriendsCircle] est un widget qui affiche un ami sous forme d'avatar circulaire avec son nom. /// [FriendsCircle] est un widget qui affiche un ami sous forme d'avatar circulaire avec son nom.
/// L'avatar est cliquable, permettant à l'utilisateur d'accéder aux détails de l'ami /// L'avatar est cliquable, permettant à l'utilisateur d'accéder aux détails de l'ami
/// ou de déclencher d'autres actions liées. /// ou de déclencher d'autres actions liées.
///
/// Chaque interaction avec le widget sera loguée pour assurer une traçabilité complète.
class FriendsCircle extends StatelessWidget { class FriendsCircle extends StatelessWidget {
final Friend friend; // Représente l'entité Friend à afficher (nom et image). final Friend friend; // L'entité Friend à afficher (contenant l'ID, le prénom, le nom, et l'URL de l'image).
final VoidCallback onTap; // Fonction callback exécutée lorsque l'utilisateur clique sur l'avatar. final VoidCallback onTap; // La fonction callback qui sera exécutée lors du clic sur l'avatar.
// Logger pour tracer les actions dans le terminal // Initialisation du logger pour tracer les actions dans le terminal.
final Logger _logger = Logger(); final Logger _logger = Logger();
/// Constructeur pour [FriendsCircle], prenant en entrée un ami et une fonction de callback. /// Constructeur pour [FriendsCircle], prenant en entrée un ami et une fonction de callback.
///
/// @param friend: l'ami à afficher, comprenant les informations nécessaires (nom, prénom, imageUrl).
/// @param onTap: la fonction qui sera exécutée lorsque l'utilisateur clique sur l'avatar.
FriendsCircle({ FriendsCircle({
Key? key, Key? key,
required this.friend, // L'ami à afficher (doit inclure friendId, name, imageUrl). required this.friend, // L'ami à afficher (doit inclure friendId, name, imageUrl).
@@ -21,56 +26,58 @@ class FriendsCircle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut. // 1. Récupère et assemble les prénoms et noms de l'ami, ou définit "Ami inconnu" si ces valeurs sont vides.
String displayName = [friend.friendFirstName, friend.friendLastName] String displayName = [friend.friendFirstName, friend.friendLastName]
.where((namePart) => namePart != null && namePart.isNotEmpty) .where((namePart) => namePart != null && namePart.isNotEmpty) // Exclut les parties nulles ou vides.
.join(" ") .join(" ") // Joint les parties pour obtenir un nom complet.
.trim(); .trim(); // Supprime les espaces superflus.
if (displayName.isEmpty) { if (displayName.isEmpty) {
displayName = 'Ami inconnu'; displayName = 'Ami inconnu'; // Utilise "Ami inconnu" si le nom complet est vide.
} }
// 2. Widget GestureDetector pour détecter les clics sur l'avatar de l'ami.
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
// 3. Log du clic sur l'avatar pour traçabilité dans le terminal.
_logger.i('[LOG] Avatar de ${displayName.trim()} cliqué'); _logger.i('[LOG] Avatar de ${displayName.trim()} cliqué');
onTap(); // Exécute l'action de clic définie par l'utilisateur onTap(); // Exécute la fonction de callback définie lors du clic.
}, },
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments de la colonne. mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments dans la colonne.
children: [ children: [
// 4. Animation Hero avec l'ID unique de l'ami pour effectuer une transition fluide.
Hero( Hero(
tag: friend.friendId, // Tag unique pour l'animation Hero basé sur l'ID de l'ami. tag: friend.friendId, // Tag unique pour l'animation Hero basé sur l'ID de l'ami.
child: CircleAvatar( child: CircleAvatar(
radius: 40, radius: 40, // Rayon de l'avatar circulaire.
// 5. Gestion de l'image de l'avatar. Si une image est fournie, on l'affiche.
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau ? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau.
? NetworkImage(friend.imageUrl!) ? NetworkImage(friend.imageUrl!) // Charge l'image depuis une URL réseau.
: AssetImage(friend.imageUrl!) as ImageProvider) // Utilise AssetImage si c'est une ressource locale : AssetImage(friend.imageUrl!) as ImageProvider) // Sinon, charge depuis les ressources locales.
: const AssetImage('lib/assets/images/default_avatar.png'), // Utilise AssetImage pour l'avatar par défaut : const AssetImage('lib/assets/images/default_avatar.png'), // Si aucune image, utilise l'image par défaut.
onBackgroundImageError: (error, stackTrace) { onBackgroundImageError: (error, stackTrace) {
// 6. Log d'erreur si l'image de l'avatar ne se charge pas.
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error'); _logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error');
}, },
backgroundColor: Colors.grey.shade800, // Fond si l'image ne charge pas. backgroundColor: Colors.grey.shade800, // Fond si l'image ne se charge pas correctement.
child: friend.imageUrl == null || friend.imageUrl!.isEmpty
? const Icon(Icons.person, size: 40, color: Colors.white) // Icône de remplacement si aucune image n'est disponible
: null,
), ),
), ),
const SizedBox(height: 8), // Ajoute un espace entre l'image et le texte. const SizedBox(height: 8), // 7. Ajoute un espace entre l'avatar et le nom de l'ami.
// 8. Affiche le nom de l'ami sous l'avatar, avec une gestion de dépassement du texte.
Text( Text(
displayName, // Affiche le nom de l'ami sous l'avatar ou une valeur par défaut. displayName, // Affiche le nom de l'ami sous l'avatar ou "Ami inconnu" si vide.
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white, // Couleur du texte.
fontSize: 14, fontSize: 14, // Taille de police.
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold, // Met le texte en gras.
), ),
maxLines: 1, maxLines: 1, // Limite l'affichage à une ligne.
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis, // Ajoute des points de suspension si le texte dépasse.
), ),
], ],
), ),
); );
} }
} }

View File

@@ -2,6 +2,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
/// `ImagePreviewPicker` est un widget permettant à l'utilisateur de choisir une image depuis la galerie ou de prendre une photo.
/// Ce widget affiche un aperçu de l'image sélectionnée et gère l'interaction pour choisir une nouvelle image.
///
/// Arguments :
/// - `onImagePicked`: Un callback qui renvoie le fichier image sélectionné (ou null si aucune image n'est choisie).
class ImagePreviewPicker extends StatefulWidget { class ImagePreviewPicker extends StatefulWidget {
final void Function(File?) onImagePicked; final void Function(File?) onImagePicked;
@@ -14,9 +19,14 @@ class ImagePreviewPicker extends StatefulWidget {
class _ImagePreviewPickerState extends State<ImagePreviewPicker> { class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
File? _selectedImageFile; File? _selectedImageFile;
/// Méthode pour ouvrir le modal de sélection d'image avec une animation.
Future<void> _pickImage() async { Future<void> _pickImage() async {
// Log : Ouverture du modal de sélection d'image
debugPrint('Ouverture du modal de sélection d\'image');
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
// Affichage du modal de sélection d'image
final XFile? pickedFile = await showModalBottomSheet<XFile?>( final XFile? pickedFile = await showModalBottomSheet<XFile?>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@@ -26,14 +36,14 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.camera_alt), leading: const Icon(Icons.camera_alt),
title: const Text('Take a Photo'), title: const Text('Prendre une photo'),
onTap: () async { onTap: () async {
Navigator.pop(context, await picker.pickImage(source: ImageSource.camera)); Navigator.pop(context, await picker.pickImage(source: ImageSource.camera));
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.photo_library), leading: const Icon(Icons.photo_library),
title: const Text('Choose from Gallery'), title: const Text('Choisir depuis la galerie'),
onTap: () async { onTap: () async {
Navigator.pop(context, await picker.pickImage(source: ImageSource.gallery)); Navigator.pop(context, await picker.pickImage(source: ImageSource.gallery));
}, },
@@ -44,10 +54,13 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
}, },
); );
// Si un fichier est sélectionné, mettez à jour l'état avec l'image choisie
if (pickedFile != null) { if (pickedFile != null) {
setState(() { setState(() {
_selectedImageFile = File(pickedFile.path); _selectedImageFile = File(pickedFile.path);
widget.onImagePicked(_selectedImageFile); // Pass the picked image to the parent widget.onImagePicked(_selectedImageFile); // Passez l'image au parent
// Log : Image sélectionnée
debugPrint('Image sélectionnée : ${_selectedImageFile?.path}');
}); });
} }
} }
@@ -55,23 +68,28 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: _pickImage, onTap: _pickImage, // Ouvre le modal lors du clic
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
'Aperçu de l\'image (16:9)', 'Aperçu de l\'image (16:9)',
style: TextStyle(color: Colors.white70), style: TextStyle(color: Colors.blueGrey),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
AspectRatio( AnimatedContainer(
aspectRatio: 16 / 9, duration: const Duration(milliseconds: 300), // Animation douce lors du changement d'image
child: Container( padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black26, color: Colors.blueGrey.withOpacity(0.1), // Fond légèrement opaque
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(12.0), // Bordures arrondies
border: Border.all(color: Colors.white70, width: 1), border: Border.all(
color: _selectedImageFile != null ? Colors.blue : Colors.blueGrey,
width: 2.0, // Bordure visible autour de l'image
), ),
),
child: AspectRatio(
aspectRatio: 16 / 9, // Maintient l'aspect ratio de l'image
child: _selectedImageFile != null child: _selectedImageFile != null
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
@@ -88,7 +106,7 @@ class _ImagePreviewPickerState extends State<ImagePreviewPicker> {
: const Center( : const Center(
child: Text( child: Text(
'Cliquez pour ajouter une image', 'Cliquez pour ajouter une image',
style: TextStyle(color: Colors.white54), style: TextStyle(color: Colors.blueGrey),
), ),
), ),
), ),

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
class LinkField extends StatelessWidget {
final FormFieldSetter<String> onSaved;
const LinkField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Lien (optionnel)',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.link, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
onSaved: onSaved,
);
}
}

View File

@@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../screens/location/location_picker_Screen.dart';
class LocationField extends StatelessWidget {
final String location;
final LatLng? selectedLatLng;
final Function(LatLng?) onLocationPicked;
const LocationField({Key? key, required this.location, this.selectedLatLng, required this.onLocationPicked}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
final LatLng? pickedLocation = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LocationPickerScreen(),
),
);
if (pickedLocation != null) {
onLocationPicked(pickedLocation);
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(10.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedLatLng == null
? 'Sélectionnez une localisation'
: 'Localisation: $location',
style: const TextStyle(color: Colors.white70),
),
const Icon(Icons.location_on, color: Colors.white70),
],
),
),
);
}
}

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Bouton de soumission avec un gradient visuel et des ombres
/// Utilisé pour l'envoi d'un formulaire d'événement
class SubmitButton extends StatelessWidget { class SubmitButton extends StatelessWidget {
/// Fonction à exécuter lors de l'appui sur le bouton
final VoidCallback onPressed; final VoidCallback onPressed;
const SubmitButton({Key? key, required this.onPressed}) : super(key: key); const SubmitButton({Key? key, required this.onPressed}) : super(key: key);
@@ -8,11 +11,12 @@ class SubmitButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
// Décoration du bouton avec un dégradé de couleurs et une ombre
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( gradient: const LinearGradient(
colors: [ colors: [
Color(0xFF1DBF73), // Start of the gradient Color(0xFF1DBF73), // Dégradé vert clair
Color(0xFF11998E), // End of the gradient Color(0xFF11998E), // Dégradé vert foncé
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -22,18 +26,18 @@ class SubmitButton extends StatelessWidget {
color: Colors.black.withOpacity(0.2), color: Colors.black.withOpacity(0.2),
spreadRadius: 2, spreadRadius: 2,
blurRadius: 8, blurRadius: 8,
offset: const Offset(2, 4), // Shadow position offset: const Offset(2, 4), // Position de l'ombre
), ),
], ],
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed, onPressed: onPressed, // Appel de la fonction passée en paramètre
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent, // Button background is transparent to show gradient backgroundColor: Colors.transparent, // Fond transparent pour voir le dégradé
shadowColor: Colors.transparent, // Remove the default shadow shadowColor: Colors.transparent, // Suppression de l'ombre par défaut
padding: const EdgeInsets.symmetric(vertical: 14.0), padding: const EdgeInsets.symmetric(vertical: 14.0),
minimumSize: const Size(double.infinity, 50), // Bigger button size minimumSize: const Size(double.infinity, 50), // Taille du bouton
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
@@ -41,10 +45,10 @@ class SubmitButton extends StatelessWidget {
child: const Text( child: const Text(
'Créer l\'événement', 'Créer l\'événement',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white, // Couleur du texte
fontSize: 16, // Increase font size fontSize: 16, // Taille du texte
fontWeight: FontWeight.bold, // Bold text fontWeight: FontWeight.bold, // Texte en gras
letterSpacing: 1.2, // Spacing between letters letterSpacing: 1.2, // Espacement entre les lettres
), ),
), ),
), ),

View File

@@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
class TitleField extends StatelessWidget {
final FormFieldSetter<String> onSaved;
const TitleField({Key? key, required this.onSaved}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Titre',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.title, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un titre';
}
return null;
},
onSaved: onSaved,
);
}
}

View File

@@ -788,10 +788,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:

View File

@@ -23,7 +23,7 @@ dependencies:
encrypt: ^5.0.0 encrypt: ^5.0.0
provider: ^6.0.0 provider: ^6.0.0
flare_flutter: ^3.0.0 flare_flutter: ^3.0.0
logger: ^1.1.0 logger: ^1.4.0
flutter_spinkit: ^5.1.0 flutter_spinkit: ^5.1.0
flutter_vibrate: ^1.3.0 flutter_vibrate: ^1.3.0
loading_icon_button: ^0.0.6 loading_icon_button: ^0.0.6
@@ -33,6 +33,7 @@ dependencies:
google_maps_flutter: ^2.9.0 google_maps_flutter: ^2.9.0
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
# State management with flutter_bloc # State management with flutter_bloc
flutter_bloc: ^8.0.9 flutter_bloc: ^8.0.9