refactoring

This commit is contained in:
DahoudG
2024-11-02 15:27:26 +00:00
parent 8e625c1080
commit 9cf96b7acf
44 changed files with 2281 additions and 354 deletions

View File

@@ -77,6 +77,7 @@ class EventCard extends StatelessWidget {
menuKey: menuKey,
menuContext: context,
location: event.location,
onClose: () { },
),
const Divider(color: Colors.white24),
Row(

View File

@@ -1,9 +1,9 @@
import 'package:afterwork/presentation/screens/event/event_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:afterwork/data/models/event_model.dart';
import 'package:afterwork/presentation/screens/event/event_card.dart';
import '../dialogs/add_event_dialog.dart';
import '../../state_management/event_bloc.dart';
import '../dialogs/add_event_dialog.dart';
class EventScreen extends StatefulWidget {
final String userId;
@@ -42,7 +42,8 @@ class _EventScreenState extends State<EventScreen> {
backgroundColor: const Color(0xFF1E1E2C),
actions: [
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 28, color: Color(0xFF1DBF73)),
icon: const Icon(Icons.add_circle_outline,
size: 28, color: Color(0xFF1DBF73)),
onPressed: () {
// Naviguer vers une nouvelle page pour ajouter un événement
Navigator.push(
@@ -95,7 +96,6 @@ class _EventScreenState extends State<EventScreen> {
},
status: event.status,
);
},
);
} else if (state is EventError) {

View File

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../data/providers/friends_provider.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.
class FriendsScreen extends StatefulWidget {
final String userId; // Identifiant de l'utilisateur pour récupérer ses amis
const FriendsScreen({Key? key, required this.userId}) : super(key: key);
@override
_FriendsScreenState createState() => _FriendsScreenState();
}
class _FriendsScreenState extends State<FriendsScreen> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
// Initialisation du contrôleur de défilement pour la gestion de la pagination.
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
// 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
Provider.of<FriendsProvider>(context, listen: false).fetchFriends(widget.userId);
}
@override
void dispose() {
// Nettoyage du contrôleur de défilement pour éviter les fuites de mémoire.
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
debugPrint("[LOG] Dispose : contrôleur de défilement supprimé");
}
/// Méthode déclenchée lors du défilement de la liste.
/// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis.
void _onScroll() {
final provider = Provider.of<FriendsProvider>(context, listen: false);
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent &&
!provider.isLoading && provider.hasMore) {
debugPrint("[LOG] Scroll : fin de liste atteinte, chargement de la page suivante");
// Charger plus d'amis si on atteint la fin de la liste
provider.fetchFriends(widget.userId);
}
}
@override
Widget build(BuildContext context) {
// Accès au fournisseur pour gérer les données et les états des amis.
final friendsProvider = Provider.of<FriendsProvider>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: const Text('Mes Amis'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
// Log de l'action de rafraîchissement
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
// Rafraîchir la liste des amis
friendsProvider.fetchFriends(widget.userId);
},
),
],
),
body: SafeArea(
child: Column(
children: [
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
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.
if (friendsProvider.isLoading && friendsProvider.friendsList.isEmpty) {
debugPrint("[LOG] Chargement : affichage de l'indicateur de progression");
return const Center(child: CircularProgressIndicator());
}
// Si la liste est vide après le chargement, afficher un message indiquant qu'aucun ami n'a été trouvé.
if (friendsProvider.friendsList.isEmpty) {
debugPrint("[LOG] Liste vide : Aucun ami trouvé");
return const Center(
child: Text('Aucun ami trouvé'),
);
}
// 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,
crossAxisSpacing: 10,
),
itemCount: friendsProvider.friendsList.length,
itemBuilder: (context, index) {
final friend = friendsProvider.friendsList[index];
debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}");
return FriendsCircle(
friend: friend,
onTap: () {
// Log pour l'action de visualisation des détails d'un ami
debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}");
// Naviguer vers l'écran des détails de l'ami
FriendDetailScreen.open(
context,
friend.friendId,
friend.firstName ?? 'Ami inconnu',
friend.imageUrl ?? '',
);
},
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../assets/animations/friend_expanding_card.dart';
import '../../../data/providers/friends_provider.dart';
import '../../../domain/entities/friend.dart';
import '../../widgets/friend_detail_screen.dart';
import '../../widgets/friends_appbar.dart';
import '../../widgets/search_friends.dart';
/// [FriendsScreenWithProvider] est un écran qui affiche la liste des amis.
/// Il utilise le provider [FriendsProvider] pour gérer les états et les données.
/// Chaque action est loguée pour permettre une traçabilité complète.
class FriendsScreenWithProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: FriendsAppBar(),
body: SafeArea(
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: SearchFriends(),
),
Expanded(
child: Consumer<FriendsProvider>(
builder: (context, friendsProvider, _) {
final friends = friendsProvider.friendsList;
if (friends.isEmpty) {
return const Center(
child: Text(
'Aucun ami trouvé',
style: TextStyle(color: Colors.white70),
),
);
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];
return Dismissible(
key: Key(friend.friendId),
background: Container(
color: Colors.redAccent,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
debugPrint("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}");
friendsProvider.removeFriend(friend.friendId);
},
child: FriendExpandingCard(
name: friend.firstName ?? 'Ami inconnu',
imageUrl: friend.imageUrl ?? '',
description: "Amis depuis ${friend.friendId}",
onTap: () => _navigateToFriendDetail(context, friend),
onMessageTap: () {
debugPrint("[LOG] Envoi d'un message à l'ami : ${friend.firstName ?? 'Ami inconnu'}");
},
onRemoveTap: () {
debugPrint("[LOG] Tentative de suppression de l'ami : ${friend.firstName ?? 'Ami inconnu'}");
friendsProvider.removeFriend(friend.friendId);
},
),
);
},
);
},
),
),
],
),
),
);
}
/// Navigue vers l'écran des détails de l'utilisateur (ami) récupéré via son `friendId`.
void _navigateToFriendDetail(BuildContext context, Friend friend) {
debugPrint("[LOG] Navigation vers les détails de l'ami : ${friend.firstName ?? 'Ami inconnu'}");
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => FriendDetailScreen(
name: friend.firstName ?? 'Ami inconnu',
imageUrl: friend.imageUrl ?? '',
friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets
),
));
}
}

