From 6cdd0ac2a6f1914b88338dca4923e1b7c1aed5b8 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Wed, 15 May 2024 14:33:15 +0300 Subject: [PATCH 01/11] feat(feed): Author fetch API connect --- .../blog_post_screen/bloc/blog_post_bloc.dart | 59 ++++++++++++++++++- .../bloc/blog_post_event.dart | 14 +++++ .../bloc/blog_post_state.dart | 19 ++++-- 3 files changed, 85 insertions(+), 7 deletions(-) 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 74afd4d1..7c9f9ab3 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 @@ -44,9 +44,13 @@ class BlogPostBloc extends Bloc { var currentState = state as BlogPostLoadedState; emit(currentState.copyWith( blog_postModelObj: currentState.blog_postModelObj, - blog_post: currentState.blog_post)); + blog_post: currentState.blog_post, + author: currentState.author, + comments: currentState.comments)); } else { emit(BlogPostLoadedState(blog_post: data)); + Future> author = _fetchAuthor(data['id']); + emit(BlogPostLoadedState(blog_post: data, author: author)); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -85,4 +89,57 @@ class BlogPostBloc extends Bloc { emit(BlogPostErrorState('Ошибка: ${response.statusCode}')); } } + + Future> _fetchAuthor(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getAuthor/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + + final data = jsonDecode(response.body); + return data; + } + + FutureOr _fetchComments(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getComments/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is List>) { + print(data); + var currentState = state as BlogPostLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + blog_post: currentState.blog_post, + author: currentState.author, + comments: currentState.comments)); + } else { + emit(BlogPostErrorState('Неверный формат комментариев')); + } + } else { + emit(BlogPostErrorState('Ошибка сервера: ${response.statusCode}')); + } + } } diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart index de77e332..415809b3 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart @@ -21,3 +21,17 @@ class DeleteEntry extends BlogPostEvent { @override List get props => [id]; } + +class FetchAuthor extends BlogPostEvent { + FetchAuthor(this.id); + final int id; + @override + List get props => [id]; +} + +class FetchComments extends BlogPostEvent { + FetchComments(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart index 033ba293..d95856a0 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart @@ -22,18 +22,25 @@ class BlogPostState extends Equatable { } class BlogPostLoadedState extends BlogPostState { - BlogPostLoadedState({this.blog_postModelObj, this.blog_post}); + BlogPostLoadedState( + {this.blog_postModelObj, this.blog_post, this.author, this.comments}); BlogPostModel? blog_postModelObj; Map? blog_post; + Map? author; + List?>? comments; @override - List get props => [blog_postModelObj, blog_post]; + List get props => [blog_postModelObj, blog_post, author, comments]; BlogPostLoadedState copyWith( - {BlogPostModel? blog_postModelObj, Map? blog_post}) { + {BlogPostModel? blog_postModelObj, + Map? blog_post, + Map? author, + List?>? comments}) { return BlogPostLoadedState( - blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, - blog_post: blog_post ?? this.blog_post, - ); + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + blog_post: blog_post ?? this.blog_post, + author: author ?? this.author, + comments: comments ?? this.comments); } } From fc8301bbe11f4cfdafaef96d8d3be190d37f6911 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Wed, 15 May 2024 14:49:19 +0300 Subject: [PATCH 02/11] feat(feed): Fetching author and comments for blog posts --- .../blog_post_screen/bloc/blog_post_bloc.dart | 36 ++++++++----------- .../bloc/blog_post_state.dart | 4 +-- 2 files changed, 16 insertions(+), 24 deletions(-) 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 7c9f9ab3..3c136862 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 @@ -49,8 +49,11 @@ class BlogPostBloc extends Bloc { comments: currentState.comments)); } else { emit(BlogPostLoadedState(blog_post: data)); - Future> author = _fetchAuthor(data['id']); + 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)); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -90,7 +93,7 @@ class BlogPostBloc extends Bloc { } } - Future> _fetchAuthor(int id) async { + _fetchAuthor(int id) async { final box = await Hive.openBox('token'); final token = box.get('token'); @@ -100,18 +103,19 @@ class BlogPostBloc extends Bloc { ); } - final response = await http.post( + final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getAuthor/$id'), headers: { 'Authorization': 'Bearer $token', }, ); - + print(response.body); final data = jsonDecode(response.body); + print(data); return data; } - FutureOr _fetchComments(int id) async { + _fetchComments(int id) async { final box = await Hive.openBox('token'); final token = box.get('token'); if (token == null) { @@ -119,27 +123,15 @@ class BlogPostBloc extends Bloc { AppRoutes.authScreen, ); } - final response = await http.post( + final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getComments/$id'), headers: { 'Authorization': 'Bearer $token', }, ); - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - if (data is List>) { - print(data); - var currentState = state as BlogPostLoadedState; - emit(currentState.copyWith( - blog_postModelObj: currentState.blog_postModelObj, - blog_post: currentState.blog_post, - author: currentState.author, - comments: currentState.comments)); - } else { - emit(BlogPostErrorState('Неверный формат комментариев')); - } - } else { - emit(BlogPostErrorState('Ошибка сервера: ${response.statusCode}')); - } + print(response.body); + final data = jsonDecode(response.body); + print(data); + return data; } } diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart index d95856a0..610b528b 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart @@ -28,14 +28,14 @@ class BlogPostLoadedState extends BlogPostState { BlogPostModel? blog_postModelObj; Map? blog_post; Map? author; - List?>? comments; + List? comments; @override List get props => [blog_postModelObj, blog_post, author, comments]; BlogPostLoadedState copyWith( {BlogPostModel? blog_postModelObj, Map? blog_post, Map? author, - List?>? comments}) { + List? comments}) { return BlogPostLoadedState( blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, blog_post: blog_post ?? this.blog_post, From 02cd34a4adec477f81efed486eaf95e5b68e97b5 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Thu, 16 May 2024 16:04:04 +0300 Subject: [PATCH 03/11] feat(feed): Comment and author info display --- assets/images/img_send.svg | 3 + lib/core/utils/image_constant.dart | 2 + .../blog_post_screen/bloc/blog_post_bloc.dart | 30 +- .../blog_post_screen/blog_post_screen.dart | 277 ++++++++++++------ .../models/blog_post_model.dart | 17 +- .../models/comment_model.dart | 29 ++ .../widgets/comment_widget.dart | 107 +++++++ 7 files changed, 363 insertions(+), 102 deletions(-) create mode 100644 assets/images/img_send.svg create mode 100644 lib/presentation/feed/blog_post_screen/models/comment_model.dart create mode 100644 lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart diff --git a/assets/images/img_send.svg b/assets/images/img_send.svg new file mode 100644 index 00000000..5dedda3d --- /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 8c9c0981..07aa9abb 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 3c136862..c77a0dc6 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 293dfb2b..77cdd642 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 a005b747..67cbe661 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 00000000..3c8c5934 --- /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 00000000..caeeea71 --- /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)), + ], + ), + ); + } +} From 705dd15bf0f8f94473f281011434a7f6b115ecbd Mon Sep 17 00:00:00 2001 From: maxxkim Date: Fri, 17 May 2024 10:58:23 +0300 Subject: [PATCH 04/11] feat(feed): Figma layouts implementation --- .../chat/chat_select_screen/models/chat_select_model.dart | 1 - .../feed/blog_post_screen/bloc/blog_post_bloc.dart | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart b/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart index cc47e820..2b90449e 100644 --- a/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart +++ b/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart @@ -7,7 +7,6 @@ import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_entr /// and is typically used to hold data that is passed between different parts of the application. class ChatSelectModel extends Equatable { ChatSelectModel({this.chatList = const []}); - List chatList; ChatSelectModel copyWith({List? chatList}) { return ChatSelectModel( 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 c77a0dc6..00e80f16 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 @@ -136,8 +136,8 @@ class BlogPostBloc extends Bloc { for (int i = 0; i < comments.length; i++) { list.add(CommentModel( id: comments[i]['id'].toString(), - userImage: '', - username: 'Вася Пупкин', + userImage: comments[i]['profile_photo_path'], + username: comments[i]['name'], dateText: DateFormat('dd.MM') .format(DateTime.parse(comments[i]['updated_at'])), greetingText: comments[i]['body'])); From e71f15f664574577153fcc557fd60397d8287dda Mon Sep 17 00:00:00 2001 From: maxxkim Date: Mon, 20 May 2024 12:00:59 +0300 Subject: [PATCH 05/11] feat(feed): Blog post show layouts finished --- .../blog_post_screen/bloc/blog_post_bloc.dart | 1 + .../blog_post_screen/blog_post_screen.dart | 187 ++++++++++++++---- 2 files changed, 155 insertions(+), 33 deletions(-) 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 00e80f16..ea2db07e 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 @@ -134,6 +134,7 @@ class BlogPostBloc extends Bloc { List fillCommentList(List comments) { List list = []; for (int i = 0; i < comments.length; i++) { + print(comments[i]); list.add(CommentModel( id: comments[i]['id'].toString(), userImage: comments[i]['profile_photo_path'], 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 77cdd642..bdded69c 100644 --- a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -4,6 +4,7 @@ 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:intl/intl.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'; @@ -148,48 +149,168 @@ class BlogPostScreen extends StatelessWidget { ], ), child: Column(mainAxisSize: MainAxisSize.min, children: [ - CachedNetworkImage( - imageUrl: - dotenv.env['STORAGE_URL']! + (state.blog_post?['image'] ?? ''), - imageBuilder: (context, imageProvider) => ClipRRect( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Align( + alignment: Alignment.center, + child: Text( + state.blog_post?['title'], + style: CustomTextStyles.titleMediumBlack, + ), + ), + Spacer(), + PopupMenuButton( + onSelected: (value) => tapMenu(value, context, state), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + ), + ], + ), + SizedBox(height: 8.0), + _buildImage(state), + SizedBox(height: 16.0), + Text( + state.blog_post?['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + Align( + alignment: Alignment.topLeft, + child: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(state.blog_post?['updated_at'])), + style: theme.textTheme.bodyMedium, + )), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 24), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 8), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + style: CustomTextStyles.titleMediumSemiBold, + ) + ]), + ), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 24), + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.share_outlined), + ), + Text( + "Поделиться", + style: CustomTextStyles.titleMediumSemiBold, + ) + ])) + ]), + ]), + ); + } + + _buildImage(BlogPostLoadedState state) { + return CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + 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(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 48.0), + child: Row( + children: [ + Icon( + Icons.remove_red_eye, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + Row( + children: [ + Icon( + Icons.favorite, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(right: 48.0), + child: Row( + children: [ + Icon( + Icons.comment_outlined, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '${state.comments?.length}', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + ], ), - child: Text('Редактировать'), ), - ElevatedButton( - onPressed: () => context - .read() - .add(DeleteEntry(state.blog_post?['id'])), - child: Text('Удалить'), - ), - ], - ) - ]), + ), + ], + ), + placeholder: (context, url) => CircularProgressIndicator(), ); } + void tapMenu(String value, BuildContext context, BlogPostLoadedState state) { + if (value == 'edit') { + Navigator.pushNamed( + context, + AppRoutes.blogPostEditScreen, + arguments: state.blog_post?['id'].toString(), + ); + } else if (value == 'delete') { + context.read().add(DeleteEntry(state.blog_post?['id'])); + } + } + _buildTextField(BuildContext context, BlogPostLoadedState state) { return Container( margin: EdgeInsets.only( @@ -216,7 +337,7 @@ class BlogPostScreen extends StatelessWidget { Column( children: [ CustomTextFormField( - maxLines: 8, + maxLines: 4, ), SizedBox(height: 15.h), CustomOutlinedButton( From 4642fea47e2535919eb5eea2fdd625fb0e5e6a11 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Mon, 20 May 2024 16:07:00 +0300 Subject: [PATCH 06/11] feat(feed): BlogPost comment creation event and API --- .../blog_post_screen/bloc/blog_post_bloc.dart | 43 ++++++++++++++++++- .../bloc/blog_post_event.dart | 7 +++ .../bloc/blog_post_state.dart | 15 ++++++- .../blog_post_screen/blog_post_screen.dart | 5 ++- 4 files changed, 66 insertions(+), 4 deletions(-) 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 ea2db07e..b4889913 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 @@ -18,6 +18,7 @@ class BlogPostBloc extends Bloc { BlogPostBloc(BlogPostState initialState) : super(initialState) { on(_onInitialize); on(_deleteEntry); + on(_createComment); } _onInitialize( @@ -48,10 +49,13 @@ class BlogPostBloc extends Bloc { blog_postModelObj: currentState.blog_postModelObj, blog_post: currentState.blog_post, author: currentState.author, - comments: currentState.comments)); + comments: currentState.comments, + token: currentState.token)); } else { emit(BlogPostLoadedState( blog_post: data, + token: token, + commentController: TextEditingController(), author: await _fetchAuthor(data['id']), comments: fillCommentList(await _fetchComments(data['id'])))); } @@ -145,4 +149,41 @@ class BlogPostBloc extends Bloc { } return list; } + + FutureOr _createComment( + CreateComment event, Emitter emit) async { + if (state is BlogPostLoadedState) { + var currentState = state as BlogPostLoadedState; + var updatedState = currentState.copyWith( + commentController: TextEditingController(text: event.body), + ); + + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await _sendCommentData( + currentState.token, event.body, currentState.blog_post?['id']); + print(response.body); + + if (response.statusCode == 200) { + emit(updatedState); // Передаем обновленное состояние в emit + } + } + } + + Future _sendCommentData( + String? token, String body, int id) async { + String url = dotenv.env['SERVER_ROOT']! + '/feed/createComment/$id'; + final Map credentials = { + 'body': body, + }; + + var response = await http.post(Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8); + + return response; + } } diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart index 415809b3..e1435112 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart @@ -35,3 +35,10 @@ class FetchComments extends BlogPostEvent { @override List get props => [id]; } + +class CreateComment extends BlogPostEvent { + CreateComment(this.body); + final String body; + @override + List get props => [body]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart index 610b528b..76226025 100644 --- a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart @@ -23,24 +23,35 @@ class BlogPostState extends Equatable { class BlogPostLoadedState extends BlogPostState { BlogPostLoadedState( - {this.blog_postModelObj, this.blog_post, this.author, this.comments}); + {this.blog_postModelObj, + this.blog_post, + this.author, + this.comments, + this.token, + this.commentController}); BlogPostModel? blog_postModelObj; Map? blog_post; Map? author; List? comments; + TextEditingController? commentController; + String? token; @override List get props => [blog_postModelObj, blog_post, author, comments]; BlogPostLoadedState copyWith( {BlogPostModel? blog_postModelObj, Map? blog_post, Map? author, + TextEditingController? commentController, + String? token, List? comments}) { return BlogPostLoadedState( blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, blog_post: blog_post ?? this.blog_post, author: author ?? this.author, - comments: comments ?? this.comments); + comments: comments ?? this.comments, + token: token ?? this.token, + commentController: commentController ?? this.commentController); } } 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 bdded69c..7312e358 100644 --- a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -337,6 +337,7 @@ class BlogPostScreen extends StatelessWidget { Column( children: [ CustomTextFormField( + controller: state.commentController, maxLines: 4, ), SizedBox(height: 15.h), @@ -347,7 +348,9 @@ class BlogPostScreen extends StatelessWidget { decoration: BoxDecoration( color: Colors.transparent, ), - onPressed: () => print("tap"), + onPressed: () => context + .read() + .add(CreateComment(state.commentController!.text)), // Установите прозрачный фон ) ], From a27355516cfd3bb4bc912f57c78a38f7c13b4fdc Mon Sep 17 00:00:00 2001 From: maxxkim Date: Mon, 20 May 2024 17:29:19 +0300 Subject: [PATCH 07/11] refactor(feed): Removed unused class and changed reply button size --- lib/data/apiClient/api_client.dart | 3 --- .../feed/blog_post_screen/widgets/comment_widget.dart | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 lib/data/apiClient/api_client.dart diff --git a/lib/data/apiClient/api_client.dart b/lib/data/apiClient/api_client.dart deleted file mode 100644 index d629b311..00000000 --- a/lib/data/apiClient/api_client.dart +++ /dev/null @@ -1,3 +0,0 @@ -//import 'package:eatvio_app/core/app_export.dart'; - -class ApiClient {} diff --git a/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart index caeeea71..c224966c 100644 --- a/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart +++ b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart @@ -97,8 +97,8 @@ class CommentWidget extends StatelessWidget { ), CustomImageView( imagePath: ImageConstant.imgSend, - height: 40.v, - width: 40.h, + height: 30.v, + width: 30.h, margin: EdgeInsets.only(left: 5)), ], ), From 0eab98cb79d7e24ebc609b2f0651110529a7fd98 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Tue, 21 May 2024 11:37:57 +0300 Subject: [PATCH 08/11] feat(feed): Layouts by Figma for feed --- .../feed/feed_screen/feed_screen.dart | 145 ++++++++++++++---- 1 file changed, 117 insertions(+), 28 deletions(-) diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index e0712d31..947bab58 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; class FeedScreen extends StatelessWidget { static Widget builder(BuildContext context) { @@ -117,6 +118,7 @@ class FeedScreen extends StatelessWidget { itemCount: state.posts?.length ?? 0, itemBuilder: (context, index) { final post = state.posts?[index]; + print(post); return Container( margin: EdgeInsets.only(bottom: 10), decoration: BoxDecoration( @@ -131,38 +133,125 @@ class FeedScreen extends StatelessWidget { ), ], ), - child: ListTile( - leading: CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + post['image'], - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, + child: Column(children: [ + ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + 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, + ), ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, + title: Text( + post?['title'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(post?['updated_at'])), + style: theme.textTheme.bodyMedium, + ), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostScreen, + arguments: post?['id'].toString(), + ), + trailing: PopupMenuButton( + onSelected: (value) => print("tap"), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], ), ), - title: Text( - post?['title'] ?? '', - style: CustomTextStyles.titleMediumSemiBold, - ), - subtitle: Text(post?['description'] ?? ''), - onTap: () => Navigator.pushNamed( - context, - AppRoutes.blogPostScreen, - arguments: post?['id'].toString(), - ), - trailing: GestureDetector( - onTap: () => Navigator.pushNamed( - context, - AppRoutes.blogPostEditScreen, - arguments: post?['id'].toString(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Align( + alignment: Alignment.center, + child: Text( + post?['title'], + style: CustomTextStyles.titleMediumBlack, + ), + ), + Spacer(), + ], + ), + SizedBox(height: 8.0), + Text( + post?['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + CachedNetworkImage( + imageUrl: + dotenv.env['STORAGE_URL']! + (post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + ], ), - child: Icon(Icons.edit)), - )); + placeholder: (context, url) => + CircularProgressIndicator(), + ), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 4), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.comment_outlined), + ), + Text( + "Комментировать", + ) + ]), + ), + Spacer(), + Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + ) + ]), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.share_outlined), + ), + ]), + ], + ), + ), + ])); }, ); } From 35bafdc7014e83740cb476bbae47b9b1f805c159 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Tue, 21 May 2024 12:30:49 +0300 Subject: [PATCH 09/11] feat(feed): Author name and avatar fetched from API --- .../feed/feed_screen/feed_screen.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index 947bab58..b15ab6df 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -118,7 +118,7 @@ class FeedScreen extends StatelessWidget { itemCount: state.posts?.length ?? 0, itemBuilder: (context, index) { final post = state.posts?[index]; - print(post); + print(post['author']['name']); return Container( margin: EdgeInsets.only(bottom: 10), decoration: BoxDecoration( @@ -136,7 +136,8 @@ class FeedScreen extends StatelessWidget { child: Column(children: [ ListTile( leading: CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageUrl: dotenv.env['STORAGE_URL']! + + (post["author"]['profile_photo_path'] ?? ''), imageBuilder: (context, imageProvider) => CircleAvatar( backgroundImage: imageProvider, radius: 25, @@ -149,12 +150,12 @@ class FeedScreen extends StatelessWidget { ), ), title: Text( - post?['title'] ?? '', + post?['author']['name'] ?? '', style: CustomTextStyles.titleMediumSemiBold, ), subtitle: Text( DateFormat('dd.MM.yyyy') - .format(DateTime.parse(post?['updated_at'])), + .format(DateTime.parse(post?['post']['updated_at'])), style: theme.textTheme.bodyMedium, ), onTap: () => Navigator.pushNamed( @@ -188,7 +189,7 @@ class FeedScreen extends StatelessWidget { Align( alignment: Alignment.center, child: Text( - post?['title'], + post?['post']['title'], style: CustomTextStyles.titleMediumBlack, ), ), @@ -197,13 +198,13 @@ class FeedScreen extends StatelessWidget { ), SizedBox(height: 8.0), Text( - post?['description'], + post?['post']['description'], style: CustomTextStyles.bodyMediumBluegray800, ), SizedBox(height: 16.0), CachedNetworkImage( - imageUrl: - dotenv.env['STORAGE_URL']! + (post?['image'] ?? ''), + imageUrl: dotenv.env['STORAGE_URL']! + + (post?['post']['image'] ?? ''), imageBuilder: (context, imageProvider) => Stack( children: [ ClipRRect( From 6ad23e240fef444bf9d678144637b7c13d1d8384 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Tue, 21 May 2024 16:04:00 +0300 Subject: [PATCH 10/11] feat(feed): Comment creation API and state update --- .../blog_post_screen/bloc/blog_post_bloc.dart | 40 ++++++++++--------- .../feed/feed_screen/bloc/feed_bloc.dart | 3 ++ .../feed/feed_screen/feed_screen.dart | 3 +- 3 files changed, 25 insertions(+), 21 deletions(-) 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 b4889913..e1a98497 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 @@ -73,6 +73,27 @@ class BlogPostBloc extends Bloc { } } + FutureOr _createComment( + CreateComment event, Emitter emit) async { + if (state is BlogPostLoadedState) { + var currentState = state as BlogPostLoadedState; + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await _sendCommentData( + currentState.token, event.body, currentState.blog_post?['id']); + + if (response.statusCode == 200) { + var updatedComments = fillCommentList( + await _fetchComments(currentState.blog_post?['id'])); + var updatedState = currentState.copyWith( + comments: updatedComments, + commentController: + TextEditingController(), // Clear the comment input field + ); + emit(updatedState); // Emit the updated state to trigger a screen update + } + } + } + FutureOr _deleteEntry( DeleteEntry event, Emitter emit) async { final box = await Hive.openBox('token'); @@ -150,25 +171,6 @@ class BlogPostBloc extends Bloc { return list; } - FutureOr _createComment( - CreateComment event, Emitter emit) async { - if (state is BlogPostLoadedState) { - var currentState = state as BlogPostLoadedState; - var updatedState = currentState.copyWith( - commentController: TextEditingController(text: event.body), - ); - - // Отправляем данные по HTTP маршруту и ждем ответ - var response = await _sendCommentData( - currentState.token, event.body, currentState.blog_post?['id']); - print(response.body); - - if (response.statusCode == 200) { - emit(updatedState); // Передаем обновленное состояние в emit - } - } - } - Future _sendCommentData( String? token, String body, int id) async { String url = dotenv.env['SERVER_ROOT']! + '/feed/createComment/$id'; diff --git a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart index 984bbbf7..3932f495 100644 --- a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart +++ b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart @@ -34,6 +34,9 @@ class FeedBloc extends Bloc { } final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/index'), + headers: { + 'Authorization': 'Bearer $token', + }, ); print('Got response'); if (response.statusCode == 200) { diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index b15ab6df..4ebff264 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -118,7 +118,6 @@ class FeedScreen extends StatelessWidget { itemCount: state.posts?.length ?? 0, itemBuilder: (context, index) { final post = state.posts?[index]; - print(post['author']['name']); return Container( margin: EdgeInsets.only(bottom: 10), decoration: BoxDecoration( @@ -161,7 +160,7 @@ class FeedScreen extends StatelessWidget { onTap: () => Navigator.pushNamed( context, AppRoutes.blogPostScreen, - arguments: post?['id'].toString(), + arguments: post?['post']['id'].toString(), ), trailing: PopupMenuButton( onSelected: (value) => print("tap"), From e262c6993a190401d60736af2107f2fa6a3f10e4 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Wed, 22 May 2024 15:23:55 +0300 Subject: [PATCH 11/11] feat(feed): Search field (static), menu bar added. Feed and comment scroll fixed --- .../blog_post_screen/blog_post_screen.dart | 46 +- .../feed/feed_screen/feed_screen.dart | 410 ++++++++++-------- .../feed_screen/widgets/feed_scroll_item.dart | 23 + .../feed_screen/widgets/feed_search_view.dart | 151 +++++++ 4 files changed, 423 insertions(+), 207 deletions(-) create mode 100644 lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart create mode 100644 lib/presentation/feed/feed_screen/widgets/feed_search_view.dart 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 7312e358..04637420 100644 --- a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -1,6 +1,5 @@ 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'; @@ -150,15 +149,10 @@ class BlogPostScreen extends StatelessWidget { ), child: Column(mainAxisSize: MainAxisSize.min, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Spacer(), - Align( - alignment: Alignment.center, - child: Text( - state.blog_post?['title'], - style: CustomTextStyles.titleMediumBlack, - ), + Text( + state.blog_post?['title'], + style: CustomTextStyles.titleMediumBlack, ), Spacer(), PopupMenuButton( @@ -361,20 +355,26 @@ class BlogPostScreen extends StatelessWidget { } _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, - ); - }); + if (state.comments != null && state.comments!.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 0; i < state.comments!.length; i++) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CommentWidget(state.comments![i]), + SizedBox(height: 10.0), // Separator + ], + ), + ], + ); + } else { + // Return a message or placeholder widget when there are no comments + return Center( + child: Text('No comments available'), + ); + } } _buildBottomBar(BuildContext context) { diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index 4ebff264..a7cd48fe 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -1,11 +1,11 @@ import 'bloc/feed_bloc.dart'; import 'models/feed_model.dart'; -import 'package:eatvio_app/core/app_export.dart'; -import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; -import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; -import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; -import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_search_view.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_scroll_item.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -29,7 +29,7 @@ class FeedScreen extends StatelessWidget { ); } else if (state is FeedLoadedState) { // Данные загружены, можно строить экран. - return _buildSetingsScreen(context, state); + return _buildFeedScreen(context, state); } else { // Другие состояния, если они есть. return Container(); // Пустой контейнер для необработанных состояний. @@ -38,7 +38,7 @@ class FeedScreen extends StatelessWidget { ); } - _buildSetingsScreen(BuildContext context, FeedLoadedState state) { + _buildFeedScreen(BuildContext context, FeedLoadedState state) { mediaQueryData = MediaQuery.of(context); return SafeArea( child: Scaffold( @@ -53,53 +53,10 @@ class FeedScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: 10), - Container( - 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: CustomAppBar( - height: 54.v, - leadingWidth: 28.h, - leading: AppbarLeadingImage( - imagePath: ImageConstant.imgArrowLeft, - margin: EdgeInsets.only( - left: 16.h, top: 15.v, bottom: 15.v), - onTap: onTapBack(context), - ), - title: Padding( - padding: EdgeInsets.only(left: 20.h), - child: Row( - children: [ - AppbarSubtitle( - text: "Лента", - margin: EdgeInsets.only( - left: 80.h, top: 12.v, bottom: 9.v), - ), - Spacer(), - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: GestureDetector( - onTap: () => Navigator.pushNamed( - context, - AppRoutes.blogPostCreateScreen, - ), - child: Icon(Icons.create_outlined)), - ) - ], - ), - ), - ), - ), + SizedBox(height: 20), + _buildSearch(), + SizedBox(height: 20), + _buildMenu(context, state), SizedBox(height: 20), _buildBlogPostList(context, state) ], @@ -112,148 +69,202 @@ class FeedScreen extends StatelessWidget { ); } + _buildSearch() { + return Container( + margin: EdgeInsets.only(bottom: 10), + 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: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск записей", + ), + SizedBox(height: 10), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + text: "Найти запись", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "+", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ) + ], + ), + ), + ); + } + _buildBlogPostList(BuildContext context, FeedLoadedState state) { - return ListView.builder( - shrinkWrap: true, // Add this line - itemCount: state.posts?.length ?? 0, - itemBuilder: (context, index) { - final post = state.posts?[index]; - return Container( - margin: EdgeInsets.only(bottom: 10), - 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), + List postWidgets = []; + + if (state.posts != null) { + for (var post in state.posts!) { + postWidgets.add(_buildFeedEntry(post, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: postWidgets, + ); + } + + _buildFeedEntry(post, BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + 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(children: [ + ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (post["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, + ), + ), + title: Text( + post?['author']['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(post?['post']['updated_at'])), + style: theme.textTheme.bodyMedium, + ), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostScreen, + arguments: post?['post']['id'].toString(), + ), + trailing: PopupMenuButton( + onSelected: (value) => print("tap"), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), ), ], ), - child: Column(children: [ - ListTile( - leading: CachedNetworkImage( + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Align( + alignment: Alignment.center, + child: Text( + post?['post']['title'], + style: CustomTextStyles.titleMediumBlack, + ), + ), + Spacer(), + ], + ), + SizedBox(height: 8.0), + Text( + post?['post']['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + CachedNetworkImage( imageUrl: dotenv.env['STORAGE_URL']! + - (post["author"]['profile_photo_path'] ?? ''), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, + (post?['post']['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + ], ), placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), ), - title: Text( - post?['author']['name'] ?? '', - style: CustomTextStyles.titleMediumSemiBold, - ), - subtitle: Text( - DateFormat('dd.MM.yyyy') - .format(DateTime.parse(post?['post']['updated_at'])), - style: theme.textTheme.bodyMedium, - ), - onTap: () => Navigator.pushNamed( - context, - AppRoutes.blogPostScreen, - arguments: post?['post']['id'].toString(), - ), - trailing: PopupMenuButton( - onSelected: (value) => print("tap"), - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'edit', - child: Text('Редактировать'), - ), - PopupMenuItem( - value: 'delete', - child: Text('Удалить'), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Spacer(), - Align( - alignment: Alignment.center, - child: Text( - post?['post']['title'], - style: CustomTextStyles.titleMediumBlack, - ), - ), - Spacer(), - ], - ), - SizedBox(height: 8.0), - Text( - post?['post']['description'], - style: CustomTextStyles.bodyMediumBluegray800, - ), - SizedBox(height: 16.0), - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + - (post?['post']['image'] ?? ''), - imageBuilder: (context, imageProvider) => Stack( - children: [ - ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(8.0)), - child: Image( - image: imageProvider, - ), - ), - ], - ), - placeholder: (context, url) => - CircularProgressIndicator(), - ), - SizedBox(height: 16.0), - Row(children: [ - Padding( - padding: EdgeInsets.only(left: 4), - child: Row(children: [ - Padding( - padding: EdgeInsets.only(right: 4), - child: Icon(Icons.comment_outlined), - ), - Text( - "Комментировать", - ) - ]), - ), - Spacer(), - Row(children: [ - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon(Icons.favorite_outline), - ), - Text( - "Нравится", - ) - ]), - Spacer(), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 4), + child: Row(children: [ Padding( padding: EdgeInsets.only(right: 4), - child: Icon(Icons.share_outlined), + child: Icon(Icons.comment_outlined), ), + Text( + "Комментировать", + ) ]), - ], - ), - ), - ])); - }, - ); + ), + Spacer(), + Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + ) + ]), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.share_outlined), + ), + ]), + ], + ), + ), + ])); } Function onTapBack(BuildContext context) { @@ -269,4 +280,35 @@ class FeedScreen extends StatelessWidget { onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); } + + _buildMenu(BuildContext context, FeedLoadedState state) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + 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: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ScrollItem(title: 'Все'), + ScrollItem(title: 'Рецепты'), + ScrollItem(title: 'Диеты'), + ScrollItem(title: 'Планы'), + ScrollItem(title: 'Записи'), + ], + ), + ), + ); + } } diff --git a/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart b/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart new file mode 100644 index 00000000..2c407394 --- /dev/null +++ b/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class ScrollItem extends StatelessWidget { + final String title; + + const ScrollItem({required this.title}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Text( + title, + style: CustomTextStyles.titleMediumSemiBold, + ), + ); + } +} diff --git a/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart b/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart new file mode 100644 index 00000000..20cce2ed --- /dev/null +++ b/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class CustomSearchView extends StatelessWidget { + CustomSearchView( + {Key? key, + this.alignment, + this.width, + this.scrollPadding, + this.controller, + this.focusNode, + this.autofocus = false, + this.textStyle, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.contentPadding, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.onChanged}) + : super( + key: key, + ); + + final Alignment? alignment; + + final double? width; + + final TextEditingController? scrollPadding; + + final TextEditingController? controller; + + final FocusNode? focusNode; + + final bool? autofocus; + + final TextStyle? textStyle; + + final TextInputType? textInputType; + + final int? maxLines; + + final String? hintText; + + final TextStyle? hintStyle; + + final Widget? prefix; + + final BoxConstraints? prefixConstraints; + + final Widget? suffix; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final FormFieldValidator? validator; + + final Function(String)? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: searchViewWidget(context)) + : searchViewWidget(context); + } + + searchViewWidget(BuildContext context) => SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + scrollPadding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + controller: controller, + focusNode: focusNode, + onTapOutside: (event) => onTapOutside(), + autofocus: autofocus!, + style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + keyboardType: textInputType, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + onChanged: (String value) => onChanged?.call(value), + ), + ); + + void onTapOutside() { + if (focusNode != null) { + focusNode?.unfocus(); + } else { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + InputDecoration get decoration => InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + prefixIcon: prefix ?? + SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.search)), + ), + prefixIconConstraints: prefixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + top: 11.v, + right: 11.h, + bottom: 11.v, + ), + fillColor: fillColor ?? Colors.grey[200], + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + ); +}