diff --git a/frontend/lib/models/post_models.dart b/frontend/lib/models/post_models.dart index a70eb19..2b24ede 100644 --- a/frontend/lib/models/post_models.dart +++ b/frontend/lib/models/post_models.dart @@ -30,6 +30,7 @@ class Post { final int likes; final int comments; final DateTime creationDate; + final bool liked; Post({ required this.id, @@ -39,6 +40,7 @@ class Post { required this.likes, required this.comments, required this.creationDate, + this.liked = false, }); factory Post.fromJson(Map json) { @@ -50,6 +52,7 @@ class Post { likes: int.tryParse(json['likes']?.toString() ?? '0') ?? 0, comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0, creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()), + liked: json['liked'] == true, ); } @@ -62,8 +65,31 @@ class Post { 'likes': likes, 'comments': comments, 'creationDate': creationDate.toIso8601String(), + 'liked': liked, }; } + + Post copyWith({ + String? id, + String? creatorId, + PostCreator? creator, + String? body, + int? likes, + int? comments, + DateTime? creationDate, + bool? liked, + }) { + return Post( + id: id ?? this.id, + creatorId: creatorId ?? this.creatorId, + creator: creator ?? this.creator, + body: body ?? this.body, + likes: likes ?? this.likes, + comments: comments ?? this.comments, + creationDate: creationDate ?? this.creationDate, + liked: liked ?? this.liked, + ); + } } class CreatePostRequest { diff --git a/frontend/lib/screens/pages/feed_page.dart b/frontend/lib/screens/pages/feed_page.dart index 3729d2f..99b7a31 100644 --- a/frontend/lib/screens/pages/feed_page.dart +++ b/frontend/lib/screens/pages/feed_page.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:share_plus/share_plus.dart'; import '../create_post_screen.dart'; import '../../services/post_service.dart'; -import '../../models/post_models.dart'; -import '../../utils/invitation_utils.dart'; +import '../../widgets/posts_list.dart'; class FeedPage extends StatefulWidget { @override @@ -11,98 +9,8 @@ class FeedPage extends StatefulWidget { } class _FeedPageState extends State { - bool _isRefreshing = false; - bool _isLoading = true; - List _posts = []; - String _errorMessage = ''; + final GlobalKey _postsListKey = GlobalKey(); - @override - void initState() { - super.initState(); - _loadPosts(); - } - - Future _loadPosts() async { - setState(() { - _isLoading = true; - _errorMessage = ''; - }); - - try { - final result = await PostService.getAllPosts(); - 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; - }); - } - } - - final List> mockPosts = [ - { - 'id': '1', - 'user': { - 'displayName': 'Abu Khalid (Aqeel)', - 'avatar': 'A', - 'avatar_color': Color(0xFF32B0A5), - }, - 'content': 'Free hasawi khalas dates! Drop by my office to grab some 😉', - 'timestamp': '42 minutes ago', - 'likes': 12, - 'comments': 3, - 'isLiked': false, - }, - { - 'id': '2', - 'user': { - 'displayName': 'Sarah Khalid', - 'avatar': 'S', - 'avatar_color': Color(0xFF4600B9), - }, - 'content': - 'Alhamdulillah, I am happy to tell you I have been blessed with a baby ❤️', - 'timestamp': '4 hours ago', - 'likes': 28, - 'comments': 7, - 'isLiked': true, - }, - { - 'id': '3', - 'user': { - 'displayName': 'Omar Hassan', - 'avatar': 'O', - 'avatar_color': Color(0xFF6A4C93), - }, - 'content': - 'The sunset view from my balcony tonight is absolutely breathtaking. Sometimes you just need to pause and appreciate the beauty around us.', - 'timestamp': '1 day ago', - 'likes': 45, - 'comments': 12, - 'isLiked': false, - }, - { - 'id': '4', - 'user': { - 'displayName': 'Fatima Al-Zahra', - 'avatar': 'F', - 'avatar_color': Color(0xFF32B0A5), - }, - 'content': - 'Finished reading an incredible book today. "The Seven Habits of Highly Effective People" - highly recommend it to anyone looking for personal development!', - 'timestamp': '2 days ago', - 'likes': 19, - 'comments': 5, - 'isLiked': true, - }, - ]; @override Widget build(BuildContext context) { @@ -119,15 +27,18 @@ class _FeedPageState extends State { automaticallyImplyLeading: false, actions: [ IconButton( - onPressed: _loadPosts, + onPressed: () => _postsListKey.currentState?.refreshPosts(), icon: Icon(Icons.refresh), tooltip: 'Refresh', ), ], ), - body: RefreshIndicator( - onRefresh: _refreshFeed, - child: _buildBody(), + body: PostsList( + key: _postsListKey, + fetchPosts: PostService.getAllPosts, + emptyStateTitle: 'Nothing here..', + emptyStateSubtitle: 'Create the first post!', + showRefreshIndicator: true, ), floatingActionButton: FloatingActionButton( onPressed: _navigateToCreatePost, @@ -137,101 +48,6 @@ class _FeedPageState extends State { ); } - 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( - 'Nothing here..', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.grey[600], - ), - ), - SizedBox(height: 8), - Text( - 'Create the first post!', - 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], - ); - }, - ); - } - - Future _refreshFeed() async { - await _loadPosts(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Feed refreshed!'), - backgroundColor: Color(0xFF6A4C93), - ), - ); - } Future _navigateToCreatePost() async { final result = await Navigator.push( @@ -241,180 +57,8 @@ class _FeedPageState extends State { // If post was created successfully, refresh the feed if (result == true) { - _loadPosts(); + _postsListKey.currentState?.refreshPosts(); } } } -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 { - 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); - } - - @override - Widget build(BuildContext context) { - final creator = widget.post.creator; - final avatarColor = _getAvatarColor(creator.displayName); - final avatarLetter = _getAvatarLetter(creator.displayName); - final relativeTime = InvitationUtils.getRelativeTime(widget.post.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( - widget.post.body, - style: TextStyle( - fontSize: 15, - height: 1.4, - color: Colors.black87, - ), - ), - SizedBox(height: 16), - Row( - children: [ - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Like feature coming soon!')), - ); - }, - icon: Icon( - Icons.favorite_border, - color: Colors.grey[600], - size: 24, - ), - ), - Text( - '${widget.post.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( - '${widget.post.comments}', - style: TextStyle( - color: Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - Spacer(), - IconButton( - onPressed: () => _sharePost(widget.post), - icon: Icon( - Icons.share_outlined, - color: Colors.grey[600], - size: 24, - ), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 499979c..2dcc633 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -5,6 +5,7 @@ import '../../services/user_service.dart'; import '../../services/auth_service.dart'; import '../../services/post_service.dart'; import '../../models/post_models.dart'; +import '../../widgets/posts_list.dart'; class ProfilePage extends StatefulWidget { @override @@ -17,16 +18,15 @@ class _ProfilePageState extends State { bool isLoading = false; Map? userData; bool isLoadingUser = true; - List userPosts = []; - bool isLoadingPosts = true; int totalLikes = 0; + int totalPosts = 0; @override void initState() { super.initState(); _loadFCMToken(); _loadUserData(); - _loadUserPosts(); + _loadUserStats(); } @override @@ -74,22 +74,17 @@ class _ProfilePageState extends State { }); } - Future _loadUserPosts() async { - setState(() { - isLoadingPosts = true; - }); - + Future _loadUserStats() async { final result = await PostService.getUserPosts(); setState(() { - isLoadingPosts = false; if (result['success'] == true) { - userPosts = result['posts'] as List; - totalLikes = userPosts.fold(0, (sum, post) => sum + post.likes); + final posts = result['posts'] as List; + totalPosts = posts.length; + totalLikes = posts.fold(0, (sum, post) => sum + post.likes); } else { - userPosts = []; + totalPosts = 0; totalLikes = 0; - _showErrorAlert(result['message'] ?? 'Failed to load posts'); } }); } @@ -128,21 +123,6 @@ class _ProfilePageState extends State { ).push(MaterialPageRoute(builder: (context) => SettingsPage())); } - String _formatTimestamp(DateTime dateTime) { - final now = DateTime.now(); - final difference = now.difference(dateTime); - - if (difference.inDays > 0) { - return '${difference.inDays}d ago'; - } else if (difference.inHours > 0) { - return '${difference.inHours}h ago'; - } else if (difference.inMinutes > 0) { - return '${difference.inMinutes}m ago'; - } else { - return 'Just now'; - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -234,7 +214,7 @@ class _ProfilePageState extends State { Column( children: [ Text( - '${userPosts.length}', + '$totalPosts', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -281,169 +261,16 @@ class _ProfilePageState extends State { margin: EdgeInsets.symmetric(horizontal: 16), ), SizedBox(height: 16), - if (isLoadingPosts) - Container( - padding: EdgeInsets.all(32), - child: Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Color(0xFF6A4C93), - ), - ), - ), - ) - else if (userPosts.isEmpty) - 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], - ), - ), - ], - ), - ), - ) - else - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 16), - itemCount: userPosts.length, - itemBuilder: (context, index) { - final post = userPosts[index]; - return Container( - margin: EdgeInsets.only(bottom: 16), - 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: 16, - backgroundColor: Color(0xFF6A4C93), - child: Text( - post.creator.displayName.isNotEmpty - ? post.creator.displayName - .substring(0, 1) - .toUpperCase() - : 'U', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - post.creator.displayName.isNotEmpty - ? post.creator.displayName - : 'Unknown User', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Colors.black87, - ), - ), - Text( - _formatTimestamp(post.creationDate), - style: TextStyle( - color: Colors.grey[600], - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - SizedBox(height: 12), - Text( - post.body, - style: TextStyle( - fontSize: 15, - height: 1.4, - color: Colors.black87, - ), - ), - SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.favorite_border, - color: Colors.grey[600], - size: 20, - ), - SizedBox(width: 4), - Text( - '${post.likes}', - style: TextStyle( - color: Colors.grey[700], - fontSize: 14, - ), - ), - SizedBox(width: 16), - Icon( - Icons.chat_bubble_outline, - color: Colors.grey[600], - size: 20, - ), - SizedBox(width: 4), - Text( - '${post.comments}', - style: TextStyle( - color: Colors.grey[700], - fontSize: 14, - ), - ), - ], - ), - ], - ), - ), - ); - }, - ), + ProfilePostsList( + fetchPosts: PostService.getUserPosts, + onStatsUpdate: (posts, likes) { + setState(() { + totalPosts = posts; + totalLikes = likes; + }); + }, + ), ], - SizedBox(height: 24), ], ), ), @@ -547,7 +374,7 @@ class _SettingsPageState extends State { bottom: PreferredSize( preferredSize: Size.fromHeight(1), child: Container(height: 1, color: Colors.grey[200]), - ), + ), automaticallyImplyLeading: true, ), body: Padding( diff --git a/frontend/lib/services/post_service.dart b/frontend/lib/services/post_service.dart index e735550..5149d27 100644 --- a/frontend/lib/services/post_service.dart +++ b/frontend/lib/services/post_service.dart @@ -95,4 +95,64 @@ class PostService { return {'success': false, 'message': 'Network error: $e', 'post': null}; } } + + static Future> likePost(String postId) async { + try { + final response = await HttpService.post( + '/posts/like', + {'postId': postId}, + ); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + return { + 'success': responseData['status'] ?? false, + 'message': responseData['message'] ?? '', + 'post': responseData['data'] != null + ? Post.fromJson(responseData['data']) + : null, + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to like post', + 'post': null, + }; + } + } catch (e) { + print('Error liking post: $e'); + return {'success': false, 'message': 'Network error: $e', 'post': null}; + } + } + + static Future> unlikePost(String postId) async { + try { + final response = await HttpService.post( + '/posts/unlike', + {'postId': postId}, + ); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + return { + 'success': responseData['status'] ?? false, + 'message': responseData['message'] ?? '', + 'post': responseData['data'] != null + ? Post.fromJson(responseData['data']) + : null, + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to unlike post', + 'post': null, + }; + } + } catch (e) { + print('Error unliking post: $e'); + return {'success': false, 'message': 'Network error: $e', 'post': null}; + } + } } diff --git a/frontend/lib/widgets/posts_list.dart b/frontend/lib/widgets/posts_list.dart new file mode 100644 index 0000000..aacd0d2 --- /dev/null +++ b/frontend/lib/widgets/posts_list.dart @@ -0,0 +1,577 @@ +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), + ], + ); + } +} \ No newline at end of file