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, + ), + ); +}