diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart index fee6cd51..e07d2dc6 100644 --- a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart @@ -36,9 +36,11 @@ class BlogPostCreateBloc blog_postModelObj: currentState.blog_postModelObj, title: currentState.title, description: currentState.description, - youtube_url: currentState.youtube_url)); + youtube_url: currentState.youtube_url, + token: currentState.token)); } else { emit(BlogPostCreateLoadedState( + token: token, title: TextEditingController(), description: TextEditingController(), youtube_url: TextEditingController())); @@ -71,7 +73,8 @@ class BlogPostCreateBloc String? description, ) async { String url = '${dotenv.env['SERVER_ROOT']!}/feed/create'; - + final box = await Hive.openBox('token'); + final token = box.get('token'); final Map credentials = { 'title': title, 'description': description, @@ -83,6 +86,7 @@ class BlogPostCreateBloc Uri.parse(url), headers: { 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', }, body: json.encode(credentials), encoding: utf8, diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart index 0cc3c405..de3bab48 100644 --- a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart @@ -23,26 +23,32 @@ class BlogPostCreateState extends Equatable { class BlogPostCreateLoadedState extends BlogPostCreateState { BlogPostCreateLoadedState( - {this.blog_postModelObj, this.title, this.description, this.youtube_url}); + {this.token, + this.blog_postModelObj, + this.title, + this.description, + this.youtube_url}); BlogPostCreateModel? blog_postModelObj; TextEditingController? title; TextEditingController? description; TextEditingController? youtube_url; + String? token; @override List get props => [blog_postModelObj, title, description, youtube_url]; - BlogPostCreateLoadedState copyWith({ - BlogPostCreateModel? blog_postModelObj, - TextEditingController? title, - TextEditingController? description, - TextEditingController? youtube_url, - }) { + BlogPostCreateLoadedState copyWith( + {BlogPostCreateModel? blog_postModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + String? token}) { return BlogPostCreateLoadedState( blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, title: title ?? this.title, description: description ?? this.description, youtube_url: youtube_url ?? this.youtube_url, + token: token ?? this.token, ); } } diff --git a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart index 6950af55..fc1d7c14 100644 --- a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart +++ b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart @@ -3,6 +3,8 @@ import 'models/blog_post_create_model.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_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; class BlogPostCreateScreen extends StatelessWidget { const BlogPostCreateScreen({Key? key}) : super(key: key); @@ -44,61 +46,123 @@ class BlogPostCreateScreen extends StatelessWidget { child: Scaffold( resizeToAvoidBottomInset: false, body: Container( - width: double.maxFinite, - 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), + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 16.0), - TextFormField( - controller: state.title, - decoration: InputDecoration(labelText: 'Заголовок'), - ), - SizedBox(height: 16.0), - TextFormField( - controller: state.description, - decoration: InputDecoration(labelText: 'Описание'), - ), - SizedBox(height: 16.0), - TextFormField( - controller: state.youtube_url, - decoration: InputDecoration(labelText: 'Ссылка'), - ), - SizedBox(height: 16.0), - // Здесь нужно добавить виджет для загрузки картинки - ElevatedButton( - onPressed: () => context - .read() - .add(CreateBlogPost()), - child: Text('Создать'), - ) - ]), - ) - ]), - ), + 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: [ + Text("Заголовок новости", + style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.title, + validator: _validateTitle, + ), + SizedBox(height: 8.0), + Text("Ссылка на YouTube", + style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.youtube_url, + validator: _validateYouTubeLink, + ), + SizedBox(height: 8.0), + Text("Описание", style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.description, + validator: _validateDescription, + maxLines: 6, + ), + SizedBox(height: 16.0), + // Add widget for image upload here + CustomOutlinedButton( + onPressed: () => _submitForm( + context, + state.title!.text, + state.description!.text, + state.youtube_url!.text), + buttonStyle: + CustomButtonStyles.outlinePrimaryTL15, + text: 'Создать', + ), + ], + ), + ])) + ])), bottomNavigationBar: _buildBottomBar(context))); } } +_submitForm( + BuildContext context, String title, String description, String youtubeURL) { + String? titleError = _validateTitle(title); + String? descriptionError = _validateDescription(description); + String? youtubeURLError = _validateYouTubeLink(youtubeURL); + + if (titleError != null || + descriptionError != null || + youtubeURLError != null) { + // Display error messages or handle invalid input as needed + // For example, show a snackbar with the error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(titleError ?? descriptionError ?? youtubeURLError!)), + ); + } else { + // Proceed with form submission + context.read().add(CreateBlogPost()); + } +} + +String? _validateTitle(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите заголовок'; + } + return null; +} + +String? _validateDescription(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите описание'; + } + return null; +} + +String? _validateYouTubeLink(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите ссылку'; + } else if (!_isValidUrl(value)) { + return 'Пожалуйста, введите корректную ссылку'; + } + return null; +} + +bool _isValidUrl(String url) { + final RegExp urlRegExp = + new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$"); + return urlRegExp.hasMatch(url); +} + _buildBottomBar(BuildContext context) { return CustomBottomBar(onChanged: (BottomBarEnum type) {}); } 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 e1a98497..d0655339 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 @@ -80,7 +80,6 @@ class BlogPostBloc extends Bloc { // Отправляем данные по 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'])); @@ -159,7 +158,6 @@ 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'], @@ -175,6 +173,7 @@ class BlogPostBloc extends Bloc { String? token, String body, int id) async { String url = dotenv.env['SERVER_ROOT']! + '/feed/createComment/$id'; final Map credentials = { + 'id': id, 'body': body, }; diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index a7cd48fe..d5d25004 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -54,7 +54,7 @@ class FeedScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(height: 20), - _buildSearch(), + _buildSearch(context), SizedBox(height: 20), _buildMenu(context, state), SizedBox(height: 20), @@ -69,7 +69,7 @@ class FeedScreen extends StatelessWidget { ); } - _buildSearch() { + _buildSearch(BuildContext context) { return Container( margin: EdgeInsets.only(bottom: 10), decoration: BoxDecoration( @@ -106,6 +106,10 @@ class FeedScreen extends StatelessWidget { flex: 1, child: CustomOutlinedButton( text: "+", + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.blogPostCreateScreen, + ), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, ), ), diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart new file mode 100644 index 00000000..d1065260 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/models/glossary_screen_model.dart'; +part 'glossary_screen_event.dart'; +part 'glossary_screen_state.dart'; + +/// A bloc that manages the state of a Glossary according to the event that is dispatched to it. +class GlossaryBloc extends Bloc { + GlossaryBloc(GlossaryState initialState) : super(initialState) { + on(_onInitialize); + on(_changeProductGroup); + } + + _onInitialize( + GlossaryInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final responseProductGroups = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/getProductGroups'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (responseProductGroups.statusCode == 200) { + final dataProductGroups = jsonDecode(responseProductGroups.body); + if (dataProductGroups is List) { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + token: currentState.token, + productGroups: currentState.productGroups, + productList: currentState.productList)); + } else { + List productGroupsModel = + _fillProductGroups(dataProductGroups); + emit(GlossaryLoadedState( + token: token, + productGroups: productGroupsModel, + productList: [])); + await _getProductListById(token, emit, productGroupsModel[0].id); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(GlossaryErrorState('Список категорий некорректного формата')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(GlossaryErrorState( + 'Ошибка сервера: ${responseProductGroups.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(GlossaryErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + void _changeProductGroup( + ChangeProductGroup event, Emitter emit) async { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + await _getProductListById(currentState.token, emit, event.id); + } + } + + Future _getProductListById( + token, Emitter emit, int? productGroupID) async { + final responseProductList = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/index/$productGroupID'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + final dataProductList = jsonDecode(responseProductList.body); + if (dataProductList is List) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + productGroups: currentState.productGroups, + productList: dataProductList)); + } else { + emit(GlossaryErrorState('Список продуктов некорректного формата')); + } + } + + List _fillProductGroups(List productGroups) { + List result = []; + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: i == 0, + ), + ); + } + return result; + } +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart new file mode 100644 index 00000000..f3fb4dd2 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart @@ -0,0 +1,30 @@ +// ignore_for_file: must_be_immutable + +part of 'glossary_screen_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Glossary widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class GlossaryEvent extends Equatable {} + +/// Event that is dispatched when the Glossary widget is first created. +class GlossaryInitialEvent extends GlossaryEvent { + @override + List get props => []; +} + +class DeleteEntry extends GlossaryEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} + +class ChangeProductGroup extends GlossaryEvent { + ChangeProductGroup(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart new file mode 100644 index 00000000..75363864 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'glossary_screen_bloc.dart'; + +/// Represents the state of Glossary in the application. +/// + +class GlossaryState extends Equatable { + GlossaryState({ + this.glossaryModelObj, + }); + + GlossaryModel? glossaryModelObj; + + @override + List get props => [glossaryModelObj]; + GlossaryState copyWith({GlossaryModel? glossaryModelObj}) { + return GlossaryState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + ); + } +} + +class GlossaryLoadedState extends GlossaryState { + GlossaryLoadedState( + {this.glossaryModelObj, + this.productGroups, + this.productList, + this.token}); + + GlossaryModel? glossaryModelObj; + String? token; + List? productGroups; + List? productList; + @override + List get props => + [glossaryModelObj, productGroups, productList, token]; + GlossaryLoadedState copyWith( + {GlossaryModel? glossaryModelObj, + String? token, + List? productGroups, + List? productList}) { + return GlossaryLoadedState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + productGroups: productGroups ?? this.productGroups, + productList: productList ?? this.productList, + token: token ?? this.token, + ); + } +} + +class GlossaryErrorState extends GlossaryState { + final String errorMessage; + + GlossaryErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class GlossaryInitialState extends GlossaryState { + GlossaryInitialState({required GlossaryModel glossaryModelObj}) + : super(glossaryModelObj: glossaryModelObj); +} diff --git a/lib/presentation/glossary/glossary_screen/glossary_screen.dart b/lib/presentation/glossary/glossary_screen/glossary_screen.dart new file mode 100644 index 00000000..65dc65cb --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/glossary_screen.dart @@ -0,0 +1,333 @@ +import 'bloc/glossary_screen_bloc.dart'; +import 'models/glossary_screen_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart'; + +class GlossaryScreen extends StatelessWidget { + const GlossaryScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => + GlossaryBloc(GlossaryState(glossaryModelObj: GlossaryModel())) + ..add(GlossaryInitialEvent()), + child: GlossaryScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is GlossaryInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is GlossaryLoadedState) { + // Данные загружены, можно строить экран. + return _buildGlossary(context, state); + } else if (state is GlossaryErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildGlossary(BuildContext context, GlossaryLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 20), + _buildSearch(context), + SizedBox(height: 20), + _buildMenu(context), + SizedBox(height: 20), + _buildProducts(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildSearch(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: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск записей", + ), + SizedBox(height: 10), + CustomOutlinedButton( + text: "Найти продукт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ], + ), + ), + ); + } + + _buildMenu(BuildContext context) { + 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: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Поиск по алфавиту", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(33, _fillAlphabet), + ), + ), + ]), + ); + } + + _buildProducts(BuildContext context, GlossaryLoadedState 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: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Группа продуктов", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + CustomDropDown( + items: state.productGroups, + defaultValue: state.productGroups?[0], + fillColor: Colors.grey[200]!, + hintText: 'Выберите категорию', + borderDecoration: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey[200]!, width: 1), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 9, + ), + onChanged: _changeProductGroup(context, state), + width: MediaQuery.of(context) + .size + .width, // Set a percentage value as per your layout + ), + SizedBox( + height: 8, + ), + Align( + alignment: Alignment.centerLeft, + child: Text("*КБЖУ указано за 100гр")), + SizedBox( + height: 8, + ), + _buildProductList(context, state) + ]), + ); + } + + _buildProductList(BuildContext context, GlossaryLoadedState state) { + List postWidgets = []; + + if (state.productList != null) { + for (var post in state.productList!) { + postWidgets.add(_buildProductEntry(post, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: postWidgets, + ); + } + + _buildProductEntry(product, 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: 1, + blurRadius: 3, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + ListTile( + trailing: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (product['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, + ), + ), + title: Text( + product?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row(children: [ + Column(children: [ + Text("Ккал - ${product['energy_on_100gr'].toString()}"), + SizedBox(height: 8), + Text("Белки - ${product['proteins_on_100gr'].toString()}") + ]), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Column(children: [ + Text("Жиры - ${product['fats_on_100gr'].toString()}"), + SizedBox(height: 8), + Text( + "Углеводы - ${product['carbohydrates_on_100gr'].toString()}") + ]), + ) + ]) + ], + ), + ), + ])); + } + + Widget _fillAlphabet(int index) { + return ScrollItem(title: String.fromCharCode(1040 + index)); + } + + List fillProductGroups(List productGroups) { + List result = []; + + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: true, + ), + ); + } + return result; + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.glossaryScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.glossaryScreen: + return GlossaryScreen.builder(context); + default: + return GlossaryScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + _changeProductGroup(BuildContext context, GlossaryLoadedState state) { + return (value) { + context.read().add(ChangeProductGroup(value.id)); + }; + } +} diff --git a/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart new file mode 100644 index 00000000..d19bbb54 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [glossary_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class GlossaryModel extends Equatable { + GlossaryModel(); + GlossaryModel copyWith() { + return GlossaryModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart new file mode 100644 index 00000000..2c407394 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_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/glossary/glossary_screen/widgets/glossary_search_widget.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart new file mode 100644 index 00000000..20cce2ed --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.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, + ), + ); +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 7163f5e5..6e29d6b0 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -38,6 +38,7 @@ import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/recipe_edit_sc import 'package:eatvio_app/presentation/feed/blog_post_screen/blog_post_screen.dart'; import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart'; import 'package:eatvio_app/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/glossary_screen.dart'; class AppRoutes { // REGISTRATION @@ -126,6 +127,8 @@ class AppRoutes { static const String planCreateScreen = '/planCreate'; + static const String glossaryScreen = '/glossary'; + static Map get routes => { dateOfBirthScreen: DateOfBirthScreen.builder, cityOfLivingScreen: CityOfLivingScreen.builder, @@ -167,5 +170,6 @@ class AppRoutes { blogPostScreen: BlogPostScreen.builder, blogPostEditScreen: BlogPostEditScreen.builder, blogPostCreateScreen: BlogPostCreateScreen.builder, + glossaryScreen: GlossaryScreen.builder, }; } diff --git a/lib/widgets/custom_drop_down.dart b/lib/widgets/custom_drop_down.dart index 55f1231f..759efc1a 100644 --- a/lib/widgets/custom_drop_down.dart +++ b/lib/widgets/custom_drop_down.dart @@ -23,6 +23,7 @@ class CustomDropDown extends StatelessWidget { this.filled = true, this.validator, this.onChanged, + this.defaultValue, }) : super( key: key, ); @@ -61,6 +62,8 @@ class CustomDropDown extends StatelessWidget { final bool? filled; + final SelectionPopupModel? defaultValue; + final FormFieldValidator? validator; final Function(SelectionPopupModel)? onChanged; @@ -77,18 +80,19 @@ class CustomDropDown extends StatelessWidget { _buildDropDownWidget(BuildContext context) { return SizedBox( - width: width ?? double.maxFinite, + width: width ?? MediaQuery.of(context).size.width - 20, child: DropdownButtonFormField( focusNode: focusNode ?? FocusNode(), icon: icon, autofocus: autofocus!, + isExpanded: true, style: textStyle ?? Theme.of(context).textTheme.titleMedium, items: items?.map((SelectionPopupModel item) { return DropdownMenuItem( value: item, child: Text( item.title, - overflow: TextOverflow.ellipsis, + overflow: TextOverflow.fade, style: hintStyle ?? Theme.of(context).textTheme.titleMedium, ), ); diff --git a/lib/widgets/custom_navigation_menu.dart b/lib/widgets/custom_navigation_menu.dart index 674dd0f2..4bbdc758 100644 --- a/lib/widgets/custom_navigation_menu.dart +++ b/lib/widgets/custom_navigation_menu.dart @@ -41,10 +41,8 @@ class CustomNavigationMenu extends StatelessWidget { ), MenuItem( icon: ImageConstant.imgFinance, - text: 'Финансы', - onTap: () { - // Действие при нажатии на "Финансы" - }, + text: 'Глоссарий', + onTap: () => Navigator.pushNamed(context, AppRoutes.glossaryScreen), ), MenuItem( icon: ImageConstant.imgSupport,