View File

@@ -6,17 +6,17 @@ 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'; // Importez l'écran de notifications
import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; // Écran de notifications
import '../../../core/constants/colors.dart';
import '../../../core/theme/theme_provider.dart'; // Pour basculer le thème
import '../../../core/theme/theme_provider.dart';
import '../friends/friends_screen.dart'; // Écran des amis
class HomeScreen extends StatefulWidget {
final EventRemoteDataSource eventRemoteDataSource;
final String userId;
final String userName;
final String userLastName;
final String userProfileImage; // Ajouter un champ pour l'image de profil de l'utilisateur
final String userProfileImage; // Image de profil de l'utilisateur
const HomeScreen({
Key? key,
@@ -69,54 +69,53 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
backgroundColor: AppColors.backgroundColor, // Gère dynamiquement la couleur d'arrière-plan
backgroundColor: AppColors.backgroundColor,
floating: true,
pinned: true,
snap: true,
elevation: 2, // Réduction de l'élévation pour un design plus léger
elevation: 2,
leading: Padding(
padding: const EdgeInsets.all(4.0), // Réduction du padding
padding: const EdgeInsets.all(4.0), // Ajustement du padding
child: Image.asset(
'lib/assets/images/logo.png',
height: 40, // Taille réduite du logo
height: 40, // Taille ajustée du logo
),
),
actions: [
_buildActionIcon(Icons.add, 'Publier', context),
_buildActionIcon(Icons.search, 'Rechercher', context),
_buildActionIcon(Icons.message, 'Message', context),
_buildNotificationsIcon(context, 45),
_buildNotificationsIcon(context, 5), // Gérer la logique des notifications ici
// Ajout du bouton pour basculer entre les thèmes
// Bouton pour basculer entre les thèmes
Switch(
value: themeProvider.isDarkMode,
onChanged: (value) {
themeProvider.toggleTheme(); // Bascule le thème lorsqu'on clique
themeProvider.toggleTheme(); // Changer le thème
},
activeColor: AppColors.accentColor,
),
],
bottom: TabBar(
controller: _tabController,
indicatorColor: AppColors.lightPrimary, // Tab active en bleu
indicatorColor: AppColors.lightPrimary,
labelStyle: const TextStyle(
fontSize: 12, // Réduction de la taille du texte des onglets
fontSize: 12, // Taille réduite du texte
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: const TextStyle(
fontSize: 11, // Réduction pour les onglets non sélectionnés
fontSize: 11, // Taille ajustée pour les onglets non sélectionnés
),
// Changement des couleurs pour les tabs non sélectionnées et sélectionnées
labelColor: AppColors.lightPrimary, // Tab active en bleu
unselectedLabelColor: AppColors.iconSecondary, // Tabs non sélectionnées en blanc
labelColor: AppColors.lightPrimary,
unselectedLabelColor: AppColors.iconSecondary,
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.notifications, size: 24), text: 'Notifications'),
_buildProfileTab(),
const Tab(icon: Icon(Icons.people_alt_outlined, size: 24), text: 'Ami(e)s'),
_buildProfileTab(), // Onglet profil
],
),
),
@@ -133,7 +132,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
),
const EstablishmentsScreen(),
const SocialScreen(),
const NotificationsScreen(),
FriendsScreen(userId: widget.userId), // Correction ici : passer l'userId
const ProfileScreen(),
],
),
@@ -141,27 +140,26 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
);
}
// Widget pour afficher la photo de profil de l'utilisateur dans l'onglet
// 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, // Définir la couleur de la bordure ici
color: Colors.blue,
width: 2.0,
),
),
child: CircleAvatar(
radius: 16, // Ajustez la taille si nécessaire
backgroundColor: Colors.grey[200], // Couleur de fond pour le cas où l'image ne charge pas
radius: 16,
backgroundColor: Colors.grey[200], // Couleur de fond par défaut
child: ClipOval(
child: FadeInImage.assetNetwork(
placeholder: 'lib/assets/images/user_placeholder.png', // Chemin de l'image par défaut
placeholder: 'lib/assets/images/user_placeholder.png',
image: widget.userProfileImage,
fit: BoxFit.cover,
imageErrorBuilder: (context, error, stackTrace) {
// Si l'image ne charge pas, afficher une image par défaut
return Image.asset('lib/assets/images/profile_picture.png', fit: BoxFit.cover);
},
),
@@ -171,12 +169,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
);
}
// Widget pour afficher l'icône de notifications avec un badge si nécessaire
// 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 de positionner le badge en dehors des limites du Stack
clipBehavior: Clip.none, // Permet d'afficher le badge en dehors des limites
children: [
CircleAvatar(
backgroundColor: AppColors.surface,
@@ -184,7 +182,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
child: IconButton(
icon: const Icon(Icons.notifications, color: AppColors.darkOnPrimary, size: 20),
onPressed: () {
// Rediriger vers l'écran des notifications
Navigator.push(
context,
MaterialPageRoute(
@@ -194,7 +191,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
},
),
),
// Affiche le badge si le nombre de notifications est supérieur à 0
if (notificationCount > 0)
Positioned(
right: -6,
@@ -202,7 +198,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red, // Couleur du badge
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
@@ -210,7 +206,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
minHeight: 18,
),
child: Text(
notificationCount > 99 ? '99+' : '$notificationCount', // Affiche "99+" si le nombre dépasse 99
notificationCount > 99 ? '99+' : '$notificationCount',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
@@ -225,14 +221,15 @@ 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), // Réduction de l'espacement
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: CircleAvatar(
backgroundColor: AppColors.surface,
radius: 18, // Réduction de la taille des avatars
radius: 18,
child: IconButton(
icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), // Taille réduite de l'icône
icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20),
onPressed: () {
_onMenuSelected(context, label);
},

View File

@@ -10,13 +10,14 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:http/http.dart' as http;
import 'package:loading_icon_button/loading_icon_button.dart';
import 'package:provider/provider.dart';
import '../../../core/errors/exceptions.dart';
import '../../../core/theme/theme_provider.dart';
import '../../../data/datasources/event_remote_data_source.dart';
import '../signup/SignUpScreen.dart';
/// Écran de connexion pour l'application AfterWork.
/// Ce fichier contient des fonctionnalités comme la gestion de la connexion,
/// l'authentification avec mot de passe en clair, la gestion des erreurs et un thème jour/nuit.
/// L'écran de connexion où les utilisateurs peuvent s'authentifier.
/// Toutes les actions sont loguées pour permettre un suivi dans le terminal et détecter les erreurs.
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -24,19 +25,21 @@ class LoginScreen extends StatefulWidget {
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>(); // Clé pour valider le formulaire de connexion.
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
// Clé globale pour la validation du formulaire
final _formKey = GlobalKey<FormState>();
// Champs utilisateur
String _email = ''; // Email de l'utilisateur
String _password = ''; // Mot de passe de l'utilisateur
// Variables pour stocker l'email et le mot de passe saisis par l'utilisateur
String _email = '';
String _password = '';
// États de gestion
bool _isPasswordVisible = false; // Pour afficher/masquer le mot de passe
bool _isSubmitting = false; // Indicateur pour l'état de soumission du formulaire
bool _showErrorMessage = false; // Affichage des erreurs
// États de l'écran
bool _isPasswordVisible = false; // Pour basculer la visibilité du mot de passe
bool _isSubmitting = false; // Indique si la soumission du formulaire est en cours
bool _showErrorMessage = false; // Affiche un message d'erreur si nécessaire
// Services pour les opérations
// Sources de données et services
final UserRemoteDataSource _userRemoteDataSource = UserRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
final PreferencesHelper _preferencesHelper = PreferencesHelper();
@@ -44,7 +47,7 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
// Contrôleur pour le bouton de chargement
final _btnController = LoadingButtonController();
// Contrôleur d'animation pour la transition des écrans
// Contrôleur d'animation pour gérer la transition entre les écrans
late AnimationController _animationController;
@override
@@ -54,25 +57,25 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
vsync: this,
duration: const Duration(milliseconds: 500),
);
print("Contrôleur d'animation initialisé.");
debugPrint("[LOG] Contrôleur d'animation initialisé.");
}
@override
void dispose() {
_animationController.dispose();
print("Ressources d'animation libérées.");
debugPrint("[LOG] Ressources d'animation libérées.");
super.dispose();
}
/// Fonction pour basculer la visibilité du mot de passe
/// Bascule la visibilité du mot de passe et logue l'état actuel.
void _togglePasswordVisibility() {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
print("Visibilité du mot de passe basculée: $_isPasswordVisible");
debugPrint("[LOG] Visibilité du mot de passe basculée: $_isPasswordVisible");
}
/// Fonction pour afficher un toast via FlutterToast
/// Affiche un toast avec le message spécifié et logue l'action.
void _showToast(String message) {
Fluttertoast.showToast(
msg: message,
@@ -83,60 +86,79 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
textColor: Colors.white,
fontSize: 16.0,
);
debugPrint("[LOG] Toast affiché : $message");
}
/// Fonction soumettre le formulaire
/// Soumet le formulaire de connexion et tente d'authentifier l'utilisateur.
/// Toutes les étapes et erreurs sont loguées pour une traçabilité complète.
Future<void> _submit() async {
print("Tentative de soumission du formulaire de connexion.");
debugPrint("[LOG] Tentative de soumission du formulaire de connexion.");
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitting = true;
_showErrorMessage = false;
});
_formKey.currentState!.save();
_formKey.currentState!.save(); // Sauvegarde des données saisies
try {
_btnController.start();
_btnController.start(); // Démarre l'animation de chargement du bouton
debugPrint("[LOG] Appel à l'API pour authentifier l'utilisateur.");
// Appel à l'API pour authentifier l'utilisateur
final UserModel user = await _userRemoteDataSource.authenticateUser(_email, _password);
if (user == null) {
throw Exception("L'utilisateur n'a pas été trouvé ou l'authentification a échoué.");
}
print("Utilisateur authentifié : ${user.userId}");
await _secureStorage.saveUserId(user.userId);
await _preferencesHelper.saveUserName(user.nom);
await _preferencesHelper.saveUserLastName(user.prenoms);
_showToast("Connexion réussie !");
// Validation de l'ID utilisateur
if (user.userId.isNotEmpty) {
debugPrint("[LOG] Utilisateur authentifié avec succès. ID: ${user.userId}");
// Navigation vers la page d'accueil
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(
eventRemoteDataSource: EventRemoteDataSource(http.Client()),
userId: user.userId,
userName: user.nom,
userLastName: user.prenoms,
userProfileImage: 'lib/assets/images/profile_picture.png',
// Sauvegarde des informations utilisateur
await _secureStorage.saveUserId(user.userId);
await _preferencesHelper.saveUserName(user.nom);
await _preferencesHelper.saveUserLastName(user.prenoms);
_showToast("Connexion réussie !");
// Redirection vers l'écran d'accueil
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(
userId: user.userId,
userName: user.nom,
userLastName: user.prenoms,
userProfileImage: 'lib/assets/images/profile_picture.png',
eventRemoteDataSource: EventRemoteDataSource(http.Client()),
),
),
),
);
);
} else {
debugPrint("[ERROR] L'ID utilisateur est manquant dans la réponse.");
_showToast("Erreur : ID utilisateur manquant.");
}
} catch (e) {
print("Erreur lors de l'authentification : $e");
_btnController.error();
_showToast("Erreur lors de la connexion : ${e.toString()}");
// Gestion des erreurs spécifiques et log de chaque type d'erreur
if (e is ServerExceptionWithMessage) {
debugPrint("[ERROR] Erreur serveur : ${e.message}");
_showToast("Erreur serveur : ${e.message}");
} else if (e is UnauthorizedException) {
debugPrint("[ERROR] Erreur d'authentification : ${e.message}");
_showToast("Erreur : ${e.message}");
} else {
debugPrint("[ERROR] Erreur lors de la connexion : $e");
_showToast("Erreur lors de la connexion : ${e.toString()}");
}
_btnController.error(); // Affiche une erreur sur le bouton
setState(() {
_showErrorMessage = true;
});
} finally {
_btnController.reset();
_btnController.reset(); // Réinitialise l'état du bouton
setState(() {
_isSubmitting = false;
_isSubmitting = false; // Réinitialise l'état de chargement
});
}
} else {
print("Échec de validation du formulaire.");
debugPrint("[ERROR] Validation du formulaire échouée.");
_btnController.reset();
_showToast("Veuillez vérifier les informations saisies.");
}
@@ -152,19 +174,21 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
return Scaffold(
body: Stack(
children: [
// Arrière-plan animé
AnimatedContainer(
duration: const Duration(seconds: 3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.secondary
theme.colorScheme.secondary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// Spinner de chargement lors de la soumission
if (_isSubmitting)
const Center(
child: SpinKitFadingCircle(
@@ -172,6 +196,7 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
size: 50.0,
),
),
// Icône de changement de thème
Positioned(
top: 40,
right: 20,
@@ -182,10 +207,11 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
),
onPressed: () {
themeProvider.toggleTheme();
print("Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}");
debugPrint("[LOG] Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}");
},
),
),
// Formulaire de connexion
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
@@ -209,18 +235,18 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
icon: Icons.email,
validator: (value) {
if (value == null || value.isEmpty) {
print("Erreur : champ email vide.");
debugPrint("[ERROR] Champ email vide.");
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
print("Erreur : email invalide.");
debugPrint("[ERROR] Email invalide.");
return 'Veuillez entrer un email valide';
}
return null;
},
onSaved: (value) {
_email = value!;
print("Email enregistré : $_email");
debugPrint("[LOG] Email enregistré : $_email");
},
),
const SizedBox(height: 20),
@@ -230,27 +256,25 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
obscureText: !_isPasswordVisible,
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
color: theme.iconTheme.color,
),
onPressed: _togglePasswordVisibility,
),
validator: (value) {
if (value == null || value.isEmpty) {
print("Erreur : champ mot de passe vide.");
debugPrint("[ERROR] Champ mot de passe vide.");
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
print("Erreur : mot de passe trop court.");
debugPrint("[ERROR] Mot de passe trop court.");
return 'Le mot de passe doit comporter au moins 6 caractères';
}
return null;
},
onSaved: (value) {
_password = value!;
print("Mot de passe enregistré.");
debugPrint("[LOG] Mot de passe enregistré.");
},
),
const SizedBox(height: 30),
@@ -272,28 +296,26 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
const SizedBox(height: 20),
TextButton(
onPressed: () {
print("Redirection vers la page d'inscription");
debugPrint("[LOG] Redirection vers la page d'inscription.");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignUpScreen(),
builder: (context) => const SignUpScreen(),
),
);
},
child: Text(
'Pas encore de compte ? Inscrivez-vous',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70),
),
),
TextButton(
onPressed: () {
print("Mot de passe oublié");
debugPrint("[LOG] Mot de passe oublié cliqué.");
},
child: Text(
'Mot de passe oublié ?',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70),
),
),
if (_showErrorMessage)
@@ -326,9 +348,8 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
),
if (isKeyboardVisible)
Text(
'© 2024 LionsDev',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
'© 2024',
style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70),
textAlign: TextAlign.center,
),
],
@@ -339,7 +360,7 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
);
}
/// Widget réutilisable pour les champs de texte avec validation et design amélioré
/// Méthode pour construire les champs de formulaire avec les styles adaptés.
Widget _buildTextFormField({
required String label,
required IconData icon,

View File

@@ -89,9 +89,6 @@ class _SignUpScreenState extends State<SignUpScreen> {
// Envoi des informations pour créer un nouvel utilisateur
final createdUser = await _userRemoteDataSource.createUser(user);
if (createdUser == null) {
throw Exception("La création du compte a échoué.");
}
print("Utilisateur créé : ${createdUser.userId}");

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
import '../../../data/models/social_post_model.dart';
import '../../widgets/social_header_widget.dart';
import '../../widgets/social_interaction_row.dart';
import '../../widgets/swipe_background.dart'; // Import du widget de swipe
class SocialCard extends StatelessWidget {
final SocialPost post;
final VoidCallback onLike;
final VoidCallback onComment;
final VoidCallback onShare;
final VoidCallback onDeletePost;
final VoidCallback onEditPost;
const SocialCard({
Key? key,
required this.post,
required this.onLike,
required this.onComment,
required this.onShare,
required this.onDeletePost,
required this.onEditPost,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(post.postText),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
onDeletePost();
},
background: SwipeBackground(
color: Colors.red,
icon: Icons.delete,
label: 'Supprimer',
),
child: Card(
color: AppColors.cardColor,
margin: const EdgeInsets.symmetric(vertical: 10.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SocialHeaderWidget(
post: post,
onEditPost: () {
print('Modifier le post');
},
menuKey: GlobalKey(),
menuContext: context,
onClosePost: () {
print('Close post');
},
),
const SizedBox(height: 8),
Text (
post.postText,
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
const SizedBox(height: 8),
if (post.postImage.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(post.postImage, fit: BoxFit.cover),
),
const SizedBox(height: 8),
Row(
children: post.tags
.map((tag) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
tag,
style: TextStyle(
color: AppColors.accentColor,
fontSize: 12,
),
),
))
.toList(),
),
const SizedBox(height: 8),
SocialInteractionRow(
post: post,
onLike: onLike,
onComment: onComment,
onShare: onShare,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import '../../../data/models/social_post_model.dart';
import 'social_card.dart'; // Import de la SocialCard
class SocialContent extends StatefulWidget {
const SocialContent({super.key});
@override
_SocialContentState createState() => _SocialContentState();
}
class _SocialContentState extends State<SocialContent> {
final List<SocialPost> _posts = [
SocialPost(
userName: 'John Doe',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Une belle journée au parc avec des amis ! 🌳🌞',
postImage: 'lib/assets/images/placeholder.png',
likes: 12,
comments: 4,
badges: ['Explorer', 'Photographe'],
tags: ['#Nature', '#FunDay'],
shares: 25,
),
SocialPost(
userName: 'Jane Smith',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Mon nouveau chat est tellement mignon 🐱',
postImage: 'lib/assets/images/placeholder.png',
likes: 30,
comments: 8,
badges: ['Animal Lover', 'Partageur'],
tags: ['#Chat', '#Cuteness'],
shares: 25,
),
SocialPost(
userName: 'Alice Brown',
userImage: 'lib/assets/images/profile_picture.png',
postText: 'Café du matin avec une vue magnifique ☕️',
postImage: 'lib/assets/images/placeholder.png',
likes: 45,
comments: 15,
badges: ['Gourmet', 'Partageur'],
tags: ['#Café', '#MorningVibes'],
shares: 25,
),
];
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return SocialCard(
post: post,
onLike: () {
setState(() {
_posts[index] = SocialPost(
userName: post.userName,
userImage: post.userImage,
postText: post.postText,
postImage: post.postImage,
likes: post.likes + 1,
comments: post.comments,
badges: post.badges,
tags: post.tags,
shares: post.shares + 1,
);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Like ajouté')),
);
},
onComment: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Commentaire ajouté')),
);
},
onShare: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post partagé')),
);
},
onDeletePost: () {
setState(() {
_posts.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post supprimé')),
);
},
onEditPost: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Post modifié')),
);
},
);
},
);
}
}

