import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../../../core/constants/design_system.dart'; import '../../../data/datasources/social_remote_data_source.dart'; import '../../../domain/entities/social_post.dart'; import '../../widgets/comments_bottom_sheet.dart'; import '../../widgets/custom_snackbar.dart'; import '../../widgets/modern_empty_state.dart'; import '../../widgets/share_post_dialog.dart'; import '../../widgets/shimmer_loading.dart'; import 'social_card.dart'; /// Widget de contenu social moderne avec stories et feed de posts. class SocialContent extends StatefulWidget { const SocialContent({ super.key, this.userId, this.refreshTrigger, }); /// ID de l'utilisateur pour charger les posts de ses amis final String? userId; /// Notifier pour déclencher un refresh depuis l'extérieur final ValueNotifier? refreshTrigger; @override State createState() => _SocialContentState(); } class _SocialContentState extends State { final SocialRemoteDataSource _dataSource = SocialRemoteDataSource(http.Client()); final ScrollController _scrollController = ScrollController(); List _posts = []; bool _isLoading = true; bool _isLoadingMore = false; String? _errorMessage; @override void initState() { super.initState(); loadPosts(); _scrollController.addListener(_onScroll); // Écouter les changements du refreshTrigger pour actualiser widget.refreshTrigger?.addListener(_onRefreshTriggered); } @override void dispose() { widget.refreshTrigger?.removeListener(_onRefreshTriggered); _scrollController.dispose(); super.dispose(); } /// Appelé quand le refreshTrigger change void _onRefreshTriggered() { loadPosts(); } /// Gère le scroll pour la pagination void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !_isLoadingMore && !_isLoading) { _loadMorePosts(); } } /// Charge les posts initiaux (méthode publique pour permettre le refresh depuis l'extérieur) Future loadPosts() async { if (!mounted) return; setState(() { _isLoading = true; _errorMessage = null; }); try { // Si userId est fourni, charger les posts de l'utilisateur et de ses amis // Sinon, charger tous les posts final posts = widget.userId != null ? await _dataSource.getPostsByFriends(userId: widget.userId!) : await _dataSource.getPosts(); if (mounted) { setState(() { _posts = posts.map((model) => model.toEntity()).toList(); _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _errorMessage = e.toString(); _isLoading = false; }); } } } /// Charge plus de posts (pagination) Future _loadMorePosts() async { if (_isLoadingMore) return; setState(() { _isLoadingMore = true; }); try { // TODO: Implémenter la pagination avec offset/limit await Future.delayed(const Duration(seconds: 1)); if (mounted) { setState(() { _isLoadingMore = false; }); } } catch (e) { if (mounted) { setState(() { _isLoadingMore = false; }); } } } Future _handleLike(String postId, int index) async { final post = _posts[index]; final wasLiked = post.isLikedByCurrentUser; // Mise à jour optimiste (UI instantanée) setState(() { _posts[index] = post.copyWith( isLikedByCurrentUser: !wasLiked, likesCount: wasLiked ? post.likesCount - 1 : post.likesCount + 1, ); }); try { final updatedPost = await _dataSource.likePost(postId); if (mounted) { setState(() { _posts[index] = updatedPost.toEntity(); }); } } catch (e) { // Rollback en cas d'erreur if (mounted) { setState(() { _posts[index] = post; }); context.showError('Erreur lors du like'); } } } Future _handleComment(String postId, int index) async { await CommentsBottomSheet.show( context: context, postId: postId, onCommentAdded: () { // Recharger le post pour avoir le compteur de commentaires à jour _refreshPost(postId, index); }, ); } Future _refreshPost(String postId, int index) async { try { final posts = await _dataSource.getPosts(); final updatedPost = posts.firstWhere((p) => p.id == postId); if (mounted) { setState(() { _posts[index] = updatedPost.toEntity(); }); } } catch (e) { // Silently fail, pas besoin d'afficher une erreur } } Future _handleShare(String postId, int index) async { final post = _posts[index]; // Ouvre le dialogue de partage avec plusieurs options await SharePostDialog.show( context: context, post: post, onShareConfirmed: () => _confirmShare(postId, index), ); } /// Confirme le partage sur le fil (incrémente le compteur) Future _confirmShare(String postId, int index) async { try { final updatedPost = await _dataSource.sharePost(postId); if (mounted) { setState(() { _posts[index] = updatedPost.toEntity(); }); context.showSuccess('Post partagé avec succès'); } } catch (e) { if (mounted) { context.showError('Erreur lors du partage: ${e.toString()}'); } } } Future _handleDelete(String postId, int index) async { try { await _dataSource.deletePost(postId); if (mounted) { setState(() { _posts.removeAt(index); }); context.showSuccess('Post supprimé avec succès'); } } catch (e) { if (mounted) { context.showError('Erreur lors de la suppression: ${e.toString()}'); } } } void _handleEdit(SocialPost post, int index) { // TODO: Implémenter l'édition avec un dialog context.showInfo('Fonctionnalité d\'édition à venir'); } @override Widget build(BuildContext context) { if (_isLoading) { return const SkeletonList( itemCount: 3, skeletonWidget: SocialPostSkeleton(), ); } if (_errorMessage != null) { return _buildErrorState(context); } if (_posts.isEmpty) { return RefreshIndicator( onRefresh: loadPosts, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: SizedBox( height: MediaQuery.of(context).size.height - 200, child: const ModernEmptyState( illustration: EmptyStateIllustration.social, title: 'Aucun post disponible', description: 'Soyez le premier à publier quelque chose et commencer les discussions !', ), ), ), ); } return RefreshIndicator( onRefresh: loadPosts, child: CustomScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ // TODO: Section Stories (en attente de l'endpoint backend) // SliverToBoxAdapter(child: _buildStoriesSection()), // Padding top const SliverToBoxAdapter( child: SizedBox(height: DesignSystem.spacingSm), ), // Liste des posts SliverPadding( padding: const EdgeInsets.symmetric( horizontal: DesignSystem.spacingMd, ), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index >= _posts.length) { return _isLoadingMore ? const Padding( padding: EdgeInsets.all(DesignSystem.spacingLg), child: Center( child: CircularProgressIndicator(strokeWidth: 2.5), ), ) : const SizedBox.shrink(); } final post = _posts[index]; return SocialCard( key: ValueKey(post.id), post: post, onLike: () => _handleLike(post.id, index), onComment: () => _handleComment(post.id, index), onShare: () => _handleShare(post.id, index), onDeletePost: () => _handleDelete(post.id, index), onEditPost: () => _handleEdit(post, index), // Badge vérifié retiré - pas de données backend showVerifiedBadge: false, ); }, childCount: _posts.length + (_isLoadingMore ? 1 : 0), ), ), ), // Padding bottom const SliverToBoxAdapter( child: SizedBox(height: DesignSystem.spacingXl), ), ], ), ); } // TODO: Méthodes stories à implémenter quand l'endpoint backend sera disponible // Widget _buildStoriesSection() { ... } // Widget _buildStoryItem() { ... } /// Construit l'état d'erreur Widget _buildErrorState(BuildContext context) { final theme = Theme.of(context); return RefreshIndicator( onRefresh: loadPosts, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: SizedBox( height: MediaQuery.of(context).size.height - 200, child: Center( child: Padding( padding: const EdgeInsets.all(DesignSystem.spacingXl), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline_rounded, size: 64, color: theme.colorScheme.error.withOpacity(0.5), ), const SizedBox(height: DesignSystem.spacingLg), Text( 'Erreur de chargement', style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: DesignSystem.spacingSm), Text( _errorMessage!, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), textAlign: TextAlign.center, ), const SizedBox(height: DesignSystem.spacingXl), FilledButton.icon( onPressed: loadPosts, icon: const Icon(Icons.refresh_rounded, size: 20), label: const Text('Réessayer'), ), ], ), ), ), ), ), ); } }