diff --git a/assets/images/img_send.svg b/assets/images/img_send.svg new file mode 100644 index 0000000..5dedda3 --- /dev/null +++ b/assets/images/img_send.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/utils/image_constant.dart b/lib/core/utils/image_constant.dart index 8c9c098..07aa9ab 100644 --- a/lib/core/utils/image_constant.dart +++ b/lib/core/utils/image_constant.dart @@ -106,6 +106,8 @@ class ImageConstant { static String imgMic = '$imagePath/img_mic.svg'; + static String imgSend = '$imagePath/img_send.svg'; + static String imgAttach = '$imagePath/img_attach.svg'; static String imgExit = '$imagePath/img_nav_exit.svg'; diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart index 3c13686..c77a0dc 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; import 'dart:convert'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/feed/blog_post_screen/models/blog_post_model.dart'; @@ -48,12 +50,10 @@ class BlogPostBloc extends Bloc { author: currentState.author, comments: currentState.comments)); } else { - emit(BlogPostLoadedState(blog_post: data)); - final author = await _fetchAuthor(data['id']); - emit(BlogPostLoadedState(blog_post: data, author: author)); - final comments = [await _fetchComments(data['id'])]; emit(BlogPostLoadedState( - blog_post: data, author: author, comments: comments)); + blog_post: data, + author: await _fetchAuthor(data['id']), + comments: fillCommentList(await _fetchComments(data['id'])))); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -109,13 +109,11 @@ class BlogPostBloc extends Bloc { 'Authorization': 'Bearer $token', }, ); - print(response.body); final data = jsonDecode(response.body); - print(data); return data; } - _fetchComments(int id) async { + Future _fetchComments(int id) async { final box = await Hive.openBox('token'); final token = box.get('token'); if (token == null) { @@ -129,9 +127,21 @@ class BlogPostBloc extends Bloc { 'Authorization': 'Bearer $token', }, ); - print(response.body); final data = jsonDecode(response.body); - print(data); return data; } + + List fillCommentList(List comments) { + List list = []; + for (int i = 0; i < comments.length; i++) { + list.add(CommentModel( + id: comments[i]['id'].toString(), + userImage: '', + username: 'Вася Пупкин', + dateText: DateFormat('dd.MM') + .format(DateTime.parse(comments[i]['updated_at'])), + greetingText: comments[i]['body'])); + } + return list; + } } diff --git a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart index 293dfb2..77cdd64 100644 --- a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -1,8 +1,12 @@ import 'bloc/blog_post_bloc.dart'; import 'models/blog_post_model.dart'; +import 'models/comment_model.dart'; +import 'widgets/comment_widget.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -50,98 +54,203 @@ class BlogPostScreen extends StatelessWidget { decoration: AppDecoration.fillGray, child: ListView( children: [ - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.only( - left: 16.h, - right: 20.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + - (state.blog_post?['image'] ?? ''), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, - ), - placeholder: (context, url) => - CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), - ), - SizedBox(height: 16.0), - Text(state.blog_post?['title'], - style: CustomTextStyles.titleMediumBlack), - SizedBox(height: 8.0), - Text( - state.blog_post?['description'], - ), - SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - onPressed: () => Navigator.pushNamed( - context, - AppRoutes.blogPostEditScreen, - arguments: state.blog_post?['id'].toString(), - ), - child: Text('Редактировать'), - ), - ElevatedButton( - onPressed: () => context - .read() - .add(DeleteEntry(state.blog_post?['id'])), - child: Text('Удалить'), - ), - ], - ) - ]), - ) + SizedBox(height: 15.v), + _buildHeaderRow(context, state), + SizedBox(height: 15.v), + _buildBlogPostData(context, state), + SizedBox(height: 15.v), + _buildTextField(context, state), + SizedBox(height: 15.v), + _buildCommentSection(context, state), ], ), ), bottomNavigationBar: _buildBottomBar(context))); } - ///Handling route based on bottom click actions - String getCurrentRoute(BottomBarEnum type) { - switch (type) { - case BottomBarEnum.tf: - return AppRoutes.blogPostScreen; - default: - return "/"; - } + _buildHeaderRow(BuildContext context, BlogPostLoadedState state) { + final String name = state.author?['name']; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomImageView( + imagePath: ImageConstant.imgArrowLeft, + onTap: () => NavigatorService.goBack()), + Padding( + padding: EdgeInsets.only(top: 1.v), + child: Row( + children: [ + Align( + alignment: Alignment.centerRight, + child: Text( + "$name", + style: CustomTextStyles.titleMediumBlack, + ), + ), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.author?['profile_photo_path'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + ], + ), + ), + ], + ), + ); } - ///Handling page based on route - Widget getCurrentPage( - BuildContext context, - String currentRoute, - ) { - switch (currentRoute) { - case AppRoutes.blogPostScreen: - return BlogPostScreen.builder(context); - default: - return BlogPostScreen(); - } + _buildBlogPostData(BuildContext context, BlogPostLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: + dotenv.env['STORAGE_URL']! + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + placeholder: (context, url) => CircularProgressIndicator(), + ), + SizedBox(height: 16.0), + Text(state.blog_post?['title'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.blog_post?['description'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.blogPostEditScreen, + arguments: state.blog_post?['id'].toString(), + ), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.blog_post?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ); + } + + _buildTextField(BuildContext context, BlogPostLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + CustomTextFormField( + maxLines: 8, + ), + SizedBox(height: 15.h), + CustomOutlinedButton( + text: "Оставить отзыв", + margin: EdgeInsets.only(bottom: 5), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + decoration: BoxDecoration( + color: Colors.transparent, + ), + onPressed: () => print("tap"), + // Установите прозрачный фон + ) + ], + ), + ], + ), + ); + } + + _buildCommentSection(BuildContext context, BlogPostLoadedState state) { + return ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) { + return SizedBox( + height: 10.v, + ); + }, + itemCount: state.comments?.length ?? 0, + itemBuilder: (context, index) { + CommentModel model = state.comments?[index]; + return CommentWidget( + model, + ); + }); } _buildBottomBar(BuildContext context) { diff --git a/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart index a005b74..67cbe66 100644 --- a/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart +++ b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart @@ -1,19 +1,20 @@ // ignore_for_file: must_be_immutable import 'package:equatable/equatable.dart'; - -/// This class defines the variables used in the [blog_post_screen], -/// and is typically used to hold data that is passed between different parts of the application. +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; class BlogPostModel extends Equatable { final String blog_postID; + BlogPostModel({required this.blog_postID, this.comments = const []}); + List comments; - BlogPostModel({required String blog_postID}) : blog_postID = blog_postID; - - BlogPostModel copyWith(String blog_postID) { - return BlogPostModel(blog_postID: blog_postID); + BlogPostModel copyWith({String? blog_postID, List? comments}) { + return BlogPostModel( + blog_postID: blog_postID ?? this.blog_postID, + comments: comments ?? this.comments, + ); } @override - List get props => [blog_postID]; + List get props => [blog_postID, comments]; } diff --git a/lib/presentation/feed/blog_post_screen/models/comment_model.dart b/lib/presentation/feed/blog_post_screen/models/comment_model.dart new file mode 100644 index 0000000..3c8c593 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/models/comment_model.dart @@ -0,0 +1,29 @@ +import 'package:eatvio_app/core/app_export.dart'; + +/// This class is used in the [userprofile_item_widget] screen. +// ignore_for_file: must_be_immutable + +class CommentModel { + CommentModel( + {this.userImage, + this.username, + this.dateText, + this.greetingText, + required this.id}) { + userImage = userImage ?? ImageConstant.imageNotFound; + username = username; + dateText = dateText; + greetingText = greetingText; + id = id; + } + + String? userImage; + + String? username; + + String? dateText; + + String? greetingText; + + String id; +} diff --git a/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart new file mode 100644 index 0000000..caeeea7 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart @@ -0,0 +1,107 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +class CommentWidget extends StatelessWidget { + CommentWidget(this.commentModelObj, {Key? key}) + : super( + key: key, + ); + + CommentModel commentModelObj; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment + .spaceBetween, // Move the main axis alignment to space between + crossAxisAlignment: + CrossAxisAlignment.center, // Center the items vertically + children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + commentModelObj.userImage!, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + bottom: 11.v, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 227.h, + child: Row( + children: [ + Text( + commentModelObj.username!.length <= 15 + ? commentModelObj.username! + : commentModelObj.username!.substring(0, 15) + + '...', + style: theme.textTheme.titleMedium, + ), + Spacer(), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + commentModelObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + ), + SizedBox(height: 6.v), + Text( + commentModelObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ), + ], + ), + ), + ), + CustomImageView( + imagePath: ImageConstant.imgSend, + height: 40.v, + width: 40.h, + margin: EdgeInsets.only(left: 5)), + ], + ), + ); + } +}