feat: UI for creating posts
This commit is contained in:
parent
2733ce8269
commit
e146b18f1d
@ -1,5 +1,5 @@
|
|||||||
class ApiConstants {
|
class ApiConstants {
|
||||||
static const String baseUrl = 'https://api.wesal.online';
|
static const String baseUrl = 'http://localhost:8080';
|
||||||
|
|
||||||
// Auth endpoints
|
// Auth endpoints
|
||||||
static const String loginEndpoint = '/login';
|
static const String loginEndpoint = '/login';
|
||||||
@ -7,10 +7,13 @@ class ApiConstants {
|
|||||||
// User endpoints
|
// User endpoints
|
||||||
static const String getUserEndpoint = '/getUser';
|
static const String getUserEndpoint = '/getUser';
|
||||||
static const String updateUserEndpoint = '/updateUser';
|
static const String updateUserEndpoint = '/updateUser';
|
||||||
|
|
||||||
// Invitation endpoints
|
// Invitation endpoints
|
||||||
static const String invitationsEndpoint = '/invitations';
|
static const String invitationsEndpoint = '/invitations';
|
||||||
static const String getAllInvitationsEndpoint = '/invitations/all';
|
static const String getAllInvitationsEndpoint = '/invitations/all';
|
||||||
static const String acceptInvitationEndpoint = '/invitations/accept';
|
static const String acceptInvitationEndpoint = '/invitations/accept';
|
||||||
static const String createInvitationEndpoint = '/invitations/create';
|
static const String createInvitationEndpoint = '/invitations/create';
|
||||||
|
|
||||||
|
// Post endpoints
|
||||||
|
static const String createPostEndpoint = '/posts/create';
|
||||||
}
|
}
|
||||||
|
|||||||
55
frontend/lib/models/post_models.dart
Normal file
55
frontend/lib/models/post_models.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
class Post {
|
||||||
|
final String id;
|
||||||
|
final String body;
|
||||||
|
final String userId;
|
||||||
|
final String username;
|
||||||
|
final String displayName;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
Post({
|
||||||
|
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<String, dynamic> json) {
|
||||||
|
return Post(
|
||||||
|
id: json['id'] ?? '',
|
||||||
|
body: json['body'] ?? '',
|
||||||
|
userId: json['userId'] ?? '',
|
||||||
|
username: json['username'] ?? '',
|
||||||
|
displayName: json['displayName'] ?? '',
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] ?? DateTime.now().toIso8601String()),
|
||||||
|
updatedAt: DateTime.parse(json['updatedAt'] ?? DateTime.now().toIso8601String()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'body': body,
|
||||||
|
'userId': userId,
|
||||||
|
'username': username,
|
||||||
|
'displayName': displayName,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreatePostRequest {
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
CreatePostRequest({required this.body});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'body': body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
251
frontend/lib/screens/create_post_screen.dart
Normal file
251
frontend/lib/screens/create_post_screen.dart
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/post_service.dart';
|
||||||
|
|
||||||
|
class CreatePostScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_CreatePostScreenState createState() => _CreatePostScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreatePostScreenState extends State<CreatePostScreen> {
|
||||||
|
final TextEditingController _bodyController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
final int _maxCharacters = 280;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bodyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createPost() async {
|
||||||
|
if (_bodyController.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Please write something before posting'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await PostService.createPost(_bodyController.text.trim());
|
||||||
|
|
||||||
|
if (result['success']) {
|
||||||
|
Navigator.of(context).pop(true); // Return true to indicate success
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Post created successfully!'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(result['message']),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to create post. Please try again.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final remainingChars = _maxCharacters - _bodyController.text.length;
|
||||||
|
final isOverLimit = remainingChars < 0;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Create Post', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
elevation: 0,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(1),
|
||||||
|
child: Container(height: 1, color: Colors.grey[200]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading || isOverLimit || _bodyController.text.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: _createPost,
|
||||||
|
child: _isLoading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Post',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _bodyController.text.trim().isEmpty || isOverLimit
|
||||||
|
? Colors.grey
|
||||||
|
: Color(0xFF6A4C93),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF32B0A5).withOpacity(0.05),
|
||||||
|
Color(0xFF4600B9).withOpacity(0.05),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
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: [
|
||||||
|
Text(
|
||||||
|
"What's on your mind?",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _bodyController,
|
||||||
|
maxLines: 6,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Share your thoughts...',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[500]),
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.4,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
onChanged: (text) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Share your thoughts with the community',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$remainingChars',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isOverLimit ? Colors.red : Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isOverLimit)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: Text(
|
||||||
|
'Post is too long. Please keep it under $_maxCharacters characters.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
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: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lightbulb_outline,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Keep it friendly and respectful. Your post will be visible to everyone in the community.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../create_post_screen.dart';
|
||||||
|
|
||||||
class FeedPage extends StatefulWidget {
|
class FeedPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -6,6 +7,7 @@ class FeedPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FeedPageState extends State<FeedPage> {
|
class _FeedPageState extends State<FeedPage> {
|
||||||
|
bool _isRefreshing = false;
|
||||||
final List<Map<String, dynamic>> mockPosts = [
|
final List<Map<String, dynamic>> mockPosts = [
|
||||||
{
|
{
|
||||||
'id': '1',
|
'id': '1',
|
||||||
@ -79,12 +81,7 @@ class _FeedPageState extends State<FeedPage> {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: _refreshFeed,
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Feed refreshed!')));
|
|
||||||
},
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: mockPosts.length,
|
itemCount: mockPosts.length,
|
||||||
@ -107,16 +104,44 @@ class _FeedPageState extends State<FeedPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: _navigateToCreatePost,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Create post functionality coming soon!')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
child: Icon(Icons.edit, color: Colors.white),
|
child: Icon(Icons.edit, color: Colors.white),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshFeed() async {
|
||||||
|
setState(() {
|
||||||
|
_isRefreshing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate API call for refreshing feed
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isRefreshing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Feed refreshed!'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToCreatePost() async {
|
||||||
|
final result = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => CreatePostScreen()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If post was created successfully, refresh the feed
|
||||||
|
if (result == true) {
|
||||||
|
_refreshFeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostCard extends StatefulWidget {
|
class PostCard extends StatefulWidget {
|
||||||
|
|||||||
38
frontend/lib/services/post_service.dart
Normal file
38
frontend/lib/services/post_service.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import '../models/post_models.dart';
|
||||||
|
import 'http_service.dart';
|
||||||
|
|
||||||
|
class PostService {
|
||||||
|
static Future<Map<String, dynamic>> createPost(String body) async {
|
||||||
|
try {
|
||||||
|
final createPostRequest = CreatePostRequest(body: body);
|
||||||
|
final response = await HttpService.post(
|
||||||
|
'/posts/create',
|
||||||
|
createPostRequest.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to create post',
|
||||||
|
'post': null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating post: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error: $e',
|
||||||
|
'post': null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user