fix(chat): Correction race condition + Implémentation TODOs

## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
This commit is contained in:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View File

@@ -0,0 +1,544 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/design_system.dart';
import '../../../data/datasources/user_remote_data_source.dart';
import '../../../data/models/user_model.dart';
import '../../../data/providers/user_provider.dart';
import '../../../data/services/preferences_helper.dart';
import '../../../data/services/secure_storage.dart';
import '../../../domain/entities/user.dart';
import '../../widgets/animated_widgets.dart';
import '../../widgets/custom_snackbar.dart';
/// Écran d'édition de profil utilisateur avec design moderne.
///
/// Permet à l'utilisateur de modifier:
/// - Prénom et nom
/// - Email
/// - Photo de profil
/// - Mot de passe
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({required this.user, super.key});
final User user;
@override
State<EditProfileScreen> createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final UserRemoteDataSource _dataSource = UserRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
final PreferencesHelper _preferencesHelper = PreferencesHelper();
final ImagePicker _imagePicker = ImagePicker();
bool _isLoading = false;
bool _hasChanges = false;
File? _newProfileImage;
String? _newProfileImageUrl;
@override
void initState() {
super.initState();
_firstNameController.text = widget.user.userFirstName;
_lastNameController.text = widget.user.userLastName;
_emailController.text = widget.user.email;
// Écoute les changements pour activer/désactiver le bouton Sauvegarder
_firstNameController.addListener(_onFieldChanged);
_lastNameController.addListener(_onFieldChanged);
_emailController.addListener(_onFieldChanged);
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
super.dispose();
}
void _onFieldChanged() {
final hasChanges = _firstNameController.text != widget.user.userFirstName ||
_lastNameController.text != widget.user.userLastName ||
_emailController.text != widget.user.email ||
_newProfileImage != null;
if (hasChanges != _hasChanges) {
setState(() {
_hasChanges = hasChanges;
});
}
}
Future<void> _pickImage() async {
try {
final XFile? pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
);
if (pickedFile != null) {
setState(() {
_newProfileImage = File(pickedFile.path);
_hasChanges = true;
});
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de la sélection de l\'image');
}
}
}
Future<void> _saveChanges() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_hasChanges) {
context.showInfo('Aucune modification à enregistrer');
return;
}
setState(() {
_isLoading = true;
});
try {
// TODO: Si une nouvelle image est sélectionnée, l'uploader d'abord
// et récupérer l'URL depuis le backend
String profileImageUrl = widget.user.profileImageUrl;
if (_newProfileImage != null) {
// Pour l'instant, on utilise l'URL existante
// Dans une vraie implémentation, il faudrait uploader l'image
// vers le backend et récupérer la nouvelle URL
context.showWarning('L\'upload d\'image sera implémenté avec le backend');
}
// Créer le modèle utilisateur mis à jour
final updatedUser = UserModel(
userId: widget.user.userId,
userFirstName: _firstNameController.text.trim(),
userLastName: _lastNameController.text.trim(),
email: _emailController.text.trim(),
motDePasse: widget.user.motDePasse, // Mot de passe inchangé
profileImageUrl: profileImageUrl,
);
// Envoyer la mise à jour au backend
final result = await _dataSource.updateUser(updatedUser);
if (mounted) {
// Mettre à jour le provider
final userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.setUser(result.toEntity());
// Mettre à jour le stockage local
await _preferencesHelper.saveUserName(result.userFirstName);
await _preferencesHelper.saveUserLastName(result.userLastName);
setState(() {
_isLoading = false;
_hasChanges = false;
});
context.showSuccess('Profil mis à jour avec succès');
Navigator.pop(context, true); // Retourne true pour indiquer la modification
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
context.showError('Erreur lors de la mise à jour: ${e.toString()}');
}
}
}
Future<void> _changePassword() async {
final result = await showDialog<bool>(
context: context,
builder: (context) => const _ChangePasswordDialog(),
);
if (result == true && mounted) {
context.showSuccess('Mot de passe modifié avec succès');
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Modifier le profil'),
actions: [
if (_hasChanges && !_isLoading)
TextButton(
onPressed: _saveChanges,
child: const Text(
'SAUVEGARDER',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(DesignSystem.spacingLg),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Photo de profil
Center(
child: Stack(
children: [
Hero(
tag: 'user_profile_avatar_${widget.user.userId}',
child: CircleAvatar(
radius: 60,
backgroundImage: _newProfileImage != null
? FileImage(_newProfileImage!)
: NetworkImage(widget.user.profileImageUrl) as ImageProvider,
child: _newProfileImage == null &&
widget.user.profileImageUrl.isEmpty
? const Icon(Icons.person, size: 60)
: null,
),
),
Positioned(
right: 0,
bottom: 0,
child: AnimatedScaleButton(
onTap: _pickImage,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
boxShadow: DesignSystem.shadowMd,
),
child: const Icon(
Icons.camera_alt,
color: Colors.white,
size: 20,
),
),
),
),
],
),
),
const SizedBox(height: DesignSystem.spacing2xl),
// Prénom
TextFormField(
controller: _firstNameController,
decoration: InputDecoration(
labelText: 'Prénom',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le prénom est requis';
}
if (value.trim().length < 2) {
return 'Le prénom doit contenir au moins 2 caractères';
}
return null;
},
),
const SizedBox(height: DesignSystem.spacingLg),
// Nom
TextFormField(
controller: _lastNameController,
decoration: InputDecoration(
labelText: 'Nom',
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est requis';
}
if (value.trim().length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
},
),
const SizedBox(height: DesignSystem.spacingLg),
// Email
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'L\'email est requis';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Email invalide';
}
return null;
},
),
const SizedBox(height: DesignSystem.spacing2xl),
// Bouton Changer le mot de passe
OutlinedButton.icon(
onPressed: _changePassword,
icon: const Icon(Icons.lock),
label: const Text('Changer le mot de passe'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
),
const SizedBox(height: DesignSystem.spacingLg),
// Bouton Sauvegarder (grand bouton en bas)
if (_hasChanges)
FadeInWidget(
child: ElevatedButton(
onPressed: _isLoading ? null : _saveChanges,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Enregistrer les modifications',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
);
}
}
/// Dialogue pour changer le mot de passe
class _ChangePasswordDialog extends StatefulWidget {
const _ChangePasswordDialog();
@override
State<_ChangePasswordDialog> createState() => _ChangePasswordDialogState();
}
class _ChangePasswordDialogState extends State<_ChangePasswordDialog> {
final _formKey = GlobalKey<FormState>();
final _currentPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _obscureCurrentPassword = true;
bool _obscureNewPassword = true;
bool _obscureConfirmPassword = true;
@override
void dispose() {
_currentPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _changePassword() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// TODO: Appel API pour changer le mot de passe
// await _dataSource.changePassword(
// currentPassword: _currentPasswordController.text,
// newPassword: _newPasswordController.text,
// );
// Simulation pour l'instant
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
Navigator.pop(context, true);
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
context.showError('Erreur: ${e.toString()}');
}
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Changer le mot de passe'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Mot de passe actuel
TextFormField(
controller: _currentPasswordController,
obscureText: _obscureCurrentPassword,
decoration: InputDecoration(
labelText: 'Mot de passe actuel',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureCurrentPassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureCurrentPassword = !_obscureCurrentPassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Mot de passe requis';
}
return null;
},
),
const SizedBox(height: DesignSystem.spacingLg),
// Nouveau mot de passe
TextFormField(
controller: _newPasswordController,
obscureText: _obscureNewPassword,
decoration: InputDecoration(
labelText: 'Nouveau mot de passe',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscureNewPassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureNewPassword = !_obscureNewPassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nouveau mot de passe requis';
}
if (value.length < 8) {
return 'Minimum 8 caractères';
}
return null;
},
),
const SizedBox(height: DesignSystem.spacingLg),
// Confirmer nouveau mot de passe
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
labelText: 'Confirmer le mot de passe',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Confirmation requise';
}
if (value != _newPasswordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _isLoading ? null : _changePassword,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Changer'),
),
],
);
}
}

