Files
Bahla-Front/lib/ui/common/post_card_widget.dart
2025-08-08 17:49:03 +02:00

479 lines
15 KiB
Dart

import 'package:flutter/material.dart';
class PostCardWidget extends StatelessWidget {
final String title;
final String content;
final String authorName;
final String authorImageUrl;
final DateTime publishDate;
final List<String>? imageUrls;
final int likesCount;
final int commentsCount;
final int sharesCount;
final VoidCallback? onLike;
final VoidCallback? onComment;
final VoidCallback? onShare;
const PostCardWidget({
Key? key,
required this.title,
required this.content,
required this.authorName,
required this.authorImageUrl,
required this.publishDate,
this.imageUrls,
this.likesCount = 0,
this.commentsCount = 0,
this.sharesCount = 0,
this.onLike,
this.onComment,
this.onShare,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
//elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec auteur et date
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Photo de profil de l'auteur
CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(authorImageUrl),
onBackgroundImageError: (_, __) {},
child: authorImageUrl.isEmpty
? const Icon(Icons.person, size: 20)
: null,
),
const SizedBox(width: 12),
// Nom et date
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
authorName,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 2),
Text(
_formatDate(publishDate),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
],
),
),
// Menu options
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.grey),
onPressed: () {
// Action pour le menu
},
),
],
),
),
// Contenu du post
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Contenu
Text(
content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white,
height: 1.4,
),
),
],
),
),
// Images avec boutons d'action si présentes
if (imageUrls != null && imageUrls!.isNotEmpty)
Container(
//margin: const EdgeInsets.symmetric(vertical: 12.0),
height: 200,
child: Stack(
children: [
// Images
PageView.builder(
itemCount: imageUrls!.length,
itemBuilder: (context, index) {
return Container(
//margin: const EdgeInsets.symmetric(horizontal: 16.0),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
child: _buildImage(imageUrls![index]),
),
);
},
),
// Boutons d'action en overlay à droite
Positioned(
right: 24,
top: 0,
bottom: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_VerticalActionButton(
icon: Icons.favorite_outline,
count: likesCount,
onPressed: onLike,
),
const SizedBox(height: 16),
_VerticalActionButton(
icon: Icons.comment_outlined,
count: commentsCount,
onPressed: onComment,
),
const SizedBox(height: 16),
_VerticalActionButton(
icon: Icons.share_outlined,
count: sharesCount,
onPressed: onShare,
),
],
),
),
],
),
),
// Si pas d'image, boutons d'action en bas à droite
if (imageUrls == null || imageUrls!.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_HorizontalActionButton(
icon: Icons.favorite_outline,
count: likesCount,
onPressed: onLike,
),
const SizedBox(width: 16),
_HorizontalActionButton(
icon: Icons.comment_outlined,
count: commentsCount,
onPressed: onComment,
),
const SizedBox(width: 16),
_HorizontalActionButton(
icon: Icons.share_outlined,
count: sharesCount,
onPressed: onShare,
),
],
),
),
],
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 7) {
return '${date.day}/${date.month}/${date.year}';
} else if (difference.inDays > 0) {
return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}min';
} else {
return 'À l\'instant';
}
}
Widget _buildImage(String imageUrl) {
// Vérifie si c'est une image d'assets ou une URL réseau
if (imageUrl.startsWith('assets/')) {
return Image.asset(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('Erreur de chargement d\'asset: $error');
return _buildErrorWidget();
},
);
} else {
return Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
print('Erreur de chargement d\'image réseau: $error');
return _buildErrorWidget();
},
);
}
}
Widget _buildErrorWidget() {
return Container(
color: Colors.grey[300],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broken_image,
size: 50,
color: Colors.grey[600],
),
const SizedBox(height: 8),
Text(
'Image non disponible',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
);
}
}
class _VerticalActionButton extends StatefulWidget {
final IconData icon;
final int? count;
final VoidCallback? onPressed;
const _VerticalActionButton({
required this.icon,
this.count,
this.onPressed,
});
@override
State<_VerticalActionButton> createState() => _VerticalActionButtonState();
}
class _VerticalActionButtonState extends State<_VerticalActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.85,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTap() async {
await _animationController.forward();
await _animationController.reverse();
widget.onPressed?.call();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
widget.icon,
size: 24,
color: Colors.white,
),
),
if (widget.count != null && widget.count! > 0) ...[
const SizedBox(height: 4),
Text(
widget.count! > 999 ? '999+' : '${widget.count}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
],
),
),
);
},
),
);
}
}
class _HorizontalActionButton extends StatefulWidget {
final IconData icon;
final int? count;
final VoidCallback? onPressed;
const _HorizontalActionButton({
required this.icon,
this.count,
this.onPressed,
});
@override
State<_HorizontalActionButton> createState() => _HorizontalActionButtonState();
}
class _HorizontalActionButtonState extends State<_HorizontalActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.85,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTap() async {
await _animationController.forward();
await _animationController.reverse();
widget.onPressed?.call();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
child: Icon(
widget.icon,
size: 20,
color: Colors.grey[900],
),
),
if (widget.count != null && widget.count! > 0) ...[
const SizedBox(height: 4),
Text(
widget.count! > 999 ? '999+' : '${widget.count}',
style: TextStyle(
color: Colors.grey[100],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
],
),
),
);
},
),
);
}
}