refactoring and checkpoint

This commit is contained in:
DahoudG
2024-09-24 00:32:20 +00:00
parent dc73ba7dcc
commit 6b12cfeb41
159 changed files with 8119 additions and 1535 deletions

View File

@@ -1,443 +1,110 @@
import 'package:flutter/material.dart';
import 'package:afterwork/data/models/event_model.dart';
import 'package:afterwork/data/models/user_model.dart';
import 'package:afterwork/core/constants/urls.dart';
import 'package:afterwork/data/services/category_service.dart';
import '../location/location_picker_screen.dart';
import 'dart:convert';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
/// Classe représentant la boîte de dialogue pour ajouter un nouvel événement.
/// Dialogue pour ajouter un nouvel événement.
/// Ce widget affiche un formulaire permettant à l'utilisateur de saisir les détails d'un événement.
/// Les logs permettent de suivre les actions de l'utilisateur dans ce dialogue.
class AddEventDialog extends StatefulWidget {
final String userId;
final String userName;
final String userLastName;
const AddEventDialog({
super.key,
Key? key,
required this.userId,
required this.userName,
required this.userLastName,
});
}) : super(key: key);
@override
_AddEventDialogState createState() => _AddEventDialogState();
}
class _AddEventDialogState extends State<AddEventDialog> {
final _formKey = GlobalKey<FormState>();
// Variables pour stocker les données de l'événement
String _title = '';
String _description = '';
DateTime? _selectedDate;
String? _imagePath;
String _location = 'Abidjan';
String _category = '';
String _link = '';
LatLng? _selectedLatLng = const LatLng(5.348722, -3.985038);
Map<String, List<String>> _categories = {};
List<String> _currentCategories = [];
String? _selectedCategoryType;
@override
void initState() {
super.initState();
_loadCategories();
}
void _loadCategories() async {
final CategoryService categoryService = CategoryService();
final categories = await categoryService.loadCategories();
setState(() {
_categories = categories;
_selectedCategoryType = categories.keys.first;
_currentCategories = categories[_selectedCategoryType] ?? [];
});
}
final _formKey = GlobalKey<FormState>(); // Clé pour valider le formulaire
String _eventName = ''; // Nom de l'événement
DateTime _selectedDate = DateTime.now(); // Date de l'événement
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
backgroundColor: const Color(0xFF2C2C3E),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTitleField(),
const SizedBox(height: 10),
_buildDescriptionField(),
const SizedBox(height: 10),
_buildDatePicker(),
const SizedBox(height: 10),
_buildLocationField(context),
const SizedBox(height: 10),
_buildCategoryField(),
const SizedBox(height: 10),
_buildImagePicker(),
const SizedBox(height: 10),
_buildLinkField(),
const SizedBox(height: 20),
_buildSubmitButton(),
],
),
),
),
),
);
}
print("Affichage du dialogue d'ajout d'événement.");
Widget _buildTitleField() {
return TextFormField(
decoration: InputDecoration(
labelText: 'Titre',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.title, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
print('Erreur: Titre est vide');
return 'Veuillez entrer un titre';
}
return null;
},
onSaved: (value) {
_title = value ?? '';
print('Titre sauvegardé: $_title');
},
);
}
Widget _buildDescriptionField() {
return TextFormField(
decoration: InputDecoration(
labelText: 'Description',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.description, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
maxLines: 3,
validator: (value) {
if (value == null || value.isEmpty) {
print('Erreur: Description est vide');
return 'Veuillez entrer une description';
}
return null;
},
onSaved: (value) {
_description = value ?? '';
print('Description sauvegardée: $_description');
},
);
}
Widget _buildDatePicker() {
return GestureDetector(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(2101),
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
print('Date sélectionnée: $_selectedDate');
});
} else {
print('Date non sélectionnée ou égale à la précédente');
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(10.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
return AlertDialog(
title: const Text('Ajouter un événement'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_selectedDate == null
? 'Sélectionnez une date'
: '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}',
style: const TextStyle(color: Colors.white70),
// Champ pour entrer le nom de l'événement
TextFormField(
decoration: const InputDecoration(labelText: 'Nom de l\'événement'),
onSaved: (value) {
_eventName = value ?? '';
print("Nom de l'événement saisi : $_eventName");
},
validator: (value) {
if (value == null || value.isEmpty) {
print("Erreur : le champ du nom de l'événement est vide.");
return 'Veuillez entrer un nom d\'événement';
}
return null;
},
),
const SizedBox(height: 20),
// Sélecteur de date pour l'événement
ElevatedButton(
onPressed: () async {
final selectedDate = await _selectDate(context);
if (selectedDate != null) {
setState(() {
_selectedDate = selectedDate;
print("Date de l'événement sélectionnée : $_selectedDate");
});
}
},
child: Text('Sélectionner la date : ${_selectedDate.toLocal()}'.split(' ')[0]),
),
const Icon(Icons.calendar_today, color: Colors.white70),
],
),
),
);
}
Widget _buildLocationField(BuildContext context) {
return GestureDetector(
onTap: () async {
final LatLng? pickedLocation = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LocationPickerScreen(),
),
);
if (pickedLocation != null) {
setState(() {
_selectedLatLng = pickedLocation;
_location = '${pickedLocation.latitude}, ${pickedLocation.longitude}';
print('Localisation sélectionnée: $_location');
});
} else {
print('Localisation non sélectionnée');
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(10.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_selectedLatLng == null
? 'Sélectionnez une localisation'
: 'Localisation: $_location',
style: const TextStyle(color: Colors.white70),
),
const Icon(Icons.location_on, color: Colors.white70),
],
),
),
);
}
/// Construction du champ de catégorie avec sélection du type et de la catégorie.
Widget _buildCategoryField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'Type de catégorie',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
),
value: _selectedCategoryType,
items: _categories.keys.map((String type) {
return DropdownMenuItem<String>(
value: type,
child: Text(type, style: const TextStyle(color: Colors.black)),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCategoryType = newValue;
_currentCategories = _categories[newValue] ?? [];
_category = ''; // Réinitialiser la catégorie sélectionnée
print('Type de catégorie sélectionné : $_selectedCategoryType');
print('Catégories disponibles pour ce type : $_currentCategories');
});
actions: [
// Bouton pour annuler l'ajout de l'événement
TextButton(
onPressed: () {
print("L'utilisateur a annulé l'ajout de l'événement.");
Navigator.of(context).pop();
},
child: const Text('Annuler'),
),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'Catégorie',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
),
value: _category.isNotEmpty ? _category : null,
items: _currentCategories.map((String category) {
return DropdownMenuItem<String>(
value: category,
child: Text(category, style: const TextStyle(color: Colors.black)),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_category = newValue ?? '';
print('Catégorie sélectionnée : $_category');
});
},
validator: (value) {
if (value == null || value.isEmpty) {
print('Erreur: Catégorie non sélectionnée');
return 'Veuillez sélectionner une catégorie';
// Bouton pour soumettre le formulaire et ajouter l'événement
TextButton(
onPressed: () {
if (_formKey.currentState?.validate() == true) {
_formKey.currentState?.save();
print("L'utilisateur a ajouté un événement : Nom = $_eventName, Date = $_selectedDate");
Navigator.of(context).pop({
'eventName': _eventName,
'eventDate': _selectedDate,
});
}
return null;
},
child: const Text('Ajouter'),
),
],
);
}
Widget _buildImagePicker() {
return GestureDetector(
onTap: () {
// Logique pour sélectionner une image
print('Image Picker activé');
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(10.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_imagePath == null
? 'Sélectionnez une image'
: 'Image sélectionnée: $_imagePath',
style: const TextStyle(color: Colors.white70),
),
const Icon(Icons.image, color: Colors.white70),
],
),
),
);
}
Widget _buildLinkField() {
return TextFormField(
decoration: InputDecoration(
labelText: 'Lien (optionnel)',
labelStyle: const TextStyle(color: Colors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.link, color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
onSaved: (value) {
_link = value ?? '';
print('Lien sauvegardé: $_link');
},
);
}
Widget _buildSubmitButton() {
return ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Formulaire validé');
EventModel newEvent = EventModel(
id: '',
title: _title,
description: _description,
date: _selectedDate?.toIso8601String() ?? '',
location: _location,
category: _category,
link: _link,
imageUrl: _imagePath ?? '',
status: 'OPEN',
creator: UserModel(
userId: widget.userId,
nom: widget.userName,
prenoms: widget.userLastName,
email: '',
motDePasse: '',
),
participants: [
UserModel(
userId: widget.userId,
nom: widget.userName,
prenoms: widget.userLastName,
email: '',
motDePasse: '',
)
],
);
Map<String, dynamic> eventData = newEvent.toJson();
print('Données JSON de l\'événement: $eventData');
final response = await http.post(
Uri.parse('${Urls.baseUrl}/events'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(eventData),
);
print('Statut de la réponse: ${response.statusCode}');
print('Réponse brute: ${response.body}');
if (response.statusCode == 201) {
print('Événement créé avec succès');
Fluttertoast.showToast(
msg: "Événement créé avec succès!",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
);
Navigator.of(context).pop(); // Fermer la boîte de dialogue
} else {
print('Erreur lors de la création de l\'événement: ${response.reasonPhrase}');
Fluttertoast.showToast(
msg: "Erreur lors de la création de l'événement",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: ${response.reasonPhrase}')),
);
}
} else {
print('Le formulaire n\'est pas valide');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1DBF73),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.symmetric(vertical: 12.0),
minimumSize: const Size(double.infinity, 40),
),
child: const Text('Ajouter l\'événement', style: TextStyle(color: Colors.white)),
/// Fonction pour afficher le sélecteur de date
Future<DateTime?> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null) {
print("Date choisie dans le sélecteur : $picked");
}
return picked;
}
}

