Refactoring + Checkpoint
This commit is contained in:
@@ -62,6 +62,7 @@ class AppRouter {
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
userFirstName: userFirstName,
|
userFirstName: userFirstName,
|
||||||
userLastName: userLastName,
|
userLastName: userLastName,
|
||||||
|
profileImageUrl: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
onTap: () {
|
||||||
maxLines: 3,
|
setState(() {
|
||||||
overflow: TextOverflow.ellipsis,
|
_isExpanded = !_isExpanded; // Change l'état d'expansion.
|
||||||
|
});
|
||||||
|
_logger.i("Changement d'état d'expansion pour la description de l'événement ${widget.event.id}");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
_isExpanded || !shouldTruncate
|
||||||
|
? descriptionText
|
||||||
|
: "${descriptionText.substring(0, _descriptionThreshold)}...",
|
||||||
|
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
maxLines: _isExpanded ? null : 3,
|
||||||
|
overflow: _isExpanded ? TextOverflow.visible : TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
if (shouldTruncate) // Bouton "Afficher plus" si la description est longue.
|
||||||
EventImage(imageUrl: event.imageUrl),
|
GestureDetector(
|
||||||
const Divider(color: Colors.white24),
|
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 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']!,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,40 +106,69 @@ 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)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
print('Voir les détails');
|
||||||
Text(
|
// Log d'action pour suivre l'interaction utilisateur
|
||||||
'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(
|
_buildElegantMenuItem(
|
||||||
value: 'edit',
|
icon: Icons.edit,
|
||||||
child: Row(
|
label: 'Modifier l\'événement',
|
||||||
children: [
|
color: AppColors.secondary, // Utilise la couleur secondaire dynamique
|
||||||
Icon(Icons.edit, color: Colors.orange.shade400, size: 18),
|
onTap: () {
|
||||||
const SizedBox(width: 10),
|
print('Modifier l\'événement');
|
||||||
Text(
|
},
|
||||||
'Modifier l\'événement',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.orange.shade700,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
_buildElegantMenuItem(
|
||||||
value: 'delete',
|
icon: Icons.delete_outline,
|
||||||
child: Row(
|
label: 'Supprimer l\'événement',
|
||||||
children: [
|
color: AppColors.errorColor, // Utilise la couleur d'erreur dynamique
|
||||||
Icon(Icons.delete_outline, color: Colors.red.shade400, size: 18),
|
onTap: () {
|
||||||
const SizedBox(width: 10),
|
_showDeleteConfirmation(context);
|
||||||
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
|
elevation: 12.0, // Niveau d'élévation du menu pour une ombre modérée
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10.0), // Ajout de bordures arrondies
|
borderRadius: BorderRadius.circular(20.0), // Coins arrondis pour un look moderne
|
||||||
side: BorderSide(color: Colors.grey.shade300), // Bordure fine et douce
|
|
||||||
),
|
),
|
||||||
color: Colors.white, // Fond blanc pur pour un contraste élégant
|
color: AppColors.customBackgroundColor, // Surface dynamique selon le thème
|
||||||
).then((value) {
|
).then((value) {
|
||||||
// Gérer les actions en fonction de la sélection
|
if (value != null) {
|
||||||
if (value == 'details') {
|
HapticFeedback.lightImpact(); // Retour haptique pour une meilleure UX
|
||||||
print('Voir les détails');
|
|
||||||
} else if (value == 'edit') {
|
|
||||||
print('Modifier l\'événement');
|
|
||||||
} else if (value == 'delete') {
|
|
||||||
print('Supprimer l\'événement');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
26
lib/presentation/widgets/fields/accessibility_field.dart
Normal file
26
lib/presentation/widgets/fields/accessibility_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/presentation/widgets/fields/attendees_field.dart
Normal file
80
lib/presentation/widgets/fields/attendees_field.dart
Normal 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
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
lib/presentation/widgets/fields/category_field.dart
Normal file
192
lib/presentation/widgets/fields/category_field.dart
Normal 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
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/presentation/widgets/fields/description_field.dart
Normal file
79
lib/presentation/widgets/fields/description_field.dart
Normal 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
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/presentation/widgets/fields/link_field.dart
Normal file
63
lib/presentation/widgets/fields/link_field.dart
Normal 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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
lib/presentation/widgets/fields/location_field.dart
Normal file
72
lib/presentation/widgets/fields/location_field.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/presentation/widgets/fields/organizer_field.dart
Normal file
54
lib/presentation/widgets/fields/organizer_field.dart
Normal 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.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/presentation/widgets/fields/parking_field.dart
Normal file
26
lib/presentation/widgets/fields/parking_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/presentation/widgets/fields/participation_fee_field.dart
Normal file
27
lib/presentation/widgets/fields/participation_fee_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/presentation/widgets/fields/privacy_rules_field.dart
Normal file
26
lib/presentation/widgets/fields/privacy_rules_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/presentation/widgets/fields/security_protocol_field.dart
Normal file
26
lib/presentation/widgets/fields/security_protocol_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/presentation/widgets/fields/tags_field.dart
Normal file
89
lib/presentation/widgets/fields/tags_field.dart
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/presentation/widgets/fields/title_field.dart
Normal file
54
lib/presentation/widgets/fields/title_field.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/presentation/widgets/fields/transport_info_field.dart
Normal file
54
lib/presentation/widgets/fields/transport_info_field.dart
Normal 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.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
// Affichage de l'image de l'ami avec animation `Hero`
|
children: [
|
||||||
Hero(
|
// Animation Hero pour une transition fluide lors de la navigation
|
||||||
tag: friendId, // Tag unique pour l'animation Hero basée sur l'ID de l'ami
|
Hero(
|
||||||
child: CircleAvatar(
|
tag: friendId,
|
||||||
radius: 50,
|
child: AnimatedContainer(
|
||||||
backgroundImage: imageProvider,
|
duration: const Duration(milliseconds: 300),
|
||||||
backgroundColor: Colors.grey.shade800,
|
curve: Curves.easeInOut,
|
||||||
onBackgroundImageError: (error, stackTrace) {
|
child: CircleAvatar(
|
||||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $name (ID: $friendId): $error');
|
radius: 80,
|
||||||
},
|
backgroundImage: imageProvider,
|
||||||
child: imageUrl.isEmpty
|
backgroundColor: Colors.grey.shade800,
|
||||||
? const Icon(Icons.person, size: 50, color: Colors.white) // Icône par défaut si aucune image n'est disponible
|
onBackgroundImageError: (error, stackTrace) {
|
||||||
: null,
|
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $friendFirstName (ID: $friendId): $error');
|
||||||
),
|
},
|
||||||
),
|
child: imageUrl.isEmpty
|
||||||
const SizedBox(height: 16), // Espacement entre l'image et le texte
|
? const Icon(Icons.person, size: 60, color: Colors.white)
|
||||||
|
: null,
|
||||||
// Affichage du nom de l'ami
|
),
|
||||||
Text(
|
|
||||||
name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24, // Taille de la police pour le nom
|
|
||||||
fontWeight: FontWeight.bold, // Texte en gras
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20), // Espacement avant le bouton
|
|
||||||
|
|
||||||
// Bouton pour envoyer un message à l'ami
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
_logger.i('[LOG] Envoi d\'un message à $name (ID: $friendId)');
|
|
||||||
// Logique future pour envoyer un message à l'ami
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.message),
|
|
||||||
label: const Text('Envoyer un message'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.teal, // Couleur de fond du bouton
|
|
||||||
foregroundColor: Colors.white, // Couleur du texte et de l'icône
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
],
|
|
||||||
|
// Affichage du nom de l'ami avec une meilleure hiérarchie visuelle
|
||||||
|
Text(
|
||||||
|
'$friendFirstName $friendLastName',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
status.name.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: status == FriendStatus.accepted
|
||||||
|
? Colors.green.shade400
|
||||||
|
: status == FriendStatus.pending
|
||||||
|
? Colors.orange.shade400
|
||||||
|
: Colors.red.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Affichage des informations supplémentaires
|
||||||
|
_buildInfoRow('Dernière interaction:', lastInteraction),
|
||||||
|
_buildInfoRow('Date d\'ajout:', dateAdded),
|
||||||
|
|
||||||
|
const SizedBox(height: 30), // Espacement avant le bouton
|
||||||
|
|
||||||
|
// Bouton pour envoyer un message à l'ami avec animation
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_logger.i('[LOG] Envoi d\'un message à $friendFirstName (ID: $friendId)');
|
||||||
|
// Logique future pour envoyer un message à l'ami
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.message),
|
||||||
|
label: const Text('Envoyer un message'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.teal, // Couleur du bouton
|
||||||
|
foregroundColor: Colors.white, // Couleur du texte et icône
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30), // Coins arrondis
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
elevation: 5, // Ombre pour effet de survol
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widget réutilisable pour afficher une ligne d'information avec un texte d'introduction et une valeur.
|
||||||
|
Widget _buildInfoRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white70, // Couleur plus claire pour les valeurs
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user