Refactoring + Checkpoint

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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