import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import '../models/post_models.dart'; import '../services/post_service.dart'; import '../utils/invitation_utils.dart'; class PostsList extends StatefulWidget { final Future> Function() fetchPosts; final String emptyStateTitle; final String emptyStateSubtitle; final bool showRefreshIndicator; final Widget? floatingActionButton; const PostsList({ Key? key, required this.fetchPosts, this.emptyStateTitle = 'Nothing here..', this.emptyStateSubtitle = 'Create the first post!', this.showRefreshIndicator = true, this.floatingActionButton, }) : super(key: key); @override PostsListState createState() => PostsListState(); } class PostsListState extends State { bool _isLoading = true; List _posts = []; String _errorMessage = ''; @override void initState() { super.initState(); _loadPosts(); } Future _loadPosts() async { setState(() { _isLoading = true; _errorMessage = ''; }); try { final result = await widget.fetchPosts(); setState(() { if (result['success']) { _posts = result['posts']; } else { _errorMessage = result['message'] ?? 'Failed to load posts'; } _isLoading = false; }); } catch (e) { setState(() { _errorMessage = 'Network error: $e'; _isLoading = false; }); } } Future _refreshPosts() async { await _loadPosts(); if (widget.showRefreshIndicator) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Posts refreshed!'), backgroundColor: Color(0xFF6A4C93), ), ); } } @override Widget build(BuildContext context) { Widget body = _buildBody(); if (widget.showRefreshIndicator) { body = RefreshIndicator( onRefresh: _refreshPosts, child: body, ); } return body; } Widget _buildBody() { if (_isLoading) { return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), ), ); } if (_errorMessage.isNotEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: Colors.grey[400], ), SizedBox(height: 16), Text( _errorMessage, style: TextStyle( fontSize: 16, color: Colors.grey[600], ), textAlign: TextAlign.center, ), SizedBox(height: 16), ElevatedButton( onPressed: _loadPosts, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, ), child: Text('Try Again'), ), ], ), ); } if (_posts.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.post_add, size: 64, color: Colors.grey[400], ), SizedBox(height: 16), Text( widget.emptyStateTitle, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[600], ), ), SizedBox(height: 8), Text( widget.emptyStateSubtitle, style: TextStyle( fontSize: 14, color: Colors.grey[500], ), ), ], ), ); } return ListView.builder( padding: EdgeInsets.symmetric(vertical: 8), itemCount: _posts.length, itemBuilder: (context, index) { return PostCard( post: _posts[index], ); }, ); } void refreshPosts() { _loadPosts(); } } class PostCard extends StatefulWidget { final Post post; const PostCard({Key? key, required this.post}) : super(key: key); @override _PostCardState createState() => _PostCardState(); } class _PostCardState extends State { late Post _currentPost; bool _isLiking = false; @override void initState() { super.initState(); _currentPost = widget.post; } @override void didUpdateWidget(PostCard oldWidget) { super.didUpdateWidget(oldWidget); // Update current post if the widget's post changed if (oldWidget.post.id != widget.post.id) { _currentPost = widget.post; } } Color _getAvatarColor(String displayName) { final colors = [ Color(0xFF32B0A5), Color(0xFF4600B9), Color(0xFF6A4C93), Color(0xFFFF6347), Color(0xFF32CD32), Color(0xFF9932CC), ]; int hash = displayName.hashCode; return colors[hash.abs() % colors.length]; } String _getAvatarLetter(String displayName) { return displayName.isNotEmpty ? displayName[0].toUpperCase() : '?'; } void _sharePost(Post post) { final shareText = '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}'; Share.share(shareText); } Future _toggleLike() async { if (_isLiking) return; // Prevent multiple simultaneous requests setState(() { _isLiking = true; // Optimistic update - immediately change UI _currentPost = _currentPost.copyWith( liked: !_currentPost.liked, likes: _currentPost.liked ? _currentPost.likes - 1 : _currentPost.likes + 1, ); }); try { Map result; if (widget.post.liked) { result = await PostService.unlikePost(widget.post.id); } else { result = await PostService.likePost(widget.post.id); } if (result['success'] && result['post'] != null) { // Update with server response setState(() { _currentPost = result['post']; _isLiking = false; }); } else { // Revert optimistic update on failure setState(() { _currentPost = widget.post; _isLiking = false; }); // Show error message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? 'Failed to update like'), backgroundColor: Colors.red, ), ); } } } catch (e) { // Revert optimistic update on error setState(() { _currentPost = widget.post; _isLiking = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Network error occurred'), backgroundColor: Colors.red, ), ); } } } @override Widget build(BuildContext context) { final creator = _currentPost.creator; final avatarColor = _getAvatarColor(creator.displayName); final avatarLetter = _getAvatarLetter(creator.displayName); final relativeTime = InvitationUtils.getRelativeTime(_currentPost.creationDate); return Container( margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: Offset(0, 2), ), ], ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( radius: 20, backgroundColor: avatarColor, child: Text( avatarLetter, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( creator.displayName, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.black87, ), ), SizedBox(height: 2), Text( relativeTime, style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], ), ), IconButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('More options pressed')), ); }, icon: Icon(Icons.more_horiz, color: Colors.grey[600]), ), ], ), SizedBox(height: 12), Text( _currentPost.body, style: TextStyle( fontSize: 15, height: 1.4, color: Colors.black87, ), ), SizedBox(height: 16), Row( children: [ IconButton( onPressed: _toggleLike, icon: Icon( _currentPost.liked ? Icons.favorite : Icons.favorite_border, color: _currentPost.liked ? Colors.red : Colors.grey[600], size: 24, ), ), Text( '${_currentPost.likes}', style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), SizedBox(width: 16), IconButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Comments feature coming soon!')), ); }, icon: Icon( Icons.chat_bubble_outline, color: Colors.grey[600], size: 24, ), ), Text( '${_currentPost.comments}', style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), Spacer(), IconButton( onPressed: () => _sharePost(_currentPost), icon: Icon( Icons.share_outlined, color: Colors.grey[600], size: 24, ), ), ], ), ], ), ), ); } } class ProfilePostsList extends StatefulWidget { final Future> Function() fetchPosts; final Function(int posts, int likes)? onStatsUpdate; const ProfilePostsList({ Key? key, required this.fetchPosts, this.onStatsUpdate, }) : super(key: key); @override _ProfilePostsListState createState() => _ProfilePostsListState(); } class _ProfilePostsListState extends State { bool _isLoading = true; List _posts = []; String _errorMessage = ''; @override void initState() { super.initState(); _loadPosts(); } Future _loadPosts() async { setState(() { _isLoading = true; _errorMessage = ''; }); try { final result = await widget.fetchPosts(); setState(() { if (result['success']) { _posts = result['posts']; // Update parent stats if (widget.onStatsUpdate != null) { final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes); widget.onStatsUpdate!(_posts.length, totalLikes); } } else { _errorMessage = result['message'] ?? 'Failed to load posts'; } _isLoading = false; }); } catch (e) { setState(() { _errorMessage = 'Network error: $e'; _isLoading = false; }); } } @override Widget build(BuildContext context) { if (_isLoading) { return Container( padding: EdgeInsets.all(32), child: Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( Color(0xFF6A4C93), ), ), ), ); } if (_errorMessage.isNotEmpty) { return Container( padding: EdgeInsets.all(32), child: Center( child: Column( children: [ Icon( Icons.error_outline, size: 64, color: Colors.grey[400], ), SizedBox(height: 16), Text( _errorMessage, style: TextStyle( fontSize: 16, color: Colors.grey[600], ), textAlign: TextAlign.center, ), SizedBox(height: 16), ElevatedButton( onPressed: _loadPosts, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, ), child: Text('Try Again'), ), ], ), ), ); } if (_posts.isEmpty) { return Container( padding: EdgeInsets.all(32), child: Center( child: Column( children: [ Icon( Icons.post_add, size: 64, color: Colors.grey[400], ), SizedBox(height: 16), Text( 'No posts yet', style: TextStyle( fontSize: 18, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), SizedBox(height: 8), Text( 'Start sharing your thoughts!', style: TextStyle( fontSize: 14, color: Colors.grey[500], ), ), ], ), ), ); } return Column( children: [ ..._posts.map((post) => Container( margin: EdgeInsets.only(bottom: 16, left: 16, right: 16), child: PostCard(post: post), ) ), SizedBox(height: 24), ], ); } }