View File

@@ -1,91 +1,275 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/colors.dart';
import '../../../data/providers/user_provider.dart';
import '../../widgets/cards/account_deletion_card.dart';
import '../../widgets/cards/statistics_section_card.dart';
import '../../widgets/cards/support_section_card.dart';
import '../../widgets/custom_list_tile.dart';
import '../../widgets/cards/edit_options_card.dart';
import '../../widgets/cards/expandable_section_card.dart';
import '../../widgets/profile_header.dart';
import '../../widgets/cards/user_info_card.dart';
import '../../../core/constants/design_system.dart';
import '../../../core/theme/theme_provider.dart';
import '../../../core/utils/page_transitions.dart';
import '../../../data/providers/user_provider.dart';
import '../../../data/services/secure_storage.dart';
import '../../widgets/animated_widgets.dart';
import '../../widgets/custom_snackbar.dart';
import '../notifications/notifications_screen.dart';
import '../settings/settings_screen.dart';
import 'edit_profile_screen.dart';
/// Écran de profil moderne et épuré.
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final userProvider = Provider.of<UserProvider>(context);
final user = userProvider.user;
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: CustomScrollView(
slivers: [
ProfileHeader(user: user),
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 10),
UserInfoCard(user: user),
const SizedBox(height: 10),
const EditOptionsCard(),
const SizedBox(height: 10),
StatisticsSectionCard(user: user),
const SizedBox(height: 10),
ExpandableSectionCard(
title: 'Historique',
icon: Icons.history,
children: [
CustomListTile(
icon: Icons.event_note,
label: 'Historique des Événements',
onTap: () => print("[LOG] Accès à l'historique des événements."),
// AppBar avec image de fond et avatar
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
// Fond dégradé
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withOpacity(0.7),
],
),
),
CustomListTile(
icon: Icons.history,
label: 'Historique des Publications',
onTap: () => print("[LOG] Accès à l'historique des publications."),
),
// Avatar centré
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Hero(
tag: 'user_profile_avatar_${user.userId}',
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: theme.scaffoldBackgroundColor,
width: 4,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: CircleAvatar(
radius: 50,
backgroundImage: user.profileImageUrl != null &&
user.profileImageUrl!.isNotEmpty
? NetworkImage(user.profileImageUrl!)
: null,
child: user.profileImageUrl == null ||
user.profileImageUrl!.isEmpty
? Icon(
Icons.person_rounded,
size: 50,
color: theme.colorScheme.onPrimary,
)
: null,
),
),
),
),
CustomListTile(
icon: Icons.bookmark,
label: 'Historique de Réservations',
onTap: () => print("[LOG] Accès à l'historique des réservations."),
),
],
),
],
),
),
),
// Contenu
SliverToBoxAdapter(
child: Column(
children: [
const SizedBox(height: DesignSystem.spacingLg),
// Nom et email
Text(
'${user.userFirstName} ${user.userLastName}',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
ExpandableSectionCard(
title: 'Préférences et Paramètres',
icon: Icons.settings,
children: [
CustomListTile(
icon: Icons.privacy_tip,
label: 'Paramètres de confidentialité',
onTap: () => print("[LOG] Accès aux paramètres de confidentialité."),
),
CustomListTile(
icon: Icons.notifications,
label: 'Notifications',
onTap: () => print("[LOG] Accès aux paramètres de notifications."),
),
CustomListTile(
icon: Icons.language,
label: 'Langue de l\'application',
onTap: () => print("[LOG] Accès aux paramètres de langue."),
),
CustomListTile(
icon: Icons.format_paint,
label: 'Thème de l\'application',
onTap: () => print("[LOG] Accès aux paramètres de thème."),
),
],
const SizedBox(height: DesignSystem.spacingSm),
Text(
user.email,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: DesignSystem.spacingXl),
// Bouton Éditer le profil
Padding(
padding: DesignSystem.paddingHorizontal(DesignSystem.spacingXl),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
context.pushSlideRight(EditProfileScreen(user: user));
},
icon: const Icon(Icons.edit_rounded, size: 18),
label: const Text('Éditer le profil'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
),
),
),
const SizedBox(height: DesignSystem.spacingXl),
// Statistiques
_buildStatsRow(theme, user),
const SizedBox(height: DesignSystem.spacingXl),
// Sections
Padding(
padding: DesignSystem.paddingHorizontal(DesignSystem.spacingLg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Historique
_buildSectionHeader(theme, 'Historique'),
const SizedBox(height: DesignSystem.spacingSm),
_buildListTile(
context,
theme,
Icons.event_note_rounded,
'Historique des Événements',
() => context.showInfo('Historique des événements à venir'),
),
_buildListTile(
context,
theme,
Icons.article_rounded,
'Historique des Publications',
() => context.showInfo('Historique des publications à venir'),
),
_buildListTile(
context,
theme,
Icons.bookmark_rounded,
'Historique de Réservations',
() => context.showInfo('Historique des réservations à venir'),
),
const SizedBox(height: DesignSystem.spacingXl),
// Section Paramètres
_buildSectionHeader(theme, 'Paramètres'),
const SizedBox(height: DesignSystem.spacingSm),
_buildListTile(
context,
theme,
Icons.privacy_tip_rounded,
'Confidentialité',
() => context.showInfo('Paramètres de confidentialité à venir'),
),
_buildListTile(
context,
theme,
Icons.notifications_rounded,
'Notifications',
() => context.pushFadeScale(const NotificationsScreen()),
),
_buildListTile(
context,
theme,
Icons.language_rounded,
'Langue',
() => context.showInfo('Sélection de langue à venir'),
),
_buildSwitchTile(
context,
theme,
themeProvider.isDarkMode ? Icons.light_mode_rounded : Icons.dark_mode_rounded,
'Mode sombre',
themeProvider.isDarkMode,
(value) => themeProvider.toggleTheme(),
),
_buildListTile(
context,
theme,
Icons.settings_rounded,
'Paramètres avancés',
() => context.pushSlideRight(const SettingsScreen()),
),
const SizedBox(height: DesignSystem.spacingXl),
// Section Aide
_buildSectionHeader(theme, 'Aide & Support'),
const SizedBox(height: DesignSystem.spacingSm),
_buildListTile(
context,
theme,
Icons.help_rounded,
'Centre d\'aide',
() => context.showInfo('Centre d\'aide à venir'),
),
_buildListTile(
context,
theme,
Icons.feedback_rounded,
'Envoyer un feedback',
() => context.showInfo('Formulaire de feedback à venir'),
),
_buildListTile(
context,
theme,
Icons.info_rounded,
'À propos',
() => context.showInfo('À propos de l\'application'),
),
const SizedBox(height: DesignSystem.spacingXl),
// Bouton Déconnexion
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
_showLogoutDialog(context);
},
icon: const Icon(Icons.logout_rounded, size: 18),
label: const Text('Se déconnecter'),
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.error,
side: BorderSide(color: theme.colorScheme.error),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
),
),
),
const SizedBox(height: DesignSystem.spacingXl),
],
),
),
const SizedBox(height: 10),
const SupportSectionCard(),
const SizedBox(height: 10),
AccountDeletionCard(context: context),
],
),
),
@@ -93,4 +277,183 @@ class ProfileScreen extends StatelessWidget {
),
);
}
Widget _buildStatsRow(ThemeData theme, dynamic user) {
return Padding(
padding: DesignSystem.paddingHorizontal(DesignSystem.spacingXl),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatItem(theme, '0', 'Publications'),
Container(
height: 40,
width: 1,
color: theme.dividerColor,
),
_buildStatItem(theme, '0', 'Amis'),
Container(
height: 40,
width: 1,
color: theme.dividerColor,
),
_buildStatItem(theme, '0', 'Événements'),
],
),
);
}
Widget _buildStatItem(ThemeData theme, String value, String label) {
return Column(
children: [
Text(
value,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
fontSize: 12,
),
),
],
);
}
Widget _buildSectionHeader(ThemeData theme, String title) {
return Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 15,
),
);
}
Widget _buildListTile(
BuildContext context,
ThemeData theme,
IconData icon,
String title,
VoidCallback onTap,
) {
return AnimatedCard(
margin: const EdgeInsets.only(bottom: DesignSystem.spacingSm),
borderRadius: DesignSystem.borderRadiusMd,
elevation: 0.5,
hoverElevation: 2,
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingLg,
vertical: DesignSystem.spacingMd,
),
onTap: onTap,
child: Row(
children: [
Icon(
icon,
size: 22,
color: theme.colorScheme.primary,
),
const SizedBox(width: DesignSystem.spacingLg),
Expanded(
child: Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
),
),
),
Icon(
Icons.chevron_right_rounded,
size: 20,
color: theme.colorScheme.onSurface.withOpacity(0.3),
),
],
),
);
}
Widget _buildSwitchTile(
BuildContext context,
ThemeData theme,
IconData icon,
String title,
bool value,
ValueChanged<bool> onChanged,
) {
return AnimatedCard(
margin: const EdgeInsets.only(bottom: DesignSystem.spacingSm),
borderRadius: DesignSystem.borderRadiusMd,
elevation: 0.5,
hoverElevation: 1,
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingLg,
vertical: DesignSystem.spacingSm,
),
child: Row(
children: [
Icon(
icon,
size: 22,
color: theme.colorScheme.primary,
),
const SizedBox(width: DesignSystem.spacingLg),
Expanded(
child: Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
),
),
),
Switch(
value: value,
onChanged: onChanged,
),
],
),
);
}
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Déconnexion'),
content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () async {
// Fermer le dialogue
Navigator.pop(context);
// Supprimer les informations de l'utilisateur du stockage sécurisé
final secureStorage = SecureStorage();
await secureStorage.deleteUserInfo();
// Naviguer vers l'écran de connexion et supprimer toute la pile de navigation
if (context.mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
context.showSuccess('Déconnexion effectuée');
}
},
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Déconnecter'),
),
],
),
);
}
}