feat : Posts in Event Details
This commit is contained in:
478
lib/ui/common/post_card_widget.dart
Normal file
478
lib/ui/common/post_card_widget.dart
Normal file
@@ -0,0 +1,478 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import 'event_details_viewmodel.dart';
|
||||
import '../../common/post_card_widget.dart';
|
||||
|
||||
class EventDetailsView extends StackedView<EventDetailsViewModel> {
|
||||
final int eventId;
|
||||
@@ -203,13 +204,28 @@ class EventDetailsView extends StackedView<EventDetailsViewModel> {
|
||||
child: viewModel.selectedTabIndex == 0
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(50, (index) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
child: ListTile(
|
||||
title: Text('Publication ${index + 1}'),
|
||||
subtitle: Text('Description de la publication ${index + 1}'),
|
||||
),
|
||||
children: List.generate(10, (index) {
|
||||
return PostCardWidget(
|
||||
title: 'Publication ${index + 1}',
|
||||
content: 'Voici le contenu de la publication ${index + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
authorName: 'L\'Octonelle',
|
||||
authorImageUrl: '', // URL vide pour utiliser l'icône par défaut
|
||||
publishDate: DateTime.now().subtract(Duration(days: index)),
|
||||
imageUrls: index % 3 == 0 ? ['assets/images/Affiche.jpg'] : null,
|
||||
likesCount: (index + 1) * 5,
|
||||
commentsCount: (index + 1) * 2,
|
||||
onLike: () {
|
||||
// Action lors du clic sur "J'aime"
|
||||
print('Like publication ${index + 1}');
|
||||
},
|
||||
onComment: () {
|
||||
// Action lors du clic sur "Commenter"
|
||||
print('Comment publication ${index + 1}');
|
||||
},
|
||||
onShare: () {
|
||||
// Action lors du clic sur "Partager"
|
||||
print('Share publication ${index + 1}');
|
||||
},
|
||||
);
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -10,3 +10,15 @@ class EventDetailsViewModel extends BaseViewModel {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class Post {
|
||||
final String id;
|
||||
final String title;
|
||||
final String content;
|
||||
|
||||
Post({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user