View File

@@ -1,18 +1,35 @@
import 'package:flutter/material.dart';
/// Écran des établissements.
/// Cet écran affiche une liste des établissements disponibles.
/// Les logs permettent de tracer les actions de navigation et d'affichage dans cet écran.
class EstablishmentsScreen extends StatelessWidget {
const EstablishmentsScreen({super.key});
const EstablishmentsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Établissements',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
print("Affichage de l'écran des établissements.");
return Scaffold(
appBar: AppBar(
title: const Text('Établissements'),
backgroundColor: Colors.blueAccent,
),
body: ListView.builder(
itemCount: 10, // Exemple : 10 établissements fictifs pour l'affichage
itemBuilder: (context, index) {
print("Affichage de l'établissement numéro $index.");
return ListTile(
leading: const Icon(Icons.location_city),
title: Text('Établissement $index'),
subtitle: const Text('Description de l\'établissement'),
onTap: () {
print("L'utilisateur a sélectionné l'établissement numéro $index.");
// Logique pour ouvrir les détails de l'établissement
},
);
},
),
);
}

View File

@@ -1,429 +1,172 @@
import 'package:flutter/material.dart';
import 'package:afterwork/data/datasources/event_remote_data_source.dart';
import '../../../core/utils/date_formatter.dart';
import 'package:afterwork/data/models/event_model.dart';
import 'package:afterwork/core/utils/date_formatter.dart'; // Importer DateFormatter
/// Widget pour afficher une carte d'événement.
/// Cette classe est utilisée pour afficher les détails d'un événement,
/// incluant son titre, sa description, son image, et des actions possibles
/// telles que réagir, commenter, partager, participer, et fermer ou rouvrir l'événement.
class EventCard extends StatelessWidget {
// Identifiant unique de l'événement
final String eventId;
// Source de données distante pour les opérations sur l'événement
final EventRemoteDataSource eventRemoteDataSource;
// Identifiant de l'utilisateur
final EventModel event;
final String userId;
// Nom de l'utilisateur
final String userName;
// Prénom de l'utilisateur
final String userLastName;
// URL de l'image de profil de l'utilisateur
final String profileImage;
// Nom complet de l'utilisateur (nom + prénom)
final String name;
// Date de publication de l'événement
final String datePosted;
// Titre de l'événement
final String eventTitle;
// Description de l'événement
final String eventDescription;
// URL de l'image de l'événement
final String eventImageUrl;
// Statut de l'événement (e.g., "OPEN", "CLOSED")
final String eventStatus;
// Catégorie de l'événement
final String eventCategory;
// Nombre de réactions à l'événement
final int reactionsCount;
// Nombre de commentaires sur l'événement
final int commentsCount;
// Nombre de partages de l'événement
final int sharesCount;
// Callback pour l'action "Réagir"
final VoidCallback onReact;
// Callback pour l'action "Commenter"
final VoidCallback onComment;
// Callback pour l'action "Partager"
final VoidCallback onShare;
// Callback pour l'action "Participer"
final VoidCallback onParticipate;
// Callback pour afficher plus d'options
final VoidCallback onMoreOptions;
// Callback pour fermer l'événement
final VoidCallback onCloseEvent;
// Callback pour rouvrir l'événement
final VoidCallback onReopenEvent;
const EventCard({
Key? key,
required this.eventId,
required this.eventRemoteDataSource,
required this.event,
required this.userId,
required this.userName,
required this.userLastName,
required this.profileImage,
required this.name,
required this.datePosted,
required this.eventTitle,
required this.eventDescription,
required this.eventImageUrl,
required this.eventStatus,
required this.eventCategory,
required this.reactionsCount,
required this.commentsCount,
required this.sharesCount,
required this.onReact,
required this.onComment,
required this.onShare,
required this.onParticipate,
required this.onMoreOptions,
required this.onCloseEvent,
required this.onReopenEvent,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Log du rendu de la carte d'événement
print('Rendu de l\'EventCard pour l\'événement $eventId avec statut $eventStatus');
return AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 300),
child: Dismissible(
key: ValueKey(eventId),
direction: eventStatus == 'CLOSED'
? DismissDirection.endToStart // Permet de rouvrir avec un swipe à gauche
: DismissDirection.startToEnd, // Permet de fermer avec un swipe à droite
onDismissed: (direction) {
if (direction == DismissDirection.startToEnd) {
// Log du déclenchement de la fermeture de l'événement
print('Tentative de fermeture de l\'événement $eventId');
onCloseEvent();
} else if (direction == DismissDirection.endToStart && eventStatus == 'CLOSED') {
// Log du déclenchement de la réouverture de l'événement
print('Tentative de réouverture de l\'événement $eventId');
onReopenEvent();
}
},
background: Container(
color: Colors.red,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 20.0),
child: const Icon(Icons.delete, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.green,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20.0),
child: const Icon(Icons.replay, color: Colors.white),
),
child: Stack(
return Card(
color: const Color(0xFF2C2C3E),
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: [
Card(
color: const Color(0xFF2C2C3E),
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: [
_buildHeader(context),
const SizedBox(height: 10),
_buildEventCategory(),
const SizedBox(height: 5),
_buildEventDetails(),
const SizedBox(height: 10),
_buildEventImage(),
const SizedBox(height: 10),
Divider(color: Colors.white.withOpacity(0.2)),
_buildInteractionRow(),
const SizedBox(height: 5),
_buildParticipateButton(),
],
),
),
_buildHeader(),
const SizedBox(height: 10),
Text(
event.title,
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: 10),
_buildEventImage(),
const Divider(color: Colors.white24),
_buildInteractionRow(),
const SizedBox(height: 10),
_buildStatusAndActions(),
],
),
),
);
}
/// Construire l'en-tête de la carte avec les informations de l'utilisateur.
/// Cette méthode affiche l'image de profil, le nom de l'utilisateur, la date
/// de publication de l'événement, et le statut de l'événement.
Widget _buildHeader(BuildContext context) {
// Log du rendu de l'en-tête de la carte
print('Rendu de l\'en-tête pour l\'événement $eventId');
// Convertir la date `datePosted` en DateTime si ce n'est pas déjà fait
DateTime dateTimePosted = DateTime.parse(datePosted);
Widget _buildHeader() {
// Convertir la date de l'événement (de String à DateTime)
DateTime? eventDate;
try {
eventDate = DateTime.parse(event.startDate);
} catch (e) {
eventDate = null; // Gérer le cas où la date ne serait pas valide
}
// Utiliser le DateFormatter pour formater la date
String formattedDate = DateFormatter.formatDate(dateTimePosted);
// Utiliser DateFormatter pour afficher une date lisible si elle est valide
String formattedDate = eventDate != null ? DateFormatter.formatDate(eventDate) : 'Date inconnue';
return Row(
children: [
CircleAvatar(
backgroundImage: AssetImage(profileImage),
radius: 25,
),
CircleAvatar(backgroundImage: NetworkImage(event.imageUrl ?? 'lib/assets/images/placeholder.png')),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Text(
formattedDate,
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(width: 10),
_buildStatusBadge(), // Badge de statut aligné sur la même ligne que la date du post
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$userName $userLastName',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 5),
Text(
formattedDate, // Utiliser la date formatée ici
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
const Spacer(),
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.white),
onPressed: () {
// Log du déclenchement du bouton "Plus d'options"
print('Plus d\'options déclenché pour l\'événement $eventId');
onMoreOptions();
// Logique d'affichage d'options supplémentaires pour l'événement.
// Vous pouvez utiliser un menu déroulant ou une boîte de dialogue ici.
},
),
if (eventStatus != 'CLOSED') // Masquer le bouton de fermeture si l'événement est fermé
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () {
// Log du déclenchement du bouton de fermeture de l'événement
print('Tentative de fermeture de l\'événement $eventId');
onCloseEvent();
},
),
],
);
}
/// Afficher la catégorie de l'événement au-dessus du titre.
/// Cette méthode affiche la catégorie en italique pour distinguer le type d'événement.
Widget _buildEventCategory() {
// Log du rendu de la catégorie de l'événement
print('Affichage de la catégorie pour l\'événement $eventId: $eventCategory');
return Text(
eventCategory,
style: const TextStyle(
color: Colors.blueAccent,
fontSize: 14,
fontStyle: FontStyle.italic, // Style en italique
fontWeight: FontWeight.w400, // Titre fin
),
);
}
/// Afficher les détails de l'événement.
/// Cette méthode affiche le titre et la description de l'événement.
Widget _buildEventDetails() {
// Log du rendu des détails de l'événement
print('Affichage des détails pour l\'événement $eventId');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
eventTitle,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Text(
eventDescription,
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
],
);
}
/// Afficher l'image de l'événement.
/// Cette méthode affiche l'image associée à l'événement.
Widget _buildEventImage() {
// Log du rendu de l'image de l'événement
print('Affichage de l\'image pour l\'événement $eventId');
return ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(
eventImageUrl,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
// Log de l'erreur lors du chargement de l'image
print('Erreur de chargement de l\'image pour l\'événement $eventId: $error');
return Image.asset(
'lib/assets/images/placeholder.png',
height: 180,
width: double.infinity,
fit: BoxFit.cover,
);
},
event.imageUrl ?? 'lib/assets/images/placeholder.png',
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset('lib/assets/images/placeholder.png'); // Image par défaut si erreur de chargement
}
),
);
}
/// Afficher les icônes d'interaction (réagir, commenter, partager).
/// Cette méthode affiche les boutons pour réagir, commenter, et partager l'événement.
Widget _buildInteractionRow() {
// Log du rendu de la ligne d'interaction de l'événement
print('Affichage des interactions pour l\'événement $eventId');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), // Réduire le padding vertical
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, // Utiliser spaceAround pour réduire l'espace
children: [
Expanded(
child: _buildIconButton(
icon: Icons.thumb_up_alt_outlined,
label: 'Réagir',
count: reactionsCount,
onPressed: () {
// Log de l'action "Réagir"
print('Réaction à l\'événement $eventId');
onReact();
},
),
),
Expanded(
child: _buildIconButton(
icon: Icons.comment_outlined,
label: 'Commenter',
count: commentsCount,
onPressed: () {
// Log de l'action "Commenter"
print('Commentaire sur l\'événement $eventId');
onComment();
},
),
),
Expanded(
child: _buildIconButton(
icon: Icons.share_outlined,
label: 'Partager',
count: sharesCount,
onPressed: () {
// Log de l'action "Partager"
print('Partage de l\'événement $eventId');
onShare();
},
),
),
],
),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildIconButton(Icons.thumb_up_alt_outlined, 'Réagir', event.reactionsCount, onReact),
_buildIconButton(Icons.comment_outlined, 'Commenter', event.commentsCount, onComment),
_buildIconButton(Icons.share_outlined, 'Partager', event.sharesCount, onShare),
],
);
}
/// Bouton d'interaction personnalisé.
/// Cette méthode construit un bouton avec une icône et un label pour l'interaction.
Widget _buildIconButton({
required IconData icon,
required String label,
required int count,
required VoidCallback onPressed,
}) {
// Log de la construction du bouton d'interaction
print('Construction du bouton $label pour l\'événement $eventId');
Widget _buildIconButton(IconData icon, String label, int count, VoidCallback onPressed) {
return TextButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: const Color(0xFF1DBF73), size: 20),
label: Text(
'$label ($count)',
style: const TextStyle(color: Colors.white70, fontSize: 12),
overflow: TextOverflow.ellipsis,
),
);
}
/// Bouton pour participer à l'événement.
/// Cette méthode construit un bouton qui permet de participer à l'événement.
/// Si l'événement est fermé, le bouton est caché.
Widget _buildParticipateButton() {
// Log de la construction du bouton "Participer"
print('Construction du bouton "Participer" pour l\'événement $eventId avec statut $eventStatus');
// Si l'événement est fermé, ne rien retourner (pas de bouton)
if (eventStatus == 'CLOSED') {
print('L\'événement $eventId est fermé, le bouton "Participer" est caché.');
return SizedBox.shrink(); // Retourne un widget vide pour ne pas occuper d'espace
}
// Sinon, retourner le bouton "Participer"
return ElevatedButton(
onPressed: onParticipate,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1DBF73),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
// Widget pour afficher le statut de l'événement et les actions associées (fermer, réouvrir)
Widget _buildStatusAndActions() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
event.status == 'closed' ? 'Événement fermé' : 'Événement ouvert',
style: TextStyle(
color: event.status == 'closed' ? Colors.red : Colors.green,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
padding: const EdgeInsets.symmetric(vertical: 12.0),
minimumSize: const Size(double.infinity, 40),
),
child: const Text(
'Participer',
style: TextStyle(color: Colors.white),
),
);
}
/// Construire un badge pour afficher le statut de l'événement.
/// Cette méthode affiche un badge avec le statut de l'événement ("OPEN" ou "CLOSED").
Widget _buildStatusBadge() {
// Log de la construction du badge de statut
print('Construction du badge de statut pour l\'événement $eventId: $eventStatus');
Color badgeColor;
switch (eventStatus) {
case 'CLOSED':
badgeColor = Colors.redAccent;
break;
case 'OPEN':
default:
badgeColor = Colors.greenAccent;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.7),
borderRadius: BorderRadius.circular(8.0),
),
child: Text(
eventStatus.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 10, // Réduction de la taille du texte
fontWeight: FontWeight.bold,
event.status == 'closed'
? ElevatedButton(
onPressed: onReopenEvent,
child: const Text('Rouvrir l\'événement'),
)
: ElevatedButton(
onPressed: onCloseEvent,
child: const Text('Fermer l\'événement'),
),
),
],
);
}
}