View File

@@ -1,19 +1,25 @@
import 'package:flutter/material.dart';
import 'social_content.dart'; // Import du fichier qui contient SocialContent
class SocialScreen extends StatelessWidget {
const SocialScreen({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Social',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
return Scaffold(
backgroundColor: const Color(0xFF1E1E2C), // Fond noir pour correspondre à un thème sombre
appBar: AppBar(
title: const Text(
'Social',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.black, // AppBar avec fond noir pour un design cohérent
),
body: SocialContent(), // Appel à SocialContent pour afficher le contenu
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:afterwork/core/utils/date_formatter.dart';
import 'event_status_badge.dart';
import 'event_menu.dart';
class EventHeader extends StatelessWidget {
@@ -8,9 +7,10 @@ class EventHeader extends StatelessWidget {
final String userLastName;
final String? eventDate;
final String? imageUrl;
final String location; // Ajout du paramètre "location" pour le lieu de l'événement
final String location;
final GlobalKey menuKey;
final BuildContext menuContext;
final VoidCallback onClose; // Ajout d'un callback pour l'action de fermeture
const EventHeader({
Key? key,
@@ -18,9 +18,10 @@ class EventHeader extends StatelessWidget {
required this.userLastName,
this.eventDate,
this.imageUrl,
required this.location, // Initialisation de "location"
required this.location,
required this.menuKey,
required this.menuContext,
required this.onClose, // Initialisation du callback de fermeture
}) : super(key: key);
@override
@@ -33,68 +34,85 @@ class EventHeader extends StatelessWidget {
}
String formattedDate = date != null ? DateFormatter.formatDate(date) : 'Date inconnue';
return Row(
return Stack(
children: [
CircleAvatar(
backgroundColor: Colors.grey.shade800,
backgroundImage: imageUrl != null && imageUrl!.isNotEmpty
? NetworkImage(imageUrl!)
: const AssetImage('lib/assets/images/placeholder.png') as ImageProvider,
radius: 22,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$userName $userLastName',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
formattedDate,
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
const SizedBox(height: 4),
// Utilisation de Row pour afficher le lieu sur la même ligne
Row(
Row(
children: [
CircleAvatar(
backgroundColor: Colors.grey.shade800,
backgroundImage: imageUrl != null && imageUrl!.isNotEmpty
? NetworkImage(imageUrl!)
: const AssetImage('lib/assets/images/placeholder.png') as ImageProvider,
radius: 22,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
location.isNotEmpty ? location : 'Lieu non spécifié',
style: const TextStyle(
color: Colors.white60,
fontSize: 12,
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Text(
'$userName $userLastName',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
formattedDate,
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
const SizedBox(width: 8),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(
location.isNotEmpty ? location : 'Lieu non spécifié',
style: const TextStyle(
color: Colors.white60,
fontSize: 12,
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
],
),
],
),
),
],
),
// Ajout des boutons dans le coin supérieur droit
Positioned(
top: 0,
right: 0,
child: Row(
children: [
IconButton(
key: menuKey,
icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20),
splashRadius: 20,
onPressed: () {
showEventOptions(menuContext, menuKey);
},
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 20),
splashRadius: 20,
onPressed: onClose, // Appel du callback de fermeture
),
],
),
),
IconButton(
key: menuKey,
icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20),
splashRadius: 20,
onPressed: () {
showEventOptions(menuContext, menuKey);
},
),
],
);
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
/// [FriendCard] est un widget représentant une carte d'ami.
/// Cette carte inclut l'image de l'ami, son nom, et un bouton qui permet
/// d'interagir avec cette carte (via le `onTap`).
///
/// Ce widget est conçu pour être utilisé dans des listes d'amis, comme
/// dans la section "Mes Amis" de l'application.
class FriendCard extends StatelessWidget {
final String name; // Le nom de l'ami
final String imageUrl; // URL de l'image de profil de l'ami
final VoidCallback onTap; // Fonction callback exécutée lors d'un clic sur la carte
/// Constructeur de [FriendCard] avec des paramètres obligatoires.
const FriendCard({
Key? key,
required this.name,
required this.imageUrl,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// Lorsque l'utilisateur clique sur la carte, on déclenche la fonction onTap.
debugPrint("[LOG] Carte de l'ami $name cliquée.");
onTap(); // Exécuter le callback fourni
},
child: Card(
elevation: 4, // Élévation de la carte pour donner un effet d'ombre
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), // Bordure arrondie
color: Colors.grey.shade800, // Couleur de fond de la carte
child: Padding(
padding: const EdgeInsets.all(12.0), // Padding interne pour espacer le contenu
child: Row(
children: [
// Image de profil de l'ami affichée sous forme de cercle
Hero(
tag: name, // Le tag Hero permet de créer une transition animée vers un autre écran.
child: CircleAvatar(
backgroundImage: NetworkImage(imageUrl), // Charger l'image depuis l'URL
radius: 30, // Taille de l'avatar
),
),
const SizedBox(width: 16), // Espacement entre l'image et le nom
// Le nom de l'ami avec un texte en gras et blanc
Expanded(
child: Text(
name,
style: const TextStyle(
fontSize: 18, // Taille de la police
color: Colors.white, // Couleur du texte
fontWeight: FontWeight.bold, // Style en gras
),
),
),
// Icône de flèche indiquant que la carte est cliquable
Icon(Icons.chevron_right, color: Colors.white70),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
/// [FriendDetailScreen] affiche les détails d'un ami, incluant son nom, son image de profil,
/// et une option pour envoyer un message.
///
/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails.
class FriendDetailScreen extends StatelessWidget {
final String name; // Nom 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 Logger _logger = Logger(); // Logger pour suivre les actions dans le terminal
/// Constructeur de la classe [FriendDetailScreen].
/// [name], [imageUrl], et [friendId] doivent être fournis.
FriendDetailScreen({
Key? key,
required this.name,
required this.imageUrl,
required this.friendId,
}) : super(key: key);
/// Méthode statique pour lancer l'écran des détails d'un ami.
static void open(BuildContext context, String friendId, String name, String imageUrl) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => FriendDetailScreen(
friendId: friendId,
name: name,
imageUrl: imageUrl,
),
),
);
}
@override
Widget build(BuildContext context) {
_logger.i('[LOG] Affichage des détails de l\'ami : $name (ID: $friendId)');
// Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide.
final imageProvider = imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true
? NetworkImage(imageUrl)
: const AssetImage('lib/assets/images/default_avatar.png') as ImageProvider;
return Scaffold(
appBar: AppBar(
title: Text(name), // Titre de l'écran affichant le nom de l'ami
backgroundColor: Colors.grey.shade800,
),
body: Padding(
padding: const EdgeInsets.all(16.0), // Espacement autour du contenu
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Affichage de l'image de l'ami avec animation `Hero`
Hero(
tag: friendId, // Tag unique pour l'animation Hero basée sur l'ID de l'ami
child: CircleAvatar(
radius: 50,
backgroundImage: imageProvider,
backgroundColor: Colors.grey.shade800,
onBackgroundImageError: (error, stackTrace) {
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $name (ID: $friendId): $error');
},
child: imageUrl.isEmpty
? const Icon(Icons.person, size: 50, color: Colors.white) // Icône par défaut si aucune image n'est disponible
: null,
),
),
const SizedBox(height: 16), // Espacement entre l'image et le texte
// 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),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
/// [FriendsAppBar] est une barre d'application personnalisée utilisée dans l'écran des amis.
/// Elle permet d'ajouter et de gérer les amis avec des actions spécifiques.
/// Toutes les actions sont loguées pour une traçabilité complète.
class FriendsAppBar extends StatelessWidget implements PreferredSizeWidget {
final Logger _logger = Logger(); // Logger pour tracer toutes les actions
FriendsAppBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: Colors.grey.shade800, // Couleur de fond de la barre d'application
title: const Text(
'Mes Amis', // Titre de l'écran
style: TextStyle(color: Colors.white), // Couleur du texte
),
actions: [
Tooltip(
message: 'Ajouter un ami', // Améliore l'accessibilité pour l'icône d'ajout
child: IconButton(
icon: const Icon(Icons.group_add, color: Colors.white), // Icône pour ajouter un nouvel ami
onPressed: () {
_logger.i("[LOG] Bouton 'Ajouter un ami' pressé.");
// Logique à implémenter pour ajouter un nouvel ami
},
),
),
Tooltip(
message: 'Gérer les groupes d\'amis', // Améliore l'accessibilité pour l'icône de gestion des groupes
child: IconButton(
icon: const Icon(Icons.group, color: Colors.white), // Icône pour gérer les groupes d'amis
onPressed: () {
_logger.i("[LOG] Bouton 'Gérer les groupes' pressé.");
// Logique à implémenter pour gérer les groupes d'amis
},
),
),
],
);
}
/// Définit la taille préférée de la barre d'application.
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import '../../domain/entities/friend.dart';
/// [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
/// ou de déclencher d'autres actions liées.
class FriendsCircle extends StatelessWidget {
final Friend friend; // Représente l'entité Friend à afficher (nom et image).
final VoidCallback onTap; // Fonction callback exécutée lorsque l'utilisateur clique sur l'avatar.
// Logger pour tracer les actions dans le terminal
final Logger _logger = Logger();
/// Constructeur pour [FriendsCircle], prenant en entrée un ami et une fonction de callback.
FriendsCircle({
Key? key,
required this.friend, // L'ami à afficher (doit inclure friendId, name, imageUrl).
required this.onTap, // Action à exécuter lors du clic.
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut.
String displayName = [friend.firstName, friend.lastName]
.where((namePart) => namePart != null && namePart.isNotEmpty)
.join(" ")
.trim();
if (displayName.isEmpty) {
displayName = 'Ami inconnu';
}
return GestureDetector(
onTap: () {
_logger.i('[LOG] Avatar de ${displayName.trim()} cliqué');
onTap(); // Exécute l'action de clic définie par l'utilisateur
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments de la colonne.
children: [
Hero(
tag: friend.friendId, // Tag unique pour l'animation Hero basé sur l'ID de l'ami.
child: CircleAvatar(
radius: 40,
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide
: AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // Utilise AssetImage pour l'avatar par défaut
onBackgroundImageError: (error, stackTrace) {
_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.
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.
Text(
displayName, // Affiche le nom de l'ami sous l'avatar ou une valeur par défaut.
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
/// [SearchFriends] est un widget permettant à l'utilisateur de rechercher des amis.
/// Il inclut un champ de texte stylisé pour saisir la requête de recherche.
/// Chaque modification du texte dans le champ génère un log dans le terminal pour suivre en temps réel l'activité.
class SearchFriends extends StatelessWidget {
const SearchFriends({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextField(
style: const TextStyle(
color: Colors.white, // Le texte saisi est de couleur blanche.
),
decoration: InputDecoration(
hintText: 'Rechercher un ami...', // Indication textuelle pour aider l'utilisateur.
hintStyle: const TextStyle(
color: Colors.white54, // Style de l'indicateur avec une couleur plus claire.
),
filled: true,
fillColor: Colors.grey.shade800, // Couleur de fond du champ de recherche.
prefixIcon: const Icon(
Icons.search, // Icône de loupe pour indiquer la recherche.
color: Colors.white54, // Couleur de l'icône de recherche.
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0), // Bordure arrondie pour un style moderne.
borderSide: BorderSide.none, // Aucune bordure visible pour un look propre.
),
),
onChanged: (value) {
// Fonction appelée chaque fois que l'utilisateur modifie le texte dans le champ de recherche.
debugPrint('[LOG] Recherche d\'amis : $value'); // Log de chaque saisie.
// Vous pouvez ajouter ici la logique de filtrage de la liste des amis en fonction de la recherche.
},
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
class BadgeWidget extends StatelessWidget {
final String badge;
final IconData? icon; // Optionnel : ajouter une icône au badge
const BadgeWidget({
Key? key,
required this.badge,
this.icon,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: AppColors.accentColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: AppColors.accentColor,
width: 1.0,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
color: AppColors.accentColor,
size: 16.0,
),
const SizedBox(width: 5),
],
Text(
badge,
style: TextStyle(
color: AppColors.accentColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
import '../../../data/models/social_post_model.dart';
import 'social_badge_widget.dart'; // Import du widget BadgeWidget
class SocialHeaderWidget extends StatelessWidget {
final SocialPost post;
final VoidCallback onEditPost;
final VoidCallback onClosePost; // Ajout du callback pour la fermeture du post
final GlobalKey menuKey;
final BuildContext menuContext;
const SocialHeaderWidget({
Key? key,
required this.post,
required this.onEditPost,
required this.onClosePost, // Initialisation du callback de fermeture
required this.menuKey,
required this.menuContext,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.grey.shade800,
backgroundImage: post.userImage.isNotEmpty
? AssetImage(post.userImage)
: const AssetImage('lib/assets/images/placeholder.png'),
radius: 22,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
post.userName,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Wrap(
spacing: 6,
children: post.badges
.map((badge) => BadgeWidget(badge: badge))
.toList(),
),
],
),
),
],
),
// Ajout des boutons dans le coin supérieur droit
Positioned(
top: 0,
right: 0,
child: Row(
mainAxisSize: MainAxisSize.min, // Réduit la taille du Row au minimum
children: [
IconButton(
key: menuKey,
icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20),
splashRadius: 20,
onPressed: () {
_showOptionsMenu(menuContext, menuKey);
},
),
const SizedBox(width: 4), // Espacement entre les boutons
IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 20),
splashRadius: 20,
onPressed: onClosePost, // Appel du callback de fermeture
),
],
),
),
],
);
}
void _showOptionsMenu(BuildContext context, GlobalKey menuKey) {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
color: AppColors.backgroundColor,
child: Wrap(
children: <Widget>[
ListTile(
leading: Icon(Icons.edit, color: AppColors.iconPrimary),
title: const Text('Modifier'),
onTap: () {
Navigator.of(context).pop();
onEditPost();
},
),
ListTile(
leading: Icon(Icons.delete, color: AppColors.iconPrimary),
title: const Text('Supprimer'),
onTap: () {
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
import '../../../data/models/social_post_model.dart';
class SocialInteractionRow extends StatelessWidget {
final SocialPost post;
final VoidCallback onLike;
final VoidCallback onComment;
final VoidCallback onShare;
const SocialInteractionRow({
Key? key,
required this.post,
required this.onLike,
required this.onComment,
required this.onShare,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: _buildIconButton(Icons.thumb_up_alt_outlined, 'Jaime', post.likes, onLike),
),
Expanded(
child: _buildIconButton(Icons.comment_outlined, 'Commentaires', post.comments, onComment),
),
Expanded(
child: _buildIconButton(Icons.share_outlined, 'Partages', post.shares, onShare),
),
],
);
}
Widget _buildIconButton(IconData icon, String label, int count, VoidCallback onPressed) {
return TextButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: AppColors.accentColor, size: 18),
label: Text(
'$label ($count)',
style: const TextStyle(color: Colors.white70, fontSize: 12),
overflow: TextOverflow.ellipsis,
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:afterwork/core/constants/colors.dart';
class SubmitButton extends StatelessWidget {
final VoidCallback onPressed;