feat: UI for creating posts
This commit is contained in:
parent
2733ce8269
commit
e146b18f1d
@ -1,5 +1,5 @@
|
||||
class ApiConstants {
|
||||
static const String baseUrl = 'https://api.wesal.online';
|
||||
static const String baseUrl = 'http://localhost:8080';
|
||||
|
||||
// Auth endpoints
|
||||
static const String loginEndpoint = '/login';
|
||||
@ -7,10 +7,13 @@ class ApiConstants {
|
||||
// User endpoints
|
||||
static const String getUserEndpoint = '/getUser';
|
||||
static const String updateUserEndpoint = '/updateUser';
|
||||
|
||||
|
||||
// Invitation endpoints
|
||||
static const String invitationsEndpoint = '/invitations';
|
||||
static const String getAllInvitationsEndpoint = '/invitations/all';
|
||||
static const String acceptInvitationEndpoint = '/invitations/accept';
|
||||
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 '../create_post_screen.dart';
|
||||
|
||||
class FeedPage extends StatefulWidget {
|
||||
@override
|
||||
@ -6,6 +7,7 @@ class FeedPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FeedPageState extends State<FeedPage> {
|
||||
bool _isRefreshing = false;
|
||||
final List<Map<String, dynamic>> mockPosts = [
|
||||
{
|
||||
'id': '1',
|
||||
@ -79,12 +81,7 @@ class _FeedPageState extends State<FeedPage> {
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Feed refreshed!')));
|
||||
},
|
||||
onRefresh: _refreshFeed,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: mockPosts.length,
|
||||
@ -107,16 +104,44 @@ class _FeedPageState extends State<FeedPage> {
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Create post functionality coming soon!')),
|
||||
);
|
||||
},
|
||||
onPressed: _navigateToCreatePost,
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
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 {
|
||||
|
||||
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