View File

@@ -1,19 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:afterwork/data/models/event_model.dart';
import 'package:afterwork/data/datasources/event_remote_data_source.dart';
import 'event_card.dart';
import 'package:afterwork/presentation/screens/event/event_card.dart';
import '../../state_management/event_bloc.dart';
import '../dialogs/add_event_dialog.dart';
/// Écran principal pour afficher les événements.
class EventScreen extends StatefulWidget {
final EventRemoteDataSource eventRemoteDataSource;
final String userId;
final String userName; // Nom de l'utilisateur
final String userLastName; // Prénom de l'utilisateur
final String userName;
final String userLastName;
const EventScreen({
Key? key,
required this.eventRemoteDataSource,
required this.userId,
required this.userName,
required this.userLastName,
@@ -24,13 +22,11 @@ class EventScreen extends StatefulWidget {
}
class _EventScreenState extends State<EventScreen> {
late Future<List<EventModel>> _eventsFuture;
@override
void initState() {
super.initState();
// Récupérer la liste des événements à partir de la source de données distante
_eventsFuture = widget.eventRemoteDataSource.getAllEvents();
// Charger les événements lors de l'initialisation
context.read<EventBloc>().add(LoadEvents(widget.userId));
}
@override
@@ -60,161 +56,98 @@ class _EventScreenState extends State<EventScreen> {
);
if (eventData != null) {
try {
print('Tentative de création d\'un nouvel événement par l\'utilisateur ${widget.userId}');
await widget.eventRemoteDataSource.createEvent(eventData as EventModel);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Événement ajouté avec succès !')),
);
// Réactualiser la liste des événements après création
setState(() {
_eventsFuture = widget.eventRemoteDataSource.getAllEvents();
});
} catch (e) {
print('Erreur lors de la création de l\'événement: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur : $e')),
);
}
// Ajouter l'événement en appelant l'API via le bloc
context.read<EventBloc>().add(AddEvent(EventModel.fromJson(eventData)));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Événement ajouté avec succès !')),
);
}
},
),
],
),
body: FutureBuilder<List<EventModel>>(
future: _eventsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
body: BlocBuilder<EventBloc, EventState>(
builder: (context, state) {
if (state is EventLoading) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
print('Erreur lors de la récupération des événements: ${snapshot.error}');
return Center(child: Text('Erreur: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('Aucun événement trouvé.'));
} else if (state is EventLoaded) {
final events = state.events;
if (events.isEmpty) {
return const Center(child: Text('Aucun événement disponible.'));
}
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[index];
return EventCard(
key: ValueKey(event.id),
event: event,
userId: widget.userId,
userName: widget.userName,
userLastName: widget.userLastName,
onReact: () => _onReact(event.id),
onComment: () => _onComment(event.id),
onShare: () => _onShare(event.id),
onParticipate: () => _onParticipate(event.id),
onCloseEvent: () => _onCloseEvent(event.id),
onReopenEvent: () => _onReopenEvent(event.id),
);
},
);
} else if (state is EventError) {
return Center(child: Text('Erreur: ${state.message}'));
}
final events = snapshot.data!;
print('Nombre d\'événements récupérés: ${events.length}');
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[index];
print('Affichage de l\'événement ${event.id}');
return EventCard(
key: ValueKey(event.id),
eventRemoteDataSource: widget.eventRemoteDataSource,
userId: widget.userId,
eventId: event.id,
userName: widget.userName,
userLastName: widget.userLastName,
profileImage: 'lib/assets/images/profile_picture.png',
name: '${widget.userName} ${widget.userLastName}',
eventCategory: event.category,
datePosted: event.date,
eventTitle: event.title,
eventDescription: event.description,
eventImageUrl: event.imageUrl ?? 'lib/assets/images/placeholder.png',
eventStatus: event.status,
reactionsCount: 120, // Exemple de valeur
commentsCount: 45, // Exemple de valeur
sharesCount: 30, // Exemple de valeur
onReact: () {
print('Réaction à l\'événement ${event.id}');
},
onComment: () {
print('Commentaire sur l\'événement ${event.id}');
},
onShare: () {
print('Partage de l\'événement ${event.id}');
},
onParticipate: () {
print('Participation à l\'événement ${event.id}');
},
onCloseEvent: () => _onCloseEvent(context, event.id, index),
onMoreOptions: () {
print('Affichage des options pour l\'événement ${event.id}');
},
onReopenEvent: () => _onReopenEvent(context, event.id, index),
);
},
);
return const Center(child: Text('Aucun événement disponible.'));
},
),
backgroundColor: const Color(0xFF1E1E2C),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Recharger les événements
context.read<EventBloc>().add(LoadEvents(widget.userId));
},
backgroundColor: const Color(0xFF1DBF73),
child: const Icon(Icons.refresh),
),
);
}
/// Logique pour fermer un événement
void _onCloseEvent(BuildContext context, String eventId, int index) async {
try {
print('Tentative de fermeture de l\'événement $eventId');
// Appeler l'API pour fermer l'événement
await widget.eventRemoteDataSource.closeEvent(eventId);
print('Événement fermé avec succès');
// Montrer un message de succès AVANT de supprimer l'événement de la liste
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('L\'événement a été fermé avec succès.')),
);
// Supprimez l'événement de la liste après avoir affiché le SnackBar
setState(() {
_eventsFuture = _eventsFuture.then((events) {
events.removeAt(index);
return events;
});
});
} catch (e) {
print('Erreur lors de la fermeture de l\'événement: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la fermeture de l\'événement : $e')),
);
}
void _onReact(String eventId) {
print('Réaction à l\'événement $eventId');
// Implémentez la logique pour réagir à un événement ici
}
/// Logique pour rouvrir un événement
void _onReopenEvent(BuildContext context, String eventId, int index) async {
try {
print('Tentative de réouverture de l\'événement $eventId');
await widget.eventRemoteDataSource.reopenEvent(eventId);
print('Événement rouvert avec succès');
void _onComment(String eventId) {
print('Commentaire sur l\'événement $eventId');
// Implémentez la logique pour commenter un événement ici
}
// Montrer un message de succès
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('L\'événement a été rouvert avec succès.')),
);
void _onShare(String eventId) {
print('Partage de l\'événement $eventId');
// Implémentez la logique pour partager un événement ici
}
// Mettre à jour le statut de l'événement dans la liste des événements
setState(() {
_eventsFuture = _eventsFuture.then((events) {
final updatedEvent = EventModel(
id: events[index].id,
title: events[index].title,
description: events[index].description,
date: events[index].date,
location: events[index].location,
category: events[index].category,
link: events[index].link,
imageUrl: events[index].imageUrl,
creator: events[index].creator,
participants: events[index].participants,
status: 'OPEN', // Mettre à jour le statut à 'OPEN'
);
void _onParticipate(String eventId) {
print('Participation à l\'événement $eventId');
// Implémentez la logique pour participer à un événement ici
}
// Remplacer l'événement dans la liste
events[index] = updatedEvent;
return events;
});
});
} catch (e) {
print('Erreur lors de la réouverture de l\'événement: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la réouverture de l\'événement : $e')),
);
}
void _onCloseEvent(String eventId) {
print('Fermeture de l\'événement $eventId');
// Appeler le bloc pour fermer l'événement
context.read<EventBloc>().add(CloseEvent(eventId));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('L\'événement a été fermé avec succès.')),
);
}
void _onReopenEvent(String eventId) {
print('Réouverture de l\'événement $eventId');
// Appeler le bloc pour rouvrir l'événement
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,19 +1,163 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart'; // Importez les couleurs dynamiques
import '../../widgets/friend_suggestions.dart';
import '../../widgets/group_list.dart';
import '../../widgets/popular_activity_list.dart';
import '../../widgets/quick_action_button.dart';
import '../../widgets/recommended_event_list.dart';
import '../../widgets/section_header.dart';
import '../../widgets/story_section.dart';
class HomeContentScreen extends StatelessWidget {
const HomeContentScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Accueil',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
final size = MediaQuery.of(context).size;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 15.0), // Marges réduites
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section de bienvenue
_buildWelcomeCard(),
const SizedBox(height: 15), // Espacement vertical réduit
// Section "Moments populaires"
_buildCard(
context: context,
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
),
const SizedBox(height: 10), // Espace vertical réduit
StorySection(size: size),
],
),
),
const SizedBox(height: 15), // Espacement réduit
// Section des événements recommandés
_buildCard(
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Événements recommandés',
icon: Icons.star,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10), // Espacement réduit
RecommendedEventList(size: size),
],
),
),
const SizedBox(height: 15), // Espacement réduit
// Section des activités populaires
_buildCard(
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Activités populaires',
icon: Icons.local_activity,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10), // Espacement réduit
PopularActivityList(size: size),
],
),
),
const SizedBox(height: 15), // Espacement réduit
// Section des groupes sociaux
_buildCard(
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Groupes à rejoindre',
icon: Icons.group_add,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10), // Espacement réduit
GroupList(size: size),
],
),
),
const SizedBox(height: 15), // Espacement réduit
// Section des suggestions d'amis
_buildCard(
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Suggestions damis',
icon: Icons.person_add,
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10), // Espacement réduit
FriendSuggestions(size: size),
],
),
),
],
),
);
}
// Widget pour la carte de bienvenue
Widget _buildWelcomeCard() {
return Card(
elevation: 5,
color: AppColors.surface, // Utilisation de la couleur dynamique pour la surface
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Bienvenue, Dahoud!',
style: TextStyle(
color: AppColors.textPrimary, // Texte dynamique
fontSize: 22, // Taille de police réduite
fontWeight: FontWeight.w600, // Poids de police ajusté
),
),
Icon(Icons.waving_hand, color: Colors.orange.shade300, size: 24), // Taille de l'icône ajustée
],
),
),
);
}
// Widget générique pour créer une carte design avec des espaces optimisés
Widget _buildCard({required BuildContext context, required Widget child}) {
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
child: Padding(
padding: const EdgeInsets.all(12.0), // Padding interne réduit pour un contenu plus compact
child: child,
),
);
}
}

