302 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| import 'package:em2rp/models/user_model.dart';
 | |
| import 'package:em2rp/utils/colors.dart';
 | |
| 
 | |
| class UserCard extends StatefulWidget {
 | |
|   final UserModel user;
 | |
|   final VoidCallback onEdit;
 | |
|   final VoidCallback onDelete;
 | |
| 
 | |
|   static const double _desktopMaxWidth = 280;
 | |
| 
 | |
|   const UserCard({
 | |
|     super.key,
 | |
|     required this.user,
 | |
|     required this.onEdit,
 | |
|     required this.onDelete,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   State<UserCard> createState() => _UserCardState();
 | |
| }
 | |
| 
 | |
| class _UserCardState extends State<UserCard> {
 | |
|   ImageProvider? _profileImage;
 | |
|   String? _lastUrl;
 | |
|   bool _isLoadingImage = false;
 | |
| 
 | |
|   @override
 | |
|   void didUpdateWidget(UserCard oldWidget) {
 | |
|     super.didUpdateWidget(oldWidget);
 | |
|     if (oldWidget.user.profilePhotoUrl != widget.user.profilePhotoUrl) {
 | |
|       _loadProfileImage();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _loadProfileImage();
 | |
|   }
 | |
| 
 | |
|   void _loadProfileImage() {
 | |
|     final url = widget.user.profilePhotoUrl;
 | |
|     if (url.isNotEmpty) {
 | |
|       setState(() {
 | |
|         _isLoadingImage = true;
 | |
|         _lastUrl = url;
 | |
|       });
 | |
|       final image = NetworkImage(url);
 | |
|       image.resolve(const ImageConfiguration()).addListener(
 | |
|             ImageStreamListener(
 | |
|               (info, _) {
 | |
|                 if (mounted) {
 | |
|                   setState(() {
 | |
|                     _profileImage = image;
 | |
|                     _isLoadingImage = false;
 | |
|                   });
 | |
|                 }
 | |
|               },
 | |
|               onError: (error, stack) {
 | |
|                 if (mounted) {
 | |
|                   setState(() {
 | |
|                     _profileImage = null;
 | |
|                     _isLoadingImage = false;
 | |
|                   });
 | |
|                 }
 | |
|               },
 | |
|             ),
 | |
|           );
 | |
|     } else {
 | |
|       setState(() {
 | |
|         _profileImage = null;
 | |
|         _isLoadingImage = false;
 | |
|         _lastUrl = null;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final width = MediaQuery.of(context).size.width;
 | |
|     final isMobile = width < 600;
 | |
| 
 | |
|     return Card(
 | |
|       margin: EdgeInsets.zero,
 | |
|       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
 | |
|       elevation: 3,
 | |
|       child: Container(
 | |
|         constraints: BoxConstraints(
 | |
|           maxWidth: isMobile ? double.infinity : UserCard._desktopMaxWidth,
 | |
|         ),
 | |
|         padding: const EdgeInsets.all(12),
 | |
|         child:
 | |
|             isMobile ? _buildMobileRow(context) : _buildDesktopColumn(context),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildMobileRow(BuildContext context) {
 | |
|     return Row(
 | |
|       children: [
 | |
|         _profileAvatar(40),
 | |
|         const SizedBox(width: 12),
 | |
|         Expanded(
 | |
|           child: Column(
 | |
|             mainAxisSize: MainAxisSize.min,
 | |
|             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|             children: [
 | |
|               Text(
 | |
|                 "${widget.user.firstName} ${widget.user.lastName}",
 | |
|                 style: Theme.of(context).textTheme.titleSmall,
 | |
|                 overflow: TextOverflow.ellipsis,
 | |
|               ),
 | |
|               const SizedBox(height: 2),
 | |
|               Text(
 | |
|                 widget.user.email,
 | |
|                 style: Theme.of(context).textTheme.bodySmall,
 | |
|                 overflow: TextOverflow.ellipsis,
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|         Row(
 | |
|           mainAxisSize: MainAxisSize.min,
 | |
|           children: [
 | |
|             IconButton(
 | |
|               icon: const Icon(Icons.edit, size: 20),
 | |
|               onPressed: widget.onEdit,
 | |
|               color: AppColors.rouge,
 | |
|               padding: const EdgeInsets.all(8),
 | |
|               constraints: const BoxConstraints(
 | |
|                 minWidth: 32,
 | |
|                 minHeight: 32,
 | |
|               ),
 | |
|             ),
 | |
|             IconButton(
 | |
|               icon: const Icon(Icons.delete, size: 20),
 | |
|               onPressed: widget.onDelete,
 | |
|               color: AppColors.gris,
 | |
|               padding: const EdgeInsets.all(8),
 | |
|               constraints: const BoxConstraints(
 | |
|                 minWidth: 32,
 | |
|                 minHeight: 32,
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildDesktopColumn(BuildContext context) {
 | |
|     return LayoutBuilder(
 | |
|       builder: (context, constraints) {
 | |
|         final isNarrow = constraints.maxWidth < 200;
 | |
| 
 | |
|         return Column(
 | |
|           mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|           children: [
 | |
|             Column(
 | |
|               mainAxisSize: MainAxisSize.min,
 | |
|               children: [
 | |
|                 _profileAvatar(48),
 | |
|                 const SizedBox(height: 12),
 | |
|                 Column(
 | |
|                   mainAxisSize: MainAxisSize.min,
 | |
|                   children: [
 | |
|                     Text(
 | |
|                       "${widget.user.firstName} ${widget.user.lastName}",
 | |
|                       style: Theme.of(context).textTheme.titleSmall,
 | |
|                       textAlign: TextAlign.center,
 | |
|                       overflow: TextOverflow.ellipsis,
 | |
|                     ),
 | |
|                     const SizedBox(height: 4),
 | |
|                     Text(
 | |
|                       widget.user.email,
 | |
|                       style: Theme.of(context).textTheme.bodySmall,
 | |
|                       textAlign: TextAlign.center,
 | |
|                       overflow: TextOverflow.ellipsis,
 | |
|                     ),
 | |
|                     if (widget.user.role.isNotEmpty) ...[
 | |
|                       const SizedBox(height: 4),
 | |
|                       Text(
 | |
|                         widget.user.role,
 | |
|                         style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | |
|                               color: AppColors.gris,
 | |
|                               fontSize: 11,
 | |
|                             ),
 | |
|                         textAlign: TextAlign.center,
 | |
|                         overflow: TextOverflow.ellipsis,
 | |
|                       ),
 | |
|                     ],
 | |
|                   ],
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|             Padding(
 | |
|               padding: const EdgeInsets.only(bottom: 4),
 | |
|               child: isNarrow
 | |
|                   ? Column(
 | |
|                       mainAxisSize: MainAxisSize.min,
 | |
|                       children: [
 | |
|                         _buildButton(
 | |
|                           icon: Icons.edit,
 | |
|                           label: "Modifier",
 | |
|                           onPressed: widget.onEdit,
 | |
|                           color: AppColors.rouge,
 | |
|                           isNarrow: true,
 | |
|                         ),
 | |
|                         const SizedBox(height: 4),
 | |
|                         _buildButton(
 | |
|                           icon: Icons.delete,
 | |
|                           label: "Supprimer",
 | |
|                           onPressed: widget.onDelete,
 | |
|                           color: AppColors.gris,
 | |
|                           isNarrow: true,
 | |
|                         ),
 | |
|                       ],
 | |
|                     )
 | |
|                   : Row(
 | |
|                       mainAxisAlignment: MainAxisAlignment.center,
 | |
|                       children: [
 | |
|                         _buildButton(
 | |
|                           icon: Icons.edit,
 | |
|                           label: "Modifier",
 | |
|                           onPressed: widget.onEdit,
 | |
|                           color: AppColors.rouge,
 | |
|                           isNarrow: false,
 | |
|                         ),
 | |
|                         const SizedBox(width: 8),
 | |
|                         _buildButton(
 | |
|                           icon: Icons.delete,
 | |
|                           label: "Supprimer",
 | |
|                           onPressed: widget.onDelete,
 | |
|                           color: AppColors.gris,
 | |
|                           isNarrow: false,
 | |
|                         ),
 | |
|                       ],
 | |
|                     ),
 | |
|             ),
 | |
|           ],
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildButton({
 | |
|     required IconData icon,
 | |
|     required String label,
 | |
|     required VoidCallback onPressed,
 | |
|     required Color color,
 | |
|     required bool isNarrow,
 | |
|   }) {
 | |
|     return SizedBox(
 | |
|       height: 26,
 | |
|       width: isNarrow ? double.infinity : null,
 | |
|       child: ElevatedButton.icon(
 | |
|         icon: Icon(icon, size: 14),
 | |
|         label: Text(
 | |
|           label,
 | |
|           style: const TextStyle(fontSize: 11),
 | |
|           overflow: TextOverflow.ellipsis,
 | |
|         ),
 | |
|         onPressed: onPressed,
 | |
|         style: ElevatedButton.styleFrom(
 | |
|           backgroundColor: color,
 | |
|           foregroundColor: AppColors.blanc,
 | |
|           padding: EdgeInsets.symmetric(
 | |
|             horizontal: isNarrow ? 4 : 8,
 | |
|             vertical: 0,
 | |
|           ),
 | |
|           shape: RoundedRectangleBorder(
 | |
|             borderRadius: BorderRadius.circular(6),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _profileAvatar(double size) {
 | |
|     if (_isLoadingImage && widget.user.profilePhotoUrl.isNotEmpty) {
 | |
|       return CircleAvatar(
 | |
|         radius: size / 2,
 | |
|         backgroundColor: Colors.grey[300],
 | |
|         child: SizedBox(
 | |
|           width: size * 0.5,
 | |
|           height: size * 0.5,
 | |
|           child: const CircularProgressIndicator(strokeWidth: 2),
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
|     return CircleAvatar(
 | |
|       radius: size / 2,
 | |
|       backgroundImage: _profileImage,
 | |
|       backgroundColor: Colors.grey[200],
 | |
|       child: (widget.user.profilePhotoUrl.isEmpty || _profileImage == null)
 | |
|           ? Icon(Icons.person, size: size * 0.6, color: AppColors.noir)
 | |
|           : null,
 | |
|     );
 | |
|   }
 | |
| }
 |