From 8adb57af8a3f08227e16e4e7029e4f0f629c3c29 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Thu, 24 Jul 2025 10:17:52 +0300 Subject: [PATCH 1/2] feat: getting a specific post backend --- .../wesal/controller/PostController.java | 29 ++++++ .../online/wesal/wesal/dto/LikedUserDTO.java | 43 +++++++++ .../wesal/dto/PostWithLikesResponseDTO.java | 95 +++++++++++++++++++ .../wesal/repository/PostLikeRepository.java | 3 + .../wesal/wesal/service/PostService.java | 39 ++++++++ 5 files changed, 209 insertions(+) create mode 100644 backend/src/main/java/online/wesal/wesal/dto/LikedUserDTO.java create mode 100644 backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java diff --git a/backend/src/main/java/online/wesal/wesal/controller/PostController.java b/backend/src/main/java/online/wesal/wesal/controller/PostController.java index c649145..fddf4e5 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/PostController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/PostController.java @@ -7,6 +7,7 @@ import online.wesal.wesal.dto.ApiResponse; import online.wesal.wesal.dto.PostCreateRequestDTO; import online.wesal.wesal.dto.PostLikeRequestDTO; import online.wesal.wesal.dto.PostResponseDTO; +import online.wesal.wesal.dto.PostWithLikesResponseDTO; import online.wesal.wesal.entity.Post; import online.wesal.wesal.service.PostService; import online.wesal.wesal.service.UserService; @@ -184,4 +185,32 @@ public class PostController { return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later")); } } + + @GetMapping("/get") + @Operation(summary = "Get post by ID", description = "Get a specific post by ID with all liked users (limit 100)") + public ResponseEntity> getPostById( + @RequestParam Long id, + Authentication authentication) { + + try { + if (id == null || id <= 0) { + return ResponseEntity.badRequest().body(ApiResponse.error("Valid post ID is required")); + } + + PostWithLikesResponseDTO response = postService.getPostById(id); + return ResponseEntity.ok(ApiResponse.success(response)); + } catch (RuntimeException e) { + String message; + if (e.getMessage().contains("Post not found")) { + message = "Post not found"; + } else if (e.getMessage().contains("User not found") || e.getMessage().contains("Creator not found")) { + message = "Authentication error. Please log in again."; + } else { + message = "Something went wrong.. We're sorry but try again later"; + } + return ResponseEntity.badRequest().body(ApiResponse.error(message)); + } catch (Exception e) { + return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later")); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/LikedUserDTO.java b/backend/src/main/java/online/wesal/wesal/dto/LikedUserDTO.java new file mode 100644 index 0000000..fe11315 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/dto/LikedUserDTO.java @@ -0,0 +1,43 @@ +package online.wesal.wesal.dto; + +import online.wesal.wesal.entity.User; +import java.time.LocalDateTime; + +public class LikedUserDTO { + + private String id; + private String displayName; + private LocalDateTime likeTime; + + public LikedUserDTO() {} + + public LikedUserDTO(User user, LocalDateTime likeTime) { + this.id = String.valueOf(user.getId()); + this.displayName = user.getDisplayName(); + this.likeTime = likeTime; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public LocalDateTime getLikeTime() { + return likeTime; + } + + public void setLikeTime(LocalDateTime likeTime) { + this.likeTime = likeTime; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java b/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java new file mode 100644 index 0000000..bf36b86 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java @@ -0,0 +1,95 @@ +package online.wesal.wesal.dto; + +import online.wesal.wesal.entity.Post; +import online.wesal.wesal.entity.User; +import java.time.LocalDateTime; +import java.util.List; + +public class PostWithLikesResponseDTO { + + private String id; + private CreatorDTO creator; + private String body; + private String likes; + private String comments; + private boolean liked; + private LocalDateTime creationDate; + private List likedUsers; + + public PostWithLikesResponseDTO() {} + + public PostWithLikesResponseDTO(Post post, User creator, boolean liked, List likedUsers) { + this.id = String.valueOf(post.getId()); + this.creator = new CreatorDTO(creator); + this.body = post.getBody(); + this.likes = String.valueOf(post.getLikes()); + this.comments = String.valueOf(post.getComments()); + this.liked = liked; + this.creationDate = post.getCreationDate(); + this.likedUsers = likedUsers; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public CreatorDTO getCreator() { + return creator; + } + + public void setCreator(CreatorDTO creator) { + this.creator = creator; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getLikes() { + return likes; + } + + public void setLikes(String likes) { + this.likes = likes; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public boolean isLiked() { + return liked; + } + + public void setLiked(boolean liked) { + this.liked = liked; + } + + public LocalDateTime getCreationDate() { + return creationDate; + } + + public void setCreationDate(LocalDateTime creationDate) { + this.creationDate = creationDate; + } + + public List getLikedUsers() { + return likedUsers; + } + + public void setLikedUsers(List likedUsers) { + this.likedUsers = likedUsers; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/repository/PostLikeRepository.java b/backend/src/main/java/online/wesal/wesal/repository/PostLikeRepository.java index ca40e23..f9bfbfd 100644 --- a/backend/src/main/java/online/wesal/wesal/repository/PostLikeRepository.java +++ b/backend/src/main/java/online/wesal/wesal/repository/PostLikeRepository.java @@ -22,4 +22,7 @@ public interface PostLikeRepository extends JpaRepository { @Query("SELECT pl.postId FROM PostLike pl WHERE pl.userId = :userId") Set findAllLikedPostIdsByUserId(@Param("userId") Long userId); + + @Query("SELECT pl FROM PostLike pl WHERE pl.postId = :postId ORDER BY pl.createdAt DESC") + List findByPostIdOrderByCreatedAtDesc(@Param("postId") Long postId); } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/service/PostService.java b/backend/src/main/java/online/wesal/wesal/service/PostService.java index 3cbb492..2916e53 100644 --- a/backend/src/main/java/online/wesal/wesal/service/PostService.java +++ b/backend/src/main/java/online/wesal/wesal/service/PostService.java @@ -1,6 +1,8 @@ package online.wesal.wesal.service; +import online.wesal.wesal.dto.LikedUserDTO; import online.wesal.wesal.dto.PostResponseDTO; +import online.wesal.wesal.dto.PostWithLikesResponseDTO; import online.wesal.wesal.entity.Post; import online.wesal.wesal.entity.PostLike; import online.wesal.wesal.entity.User; @@ -155,4 +157,41 @@ public class PostService { return new PostResponseDTO(post, creator, isLiked); } + + public PostWithLikesResponseDTO getPostById(Long postId) { + // Get the post + Post post = postRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("Post not found")); + + // Get the creator + User creator = userRepository.findById(post.getCreatorId()) + .orElseThrow(() -> new RuntimeException("Creator not found")); + + // Get current user to check if they liked the post + User currentUser = userService.getCurrentUser(); + Long currentUserId = currentUser.getId(); + boolean isLiked = postLikeRepository.existsByPostIdAndUserId(postId, currentUserId); + + // Get liked users (limit 100) with their like timestamps + List postLikes = postLikeRepository.findByPostIdOrderByCreatedAtDesc(postId); + List limitedPostLikes = postLikes.stream() + .limit(100) + .collect(Collectors.toList()); + + // Get user IDs for batch fetch + List likedUserIds = limitedPostLikes.stream() + .map(PostLike::getUserId) + .collect(Collectors.toList()); + + // Batch fetch users + Map usersMap = userRepository.findAllById(likedUserIds).stream() + .collect(Collectors.toMap(User::getId, user -> user)); + + // Create LikedUserDTO list + List likedUsers = limitedPostLikes.stream() + .map(postLike -> new LikedUserDTO(usersMap.get(postLike.getUserId()), postLike.getCreatedAt())) + .collect(Collectors.toList()); + + return new PostWithLikesResponseDTO(post, creator, isLiked, likedUsers); + } } \ No newline at end of file From 526cf94360000f90307accff12214859dea64217 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Thu, 24 Jul 2025 10:34:24 +0300 Subject: [PATCH 2/2] feat: support seeing all people that liked a post --- frontend/lib/models/post_models.dart | 78 +++ .../lib/screens/pages/post_view_page.dart | 648 ++++++++++++++++++ frontend/lib/services/post_service.dart | 27 + frontend/lib/widgets/posts_list.dart | 349 +++++----- 4 files changed, 922 insertions(+), 180 deletions(-) create mode 100644 frontend/lib/screens/pages/post_view_page.dart diff --git a/frontend/lib/models/post_models.dart b/frontend/lib/models/post_models.dart index 2b24ede..9d8a1ae 100644 --- a/frontend/lib/models/post_models.dart +++ b/frontend/lib/models/post_models.dart @@ -92,6 +92,84 @@ class Post { } } +class LikedUser { + final String id; + final String displayName; + final DateTime likeTime; + + LikedUser({ + required this.id, + required this.displayName, + required this.likeTime, + }); + + factory LikedUser.fromJson(Map json) { + return LikedUser( + id: json['id']?.toString() ?? '', + displayName: json['displayName'] ?? '', + likeTime: DateTime.parse(json['likeTime'] ?? DateTime.now().toIso8601String()), + ); + } + + Map toJson() { + return { + 'id': id, + 'displayName': displayName, + 'likeTime': likeTime.toIso8601String(), + }; + } +} + +class DetailedPost { + final String id; + final PostCreator creator; + final String body; + final int likes; + final int comments; + final bool liked; + final DateTime creationDate; + final List likedUsers; + + DetailedPost({ + required this.id, + required this.creator, + required this.body, + required this.likes, + required this.comments, + required this.liked, + required this.creationDate, + required this.likedUsers, + }); + + factory DetailedPost.fromJson(Map json) { + return DetailedPost( + id: json['id']?.toString() ?? '', + creator: PostCreator.fromJson(json['creator'] ?? {}), + body: json['body'] ?? '', + likes: int.tryParse(json['likes']?.toString() ?? '0') ?? 0, + comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0, + liked: json['liked'] == true, + creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()), + likedUsers: (json['likedUsers'] as List?) + ?.map((user) => LikedUser.fromJson(user)) + .toList() ?? [], + ); + } + + Map toJson() { + return { + 'id': id, + 'creator': creator.toJson(), + 'body': body, + 'likes': likes, + 'comments': comments, + 'liked': liked, + 'creationDate': creationDate.toIso8601String(), + 'likedUsers': likedUsers.map((user) => user.toJson()).toList(), + }; + } +} + class CreatePostRequest { final String body; diff --git a/frontend/lib/screens/pages/post_view_page.dart b/frontend/lib/screens/pages/post_view_page.dart new file mode 100644 index 0000000..65860a6 --- /dev/null +++ b/frontend/lib/screens/pages/post_view_page.dart @@ -0,0 +1,648 @@ +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 PostViewPage extends StatefulWidget { + final String postId; + + const PostViewPage({Key? key, required this.postId}) : super(key: key); + + @override + _PostViewPageState createState() => _PostViewPageState(); +} + +class _PostViewPageState extends State { + bool _isLoading = true; + DetailedPost? _post; + String _errorMessage = ''; + bool _isLiking = false; + + @override + void initState() { + super.initState(); + _loadPost(); + } + + Future _loadPost() async { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + + try { + final result = await PostService.getPost(widget.postId); + setState(() { + if (result['success']) { + _post = result['post']; + } else { + _errorMessage = result['message'] ?? 'Failed to load post'; + } + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'Network error: $e'; + _isLoading = false; + }); + } + } + + 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(DetailedPost post) { + final shareText = + '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}'; + Share.share(shareText); + } + + Future _toggleLike() async { + if (_isLiking || _post == null) return; + + setState(() { + _isLiking = true; + }); + + try { + Map result; + if (_post!.liked) { + result = await PostService.unlikePost(_post!.id); + } else { + result = await PostService.likePost(_post!.id); + } + + if (result['success']) { + await _loadPost(); // Reload to get updated data + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result['message'] ?? 'Failed to update like'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Network error occurred'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + setState(() { + _isLiking = false; + }); + } + } + + Widget _buildLikedBySection() { + if (_post == null || _post!.likes == 0) return SizedBox.shrink(); + + final displayUsers = _post!.likedUsers.take(3).toList(); + + return Container( + margin: EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: _showLikedUsers, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Show up to 3 avatars + ...displayUsers.asMap().entries.map((entry) { + final index = entry.key; + final user = entry.value; + final avatarColor = _getAvatarColor(user.displayName); + final avatarLetter = _getAvatarLetter(user.displayName); + + return Container( + margin: EdgeInsets.only( + right: index < displayUsers.length - 1 ? -8 : 0, + ), + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 14, + backgroundColor: avatarColor, + child: Text( + avatarLetter, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ); + }).toList(), + SizedBox(width: 12), + Expanded( + child: Text( + _post!.likes == 1 + ? 'Liked by ${_post!.likedUsers.first.displayName}' + : _post!.likes == 2 + ? 'Liked by ${_post!.likedUsers[0].displayName} and ${_post!.likedUsers[1].displayName}' + : 'Liked by ${_post!.likedUsers[0].displayName}, ${_post!.likedUsers[1].displayName} and ${_post!.likes - 2} others', + style: TextStyle(color: Colors.grey[700], fontSize: 14), + ), + ), + Text( + 'See all', + style: TextStyle( + color: Color(0xFF6A4C93), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 4), + Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFF6A4C93)), + ], + ), + ), + ), + ); + } + + void _showLikedUsers() { + if (_post == null || _post!.likedUsers.isEmpty) return; + + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => Container( + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 50, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + SizedBox(height: 16), + Text( + 'Liked by', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 16), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: _post!.likedUsers.length, + itemBuilder: (context, index) { + final user = _post!.likedUsers[index]; + final avatarColor = _getAvatarColor(user.displayName); + final avatarLetter = _getAvatarLetter(user.displayName); + final relativeTime = InvitationUtils.getRelativeTime( + user.likeTime, + ); + + return ListTile( + leading: CircleAvatar( + radius: 20, + backgroundColor: avatarColor, + child: Text( + avatarLetter, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + user.displayName, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + subtitle: Text( + relativeTime, + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ); + }, + ), + ), + SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _buildMockComments() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Comments', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 16), + _buildComment( + 'Ahmed Ali', + 'Great post! This is exactly what I was thinking about.', + '2h', + false, + ), + _buildComment( + 'Sara Ahmed', + 'Thanks for sharing this. Very insightful!', + '1h', + false, + ), + _buildComment( + 'Mohamed Hassan', + 'I completely agree with your point here.', + '45m', + true, + replyTo: 'Sara Ahmed', + ), + ], + ), + ); + } + + Widget _buildComment( + String name, + String comment, + String time, + bool isReply, { + String? replyTo, + }) { + final avatarColor = _getAvatarColor(name); + final avatarLetter = _getAvatarLetter(name); + + return Container( + margin: EdgeInsets.only(bottom: 16, left: isReply ? 40 : 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: isReply ? 16 : 20, + backgroundColor: avatarColor, + child: Text( + avatarLetter, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: isReply ? 12 : 14, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + name, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.black87, + ), + ), + SizedBox(width: 8), + Text( + time, + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ], + ), + if (replyTo != null) ...[ + SizedBox(height: 4), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Replying to ', + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + TextSpan( + text: '@$replyTo', + style: TextStyle( + color: Color(0xFF6A4C93), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + SizedBox(height: 4), + Text( + comment, + style: TextStyle( + fontSize: 14, + height: 1.4, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + Row( + children: [ + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'Like', + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ), + SizedBox(width: 16), + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'Reply', + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.arrow_back, color: Colors.black87), + ), + title: Text( + 'Post', + style: TextStyle( + color: Colors.black87, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + backgroundColor: Color(0xFFF5F5F5), + body: _buildBody(), + ); + } + + 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: _loadPost, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: Text('Try Again'), + ), + ], + ), + ); + } + + if (_post == null) { + return Center(child: Text('Post not found')); + } + + final creator = _post!.creator; + final avatarColor = _getAvatarColor(creator.displayName); + final avatarLetter = _getAvatarLetter(creator.displayName); + final relativeTime = InvitationUtils.getRelativeTime(_post!.creationDate); + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.all(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: 24, + backgroundColor: avatarColor, + child: Text( + avatarLetter, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + creator.displayName, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + color: Colors.black87, + ), + ), + SizedBox(height: 2), + Text( + relativeTime, + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => _sharePost(_post!), + icon: Icon( + Icons.share_outlined, + color: Colors.grey[600], + ), + ), + ], + ), + SizedBox(height: 16), + Text( + _post!.body, + style: TextStyle( + fontSize: 16, + height: 1.5, + color: Colors.black87, + ), + ), + SizedBox(height: 20), + Row( + children: [ + IconButton( + onPressed: _isLiking ? null : _toggleLike, + icon: _isLiking + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + _post!.liked + ? Colors.red + : Colors.grey[600]!, + ), + ), + ) + : Icon( + _post!.liked + ? Icons.favorite + : Icons.favorite_border, + color: _post!.liked + ? Colors.red + : Colors.grey[600], + size: 28, + ), + ), + Text( + '${_post!.likes}', + style: TextStyle( + color: Colors.grey[700], + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + SizedBox(width: 20), + Icon( + Icons.chat_bubble_outline, + color: Colors.grey[600], + size: 28, + ), + SizedBox(width: 8), + Text( + '${_post!.comments}', + style: TextStyle( + color: Colors.grey[700], + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 8), + _buildLikedBySection(), + SizedBox(height: 16), + _buildMockComments(), + SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/frontend/lib/services/post_service.dart b/frontend/lib/services/post_service.dart index 5149d27..e4e973d 100644 --- a/frontend/lib/services/post_service.dart +++ b/frontend/lib/services/post_service.dart @@ -155,4 +155,31 @@ class PostService { return {'success': false, 'message': 'Network error: $e', 'post': null}; } } + + static Future> getPost(String postId) async { + try { + final response = await HttpService.get('/posts/get?id=$postId'); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + return { + 'success': responseData['status'] ?? false, + 'message': responseData['message'] ?? '', + 'post': responseData['data'] != null + ? DetailedPost.fromJson(responseData['data']) + : null, + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to fetch post', + 'post': null, + }; + } + } catch (e) { + print('Error fetching 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 index aacd0d2..f38f129 100644 --- a/frontend/lib/widgets/posts_list.dart +++ b/frontend/lib/widgets/posts_list.dart @@ -3,6 +3,7 @@ import 'package:share_plus/share_plus.dart'; import '../models/post_models.dart'; import '../services/post_service.dart'; import '../utils/invitation_utils.dart'; +import '../screens/pages/post_view_page.dart'; class PostsList extends StatefulWidget { final Future> Function() fetchPosts; @@ -61,7 +62,7 @@ class PostsListState extends State { Future _refreshPosts() async { await _loadPosts(); - + if (widget.showRefreshIndicator) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -75,14 +76,11 @@ class PostsListState extends State { @override Widget build(BuildContext context) { Widget body = _buildBody(); - + if (widget.showRefreshIndicator) { - body = RefreshIndicator( - onRefresh: _refreshPosts, - child: body, - ); + body = RefreshIndicator(onRefresh: _refreshPosts, child: body); } - + return body; } @@ -100,18 +98,11 @@ class PostsListState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.error_outline, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( _errorMessage, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[600]), textAlign: TextAlign.center, ), SizedBox(height: 16), @@ -133,11 +124,7 @@ class PostsListState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.post_add, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.post_add, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( widget.emptyStateTitle, @@ -150,10 +137,7 @@ class PostsListState extends State { SizedBox(height: 8), Text( widget.emptyStateSubtitle, - style: TextStyle( - fontSize: 14, - color: Colors.grey[500], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ], ), @@ -164,9 +148,7 @@ class PostsListState extends State { padding: EdgeInsets.symmetric(vertical: 8), itemCount: _posts.length, itemBuilder: (context, index) { - return PostCard( - post: _posts[index], - ); + return PostCard(post: _posts[index]); }, ); } @@ -179,8 +161,7 @@ class PostsListState extends State { class PostCard extends StatefulWidget { final Post post; - const PostCard({Key? key, required this.post}) - : super(key: key); + const PostCard({Key? key, required this.post}) : super(key: key); @override _PostCardState createState() => _PostCardState(); @@ -214,7 +195,7 @@ class _PostCardState extends State { Color(0xFF32CD32), Color(0xFF9932CC), ]; - + int hash = displayName.hashCode; return colors[hash.abs() % colors.length]; } @@ -224,7 +205,8 @@ class _PostCardState extends State { } void _sharePost(Post post) { - final shareText = '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}'; + final shareText = + '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}'; Share.share(shareText); } @@ -236,7 +218,9 @@ class _PostCardState extends State { // Optimistic update - immediately change UI _currentPost = _currentPost.copyWith( liked: !_currentPost.liked, - likes: _currentPost.liked ? _currentPost.likes - 1 : _currentPost.likes + 1, + likes: _currentPost.liked + ? _currentPost.likes - 1 + : _currentPost.likes + 1, ); }); @@ -260,7 +244,7 @@ class _PostCardState extends State { _currentPost = widget.post; _isLiking = false; }); - + // Show error message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -277,7 +261,7 @@ class _PostCardState extends State { _currentPost = widget.post; _isLiking = false; }); - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -294,131 +278,152 @@ class _PostCardState extends State { final creator = _currentPost.creator; final avatarColor = _getAvatarColor(creator.displayName); final avatarLetter = _getAvatarLetter(creator.displayName); - final relativeTime = InvitationUtils.getRelativeTime(_currentPost.creationDate); + 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), + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PostViewPage(postId: _currentPost.id), ), - ], - ), - 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, - ), - ), - ], + ); + }, + child: 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: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + PostViewPage(postId: _currentPost.id), + ), + ); + }, + 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, + ), + ), + ], + ), + ], + ), + ), ), ); } @@ -485,9 +490,7 @@ class _ProfilePostsListState extends State { padding: EdgeInsets.all(32), child: Center( child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Color(0xFF6A4C93), - ), + valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), ), ), ); @@ -499,18 +502,11 @@ class _ProfilePostsListState extends State { child: Center( child: Column( children: [ - Icon( - Icons.error_outline, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( _errorMessage, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[600]), textAlign: TextAlign.center, ), SizedBox(height: 16), @@ -534,11 +530,7 @@ class _ProfilePostsListState extends State { child: Center( child: Column( children: [ - Icon( - Icons.post_add, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.post_add, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( 'No posts yet', @@ -551,10 +543,7 @@ class _ProfilePostsListState extends State { SizedBox(height: 8), Text( 'Start sharing your thoughts!', - style: TextStyle( - fontSize: 14, - color: Colors.grey[500], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ], ), @@ -564,14 +553,14 @@ class _ProfilePostsListState extends State { return Column( children: [ - ..._posts.map((post) => - Container( + ..._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 +}