View File

@@ -1,21 +1,22 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // Pour ThemeProvider
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'; // Importez l'écran de notifications
import '../../../core/constants/colors.dart';
import '../../../core/theme/theme_provider.dart'; // Pour basculer le thème
/// Classe principale pour l'écran d'accueil de l'application.
/// Cette classe gère la navigation entre les différentes sections de l'application
/// en utilisant un [TabController] pour contrôler les différents onglets.
/// Les actions de l'AppBar sont également personnalisées pour offrir des fonctionnalités
/// spécifiques comme la recherche, la publication et la messagerie.
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
const HomeScreen({
Key? key,
@@ -23,6 +24,7 @@ class HomeScreen extends StatefulWidget {
required this.userId,
required this.userName,
required this.userLastName,
required this.userProfileImage, // Passer l'image de profil ici
}) : super(key: key);
@override
@@ -35,136 +37,207 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
@override
void initState() {
super.initState();
// Initialisation du TabController avec 5 onglets.
_tabController = TabController(length: 5, vsync: this);
debugPrint('HomeScreen initialisé avec userId: ${widget.userId}, userName: ${widget.userName}, userLastName: ${widget.userLastName}');
_tabController = TabController(length: 6, vsync: this); // Ajouter un onglet pour les notifications
}
@override
void dispose() {
// Nettoyage du TabController pour éviter les fuites de mémoire.
_tabController.dispose();
super.dispose();
debugPrint('HomeScreen dispose appelé');
}
/// Gestion des sélections dans le menu contextuel de l'AppBar.
void _onMenuSelected(BuildContext context, String option) {
switch (option) {
case 'Publier':
debugPrint('Option "Publier" sélectionnée');
// Rediriger vers la page de publication.
print('Publier sélectionné');
break;
case 'Story':
debugPrint('Option "Story" sélectionnée');
// Rediriger vers la page de création de Story.
print('Story sélectionné');
break;
default:
debugPrint('Option inconnue sélectionnée: $option');
break;
}
}
@override
Widget build(BuildContext context) {
// Accès au ThemeProvider
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset(
'lib/assets/images/logo.png', // Chemin correct de votre logo.
height: 40,
),
),
actions: [
// Bouton pour ajouter du contenu (Publier, Story).
CircleAvatar(
backgroundColor: Colors.white,
radius: 18,
child: PopupMenuButton<String>(
onSelected: (value) {
_onMenuSelected(context, value);
debugPrint('Menu contextuel sélectionné: $value');
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'Publier',
child: Text('Publier'),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
backgroundColor: AppColors.backgroundColor, // Gère dynamiquement la couleur d'arrière-plan
floating: true,
pinned: true,
snap: true,
elevation: 2, // Réduction de l'élévation pour un design plus léger
leading: Padding(
padding: const EdgeInsets.all(4.0), // Réduction du padding
child: Image.asset(
'lib/assets/images/logo.png',
height: 40, // Taille réduite du logo
),
const PopupMenuItem(
value: 'Story',
child: Text('Story'),
),
actions: [
_buildActionIcon(Icons.add, 'Publier', context),
_buildActionIcon(Icons.search, 'Rechercher', context),
_buildActionIcon(Icons.message, 'Message', context),
_buildNotificationsIcon(context, 45),
// Ajout du bouton pour basculer entre les thèmes
Switch(
value: themeProvider.isDarkMode,
onChanged: (value) {
themeProvider.toggleTheme(); // Bascule le thème lorsqu'on clique
},
activeColor: AppColors.accentColor,
),
],
icon: const Icon(Icons.add, color: Colors.blueAccent, size: 20),
color: Colors.white,
),
),
const SizedBox(width: 8), // Espacement entre les boutons.
bottom: TabBar(
controller: _tabController,
indicatorColor: AppColors.lightPrimary, // Tab active en bleu
labelStyle: const TextStyle(
fontSize: 12, // Réduction de la taille du texte des onglets
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: const TextStyle(
fontSize: 11, // Réduction 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
// Bouton Recherche.
CircleAvatar(
backgroundColor: Colors.white,
radius: 18,
child: IconButton(
icon: const Icon(Icons.search, color: Colors.blueAccent, size: 20),
onPressed: () {
debugPrint('Bouton Recherche appuyé');
// Implémenter la logique de recherche ici.
},
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 SizedBox(width: 8), // Espacement entre les boutons.
// Bouton Messagerie.
CircleAvatar(
backgroundColor: Colors.white,
radius: 18,
child: IconButton(
icon: const Icon(Icons.message, color: Colors.blueAccent, size: 20),
onPressed: () {
debugPrint('Bouton Messagerie appuyé');
// Implémenter la logique de messagerie ici.
},
),
),
const SizedBox(width: 8), // Espacement entre les boutons.
],
bottom: TabBar(
];
},
body: TabBarView(
controller: _tabController,
indicatorColor: Colors.blueAccent,
labelColor: Colors.white, // Couleur du texte sélectionné.
unselectedLabelColor: Colors.grey[400], // Couleur du texte non sélectionné.
onTap: (index) {
debugPrint('Onglet sélectionné: $index');
},
tabs: const [
Tab(icon: Icon(Icons.home), text: 'Accueil'),
Tab(icon: Icon(Icons.event), text: 'Événements'),
Tab(icon: Icon(Icons.location_city), text: 'Établissements'),
Tab(icon: Icon(Icons.people), text: 'Social'),
Tab(icon: Icon(Icons.person), text: 'Profil'),
children: [
const HomeContentScreen(),
EventScreen(
userId: widget.userId,
userName: widget.userName,
userLastName: widget.userLastName,
),
const EstablishmentsScreen(),
const SocialScreen(),
const NotificationsScreen(),
const ProfileScreen(),
],
),
),
body: TabBarView(
controller: _tabController,
);
}
// Widget pour afficher la photo de profil de l'utilisateur 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
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
child: ClipOval(
child: FadeInImage.assetNetwork(
placeholder: 'lib/assets/images/user_placeholder.png', // Chemin de l'image par défaut
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);
},
),
),
),
),
);
}
// Widget pour afficher l'icône de notifications avec un badge si nécessaire
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
children: [
const HomeContentScreen(), // Contenu de l'accueil.
EventScreen(
eventRemoteDataSource: widget.eventRemoteDataSource,
userId: widget.userId,
userName: widget.userName,
userLastName: widget.userLastName,
), // Écran des événements.
const EstablishmentsScreen(), // Écran des établissements.
const SocialScreen(), // Écran social.
const ProfileScreen(), // Écran du profil.
CircleAvatar(
backgroundColor: AppColors.surface,
radius: 18,
child: IconButton(
icon: const Icon(Icons.notifications, color: AppColors.darkOnPrimary, size: 20),
onPressed: () {
// Rediriger vers l'écran des notifications
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationsScreen(),
),
);
},
),
),
// Affiche le badge si le nombre de notifications est supérieur à 0
if (notificationCount > 0)
Positioned(
right: -6,
top: -6,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red, // Couleur du badge
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 18,
minHeight: 18,
),
child: Text(
notificationCount > 99 ? '99+' : '$notificationCount', // Affiche "99+" si le nombre dépasse 99
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
backgroundColor: Colors.black, // Arrière-plan de l'écran en noir.
);
}
Widget _buildActionIcon(IconData iconData, String label, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0), // Réduction de l'espacement
child: CircleAvatar(
backgroundColor: AppColors.surface,
radius: 18, // Réduction de la taille des avatars
child: IconButton(
icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), // Taille réduite de l'icône
onPressed: () {
_onMenuSelected(context, label);
},
),
),
);
}
}

