diff --git a/frontend/lib/models/post_models.dart b/frontend/lib/models/post_models.dart index ebcdb8c..a70eb19 100644 --- a/frontend/lib/models/post_models.dart +++ b/frontend/lib/models/post_models.dart @@ -1,43 +1,67 @@ -class Post { +class PostCreator { final String id; - final String body; - final String userId; - final String username; final String displayName; - final DateTime createdAt; - final DateTime updatedAt; - Post({ + PostCreator({ required this.id, - required this.body, - required this.userId, - required this.username, required this.displayName, - required this.createdAt, - required this.updatedAt, }); - factory Post.fromJson(Map json) { - return Post( - id: json['id'] ?? '', - body: json['body'] ?? '', - userId: json['userId'] ?? '', - username: json['username'] ?? '', + factory PostCreator.fromJson(Map json) { + return PostCreator( + id: json['id']?.toString() ?? '', displayName: json['displayName'] ?? '', - createdAt: DateTime.parse(json['createdAt'] ?? DateTime.now().toIso8601String()), - updatedAt: DateTime.parse(json['updatedAt'] ?? DateTime.now().toIso8601String()), ); } Map toJson() { return { 'id': id, - 'body': body, - 'userId': userId, - 'username': username, 'displayName': displayName, - 'createdAt': createdAt.toIso8601String(), - 'updatedAt': updatedAt.toIso8601String(), + }; + } +} + +class Post { + final String id; + final String creatorId; + final PostCreator creator; + final String body; + final int likes; + final int comments; + final DateTime creationDate; + + Post({ + required this.id, + required this.creatorId, + required this.creator, + required this.body, + required this.likes, + required this.comments, + required this.creationDate, + }); + + factory Post.fromJson(Map json) { + return Post( + id: json['id']?.toString() ?? '', + creatorId: json['creatorId']?.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, + creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()), + ); + } + + Map toJson() { + return { + 'id': id, + 'creatorId': creatorId, + 'creator': creator.toJson(), + 'body': body, + 'likes': likes, + 'comments': comments, + 'creationDate': creationDate.toIso8601String(), }; } } diff --git a/frontend/lib/screens/pages/feed_page.dart b/frontend/lib/screens/pages/feed_page.dart index 9d28087..2aed7e7 100644 --- a/frontend/lib/screens/pages/feed_page.dart +++ b/frontend/lib/screens/pages/feed_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import '../create_post_screen.dart'; +import '../../services/post_service.dart'; +import '../../models/post_models.dart'; +import '../../utils/invitation_utils.dart'; class FeedPage extends StatefulWidget { @override @@ -8,6 +11,40 @@ class FeedPage extends StatefulWidget { class _FeedPageState extends State { bool _isRefreshing = false; + bool _isLoading = true; + List _posts = []; + String _errorMessage = ''; + + @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', @@ -79,29 +116,17 @@ class _FeedPageState extends State { child: Container(height: 1, color: Colors.grey[200]), ), automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: _loadPosts, + icon: Icon(Icons.refresh), + tooltip: 'Refresh', + ), + ], ), body: RefreshIndicator( onRefresh: _refreshFeed, - child: ListView.builder( - padding: EdgeInsets.symmetric(vertical: 8), - itemCount: mockPosts.length, - itemBuilder: (context, index) { - return PostCard( - post: mockPosts[index], - onLikePressed: () { - setState(() { - if (mockPosts[index]['isLiked']) { - mockPosts[index]['likes']--; - mockPosts[index]['isLiked'] = false; - } else { - mockPosts[index]['likes']++; - mockPosts[index]['isLiked'] = true; - } - }); - }, - ); - }, - ), + child: _buildBody(), ), floatingActionButton: FloatingActionButton( onPressed: _navigateToCreatePost, @@ -111,17 +136,93 @@ 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 { - setState(() { - _isRefreshing = true; - }); - - // Simulate API call for refreshing feed - await Future.delayed(Duration(seconds: 1)); - - setState(() { - _isRefreshing = false; - }); + await _loadPosts(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -139,59 +240,46 @@ class _FeedPageState extends State { // If post was created successfully, refresh the feed if (result == true) { - _refreshFeed(); + _loadPosts(); } } } class PostCard extends StatefulWidget { - final Map post; - final VoidCallback onLikePressed; + final Post post; - const PostCard({Key? key, required this.post, required this.onLikePressed}) + const PostCard({Key? key, required this.post}) : super(key: key); @override _PostCardState createState() => _PostCardState(); } -class _PostCardState extends State - with SingleTickerProviderStateMixin { - late AnimationController _likeAnimationController; - late Animation _likeAnimation; - - @override - void initState() { - super.initState(); - _likeAnimationController = AnimationController( - duration: Duration(milliseconds: 300), - vsync: this, - ); - _likeAnimation = Tween(begin: 1.0, end: 1.3).animate( - CurvedAnimation( - parent: _likeAnimationController, - curve: Curves.elasticOut, - ), - ); +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]; } - @override - void dispose() { - _likeAnimationController.dispose(); - super.dispose(); - } - - void _handleLike() { - widget.onLikePressed(); - _likeAnimationController.forward().then((_) { - _likeAnimationController.reverse(); - }); + String _getAvatarLetter(String displayName) { + return displayName.isNotEmpty ? displayName[0].toUpperCase() : '?'; } @override Widget build(BuildContext context) { - final user = widget.post['user']; - final isLiked = widget.post['isLiked']; + 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), @@ -215,9 +303,9 @@ class _PostCardState extends State children: [ CircleAvatar( radius: 20, - backgroundColor: user['avatar_color'], + backgroundColor: avatarColor, child: Text( - user['avatar'], + avatarLetter, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -231,7 +319,7 @@ class _PostCardState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - user['displayName'], + creator.displayName, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, @@ -240,7 +328,7 @@ class _PostCardState extends State ), SizedBox(height: 2), Text( - widget.post['timestamp'], + relativeTime, style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], @@ -258,7 +346,7 @@ class _PostCardState extends State ), SizedBox(height: 12), Text( - widget.post['content'], + widget.post.body, style: TextStyle( fontSize: 15, height: 1.4, @@ -268,27 +356,20 @@ class _PostCardState extends State SizedBox(height: 16), Row( children: [ - AnimatedBuilder( - animation: _likeAnimation, - builder: (context, child) { - return Transform.scale( - scale: _likeAnimation.value, - child: GestureDetector( - onTap: _handleLike, - child: Container( - padding: EdgeInsets.all(12), - child: Icon( - isLiked ? Icons.favorite : Icons.favorite_border, - color: isLiked ? Colors.red : Colors.grey[600], - size: 24, - ), - ), - ), + 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']}', + '${widget.post.likes}', style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, @@ -297,9 +378,9 @@ class _PostCardState extends State SizedBox(width: 16), IconButton( onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Comments pressed'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Comments feature coming soon!')), + ); }, icon: Icon( Icons.chat_bubble_outline, @@ -308,7 +389,7 @@ class _PostCardState extends State ), ), Text( - '${widget.post['comments']}', + '${widget.post.comments}', style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, @@ -317,9 +398,9 @@ class _PostCardState extends State Spacer(), IconButton( onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Share pressed'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Share feature coming soon!')), + ); }, icon: Icon( Icons.share_outlined, diff --git a/frontend/lib/services/post_service.dart b/frontend/lib/services/post_service.dart index bfb672f..2773220 100644 --- a/frontend/lib/services/post_service.dart +++ b/frontend/lib/services/post_service.dart @@ -3,6 +3,39 @@ import '../models/post_models.dart'; import 'http_service.dart'; class PostService { + static Future> getAllPosts() async { + try { + final response = await HttpService.get('/posts/all'); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + return { + 'success': responseData['status'] ?? false, + 'message': responseData['message'] ?? '', + 'posts': + (responseData['data'] as List?) + ?.map((post) => Post.fromJson(post)) + .toList() ?? + [], + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to fetch posts', + 'posts': [], + }; + } + } catch (e) { + print('Error fetching posts: $e'); + return { + 'success': false, + 'message': 'Network error: $e', + 'posts': [], + }; + } + } + static Future> createPost(String body) async { try { final createPostRequest = CreatePostRequest(body: body); @@ -12,12 +45,14 @@ class PostService { ); final responseData = jsonDecode(response.body); - + if (response.statusCode == 200 || response.statusCode == 201) { return { 'success': responseData['status'] ?? false, 'message': responseData['message'] ?? '', - 'post': responseData['data'] != null ? Post.fromJson(responseData['data']) : null, + 'post': responseData['data'] != null + ? Post.fromJson(responseData['data']) + : null, }; } else { return { @@ -28,11 +63,7 @@ class PostService { } } catch (e) { print('Error creating post: $e'); - return { - 'success': false, - 'message': 'Network error: $e', - 'post': null, - }; + return {'success': false, 'message': 'Network error: $e', 'post': null}; } } -} \ No newline at end of file +}