View File

@@ -1,38 +1,82 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class LocationPickerScreen extends StatelessWidget {
const LocationPickerScreen({super.key});
/// Écran pour la sélection d'une localisation sur une carte.
/// L'utilisateur peut choisir un lieu en interagissant avec la carte Google Maps.
/// Des logs permettent de tracer les actions comme la sélection et l'affichage de la carte.
class LocationPickerScreen extends StatefulWidget {
const LocationPickerScreen({Key? key}) : super(key: key);
@override
_LocationPickerScreenState createState() => _LocationPickerScreenState();
}
class _LocationPickerScreenState extends State<LocationPickerScreen> {
LatLng _pickedLocation = const LatLng(37.7749, -122.4194); // Localisation par défaut (San Francisco)
late GoogleMapController _mapController; // Contrôleur de la carte Google Maps
@override
Widget build(BuildContext context) {
print('Affichage de l\'écran de sélection de localisation.');
return Scaffold(
appBar: AppBar(
title: const Text('Sélectionnez une localisation'),
backgroundColor: const Color(0xFF1E1E2C),
title: const Text('Sélectionnez un lieu'),
backgroundColor: Colors.blueAccent,
),
body: GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(48.8566, 2.3522), // Paris par défaut
zoom: 12.0,
),
markers: <Marker>{
Marker(
markerId: const MarkerId('selectedLocation'),
position: const LatLng(48.8566, 2.3522), // Position par défaut
draggable: true,
onDragEnd: (newPosition) {
print('Nouvelle position sélectionnée: $newPosition');
Navigator.of(context).pop(newPosition);
},
)
},
onTap: (position) {
print('Position tapée: $position');
Navigator.of(context).pop(position);
},
body: Column(
children: [
Expanded(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _pickedLocation,
zoom: 14,
),
onMapCreated: (controller) {
_mapController = controller;
print('Carte Google Maps créée.');
},
onTap: _selectLocation, // Sélection de la localisation sur la carte
markers: {
Marker(
markerId: const MarkerId('pickedLocation'),
position: _pickedLocation,
),
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton.icon(
onPressed: () {
print('Lieu sélectionné : $_pickedLocation');
Navigator.of(context).pop(_pickedLocation);
},
icon: const Icon(Icons.check),
label: const Text('Confirmer la localisation'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
),
),
],
),
);
}
}
/// Fonction pour gérer la sélection d'une localisation sur la carte.
/// Lorsqu'une localisation est sélectionnée, elle est ajoutée à la carte et les logs sont mis à jour.
void _selectLocation(LatLng position) {
setState(() {
_pickedLocation = position;
});
print('Localisation sélectionnée : $_pickedLocation');
}
@override
void dispose() {
_mapController.dispose();
print('Libération des ressources de la carte.');
super.dispose();
}
}

View File

@@ -1,15 +1,22 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:afterwork/data/datasources/user_remote_data_source.dart';
import 'package:afterwork/data/models/user_model.dart';
import 'package:afterwork/presentation/screens/home/home_screen.dart';
import 'package:http/http.dart' as http;
import 'package:afterwork/data/services/hash_password.dart';
import 'package:afterwork/data/services/secure_storage.dart';
import 'package:afterwork/data/services/preferences_helper.dart';
import 'package:afterwork/data/services/secure_storage.dart';
import 'package:afterwork/presentation/screens/home/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
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/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.
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -18,87 +25,91 @@ class LoginScreen extends StatefulWidget {
}
class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
String _userId = '';
String _email = '';
String _password = '';
bool _isPasswordVisible = false;
bool _isSubmitting = false;
final _formKey = GlobalKey<FormState>(); // Clé pour valider le formulaire de connexion.
// Champs utilisateur
String _email = ''; // Email de l'utilisateur
String _password = ''; // Mot de passe de l'utilisateur
// É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
// Services pour les opérations
final UserRemoteDataSource _userRemoteDataSource = UserRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
final PreferencesHelper _preferencesHelper = PreferencesHelper();
late AnimationController _controller;
late Animation<double> _buttonScaleAnimation;
// Contrôleur pour le bouton de chargement
final _btnController = LoadingButtonController();
// Contrôleur d'animation pour la transition des écrans
late AnimationController _animationController;
@override
void initState() {
super.initState();
_controller = AnimationController(
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_buttonScaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 500),
);
print("Contrôleur d'animation initialisé.");
}
@override
void dispose() {
_controller.dispose();
_animationController.dispose();
print("Ressources d'animation libérées.");
super.dispose();
}
/// Afficher/Masquer le mot de passe
/// Fonction pour basculer la visibilité du mot de passe
void _togglePasswordVisibility() {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
print("Visibilité du mot de passe basculée: $_isPasswordVisible");
}
/// Soumission du formulaire d'authentification
void _submit() async {
/// Fonction pour afficher un toast via FlutterToast
void _showToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black,
textColor: Colors.white,
fontSize: 16.0,
);
}
/// Fonction soumettre le formulaire
Future<void> _submit() async {
print("Tentative de soumission du formulaire de connexion.");
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitting = true;
_showErrorMessage = false;
});
_formKey.currentState!.save();
print("===== DEBUT DE LA SOUMISSION DU FORMULAIRE =====");
print("Email: $_email");
print("Mot de passe: $_password");
try {
print('Début de l\'authentification'); // Débogage
_btnController.start();
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é.");
}
// Hachage du mot de passe avec SHA-256
String hashedPassword = hashPassword(_password);
print("Mot de passe haché: $hashedPassword");
// Authentification via l'API avec un timeout
UserModel user = await _userRemoteDataSource
.authenticateUser(_email, hashedPassword, "unique_user_id")
.timeout(
Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('Le temps de connexion a expiré. Veuillez réessayer.');
},
);
print('Connexion réussie : ${user.userId} - ${user.email}');
// Sauvegarde des données de l'utilisateur après authentification
print("Utilisateur authentifié : ${user.userId}");
await _secureStorage.saveUserId(user.userId);
await _preferencesHelper.saveUserName(user.nom);
await _preferencesHelper.saveUserLastName(user.prenoms);
_showToast("Connexion réussie !");
print("===== SAUVEGARDE DES DONNÉES UTILISATEUR =====");
print("User ID: ${user.userId}");
print("User Name: ${user.nom}");
print("User Last Name: ${user.prenoms}");
// Navigation vers l'écran d'accueil
// Navigation vers la page d'accueil
Navigator.pushReplacement(
context,
MaterialPageRoute(
@@ -107,44 +118,74 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
userId: user.userId,
userName: user.nom,
userLastName: user.prenoms,
userProfileImage: 'lib/assets/images/profile_picture.png',
),
),
);
print("===== NAVIGATION VERS HOME SCREEN =====");
} catch (e) {
print('Erreur lors de la connexion: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur : ${e.toString()}')),
);
print("Erreur lors de l'authentification : $e");
_btnController.error();
_showToast("Erreur lors de la connexion : ${e.toString()}");
setState(() {
_showErrorMessage = true;
});
} finally {
print('Fin du processus d\'authentification'); // Débogage
_btnController.reset();
setState(() {
_isSubmitting = false;
});
}
} else {
print("===== FORMULAIRE NON VALIDE =====");
print("Échec de validation du formulaire.");
_btnController.reset();
_showToast("Veuillez vérifier les informations saisies.");
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final themeProvider = Provider.of<ThemeProvider>(context);
bool isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0;
return Scaffold(
body: Stack(
children: [
// Arrière-plan avec dégradé
Container(
decoration: const BoxDecoration(
AnimatedContainer(
duration: const Duration(seconds: 3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4A90E2), Color(0xFF9013FE)],
colors: [
theme.colorScheme.primary,
theme.colorScheme.secondary
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// Contenu de la page
if (_isSubmitting)
const Center(
child: SpinKitFadingCircle(
color: Colors.white,
size: 50.0,
),
),
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: Icon(
themeProvider.isDarkMode ? Icons.dark_mode : Icons.light_mode,
color: theme.iconTheme.color,
),
onPressed: () {
themeProvider.toggleTheme();
print("Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}");
},
),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
@@ -153,148 +194,179 @@ class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStat
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo animé
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _buttonScaleAnimation.value,
child: child,
);
},
child: GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _controller.reverse(),
child: Image.asset(
'lib/assets/images/logo.png',
height: size.height * 0.2,
),
),
Image.asset(
'lib/assets/images/logo.png',
height: size.height * 0.25,
),
const SizedBox(height: 20),
const Text(
Text(
'Bienvenue sur AfterWork',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 6,
color: Colors.black26,
),
],
),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 40),
// Champ Email
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
filled: true,
fillColor: Colors.white.withOpacity(0.1),
labelStyle: const TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.email, color: Colors.white),
),
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
_buildTextFormField(
label: 'Email',
icon: Icons.email,
validator: (value) {
if (value == null || value.isEmpty) {
print("Erreur: Le champ email est vide");
print("Erreur : champ email vide.");
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
print("Erreur: Le format de l'email est invalide");
print("Erreur : email invalide.");
return 'Veuillez entrer un email valide';
}
return null;
},
onSaved: (value) {
_email = value ?? ''; // Utiliser une chaîne vide si value est null
print("Email sauvegardé: $_email");
_email = value!;
print("Email enregistré : $_email");
},
),
const SizedBox(height: 20),
// Champ Mot de passe
TextFormField(
decoration: InputDecoration(
labelText: 'Mot de passe',
filled: true,
fillColor: Colors.white.withOpacity(0.1),
labelStyle: const TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.lock, color: Colors.white),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
color: Colors.white),
onPressed: _togglePasswordVisibility,
),
),
_buildTextFormField(
label: 'Mot de passe',
icon: Icons.lock,
obscureText: !_isPasswordVisible,
style: const TextStyle(color: Colors.white),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
),
onPressed: _togglePasswordVisibility,
),
validator: (value) {
if (value == null || value.isEmpty) {
print("Erreur: Le champ mot de passe est vide");
print("Erreur : champ mot de passe vide.");
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
print("Erreur: Le mot de passe est trop court");
print("Erreur : mot de passe trop court.");
return 'Le mot de passe doit comporter au moins 6 caractères';
}
return null;
},
onSaved: (value) {
_password = value ?? ''; // Utiliser une chaîne vide si value est null
print("Mot de passe sauvegardé: $_password");
_password = value!;
print("Mot de passe enregistré.");
},
),
const SizedBox(height: 20),
// Bouton de connexion avec animation de soumission
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16.0),
textStyle: const TextStyle(fontSize: 18),
backgroundColor: _isSubmitting ? Colors.grey : Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
width: size.width * 0.85,
child: LoadingButton(
controller: _btnController,
onPressed: _isSubmitting ? null : _submit,
iconData: Icons.login,
iconColor: theme.colorScheme.onPrimary,
child: Text(
'Connexion',
style: theme.textTheme.bodyLarge!.copyWith(
color: theme.colorScheme.onPrimary,
),
),
onPressed: _isSubmitting ? null : _submit,
child: _isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: const Text('Connexion'),
),
),
const SizedBox(height: 20),
// Lien pour s'inscrire
TextButton(
onPressed: () {
// Naviguer vers la page d'inscription
print("Redirection vers la page d'inscription");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignUpScreen(),
),
);
},
child: const Text(
child: Text(
'Pas encore de compte ? Inscrivez-vous',
style: TextStyle(color: Colors.white),
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
),
),
TextButton(
onPressed: () {
print("Mot de passe oublié");
},
child: Text(
'Mot de passe oublié ?',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
),
),
if (_showErrorMessage)
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text(
'Erreur lors de la connexion. Veuillez vérifier vos identifiants.',
style: TextStyle(color: Colors.red, fontSize: 16),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
bottom: isKeyboardVisible ? 0 : 20,
left: isKeyboardVisible ? 20 : 0,
right: isKeyboardVisible ? 20 : 0,
child: Row(
mainAxisAlignment: isKeyboardVisible
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
Image.asset(
'lib/assets/images/logolionsdev.png',
height: 30,
),
if (isKeyboardVisible)
Text(
'© 2024 LionsDev',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}
/// Widget réutilisable pour les champs de texte avec validation et design amélioré
Widget _buildTextFormField({
required String label,
required IconData icon,
bool obscureText = false,
Widget? suffixIcon,
required FormFieldValidator<String> validator,
required FormFieldSetter<String> onSaved,
}) {
final theme = Theme.of(context);
return TextFormField(
decoration: InputDecoration(
labelText: label,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
labelStyle: theme.textTheme.bodyMedium,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: BorderSide.none,
),
prefixIcon: Icon(icon, color: theme.iconTheme.color),
suffixIcon: suffixIcon,
),
obscureText: obscureText,
style: theme.textTheme.bodyLarge,
validator: validator,
onSaved: onSaved,
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Notifications'),
backgroundColor: Colors.blueAccent,
),
body: const Center(
child: Text('Liste des notifications'),
),
);
}
}

View File

@@ -1,117 +1,147 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
print("Affichage de l'écran de profil.");
return Scaffold(
appBar: AppBar(
title: const Text('Profil',
style: TextStyle(
color: Color(0xFF1DBF73), // Définit la couleur verte du texte
),
),
backgroundColor: const Color(0xFF1E1E2C),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
// Naviguer vers la page des paramètres
},
),
],
),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
_buildUserInfoCard(),
const SizedBox(height: 20),
_buildEditOptionsCard(),
const SizedBox(height: 20),
_buildStatisticsSectionCard(),
const SizedBox(height: 20),
_buildExpandableSectionCard(
title: 'Historique',
icon: Icons.history,
children: [
_buildAnimatedListTile(
icon: Icons.event_note,
label: 'Historique des Événements',
onTap: () {
// Naviguer vers l'historique des événements
},
backgroundColor: AppColors.backgroundColor,
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
backgroundColor: AppColors.darkPrimary,
flexibleSpace: FlexibleSpaceBar(
title: Text(
'Profil',
style: TextStyle(
color: AppColors.accentColor,
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
_buildAnimatedListTile(
icon: Icons.history,
label: 'Historique des Publications',
onTap: () {
// Naviguer vers l'historique des publications
},
background: Image.asset(
'lib/assets/images/profile_picture.png',
fit: BoxFit.cover,
),
_buildAnimatedListTile(
icon: Icons.bookmark,
label: 'Historique de Réservations',
onTap: () {
// Naviguer vers l'historique des réservations
),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
print("Bouton des paramètres cliqué.");
// Logique de navigation vers les paramètres
},
),
],
),
const SizedBox(height: 20),
_buildExpandableSectionCard(
title: 'Préférences et Paramètres',
icon: Icons.settings,
children: [
_buildAnimatedListTile(
icon: Icons.privacy_tip,
label: 'Paramètres de confidentialité',
onTap: () {
// Naviguer vers les paramètres de confidentialité
},
),
_buildAnimatedListTile(
icon: Icons.notifications,
label: 'Notifications',
onTap: () {
// Naviguer vers les paramètres de notification
},
),
_buildAnimatedListTile(
icon: Icons.language,
label: 'Langue de l\'application',
onTap: () {
// Naviguer vers les paramètres de langue
},
),
_buildAnimatedListTile(
icon: Icons.format_paint,
label: 'Thème de l\'application',
onTap: () {
// Naviguer vers les paramètres de thème
},
),
],
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 10),
_buildUserInfoCard(),
const SizedBox(height: 10),
_buildEditOptionsCard(),
const SizedBox(height: 10),
_buildStatisticsSectionCard(),
const SizedBox(height: 10),
_buildExpandableSectionCard(
title: 'Historique',
icon: Icons.history,
children: [
_buildAnimatedListTile(
icon: Icons.event_note,
label: 'Historique des Événements',
onTap: () {
print("Accès à l'historique des événements.");
// Logique de navigation vers l'historique des événements
},
),
_buildAnimatedListTile(
icon: Icons.history,
label: 'Historique des Publications',
onTap: () {
print("Accès à l'historique des publications.");
// Logique de navigation vers l'historique des publications
},
),
_buildAnimatedListTile(
icon: Icons.bookmark,
label: 'Historique de Réservations',
onTap: () {
print("Accès à l'historique des réservations.");
// Logique de navigation vers l'historique des réservations
},
),
],
),
const SizedBox(height: 10),
_buildExpandableSectionCard(
title: 'Préférences et Paramètres',
icon: Icons.settings,
children: [
_buildAnimatedListTile(
icon: Icons.privacy_tip,
label: 'Paramètres de confidentialité',
onTap: () {
print("Accès aux paramètres de confidentialité.");
// Logique de navigation vers les paramètres de confidentialité
},
),
_buildAnimatedListTile(
icon: Icons.notifications,
label: 'Notifications',
onTap: () {
print("Accès aux paramètres de notifications.");
// Logique de navigation vers les notifications
},
),
_buildAnimatedListTile(
icon: Icons.language,
label: 'Langue de l\'application',
onTap: () {
print("Accès aux paramètres de langue.");
// Logique de navigation vers les paramètres de langue
},
),
_buildAnimatedListTile(
icon: Icons.format_paint,
label: 'Thème de l\'application',
onTap: () {
print("Accès aux paramètres de thème.");
// Logique de navigation vers les paramètres de thème
},
),
],
),
const SizedBox(height: 10),
_buildSupportSectionCard(),
const SizedBox(height: 10),
_buildAccountDeletionCard(context),
],
),
),
const SizedBox(height: 20),
_buildSupportSectionCard(),
const SizedBox(height: 20),
_buildAccountDeletionCard(context),
],
),
backgroundColor: const Color(0xFF1E1E2C),
);
}
Widget _buildUserInfoCard() {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const CircleAvatar(
CircleAvatar(
radius: 50,
backgroundImage: AssetImage('lib/assets/images/profile_picture.png'),
backgroundColor: Colors.transparent,
@@ -152,29 +182,33 @@ class ProfileScreen extends StatelessWidget {
Widget _buildEditOptionsCard() {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: Column(
children: [
_buildAnimatedListTile(
icon: Icons.edit,
label: 'Éditer le profil',
onTap: () {
// Naviguer vers la page d'édition de profil
print("Édition du profil.");
// Logique de navigation vers l'édition du profil
},
),
_buildAnimatedListTile(
icon: Icons.camera_alt,
label: 'Changer la photo de profil',
onTap: () {
// Naviguer vers la page de changement de photo de profil
print("Changement de la photo de profil.");
// Logique de changement de la photo de profil
},
),
_buildAnimatedListTile(
icon: Icons.lock,
label: 'Changer le mot de passe',
onTap: () {
// Naviguer vers la page de changement de mot de passe
print("Changement du mot de passe.");
// Logique de changement de mot de passe
},
),
],
@@ -184,8 +218,9 @@ class ProfileScreen extends StatelessWidget {
Widget _buildStatisticsSectionCard() {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -232,8 +267,9 @@ class ProfileScreen extends StatelessWidget {
required List<Widget> children,
}) {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: ExpansionTile(
title: Text(
title,
@@ -243,9 +279,9 @@ class ProfileScreen extends StatelessWidget {
color: Colors.white,
),
),
leading: Icon(icon, color: const Color(0xFF1DBF73)),
iconColor: const Color(0xFF1DBF73),
collapsedIconColor: const Color(0xFF1DBF73),
leading: Icon(icon, color: AppColors.accentColor),
iconColor: AppColors.accentColor,
collapsedIconColor: AppColors.accentColor,
children: children,
),
);
@@ -253,8 +289,9 @@ class ProfileScreen extends StatelessWidget {
Widget _buildSupportSectionCard() {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: Column(
children: [
const Padding(
@@ -272,21 +309,24 @@ class ProfileScreen extends StatelessWidget {
icon: Icons.help,
label: 'Support et Assistance',
onTap: () {
// Naviguer vers la page de support
print("Accès au Support et Assistance.");
// Logique de navigation vers le support
},
),
_buildAnimatedListTile(
icon: Icons.article,
label: 'Conditions d\'utilisation',
onTap: () {
// Naviguer vers les conditions d'utilisation
print("Accès aux conditions d'utilisation.");
// Logique de navigation vers les conditions d'utilisation
},
),
_buildAnimatedListTile(
icon: Icons.privacy_tip,
label: 'Politique de confidentialité',
onTap: () {
// Naviguer vers la politique de confidentialité
print("Accès à la politique de confidentialité.");
// Logique de navigation vers la politique de confidentialité
},
),
],
@@ -296,8 +336,9 @@ class ProfileScreen extends StatelessWidget {
Widget _buildAccountDeletionCard(BuildContext context) {
return Card(
color: const Color(0xFF292B37),
color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
child: ListTile(
leading: const Icon(Icons.delete, color: Colors.redAccent),
title: const Text(
@@ -307,7 +348,6 @@ class ProfileScreen extends StatelessWidget {
onTap: () {
_showDeleteConfirmationDialog(context);
},
hoverColor: Colors.red.withOpacity(0.1),
),
);
}
@@ -317,7 +357,7 @@ class ProfileScreen extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: const Color(0xFF1E1E2C),
backgroundColor: AppColors.backgroundColor,
title: const Text(
'Confirmer la suppression',
style: TextStyle(color: Colors.white),
@@ -329,17 +369,17 @@ class ProfileScreen extends StatelessWidget {
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Fermer le popup
Navigator.of(context).pop();
},
child: const Text(
child: Text(
'Annuler',
style: TextStyle(color: Color(0xFF1DBF73)),
style: TextStyle(color: AppColors.accentColor),
),
),
TextButton(
onPressed: () {
// Logique de suppression du compte ici
Navigator.of(context).pop(); // Fermer le popup après la suppression
print("Suppression du compte confirmée.");
Navigator.of(context).pop();
},
child: const Text(
'Supprimer',
@@ -362,12 +402,11 @@ class ProfileScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(10),
splashColor: Colors.blueAccent.withOpacity(0.2),
child: ListTile(
leading: Icon(icon, color: const Color(0xFF1DBF73)),
leading: Icon(icon, color: AppColors.accentColor),
title: Text(
label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
),
hoverColor: Colors.blue.withOpacity(0.1),
),
);
}
@@ -378,7 +417,7 @@ class ProfileScreen extends StatelessWidget {
required String value,
}) {
return ListTile(
leading: Icon(icon, color: const Color(0xFF1DBF73)),
leading: Icon(icon, color: AppColors.accentColor),
title: Text(label, style: const TextStyle(color: Colors.white)),
trailing: Text(
value,
@@ -388,7 +427,6 @@ class ProfileScreen extends StatelessWidget {
fontSize: 16,
),
),
hoverColor: Colors.blue.withOpacity(0.1),
);
}
}

View File

@@ -0,0 +1,396 @@
import 'dart:async';
import 'package:afterwork/data/datasources/user_remote_data_source.dart';
import 'package:afterwork/data/models/user_model.dart';
import 'package:afterwork/data/services/preferences_helper.dart';
import 'package:afterwork/data/services/secure_storage.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:loading_icon_button/loading_icon_button.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/theme_provider.dart';
/// Écran d'inscription pour l'application AfterWork.
/// Permet à l'utilisateur de créer un nouveau compte avec des champs comme nom, prénom, email, mot de passe.
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
_SignUpScreenState createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final _formKey = GlobalKey<FormState>(); // Clé pour valider le formulaire
// Champs utilisateur
String _nom = ''; // Nom de l'utilisateur
String _prenoms = ''; // Prénom de l'utilisateur
String _email = ''; // Email de l'utilisateur
String _password = ''; // Mot de passe de l'utilisateur
String _confirmPassword = ''; // Confirmation du mot de passe
// É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
// Services pour les opérations
final UserRemoteDataSource _userRemoteDataSource = UserRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
final PreferencesHelper _preferencesHelper = PreferencesHelper();
// Contrôleur pour le bouton de chargement
final _btnController = LoadingButtonController();
@override
void dispose() {
_btnController.reset();
super.dispose();
}
/// Fonction pour basculer la visibilité du mot de passe
void _togglePasswordVisibility() {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
print("Visibilité du mot de passe basculée: $_isPasswordVisible");
}
Future<void> _submit() async {
print("Tentative de soumission du formulaire d'inscription.");
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitting = true;
_showErrorMessage = false;
});
_formKey.currentState!.save();
// Vérifier si le mot de passe et la confirmation correspondent
if (_password != _confirmPassword) {
setState(() {
_showErrorMessage = true;
});
print("Les mots de passe ne correspondent pas.");
_btnController.reset();
return;
}
try {
_btnController.start();
// Créer l'utilisateur avec les informations fournies
final UserModel user = UserModel(
userId: '', // L'ID sera généré côté serveur
nom: _nom,
prenoms: _prenoms,
email: _email,
motDePasse: _password, // Le mot de passe sera envoyé en clair pour l'instant
);
// 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}");
// Sauvegarder les informations de l'utilisateur
await _secureStorage.saveUserId(createdUser.userId);
await _preferencesHelper.saveUserName(createdUser.nom);
await _preferencesHelper.saveUserLastName(createdUser.prenoms);
// Rediriger vers la page d'accueil ou une page de confirmation
Navigator.pushReplacementNamed(context, '/home');
} catch (e) {
print("Erreur lors de la création du compte : $e");
_btnController.error();
setState(() {
_showErrorMessage = true;
});
} finally {
_btnController.reset();
setState(() {
_isSubmitting = false;
});
}
} else {
print("Échec de validation du formulaire.");
_btnController.reset();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // Utilisation du thème global
final size = MediaQuery.of(context).size;
final themeProvider = Provider.of<ThemeProvider>(context);
// Vérification si le clavier est visible
bool isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0;
return Scaffold(
body: Stack(
children: [
// Arrière-plan animé avec un dégradé basé sur le thème
AnimatedContainer(
duration: const Duration(seconds: 3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.secondary
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
if (_isSubmitting)
const Center(
child: CircularProgressIndicator(),
),
// Bouton pour basculer entre les modes jour et nuit
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: Icon(
themeProvider.isDarkMode ? Icons.dark_mode : Icons.light_mode,
color: theme.iconTheme.color,
),
onPressed: () {
themeProvider.toggleTheme();
print("Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}");
},
),
),
// Formulaire d'inscription
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo de l'application
Image.asset(
'lib/assets/images/logo.png',
height: size.height * 0.25,
),
const SizedBox(height: 20),
// Titre de la page
Text(
'Créer un compte AfterWork',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 40),
// Champ nom
_buildTextFormField(
label: 'Nom',
icon: Icons.person,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
onSaved: (value) {
_nom = value!;
},
),
const SizedBox(height: 20),
// Champ prénom
_buildTextFormField(
label: 'Prénoms',
icon: Icons.person_outline,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre prénom';
}
return null;
},
onSaved: (value) {
_prenoms = value!;
},
),
const SizedBox(height: 20),
// Champ email
_buildTextFormField(
label: 'Email',
icon: Icons.email,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
},
onSaved: (value) {
_email = value!;
},
),
const SizedBox(height: 20),
// Champ mot de passe
_buildTextFormField(
label: 'Mot de passe',
icon: Icons.lock,
obscureText: !_isPasswordVisible,
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
),
onPressed: _togglePasswordVisibility,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit comporter au moins 6 caractères';
}
return null;
},
onSaved: (value) {
_password = value!;
},
),
const SizedBox(height: 20),
// Champ de confirmation du mot de passe
_buildTextFormField(
label: 'Confirmer le mot de passe',
icon: Icons.lock_outline,
obscureText: !_isPasswordVisible,
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: theme.iconTheme.color,
),
onPressed: _togglePasswordVisibility,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez confirmer votre mot de passe';
}
if (value != _password) {
return 'Les mots de passe ne correspondent pas';
}
return null;
},
onSaved: (value) {
_confirmPassword = value!;
},
),
const SizedBox(height: 30),
// Bouton de création de compte
SizedBox(
width: size.width * 0.85,
child: LoadingButton(
controller: _btnController,
onPressed: _isSubmitting ? null : _submit,
iconData: Icons.person_add,
iconColor: theme.colorScheme.onPrimary,
child: Text(
'Créer un compte',
style: theme.textTheme.bodyLarge!.copyWith(
color: theme.colorScheme.onPrimary,
),
),
),
),
const SizedBox(height: 20),
// Lien pour revenir à la connexion
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
'Déjà un compte ? Connectez-vous',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
),
),
// Affichage du message d'erreur si nécessaire
if (_showErrorMessage)
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text(
'Erreur lors de la création du compte. Veuillez vérifier vos informations.',
style: TextStyle(color: Colors.red, fontSize: 16),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
// Pied de page avec logo et mention copyright
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
bottom: isKeyboardVisible ? 0 : 20,
left: isKeyboardVisible ? 20 : 0,
right: isKeyboardVisible ? 20 : 0,
child: Row(
mainAxisAlignment: isKeyboardVisible
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
Image.asset(
'lib/assets/images/logolionsdev.png',
height: 30,
),
if (isKeyboardVisible)
Text(
'© 2024 LionsDev',
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.white70),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}
/// Widget réutilisable pour les champs de texte avec validation et design amélioré
Widget _buildTextFormField({
required String label,
required IconData icon,
bool obscureText = false,
Widget? suffixIcon,
required FormFieldValidator<String> validator,
required FormFieldSetter<String> onSaved,
}) {
final theme = Theme.of(context); // Utilisation du thème global
return TextFormField(
decoration: InputDecoration(
labelText: label,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
labelStyle: theme.textTheme.bodyMedium,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: BorderSide.none,
),
prefixIcon: Icon(icon, color: theme.iconTheme.color),
suffixIcon: suffixIcon,
),
obscureText: obscureText,
style: theme.textTheme.bodyLarge,
validator: validator,
onSaved: onSaved,
);
}
}