From 2f168a2f354b09e14a4ea644c9fa7fd8dbb00b99 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 11 Dec 2024 19:15:25 +0700 Subject: [PATCH] diet creation scrren is done --- assets/icons/error_icon.svg | 5 + assets/icons/ok.svg | 3 + lib/core/app_router.dart | 11 +- lib/core/locator_service.dart | 11 +- lib/core/theme/theme_helper.dart | 6 +- lib/core/utils/string_extentions.dart | 4 - lib/core/widgets/animated_option_button.dart | 34 ++ lib/core/widgets/custom_drop_down_button.dart | 12 +- lib/core/widgets/custom_text_form_field.dart | 11 +- lib/core/widgets/diet_info_row.dart | 35 +- lib/core/widgets/main_background_bouble.dart | 34 +- .../presentation/create_diet_screen.dart | 401 +++++++++++---- .../presentation/cubit/create_diet_cubit.dart | 152 +++++- .../presentation/cubit/create_diet_state.dart | 9 + .../cubit/create_diet_state.freezed.dart | 202 +++++++- .../presentation/diet_preview_screen.dart | 3 +- .../presentation/widgets/day_widget.dart | 6 + .../diet_duration_drop_down_widget.dart | 57 ++- .../widgets/diet_image_picker_widget.dart | 140 ++--- .../presentation/widgets/error_widget.dart | 26 + .../widgets/status_drop_down_widget.dart | 57 ++- .../repositories/create_diet_repository.dart | 30 +- .../services/create_diet_service.dart | 7 +- .../services/create_diet_service.g.dart | 37 +- .../widgets/options_count_button.dart | 36 +- .../diary/presentation/bloc/diary_cubit.dart | 22 +- ...ssary_moderation_request_modal_widget.dart | 3 +- .../presentation/pages/about_diet_screen.dart | 3 +- .../presentation/widgets/author_card.dart | 31 +- .../presentation/widgets/type_badges.dart | 106 +++- .../feed/models/diets_pagination_model.dart | 27 +- .../diets_pagination_model.freezed.dart | 236 ++++++++- .../feed/models/diets_pagination_model.g.dart | 16 +- .../feed/models/plan_pagination_model.dart | 23 +- .../models/plan_pagination_model.freezed.dart | 284 +++++++---- .../feed/models/plan_pagination_model.g.dart | 27 +- .../bloc/comments_cubit/comments_cubit.dart | 10 + .../bloc/plans_cubit/plans_cubit.dart | 6 +- .../bloc/profile_cubit/profile_cubit.dart | 19 + .../bloc/profile_cubit/profile_state.dart | 13 + .../profile_cubit/profile_state.freezed.dart | 162 ++++++ .../feed/presentation/pages/feed_screen.dart | 177 ++++--- .../widgets/blog_post_widget.dart | 2 +- .../presentation/widgets/comments_widget.dart | 2 +- .../presentation/widgets/diets_widget.dart | 2 +- .../presentation/widgets/plan_widget.dart | 5 +- .../feed/repositories/feed_repository.dart | 26 + lib/features/feed/services/feed_service.dart | 12 + .../feed/services/feed_service.g.dart | 73 +++ .../presentation/cubit/about_plan_cubit.dart | 65 ++- .../presentation/cubit/about_plan_state.dart | 3 +- .../cubit/about_plan_state.freezed.dart | 44 +- .../presentation/pages/about_plan_screen.dart | 479 +++++++++++++++++- .../presentation/widgets/item_widget.dart | 26 + .../widgets/option_duration_widget.dart | 45 ++ .../presentation/widgets/option_widget.dart | 49 ++ .../presentation/widgets/options_tab.dart | 43 ++ .../plans/repositories/plan_repository.dart | 43 ++ lib/features/plans/services/plan_service.dart | 31 ++ .../plans/services/plan_service.g.dart | 161 ++++++ lib/generated/intl/messages_en.dart | 18 + lib/generated/l10n.dart | 110 ++++ lib/l10n/intl_en.arb | 13 +- 63 files changed, 3113 insertions(+), 633 deletions(-) create mode 100644 assets/icons/error_icon.svg create mode 100644 assets/icons/ok.svg create mode 100644 lib/core/widgets/animated_option_button.dart create mode 100644 lib/features/create_diet/presentation/widgets/error_widget.dart create mode 100644 lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart create mode 100644 lib/features/feed/presentation/bloc/profile_cubit/profile_state.dart create mode 100644 lib/features/feed/presentation/bloc/profile_cubit/profile_state.freezed.dart create mode 100644 lib/features/plans/presentation/widgets/item_widget.dart create mode 100644 lib/features/plans/presentation/widgets/option_duration_widget.dart create mode 100644 lib/features/plans/presentation/widgets/option_widget.dart create mode 100644 lib/features/plans/presentation/widgets/options_tab.dart create mode 100644 lib/features/plans/repositories/plan_repository.dart create mode 100644 lib/features/plans/services/plan_service.dart create mode 100644 lib/features/plans/services/plan_service.g.dart diff --git a/assets/icons/error_icon.svg b/assets/icons/error_icon.svg new file mode 100644 index 00000000..2a0fd478 --- /dev/null +++ b/assets/icons/error_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ok.svg b/assets/icons/ok.svg new file mode 100644 index 00000000..a8f5b0a6 --- /dev/null +++ b/assets/icons/ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 009c3959..899804f1 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -112,9 +112,14 @@ class AppRouter { GoRoute( path: '/about-plan-screen', name: 'aboutPlanScreen', - builder: (context, state) => AboutPlanScreen( - planModel: state.extra as PlanModel, - ), + builder: (context, state) { + final data = state.extra as Map; + return AboutPlanScreen( + plan: data['plan'] as PlanModel, + // index: data['index'] as int, + navigateToComments: data['navigateToComments'] as bool, + ); + }, ), GoRoute( path: '/about-recipes-screen', diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 63bf1fed..10d91e99 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -30,12 +30,16 @@ import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.da import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -78,9 +82,10 @@ Future init() async { sl.registerFactory(() => AboutDietCubit(dietsRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); - sl.registerFactory(() => CreateDietCubit( profileRepository: sl(),createDietRepository: sl(), glossaryRepository: sl())); + sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => CreatePlanCubit()); - + sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); + sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -96,6 +101,7 @@ Future init() async { sl.registerLazySingleton(() => DietRepository(dietsService: sl())); sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); + sl.registerLazySingleton(() => PlanRepository(planService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -111,6 +117,7 @@ Future init() async { sl.registerLazySingleton(() => DietService(sl())); sl.registerLazySingleton(() => BlogPostService(sl())); sl.registerLazySingleton(() => CreateDietService(sl())); + sl.registerLazySingleton(() => PlanService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 77f7595e..ab7d7377 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -178,10 +178,14 @@ class PrimaryColors { Color get transparentBlue => Color(0xFF2F80ED).withOpacity(0.1); Color get lightGray0 => Color(0xFFF6F6F6); - + Color get lightGray00 => Color(0xFFE9E9EA); Color get dottedBorder => Color(0xFFE6E6E6); + + Color get red => Color(0xFFCD4141); + + Color get transparentBlack => Color(0xFF1313264).withOpacity(0.3); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/core/utils/string_extentions.dart b/lib/core/utils/string_extentions.dart index 4d0e5173..f474175e 100644 --- a/lib/core/utils/string_extentions.dart +++ b/lib/core/utils/string_extentions.dart @@ -1,8 +1,4 @@ - - - extension Capitalization on String { - String capitalize() { return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } diff --git a/lib/core/widgets/animated_option_button.dart b/lib/core/widgets/animated_option_button.dart new file mode 100644 index 00000000..f196c042 --- /dev/null +++ b/lib/core/widgets/animated_option_button.dart @@ -0,0 +1,34 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class AnimatedOptionButton extends StatelessWidget { + const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap}); + final String text; + final bool isSelected; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: isSelected ? appTheme.blueA400 : appTheme.whiteA700, + ), + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + style: TextStyles.textMSemibold.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ), + child: Text(text), + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/custom_drop_down_button.dart b/lib/core/widgets/custom_drop_down_button.dart index 843d6730..714382e7 100644 --- a/lib/core/widgets/custom_drop_down_button.dart +++ b/lib/core/widgets/custom_drop_down_button.dart @@ -9,11 +9,13 @@ class CustomDropDownButton extends StatelessWidget { required this.onChanged, this.value, this.hint, + this.hasError = false, }); final T? value; final List> items; final void Function(T?)? onChanged; final Widget? hint; + final bool hasError; @override Widget build(BuildContext context) { return DropdownButtonFormField( @@ -28,7 +30,15 @@ class CustomDropDownButton extends StatelessWidget { decoration: InputDecoration( border: OutlineInputBorder( borderRadius: const BorderRadius.all(Radius.circular(20)), - borderSide: BorderSide.none, + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, ), fillColor: appTheme.lightGray0, filled: true, diff --git a/lib/core/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart index 34e1c7d7..b9588f68 100644 --- a/lib/core/widgets/custom_text_form_field.dart +++ b/lib/core/widgets/custom_text_form_field.dart @@ -39,6 +39,7 @@ class CustomTextFormField extends StatelessWidget { this.textAlignVertical, this.counter = const SizedBox.shrink(), super.key, + this.hasError = false, }); final Widget? counter; final Alignment? alignment; @@ -74,12 +75,12 @@ class CustomTextFormField extends StatelessWidget { final int? maxLength; final bool expands; final TextAlignVertical? textAlignVertical; + final bool hasError; @override Widget build(BuildContext context) { return Align( alignment: alignment ?? Alignment.center, - child: TextFormField( - + child: TextFormField( textAlignVertical: textAlignVertical, expands: expands, minLines: minLines, @@ -123,17 +124,17 @@ class CustomTextFormField extends StatelessWidget { border: borderDecoration ?? OutlineInputBorder( borderRadius: BorderRadius.circular(20), - borderSide: BorderSide.none, + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, ), enabledBorder: borderDecoration ?? OutlineInputBorder( borderRadius: BorderRadius.circular(20), - borderSide: BorderSide.none, + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, ), focusedBorder: borderDecoration ?? OutlineInputBorder( borderRadius: BorderRadius.circular(20), - borderSide: BorderSide.none, + borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none, ), ), keyboardType: keyboardType, diff --git a/lib/core/widgets/diet_info_row.dart b/lib/core/widgets/diet_info_row.dart index 06c910bb..fdf5f34a 100644 --- a/lib/core/widgets/diet_info_row.dart +++ b/lib/core/widgets/diet_info_row.dart @@ -3,19 +3,29 @@ import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/type_badges.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -class DietInfoRow extends StatelessWidget { - const DietInfoRow({super.key, required this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false}); +class InfoRow extends StatelessWidget { + const InfoRow({ + super.key, + this.diet, + this.itemsBackgroundColor, + this.disableDescriptionOfTypes = false, + this.plan, + }); final Color? itemsBackgroundColor; - final Diet diet; + final Diet? diet; + final PlanModel? plan; + final bool disableDescriptionOfTypes; @override Widget build(BuildContext context) { final s = S.of(context); + return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, @@ -31,7 +41,7 @@ class DietInfoRow extends StatelessWidget { ), SizedBox(width: 4), Text( - diet.duration.toString() + ' ' + getDayForm(diet.duration, s), + diet == null ? '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}' : '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), ), ], @@ -53,12 +63,17 @@ class DietInfoRow extends StatelessWidget { ], ), SizedBox(width: 12), - InfoBouble( - backgroundColor: itemsBackgroundColor, - children: [ - TypeBadges(type: diet.type, disableDescriptionOfTypes: disableDescriptionOfTypes), - ], - ), + if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true) + InfoBouble( + backgroundColor: itemsBackgroundColor, + children: [ + TypeBadges( + type: diet?.type, + planType: plan?.type, + disableDescriptionOfTypes: disableDescriptionOfTypes, + ), + ], + ), ], ), ); diff --git a/lib/core/widgets/main_background_bouble.dart b/lib/core/widgets/main_background_bouble.dart index 46e13f09..a901ecad 100644 --- a/lib/core/widgets/main_background_bouble.dart +++ b/lib/core/widgets/main_background_bouble.dart @@ -2,31 +2,41 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class MainBackgroundBouble extends StatelessWidget { - const MainBackgroundBouble( - {super.key, required this.child, this.width, this.padding, this.enableShadow = false,}); + const MainBackgroundBouble({ + super.key, + required this.child, + this.width, + this.padding, + this.enableShadow = false, + this.hasError = false, + }); final double? width; final Widget child; final bool enableShadow; + final bool hasError; final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( - boxShadow: enableShadow - ? [ - BoxShadow( - color: Color(0x00000000).withOpacity(0.2), - spreadRadius: 0, - blurRadius: 32, - offset: Offset(0, 0), - ), - ] - : null,), + border: hasError ? Border.all(color: appTheme.red, width: 1) : null, + boxShadow: enableShadow + ? [ + BoxShadow( + color: Color(0x00000000).withOpacity(0.2), + spreadRadius: 0, + blurRadius: 32, + offset: Offset(0, 0), + ), + ] + : null, + ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: appTheme.whiteA700, + border: hasError ? Border.all(color: appTheme.red, width: 1) : null, ), width: width ?? MediaQuery.sizeOf(context).width, padding: padding, diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index ed4fd233..a4425e10 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -1,7 +1,10 @@ +import 'dart:ui'; + import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -11,6 +14,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/day_widget. import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_image_picker_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/status_drop_down_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_type_choice_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -38,63 +42,96 @@ class CreateDietScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Scaffold( - backgroundColor: appTheme.gray100, - appBar: AppBar( - centerTitle: true, - title: Text( - s.create_diet, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 8), - MainBackgroundBouble( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - SizedBox(height: 16), - CustomTextFormField( - hintText: s.enter_diet_name, - onChanged: (value) { - context.read().setName(value); - }, - ), - SizedBox(height: 12), - SizedBox( - height: 132, - child: CustomTextFormField( - textAlignVertical: TextAlignVertical.top, - maxLines: null, - minLines: null, - expands: true, - hintText: s.enter_diet_description, - onChanged: (value) { - context.read().setDetails(value); - }, - ), - ), - SizedBox(height: 12), - DietDurationDropDownWidget(), - SizedBox(height: 12), - StatusDropDownWidget(), - SizedBox(height: 16), - DietTypeChoiceWidget(), - SizedBox(height: 16), - DietImagePickerWidget(), - SizedBox(height: 16), - ], - ), + return BlocBuilder( + builder: (context, state) { + if (state.published) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.pop(); + showPublishSuccessBottomSheet( + context: context, + text: '${state.dietData.name ?? 'Диета'}', + ); + }); + } else if (state.drafted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.pop(); + showDraftSuccessBottomSheet( + context: context, + text: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', + ); + }); + } + + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.create_diet, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), - SizedBox(height: 16), - BlocBuilder( - builder: (context, state) { - return MainBackgroundBouble( + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16), + CustomTextFormField( + hintText: s.enter_diet_name, + hasError: state.dietNameError != null, + onChanged: (value) { + context.read().setName(value); + }, + ), + if (state.dietNameError != null) ...[ + SizedBox( + height: 4, + ), + CustomErrorWidget(error: state.dietNameError ?? ''), + ], + SizedBox(height: 12), + SizedBox( + height: 132, + child: CustomTextFormField( + hasError: state.dietDescriptionError != null, + textAlignVertical: TextAlignVertical.top, + maxLines: null, + minLines: null, + expands: true, + hintText: s.enter_diet_description, + onChanged: (value) { + context.read().setDetails(value); + }, + ), + ), + if (state.dietDescriptionError != null) ...[ + SizedBox( + height: 4, + ), + CustomErrorWidget(error: state.dietDescriptionError ?? ''), + ], + SizedBox(height: 12), + DietDurationDropDownWidget(), + SizedBox(height: 12), + StatusDropDownWidget(), + SizedBox(height: 16), + DietTypeChoiceWidget(), + SizedBox(height: 16), + DietImagePickerWidget(), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 16), + MainBackgroundBouble( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center( @@ -106,71 +143,223 @@ class CreateDietScreenContent extends StatelessWidget { ), ), ), - ); - }, - ), - SizedBox(height: 8), - BlocBuilder( - builder: (context, state) { - return ListView.separated( + ), + SizedBox(height: 8), + ListView.separated( padding: const EdgeInsets.all(0), itemBuilder: (context, index) => DayWidget(index: index), itemCount: state.dietData.dailySchedule?.length ?? 0, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), separatorBuilder: (context, index) => SizedBox(height: 8), - ); - }, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + CustomOutlinedButton( + onPressed: () async { + await context.read().publishDiet(toDraft: true); + }, + text: s.to_draft, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + ), + ), + SizedBox(height: 12), + CustomOutlinedButton( + onPressed: () async { + final diet = await context.read().getDietForPreview(); + if (diet == null) return; + GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); + }, + text: s.preview, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + ), + ), + SizedBox(height: 12), + CustomOutlinedButton( + onPressed: () { + context.read().publishDiet(); + }, + text: s.publish, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ], + ), + ), + SizedBox(height: 20), + ], ), - SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + ), + ); + }, + ); + } + + Future showDraftSuccessBottomSheet({ + required BuildContext context, + required String text, + bool hasBlur = true, + bool enableDrag = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: appTheme.transparentBlack, + backgroundColor: Colors.transparent, + elevation: 0, + useSafeArea: true, + enableDrag: enableDrag, + builder: (context) => GestureDetector( + behavior: HitTestBehavior.opaque, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: hasBlur ? 24 : 0, + sigmaY: hasBlur ? 24 : 0, + tileMode: TileMode.mirror, + ), + child: SizedBox( + child: Container( + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + color: appTheme.whiteA700, + ), child: Column( children: [ - CustomOutlinedButton( - onPressed: () { - context.read().setDietState(2); - }, - text: s.to_draft, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + text, + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), ), ), - SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () async { - final diet = await context.read().getDietForPreview(); - GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); - }, - text: s.preview, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), - ), - ), - SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () { - context.read().publishDiet(); - }, - text: s.publish, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), + SizedBox( + height: 20, ), ], ), ), - SizedBox(height: 20), - ], + ), + ), + ), + ); + } + + Future showPublishSuccessBottomSheet({ + required BuildContext context, + required String text, + bool hasBlur = true, + bool enableDrag = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: appTheme.transparentBlack, + backgroundColor: Colors.transparent, + elevation: 0, + useSafeArea: true, + enableDrag: enableDrag, + builder: (context) => GestureDetector( + behavior: HitTestBehavior.opaque, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: hasBlur ? 24 : 0, + sigmaY: hasBlur ? 24 : 0, + tileMode: TileMode.mirror, + ), + child: SizedBox( + child: Container( + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + color: appTheme.whiteA700, + ), + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + text, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + 'опубликованна', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ), + ), ), ), ); diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 8f3984bd..666643a8 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'dart:io'; +import 'dart:developer'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -18,11 +21,11 @@ class CreateDietCubit extends Cubit { CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial()); void setDetails(String text) { - emit(state.copyWith(dietData: state.dietData.copyWith(details: text))); + emit(state.copyWith(dietData: state.dietData.copyWith(details: text), dietDescriptionError: null)); } void setName(String name) { - emit(state.copyWith(dietData: state.dietData.copyWith(name: name))); + emit(state.copyWith(dietData: state.dietData.copyWith(name: name), dietNameError: null)); } void setDuration(int index) { @@ -70,6 +73,7 @@ class CreateDietCubit extends Cubit { dietData: state.dietData.copyWith( duration: durationValues[index], ), + dietDurationError: null, ), ); } @@ -79,21 +83,25 @@ class CreateDietCubit extends Cubit { } void setImage(XFile? image) { - emit(state.copyWith(dietData: state.dietData.copyWith(image: image))); + emit(state.copyWith(dietData: state.dietData.copyWith(image: image), dietImageError: null)); } void setDailySchedule(List>> dailySchedule) { - emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule))); + emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule), dietDurationError: null)); } void setDietState(int index) { - emit(state.copyWith(dietStateIndex: index, dietData: state.dietData.copyWith(state: _dietStateValues[index]))); + emit(state.copyWith(dietStateIndex: index, dietData: state.dietData.copyWith(state: _dietStateValues[index]), dietStateError: null)); } void setDietType({required String key, required bool value}) { final type = Map.from(state.dietData.type); type[key] = value; - emit(state.copyWith(dietData: state.dietData.copyWith(type: type))); + emit( + state.copyWith( + dietData: state.dietData.copyWith(type: type), + ), + ); } void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) { @@ -108,6 +116,7 @@ class CreateDietCubit extends Cubit { final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); emit(state.copyWith(dietData: newDietData)); + _isDayValid(dayIndex); } } @@ -170,10 +179,12 @@ class CreateDietCubit extends Cubit { deepCopyOfDailySchedule[dayIndex] = copiedDay; final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); emit(state.copyWith(dietData: newDietData)); + _isDayValid(dayIndex); } } - Future getDietForPreview() async { + Future getDietForPreview() async { + if (!_isFromValid()) return null; final authorProfile = await profileRepository.getMyProfile(); final imageBytes = (await state.dietData.image?.readAsBytes()) ?? []; final imageInBase64 = base64.encode(imageBytes); @@ -232,10 +243,10 @@ class CreateDietCubit extends Cubit { duration: state.dietData.duration ?? 0, state: state.dietData.state ?? '', type: DietType( - meat: state.dietData.type[0], - veg: state.dietData.type[1], - carb: state.dietData.type[2], - light: state.dietData.type[3], + meat: state.dietData.type['meat'], + veg: state.dietData.type['veg'], + carb: state.dietData.type['carb'], + light: state.dietData.type['light'], ), details: state.dietData.details ?? '', dailySchedule: state.dietData.dailySchedule ?? [], @@ -256,25 +267,110 @@ class CreateDietCubit extends Cubit { return diet; } - Future publishDiet() async { + Future publishDiet({bool toDraft = false}) async { + if (!_isFromValid()) return null; try { - // final types = jsonEncode(state.dietData.type); - // if (state.dietData.image == null) { - // return; - // } - // final image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path)); - // final diet = await createDietRepository.createDiet( - // name: state.dietData.name, - // duration: state.dietData.duration, - // state: state.dietData.state, - // image: image, - // type: types, - // details: state.dietData.details, - // ); + final image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path)); + final dietState = toDraft ? _dietStateValues.last : state.dietData.state; + final diet = await createDietRepository.createDiet( + name: state.dietData.name, + duration: state.dietData.duration, + state: dietState, + image: image, + meatType: state.dietData.type['meat'] == true ? '1' : '0', + carbType: state.dietData.type['carb'] == true ? '1' : '0', + vegType: state.dietData.type['veg'] == true ? '1' : '0', + lightType: state.dietData.type['light'] == true ? '1' : '0', + details: state.dietData.details, + ); + final dailySchedule = state.dietData.dailySchedule; + createDietRepository.createDietDailySchedule( + id: diet.id, + model: {'daily_schedule': dailySchedule}, + ); + emit(state.copyWith(published: !toDraft, drafted: toDraft)); + return state.dietData.name; } catch (e, stackTrace) { - print(e); - print('---------------------------------'); - print(stackTrace); + log(e.toString(), stackTrace: stackTrace); } + return null; + } + + bool _isFromValid() { + bool result = true; + if ((state.dietData.name?.length ?? 0) < 3) { + result = false; + emit(state.copyWith(dietNameError: 'Длинна названия не может быть меньше 3 символов')); + } + if (state.dietData.name == null || state.dietData.name == '') { + result = false; + emit(state.copyWith(dietNameError: 'Поле не заполненно')); + } + if ((state.dietData.details?.length ?? 0) < 3) { + result = false; + emit(state.copyWith(dietDescriptionError: 'Длинна описания не может быть меньше 3 символов')); + } + if (state.dietData.details == null || state.dietData.details == '') { + result = false; + emit(state.copyWith(dietDescriptionError: 'Поле не заполненно')); + } + if (state.dietData.duration == null) { + result = false; + emit(state.copyWith(dietDurationError: 'Срок диеты не выбран')); + } + if (state.dietData.state == null) { + result = false; + emit(state.copyWith(dietStateError: 'Статус диеты не выбран')); + } + if (state.dietData.image == null) { + result = false; + emit(state.copyWith(dietImageError: 'Не добавлена обложка')); + } + result = _isDailyScheduleValid(); + return result; + } + + bool _isDailyScheduleValid() { + final dailySchedule = state.dietData.dailySchedule; + bool result = true; + if (dailySchedule == null) { + result = false; + } else { + final dailyScheduleErrors = state.dailyScheduleErrors.map((element) { + return element.map((element) => element).toList(); + }).toList(); + for (final (dayIndex, day) in dailySchedule.indexed) { + for (final (mealIndex, meal) in day.indexed) { + if (meal.isEmpty) { + result = false; + dailyScheduleErrors[dayIndex][mealIndex] = 'Добавьте продукты'; + } + } + } + emit(state.copyWith(dailyScheduleErrors: dailyScheduleErrors)); + } + return result; + } + + bool _isDayValid(int dayIndex) { + final dailySchedule = state.dietData.dailySchedule; + bool result = true; + if (dailySchedule == null) { + result = false; + } else { + final dailyScheduleErrors = state.dailyScheduleErrors.map((element) { + return element.map((element) => element).toList(); + }).toList(); + for (final (mealIndex, meal) in dailySchedule[dayIndex].indexed) { + if (meal.isEmpty) { + result = false; + dailyScheduleErrors[dayIndex][mealIndex] = 'Добавьте продукты'; + } else { + dailyScheduleErrors[dayIndex][mealIndex] = ''; + } + } + emit(state.copyWith(dailyScheduleErrors: dailyScheduleErrors)); + } + return result; } } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.dart index 0c554d28..16bec064 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.dart @@ -12,11 +12,20 @@ class CreateDietState with _$CreateDietState { required String? errorMessage, required CreateDietModel dietData, List>? copiedDay, + String? dietNameError, + String? dietDescriptionError, + String? dietDurationError, + String? dietStateError, + String? dietImageError, + required List> dailyScheduleErrors, int? durationIndex, int? dietStateIndex, + @Default(false) bool published, + @Default(false) bool drafted, }) = _CreateDietState; factory CreateDietState.initial() => CreateDietState( + dailyScheduleErrors: List.filled(21, ['', '', '', '', '']), isLoading: false, errorMessage: null, dietData: CreateDietModel( diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart index c58a70e9..a49cbb70 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart @@ -21,8 +21,17 @@ mixin _$CreateDietState { CreateDietModel get dietData => throw _privateConstructorUsedError; List>? get copiedDay => throw _privateConstructorUsedError; + String? get dietNameError => throw _privateConstructorUsedError; + String? get dietDescriptionError => throw _privateConstructorUsedError; + String? get dietDurationError => throw _privateConstructorUsedError; + String? get dietStateError => throw _privateConstructorUsedError; + String? get dietImageError => throw _privateConstructorUsedError; + List> get dailyScheduleErrors => + throw _privateConstructorUsedError; int? get durationIndex => throw _privateConstructorUsedError; int? get dietStateIndex => throw _privateConstructorUsedError; + bool get published => throw _privateConstructorUsedError; + bool get drafted => throw _privateConstructorUsedError; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -42,8 +51,16 @@ abstract class $CreateDietStateCopyWith<$Res> { String? errorMessage, CreateDietModel dietData, List>? copiedDay, + String? dietNameError, + String? dietDescriptionError, + String? dietDurationError, + String? dietStateError, + String? dietImageError, + List> dailyScheduleErrors, int? durationIndex, - int? dietStateIndex}); + int? dietStateIndex, + bool published, + bool drafted}); $CreateDietModelCopyWith<$Res> get dietData; } @@ -67,8 +84,16 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> Object? errorMessage = freezed, Object? dietData = null, Object? copiedDay = freezed, + Object? dietNameError = freezed, + Object? dietDescriptionError = freezed, + Object? dietDurationError = freezed, + Object? dietStateError = freezed, + Object? dietImageError = freezed, + Object? dailyScheduleErrors = null, Object? durationIndex = freezed, Object? dietStateIndex = freezed, + Object? published = null, + Object? drafted = null, }) { return _then(_value.copyWith( isLoading: null == isLoading @@ -87,6 +112,30 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> ? _value.copiedDay : copiedDay // ignore: cast_nullable_to_non_nullable as List>?, + dietNameError: freezed == dietNameError + ? _value.dietNameError + : dietNameError // ignore: cast_nullable_to_non_nullable + as String?, + dietDescriptionError: freezed == dietDescriptionError + ? _value.dietDescriptionError + : dietDescriptionError // ignore: cast_nullable_to_non_nullable + as String?, + dietDurationError: freezed == dietDurationError + ? _value.dietDurationError + : dietDurationError // ignore: cast_nullable_to_non_nullable + as String?, + dietStateError: freezed == dietStateError + ? _value.dietStateError + : dietStateError // ignore: cast_nullable_to_non_nullable + as String?, + dietImageError: freezed == dietImageError + ? _value.dietImageError + : dietImageError // ignore: cast_nullable_to_non_nullable + as String?, + dailyScheduleErrors: null == dailyScheduleErrors + ? _value.dailyScheduleErrors + : dailyScheduleErrors // ignore: cast_nullable_to_non_nullable + as List>, durationIndex: freezed == durationIndex ? _value.durationIndex : durationIndex // ignore: cast_nullable_to_non_nullable @@ -95,6 +144,14 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> ? _value.dietStateIndex : dietStateIndex // ignore: cast_nullable_to_non_nullable as int?, + published: null == published + ? _value.published + : published // ignore: cast_nullable_to_non_nullable + as bool, + drafted: null == drafted + ? _value.drafted + : drafted // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -122,8 +179,16 @@ abstract class _$$CreateDietStateImplCopyWith<$Res> String? errorMessage, CreateDietModel dietData, List>? copiedDay, + String? dietNameError, + String? dietDescriptionError, + String? dietDurationError, + String? dietStateError, + String? dietImageError, + List> dailyScheduleErrors, int? durationIndex, - int? dietStateIndex}); + int? dietStateIndex, + bool published, + bool drafted}); @override $CreateDietModelCopyWith<$Res> get dietData; @@ -146,8 +211,16 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> Object? errorMessage = freezed, Object? dietData = null, Object? copiedDay = freezed, + Object? dietNameError = freezed, + Object? dietDescriptionError = freezed, + Object? dietDurationError = freezed, + Object? dietStateError = freezed, + Object? dietImageError = freezed, + Object? dailyScheduleErrors = null, Object? durationIndex = freezed, Object? dietStateIndex = freezed, + Object? published = null, + Object? drafted = null, }) { return _then(_$CreateDietStateImpl( isLoading: null == isLoading @@ -166,6 +239,30 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> ? _value._copiedDay : copiedDay // ignore: cast_nullable_to_non_nullable as List>?, + dietNameError: freezed == dietNameError + ? _value.dietNameError + : dietNameError // ignore: cast_nullable_to_non_nullable + as String?, + dietDescriptionError: freezed == dietDescriptionError + ? _value.dietDescriptionError + : dietDescriptionError // ignore: cast_nullable_to_non_nullable + as String?, + dietDurationError: freezed == dietDurationError + ? _value.dietDurationError + : dietDurationError // ignore: cast_nullable_to_non_nullable + as String?, + dietStateError: freezed == dietStateError + ? _value.dietStateError + : dietStateError // ignore: cast_nullable_to_non_nullable + as String?, + dietImageError: freezed == dietImageError + ? _value.dietImageError + : dietImageError // ignore: cast_nullable_to_non_nullable + as String?, + dailyScheduleErrors: null == dailyScheduleErrors + ? _value._dailyScheduleErrors + : dailyScheduleErrors // ignore: cast_nullable_to_non_nullable + as List>, durationIndex: freezed == durationIndex ? _value.durationIndex : durationIndex // ignore: cast_nullable_to_non_nullable @@ -174,6 +271,14 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> ? _value.dietStateIndex : dietStateIndex // ignore: cast_nullable_to_non_nullable as int?, + published: null == published + ? _value.published + : published // ignore: cast_nullable_to_non_nullable + as bool, + drafted: null == drafted + ? _value.drafted + : drafted // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -186,9 +291,18 @@ class _$CreateDietStateImpl implements _CreateDietState { required this.errorMessage, required this.dietData, final List>? copiedDay, + this.dietNameError, + this.dietDescriptionError, + this.dietDurationError, + this.dietStateError, + this.dietImageError, + required final List> dailyScheduleErrors, this.durationIndex, - this.dietStateIndex}) - : _copiedDay = copiedDay; + this.dietStateIndex, + this.published = false, + this.drafted = false}) + : _copiedDay = copiedDay, + _dailyScheduleErrors = dailyScheduleErrors; @override final bool isLoading; @@ -206,14 +320,39 @@ class _$CreateDietStateImpl implements _CreateDietState { return EqualUnmodifiableListView(value); } + @override + final String? dietNameError; + @override + final String? dietDescriptionError; + @override + final String? dietDurationError; + @override + final String? dietStateError; + @override + final String? dietImageError; + final List> _dailyScheduleErrors; + @override + List> get dailyScheduleErrors { + if (_dailyScheduleErrors is EqualUnmodifiableListView) + return _dailyScheduleErrors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_dailyScheduleErrors); + } + @override final int? durationIndex; @override final int? dietStateIndex; + @override + @JsonKey() + final bool published; + @override + @JsonKey() + final bool drafted; @override String toString() { - return 'CreateDietState(isLoading: $isLoading, errorMessage: $errorMessage, dietData: $dietData, copiedDay: $copiedDay, durationIndex: $durationIndex, dietStateIndex: $dietStateIndex)'; + return 'CreateDietState(isLoading: $isLoading, errorMessage: $errorMessage, dietData: $dietData, copiedDay: $copiedDay, dietNameError: $dietNameError, dietDescriptionError: $dietDescriptionError, dietDurationError: $dietDurationError, dietStateError: $dietStateError, dietImageError: $dietImageError, dailyScheduleErrors: $dailyScheduleErrors, durationIndex: $durationIndex, dietStateIndex: $dietStateIndex, published: $published, drafted: $drafted)'; } @override @@ -229,10 +368,25 @@ class _$CreateDietStateImpl implements _CreateDietState { other.dietData == dietData) && const DeepCollectionEquality() .equals(other._copiedDay, _copiedDay) && + (identical(other.dietNameError, dietNameError) || + other.dietNameError == dietNameError) && + (identical(other.dietDescriptionError, dietDescriptionError) || + other.dietDescriptionError == dietDescriptionError) && + (identical(other.dietDurationError, dietDurationError) || + other.dietDurationError == dietDurationError) && + (identical(other.dietStateError, dietStateError) || + other.dietStateError == dietStateError) && + (identical(other.dietImageError, dietImageError) || + other.dietImageError == dietImageError) && + const DeepCollectionEquality() + .equals(other._dailyScheduleErrors, _dailyScheduleErrors) && (identical(other.durationIndex, durationIndex) || other.durationIndex == durationIndex) && (identical(other.dietStateIndex, dietStateIndex) || - other.dietStateIndex == dietStateIndex)); + other.dietStateIndex == dietStateIndex) && + (identical(other.published, published) || + other.published == published) && + (identical(other.drafted, drafted) || other.drafted == drafted)); } @override @@ -242,8 +396,16 @@ class _$CreateDietStateImpl implements _CreateDietState { errorMessage, dietData, const DeepCollectionEquality().hash(_copiedDay), + dietNameError, + dietDescriptionError, + dietDurationError, + dietStateError, + dietImageError, + const DeepCollectionEquality().hash(_dailyScheduleErrors), durationIndex, - dietStateIndex); + dietStateIndex, + published, + drafted); /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -261,8 +423,16 @@ abstract class _CreateDietState implements CreateDietState { required final String? errorMessage, required final CreateDietModel dietData, final List>? copiedDay, + final String? dietNameError, + final String? dietDescriptionError, + final String? dietDurationError, + final String? dietStateError, + final String? dietImageError, + required final List> dailyScheduleErrors, final int? durationIndex, - final int? dietStateIndex}) = _$CreateDietStateImpl; + final int? dietStateIndex, + final bool published, + final bool drafted}) = _$CreateDietStateImpl; @override bool get isLoading; @@ -273,9 +443,25 @@ abstract class _CreateDietState implements CreateDietState { @override List>? get copiedDay; @override + String? get dietNameError; + @override + String? get dietDescriptionError; + @override + String? get dietDurationError; + @override + String? get dietStateError; + @override + String? get dietImageError; + @override + List> get dailyScheduleErrors; + @override int? get durationIndex; @override int? get dietStateIndex; + @override + bool get published; + @override + bool get drafted; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 2da26a46..4680f763 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -297,7 +297,7 @@ class _DietPreviewScreenState extends State { ), ), SizedBox(height: 12), - DietInfoRow(diet: widget.diet), + InfoRow(diet: widget.diet), ExpandableText( text: widget.diet.details, ), @@ -306,6 +306,7 @@ class _DietPreviewScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: AuthorCard( author: widget.diet.author, + title: s.diet_author, ), ), SizedBox(height: 16), diff --git a/lib/features/create_diet/presentation/widgets/day_widget.dart b/lib/features/create_diet/presentation/widgets/day_widget.dart index 732d6a7a..ba97708d 100644 --- a/lib/features/create_diet/presentation/widgets/day_widget.dart +++ b/lib/features/create_diet/presentation/widgets/day_widget.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/add_meal_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -34,6 +35,7 @@ class _DayWidgetState extends State { return BlocBuilder( builder: (context, state) { return MainBackgroundBouble( + hasError: state.dailyScheduleErrors[widget.index].where((element) => element.isNotEmpty).isNotEmpty, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -214,6 +216,10 @@ class _DayWidgetState extends State { ), ], ), + if (state.dailyScheduleErrors[widget.index][_selectedMealIndex].isNotEmpty) ...[ + SizedBox(height: 12), + CustomErrorWidget(error: state.dailyScheduleErrors[widget.index][_selectedMealIndex]), + ], SizedBox(height: 20), ], ), diff --git a/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart b/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart index fda76a4f..97987b30 100644 --- a/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart +++ b/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -36,28 +37,40 @@ class _DietDurationDropDownWidgetState extends State final s = S.of(context); return BlocBuilder( builder: (context, state) { - return CustomDropDownButton( - hint: Text( - s.choose_diet_duration, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - onChanged: (value) { - if (value != null) { - context.read().setDuration(durationDropDownTitles.indexOf(value)); - } - }, - items: durationDropDownTitles - .mapIndexed( - (index, value) => DropdownMenuItem( - child: Text( - durationDropDownTitles[index], - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - value: durationDropDownTitles[index], - ), - ) - .toList(), - value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomDropDownButton( + hasError: state.dietDurationError != null, + hint: Text( + s.choose_diet_duration, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDuration(durationDropDownTitles.indexOf(value)); + } + }, + items: durationDropDownTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + durationDropDownTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: durationDropDownTitles[index], + ), + ) + .toList(), + value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + ), + if (state.dietDurationError != null) ...[ + SizedBox( + height: 4, + ), + CustomErrorWidget(error: state.dietDurationError ?? ''), + ], + ], ); }, ); diff --git a/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart index 42ba88a2..0e195a09 100644 --- a/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart +++ b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -70,74 +71,79 @@ class _DietImagePickerWidgetState extends State { final s = S.of(context); return BlocBuilder( builder: (context, state) { - return GestureDetector( - onTap: () { - _showImagePickerOptions(s, context.read()); - }, - child: Container( - height: 240, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - image: state.dietData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.dietData.image?.path ?? ''))) : null, - color: appTheme.gray50, + return Column( + children: [ + GestureDetector( + onTap: () { + _showImagePickerOptions(s, context.read()); + }, + child: Container( + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: state.dietData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.dietData.image?.path ?? ''))) : null, + color: appTheme.gray50, + ), + width: MediaQuery.sizeOf(context).width, + padding: state.dietData.image != null ? null : EdgeInsets.all(8), + child: state.dietData.image != null + ? Padding( + padding: const EdgeInsets.only(top: 6, right: 6), + child: GestureDetector( + onTap: () { + context.read().setImage(null); + }, + child: Align( + alignment: Alignment.topRight, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appTheme.dividerColor, + shape: BoxShape.circle, + ), + child: Icon( + Icons.close_rounded, + color: Colors.grey, + size: 16, + ), + ), + ), + ), + ) + : Stack( + children: [ + DottedBorder( + stackFit: StackFit.expand, + dashPattern: [14, 14], + color: state.dietImageError != null ? appTheme.red : appTheme.dottedBorder, + borderType: BorderType.RRect, + radius: Radius.circular(12), + child: SizedBox.expand(), + ), + Center( + child: Column( + children: [ + SizedBox( + height: 48, + ), + SvgPicture.asset('assets/icons/eatvio_logo.svg'), + SizedBox( + height: 24, + ), + Text( + s.add_cover, + style: TextStyles.textMMedium.copyWith(color: appTheme.gray), + ), + ], + ), + ), + ], + ), + ), ), - width: MediaQuery.sizeOf(context).width, - padding: state.dietData.image != null ? null : EdgeInsets.all(8), - child: state.dietData.image != null - ? Padding( - padding: const EdgeInsets.only(top: 6, right: 6), - child: GestureDetector( - onTap: () { - context.read().setImage(null); - }, - child: Align( - alignment: Alignment.topRight, - child: Container( - width: 34, - height: 34, - decoration: BoxDecoration( - color: appTheme.dividerColor, - shape: BoxShape.circle, - ), - child: Icon( - Icons.close_rounded, - color: Colors.grey, - size: 16, - ), - ), - ), - ), - ) - : Stack( - children: [ - DottedBorder( - stackFit: StackFit.expand, - dashPattern: [14, 14], - color: appTheme.dottedBorder, - borderType: BorderType.RRect, - radius: Radius.circular(12), - child: SizedBox.expand(), - ), - Center( - child: Column( - children: [ - SizedBox( - height: 48, - ), - SvgPicture.asset('assets/icons/eatvio_logo.svg'), - SizedBox( - height: 24, - ), - Text( - s.add_cover, - style: TextStyles.textMMedium.copyWith(color: appTheme.gray), - ), - ], - ), - ), - ], - ), - ), + if (state.dietImageError != null) ...[SizedBox(height: 8), CustomErrorWidget(error: state.dietImageError ?? '')], + ], ); }, ); diff --git a/lib/features/create_diet/presentation/widgets/error_widget.dart b/lib/features/create_diet/presentation/widgets/error_widget.dart new file mode 100644 index 00000000..bd9eaf9d --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/error_widget.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CustomErrorWidget extends StatelessWidget { + const CustomErrorWidget({super.key, required this.error}); + final String error; + @override + Widget build(BuildContext context) { + return Row( + children: [ + SvgPicture.asset('assets/icons/error_icon.svg', width: 14, height: 14), + SizedBox( + width: 2, + ), + Expanded( + child: Text( + error, + style: TextStyles.textSMedium.copyWith(color: appTheme.red), + ), + ), + ], + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart index d2db75ee..47443b58 100644 --- a/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart +++ b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -33,28 +34,40 @@ class _StatusDropDownWidgetState extends State { final s = S.of(context); return BlocBuilder( builder: (context, state) { - return CustomDropDownButton( - hint: Text( - s.diet_state, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - onChanged: (value) { - if (value != null) { - context.read().setDietState(dietStateTitles.indexOf(value)); - } - }, - items: dietStateTitles - .mapIndexed( - (index, value) => DropdownMenuItem( - child: Text( - dietStateTitles[index], - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - value: dietStateTitles[index], - ), - ) - .toList(), - value: state.dietStateIndex == null ? null : dietStateTitles[state.dietStateIndex ?? 0], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomDropDownButton( + hasError: state.dietStateError != null, + hint: Text( + s.diet_state, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDietState(dietStateTitles.indexOf(value)); + } + }, + items: dietStateTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + dietStateTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: dietStateTitles[index], + ), + ) + .toList(), + value: state.dietStateIndex == null ? null : dietStateTitles[state.dietStateIndex ?? 0], + ), + if (state.dietStateError != null) ...[ + SizedBox( + height: 4, + ), + CustomErrorWidget(error: state.dietStateError ?? ''), + ], + ], ); }, ); diff --git a/lib/features/create_diet/repositories/create_diet_repository.dart b/lib/features/create_diet/repositories/create_diet_repository.dart index 7d5a99db..1dbed20d 100644 --- a/lib/features/create_diet/repositories/create_diet_repository.dart +++ b/lib/features/create_diet/repositories/create_diet_repository.dart @@ -7,11 +7,14 @@ class CreateDietRepository { final CreateDietService createDietService; CreateDietRepository({required this.createDietService}); - Future createDiet({ - String? name, - int? duration, - String? state, - String? type, + Future createDiet({ + String? name, + int? duration, + String? state, + String? meatType, + String? carbType, + String? vegType, + String? lightType, String? details, File? image, }) { @@ -19,7 +22,10 @@ class CreateDietRepository { name: name, duration: duration, state: state, - type: type, + meatType: meatType, + carbType: carbType, + vegType: vegType, + lightType: lightType, details: details, image: image, ); @@ -29,9 +35,13 @@ class CreateDietRepository { required int id, required Map model, }) { - return createDietService.createDietDailySchedule( - id: id, - model: model, - ); + try { + return createDietService.createDietDailySchedule( + id: id, + model: model, + ); + } catch (e) { + rethrow; + } } } diff --git a/lib/features/create_diet/services/create_diet_service.dart b/lib/features/create_diet/services/create_diet_service.dart index ab48669a..07432e3a 100644 --- a/lib/features/create_diet/services/create_diet_service.dart +++ b/lib/features/create_diet/services/create_diet_service.dart @@ -12,11 +12,14 @@ abstract class CreateDietService { @POST('/api/diets/create') @MultiPart() - Future createDiet({ + Future createDiet({ @Part(name: 'name') String? name, @Part(name: 'duration') int? duration, @Part(name: 'state') String? state, - @Part(name: 'type') String? type, + @Part(name: 'type[meat]') String? meatType, + @Part(name: 'type[carb]') String? carbType, + @Part(name: 'type[veg]') String? vegType, + @Part(name: 'type[light]') String? lightType, @Part(name: 'details') String? details, @Part(name: 'image') File? image, }); diff --git a/lib/features/create_diet/services/create_diet_service.g.dart b/lib/features/create_diet/services/create_diet_service.g.dart index 23d1c97c..0c5a4606 100644 --- a/lib/features/create_diet/services/create_diet_service.g.dart +++ b/lib/features/create_diet/services/create_diet_service.g.dart @@ -22,11 +22,14 @@ class _CreateDietService implements CreateDietService { final ParseErrorLogger? errorLogger; @override - Future createDiet({ + Future createDiet({ String? name, int? duration, String? state, - String? type, + String? meatType, + String? carbType, + String? vegType, + String? lightType, String? details, File? image, }) async { @@ -53,10 +56,28 @@ class _CreateDietService implements CreateDietService { state, )); } - if (type != null) { + if (meatType != null) { _data.fields.add(MapEntry( - 'type', - type, + 'type[meat]', + meatType, + )); + } + if (carbType != null) { + _data.fields.add(MapEntry( + 'type[carb]', + carbType, + )); + } + if (vegType != null) { + _data.fields.add(MapEntry( + 'type[veg]', + vegType, + )); + } + if (lightType != null) { + _data.fields.add(MapEntry( + 'type[light]', + lightType, )); } if (details != null) { @@ -76,7 +97,7 @@ class _CreateDietService implements CreateDietService { )); } } - final _options = _setStreamType(Options( + final _options = _setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -94,9 +115,9 @@ class _CreateDietService implements CreateDietService { baseUrl, ))); final _result = await _dio.fetch>(_options); - late Diet _value; + late CreatedDiet _value; try { - _value = Diet.fromJson(_result.data!); + _value = CreatedDiet.fromJson(_result.data!); } on Object catch (e, s) { errorLogger?.logError(e, s, _options); rethrow; diff --git a/lib/features/create_plan/presentation/widgets/options_count_button.dart b/lib/features/create_plan/presentation/widgets/options_count_button.dart index 32e9d4a7..fddba602 100644 --- a/lib/features/create_plan/presentation/widgets/options_count_button.dart +++ b/lib/features/create_plan/presentation/widgets/options_count_button.dart @@ -1,5 +1,5 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -25,7 +25,7 @@ class OptionsCountButton extends StatelessWidget { child: Row( children: [ Expanded( - child: _AnimatedOptionButton( + child: AnimatedOptionButton( isSelected: selectedOption == null, text: s.one_option, onTap: () { @@ -34,7 +34,7 @@ class OptionsCountButton extends StatelessWidget { ), ), Expanded( - child: _AnimatedOptionButton( + child: AnimatedOptionButton( isSelected: selectedOption != null, text: s.three_options, onTap: () { @@ -50,33 +50,3 @@ class OptionsCountButton extends StatelessWidget { } } -class _AnimatedOptionButton extends StatelessWidget { - const _AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap}); - final String text; - final bool isSelected; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: isSelected ? appTheme.blueA400 : appTheme.whiteA700, - ), - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 300), - style: TextStyles.textMSemibold.copyWith( - color: isSelected ? appTheme.whiteA700 : appTheme.black, - ), - child: Text(text), - ), - ), - ), - ); - } -} diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 9e667a03..4f5c7f31 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; @@ -155,20 +156,25 @@ class DiaryCubit extends Cubit { await fetchDiary(); } - Future sendProductToModeration({ + Future sendProductToModeration({ required String name, required double energy, required double proteins, required double fats, required double carbohydrates, }) async { - return await diaryRepository.sendProductToModeration( - name: name, - energy: energy, - proteins: proteins, - fats: fats, - carbohydrates: carbohydrates, - ); + try { + return await diaryRepository.sendProductToModeration( + name: name, + energy: energy, + proteins: proteins, + fats: fats, + carbohydrates: carbohydrates, + ); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + } + return null; } fillTime({ diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart index e069b3d9..09fd00cb 100644 --- a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -271,7 +271,7 @@ class _GlossaryModerationRequestModalWidgetState extends State { ), ), SizedBox(height: 12), - DietInfoRow(diet: widget.diet), + InfoRow(diet: widget.diet), ExpandableText( text: widget.diet.details, ), @@ -404,6 +404,7 @@ class _AboutDietScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: AuthorCard( author: widget.diet.author, + title: s.diet_author, ), ), SizedBox(height: 16), diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index b3e0ea92..d35557a1 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -8,11 +7,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; class AuthorCard extends StatelessWidget { - const AuthorCard({super.key, required this.author}); + const AuthorCard({super.key, required this.author, required this.title}); final Author author; + final String title; @override Widget build(BuildContext context) { - final s = S.of(context); + // final s = S.of(context); return Container( width: MediaQuery.sizeOf(context).width, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.cardBackground), @@ -42,7 +42,7 @@ class AuthorCard extends StatelessWidget { ), ), title: Text( - s.diet_author, + title, style: TextStyles.textSSemibold.copyWith( color: appTheme.gray, ), @@ -57,18 +57,19 @@ class AuthorCard extends StatelessWidget { ), ), SizedBox(width: 10), - InfoBouble( - radius: 12, - backgroundColor: appTheme.whiteA700, - children: [ - Text( - '128-й в рейтинге', - style: TextStyles.textMMediumAuto.copyWith( - color: appTheme.blueA400, + if (author.authorPlaceInRating != null) + InfoBouble( + radius: 12, + backgroundColor: appTheme.whiteA700, + children: [ + Text( + '${author.authorPlaceInRating}-й в рейтинге', + style: TextStyles.textMMediumAuto.copyWith( + color: appTheme.blueA400, + ), ), - ), - ], - ), + ], + ), ], ), ], diff --git a/lib/features/diets/presentation/widgets/type_badges.dart b/lib/features/diets/presentation/widgets/type_badges.dart index 2cf526c8..3d227464 100644 --- a/lib/features/diets/presentation/widgets/type_badges.dart +++ b/lib/features/diets/presentation/widgets/type_badges.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -8,9 +9,10 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/svg.dart'; class TypeBadges extends StatefulWidget { - const TypeBadges({super.key, required this.type, required this.disableDescriptionOfTypes}); + const TypeBadges({super.key, this.type, required this.disableDescriptionOfTypes, this.planType}); - final DietType type; + final DietType? type; + final PlanType? planType; final bool disableDescriptionOfTypes; @override State createState() => _TypeBadgesState(); @@ -18,12 +20,11 @@ class TypeBadges extends StatefulWidget { class _TypeBadgesState extends State { final List overlaysVisibilityState = [false, false, false, false]; + final List<(String, Widget)> badgeSource = []; - @override - Widget build(BuildContext context) { - final s = S.of(context); - final List<(String, Widget)> badgeSource = []; - if (widget.type.light == true) { + void fillBadges(S s) { + badgeSource.clear(); + if (widget.type?.light == true) { badgeSource.add( ( 'assets/icons/cocktail.svg', @@ -36,7 +37,7 @@ class _TypeBadgesState extends State { ), ); } - if (widget.type.veg == true) { + if (widget.type?.veg == true) { badgeSource.add( ( 'assets/icons/broccoli.svg', @@ -49,7 +50,7 @@ class _TypeBadgesState extends State { ), ); } - if (widget.type.carb == true) { + if (widget.type?.carb == true) { badgeSource.add( ( 'assets/icons/bread.svg', @@ -62,7 +63,7 @@ class _TypeBadgesState extends State { ), ); } - if (widget.type.meat == true) { + if (widget.type?.meat == true) { badgeSource.add( ( 'assets/icons/meat2.svg', @@ -75,6 +76,66 @@ class _TypeBadgesState extends State { ), ); } + if (widget.planType?.slim == true) { + badgeSource.add( + ( + 'assets/icons/slim.png', + _InfoOverlayEntry( + onClose: closeOverlay, + assetPath: 'assets/icons/slim.png', + title: s.slim_type, + description: s.slim_type_desc, + ) + ), + ); + } + if (widget.planType?.fit == true) { + badgeSource.add( + ( + 'assets/icons/fitness.png', + _InfoOverlayEntry( + onClose: closeOverlay, + assetPath: 'assets/icons/fitness.png', + title: s.fit_type, + description: s.fit_type_desc, + ) + ), + ); + } + if (widget.planType?.wellness == true) { + badgeSource.add( + ( + 'assets/icons/yoga-pose.png', + _InfoOverlayEntry( + onClose: closeOverlay, + assetPath: 'assets/icons/yoga-pose.png', + title: s.wellness_type, + description: s.wellness_type_desc, + ) + ), + ); + } + if (widget.planType?.behavior == true) { + badgeSource.add( + ( + 'assets/icons/diet.png', + _InfoOverlayEntry( + onClose: closeOverlay, + assetPath: 'assets/icons/diet.png', + title: s.eating_behavior_type, + description: s.eating_behavior_type_desc, + ) + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + + fillBadges(s); + return Row( children: List.generate( badgeSource.length, @@ -107,11 +168,13 @@ class _TypeBadgesState extends State { }, child: Padding( padding: EdgeInsets.only(right: index != badgeSource.length - 1 ? 8.0 : 0), - child: SvgPicture.asset( - badgeSource[index].$1, - width: 16, - height: 16, - ), + child: badgeSource[index].$1.endsWith('.png') + ? Image.asset(badgeSource[index].$1, width: 20, height: 20) + : SvgPicture.asset( + badgeSource[index].$1, + width: 16, + height: 16, + ), ), ), ), @@ -150,11 +213,13 @@ class _InfoOverlayEntry extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - assetPath, - width: 20, - height: 20, - ), + assetPath.endsWith('.png') + ? Image.asset(assetPath, width: 20, height: 20) + : SvgPicture.asset( + assetPath, + width: 20, + height: 20, + ), SizedBox(width: 8), Expanded( child: Column( @@ -175,7 +240,6 @@ class _InfoOverlayEntry extends StatelessWidget { style: TextStyles.textXSSemibold.copyWith( decoration: TextDecoration.none, color: appTheme.black, - ), ), ], diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index 7518573d..ba9fecf7 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -39,6 +39,23 @@ class Diet with _$Diet { factory Diet.fromJson(Map json) => _$DietFromJson(json); } +@freezed +class CreatedDiet with _$CreatedDiet { + const factory CreatedDiet({ + @JsonKey(name: "id") required int id, + }) = _CreatedDiet; + + factory CreatedDiet.fromJson(Map json) => _$CreatedDietFromJson(json); +} + +int parseDuration(dynamic duration) { + if (duration is String) { + return int.parse(duration); + }else { + return duration; + } +} + @freezed class Author with _$Author { const factory Author({ @@ -46,7 +63,8 @@ class Author with _$Author { @JsonKey(name: "author_name") required String authorName, @JsonKey(name: "author_profile_icon") required String authorProfileIcon, @JsonKey(name: "author_is_me") required bool authorIsMe, - @JsonKey(name: "is_entity_subscribed_by_me") required bool isEntitySubscribedByMe, + @JsonKey(name: "is_entity_subscribed_by_me") bool? isEntitySubscribedByMe, + @JsonKey(name: "author_place_in_rating") int? authorPlaceInRating, }) = _Author; factory Author.fromJson(Map json) => _$AuthorFromJson(json); @@ -60,7 +78,7 @@ class DailySchedule with _$DailySchedule { @JsonKey(name: "portion") required Portion portion, @JsonKey(name: "quantity") required int quantity, @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, - @JsonKey(name: 'product_nutritional_value_id', includeFromJson: false, includeIfNull: false) String? productNutritionalValueId, + @JsonKey(name: 'product_nutritional_value_id', includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId, }) = _DailySchedule; factory DailySchedule.fromJson(Map json) => _$DailyScheduleFromJson(json); @@ -79,6 +97,7 @@ final portionValues = EnumValues({"grams": Portion.GRAMS, "milliliters": Portion @freezed class DietType with _$DietType { + const DietType._(); const factory DietType({ @JsonKey(name: "light") bool? light, @JsonKey(name: "veg") bool? veg, @@ -87,6 +106,10 @@ class DietType with _$DietType { }) = _DietType; factory DietType.fromJson(Map json) => _$DietTypeFromJson(json); + + bool hasAtLeastOneType() { + return light ?? false || (veg ?? false) || (carb ?? false) || (meat ?? false); + } } class EnumValues { diff --git a/lib/features/feed/models/diets_pagination_model.freezed.dart b/lib/features/feed/models/diets_pagination_model.freezed.dart index d30a57cc..37c410bc 100644 --- a/lib/features/feed/models/diets_pagination_model.freezed.dart +++ b/lib/features/feed/models/diets_pagination_model.freezed.dart @@ -798,6 +798,159 @@ abstract class _Diet implements Diet { throw _privateConstructorUsedError; } +CreatedDiet _$CreatedDietFromJson(Map json) { + return _CreatedDiet.fromJson(json); +} + +/// @nodoc +mixin _$CreatedDiet { + @JsonKey(name: "id") + int get id => throw _privateConstructorUsedError; + + /// Serializes this CreatedDiet to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CreatedDiet + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreatedDietCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreatedDietCopyWith<$Res> { + factory $CreatedDietCopyWith( + CreatedDiet value, $Res Function(CreatedDiet) then) = + _$CreatedDietCopyWithImpl<$Res, CreatedDiet>; + @useResult + $Res call({@JsonKey(name: "id") int id}); +} + +/// @nodoc +class _$CreatedDietCopyWithImpl<$Res, $Val extends CreatedDiet> + implements $CreatedDietCopyWith<$Res> { + _$CreatedDietCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreatedDiet + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CreatedDietImplCopyWith<$Res> + implements $CreatedDietCopyWith<$Res> { + factory _$$CreatedDietImplCopyWith( + _$CreatedDietImpl value, $Res Function(_$CreatedDietImpl) then) = + __$$CreatedDietImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({@JsonKey(name: "id") int id}); +} + +/// @nodoc +class __$$CreatedDietImplCopyWithImpl<$Res> + extends _$CreatedDietCopyWithImpl<$Res, _$CreatedDietImpl> + implements _$$CreatedDietImplCopyWith<$Res> { + __$$CreatedDietImplCopyWithImpl( + _$CreatedDietImpl _value, $Res Function(_$CreatedDietImpl) _then) + : super(_value, _then); + + /// Create a copy of CreatedDiet + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + }) { + return _then(_$CreatedDietImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreatedDietImpl implements _CreatedDiet { + const _$CreatedDietImpl({@JsonKey(name: "id") required this.id}); + + factory _$CreatedDietImpl.fromJson(Map json) => + _$$CreatedDietImplFromJson(json); + + @override + @JsonKey(name: "id") + final int id; + + @override + String toString() { + return 'CreatedDiet(id: $id)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreatedDietImpl && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + /// Create a copy of CreatedDiet + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreatedDietImplCopyWith<_$CreatedDietImpl> get copyWith => + __$$CreatedDietImplCopyWithImpl<_$CreatedDietImpl>(this, _$identity); + + @override + Map toJson() { + return _$$CreatedDietImplToJson( + this, + ); + } +} + +abstract class _CreatedDiet implements CreatedDiet { + const factory _CreatedDiet({@JsonKey(name: "id") required final int id}) = + _$CreatedDietImpl; + + factory _CreatedDiet.fromJson(Map json) = + _$CreatedDietImpl.fromJson; + + @override + @JsonKey(name: "id") + int get id; + + /// Create a copy of CreatedDiet + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreatedDietImplCopyWith<_$CreatedDietImpl> get copyWith => + throw _privateConstructorUsedError; +} + Author _$AuthorFromJson(Map json) { return _Author.fromJson(json); } @@ -813,7 +966,9 @@ mixin _$Author { @JsonKey(name: "author_is_me") bool get authorIsMe => throw _privateConstructorUsedError; @JsonKey(name: "is_entity_subscribed_by_me") - bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; + bool? get isEntitySubscribedByMe => throw _privateConstructorUsedError; + @JsonKey(name: "author_place_in_rating") + int? get authorPlaceInRating => throw _privateConstructorUsedError; /// Serializes this Author to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -834,8 +989,8 @@ abstract class $AuthorCopyWith<$Res> { @JsonKey(name: "author_name") String authorName, @JsonKey(name: "author_profile_icon") String authorProfileIcon, @JsonKey(name: "author_is_me") bool authorIsMe, - @JsonKey(name: "is_entity_subscribed_by_me") - bool isEntitySubscribedByMe}); + @JsonKey(name: "is_entity_subscribed_by_me") bool? isEntitySubscribedByMe, + @JsonKey(name: "author_place_in_rating") int? authorPlaceInRating}); } /// @nodoc @@ -857,7 +1012,8 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author> Object? authorName = null, Object? authorProfileIcon = null, Object? authorIsMe = null, - Object? isEntitySubscribedByMe = null, + Object? isEntitySubscribedByMe = freezed, + Object? authorPlaceInRating = freezed, }) { return _then(_value.copyWith( authorSlug: null == authorSlug @@ -876,10 +1032,14 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author> ? _value.authorIsMe : authorIsMe // ignore: cast_nullable_to_non_nullable as bool, - isEntitySubscribedByMe: null == isEntitySubscribedByMe + isEntitySubscribedByMe: freezed == isEntitySubscribedByMe ? _value.isEntitySubscribedByMe : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, + authorPlaceInRating: freezed == authorPlaceInRating + ? _value.authorPlaceInRating + : authorPlaceInRating // ignore: cast_nullable_to_non_nullable + as int?, ) as $Val); } } @@ -896,8 +1056,8 @@ abstract class _$$AuthorImplCopyWith<$Res> implements $AuthorCopyWith<$Res> { @JsonKey(name: "author_name") String authorName, @JsonKey(name: "author_profile_icon") String authorProfileIcon, @JsonKey(name: "author_is_me") bool authorIsMe, - @JsonKey(name: "is_entity_subscribed_by_me") - bool isEntitySubscribedByMe}); + @JsonKey(name: "is_entity_subscribed_by_me") bool? isEntitySubscribedByMe, + @JsonKey(name: "author_place_in_rating") int? authorPlaceInRating}); } /// @nodoc @@ -917,7 +1077,8 @@ class __$$AuthorImplCopyWithImpl<$Res> Object? authorName = null, Object? authorProfileIcon = null, Object? authorIsMe = null, - Object? isEntitySubscribedByMe = null, + Object? isEntitySubscribedByMe = freezed, + Object? authorPlaceInRating = freezed, }) { return _then(_$AuthorImpl( authorSlug: null == authorSlug @@ -936,10 +1097,14 @@ class __$$AuthorImplCopyWithImpl<$Res> ? _value.authorIsMe : authorIsMe // ignore: cast_nullable_to_non_nullable as bool, - isEntitySubscribedByMe: null == isEntitySubscribedByMe + isEntitySubscribedByMe: freezed == isEntitySubscribedByMe ? _value.isEntitySubscribedByMe : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, + authorPlaceInRating: freezed == authorPlaceInRating + ? _value.authorPlaceInRating + : authorPlaceInRating // ignore: cast_nullable_to_non_nullable + as int?, )); } } @@ -952,8 +1117,8 @@ class _$AuthorImpl implements _Author { @JsonKey(name: "author_name") required this.authorName, @JsonKey(name: "author_profile_icon") required this.authorProfileIcon, @JsonKey(name: "author_is_me") required this.authorIsMe, - @JsonKey(name: "is_entity_subscribed_by_me") - required this.isEntitySubscribedByMe}); + @JsonKey(name: "is_entity_subscribed_by_me") this.isEntitySubscribedByMe, + @JsonKey(name: "author_place_in_rating") this.authorPlaceInRating}); factory _$AuthorImpl.fromJson(Map json) => _$$AuthorImplFromJson(json); @@ -972,11 +1137,14 @@ class _$AuthorImpl implements _Author { final bool authorIsMe; @override @JsonKey(name: "is_entity_subscribed_by_me") - final bool isEntitySubscribedByMe; + final bool? isEntitySubscribedByMe; + @override + @JsonKey(name: "author_place_in_rating") + final int? authorPlaceInRating; @override String toString() { - return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe, isEntitySubscribedByMe: $isEntitySubscribedByMe)'; + return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe, isEntitySubscribedByMe: $isEntitySubscribedByMe, authorPlaceInRating: $authorPlaceInRating)'; } @override @@ -993,13 +1161,21 @@ class _$AuthorImpl implements _Author { (identical(other.authorIsMe, authorIsMe) || other.authorIsMe == authorIsMe) && (identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) || - other.isEntitySubscribedByMe == isEntitySubscribedByMe)); + other.isEntitySubscribedByMe == isEntitySubscribedByMe) && + (identical(other.authorPlaceInRating, authorPlaceInRating) || + other.authorPlaceInRating == authorPlaceInRating)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, authorSlug, authorName, - authorProfileIcon, authorIsMe, isEntitySubscribedByMe); + int get hashCode => Object.hash( + runtimeType, + authorSlug, + authorName, + authorProfileIcon, + authorIsMe, + isEntitySubscribedByMe, + authorPlaceInRating); /// Create a copy of Author /// with the given fields replaced by the non-null parameter values. @@ -1025,7 +1201,9 @@ abstract class _Author implements Author { required final String authorProfileIcon, @JsonKey(name: "author_is_me") required final bool authorIsMe, @JsonKey(name: "is_entity_subscribed_by_me") - required final bool isEntitySubscribedByMe}) = _$AuthorImpl; + final bool? isEntitySubscribedByMe, + @JsonKey(name: "author_place_in_rating") + final int? authorPlaceInRating}) = _$AuthorImpl; factory _Author.fromJson(Map json) = _$AuthorImpl.fromJson; @@ -1043,7 +1221,10 @@ abstract class _Author implements Author { bool get authorIsMe; @override @JsonKey(name: "is_entity_subscribed_by_me") - bool get isEntitySubscribedByMe; + bool? get isEntitySubscribedByMe; + @override + @JsonKey(name: "author_place_in_rating") + int? get authorPlaceInRating; /// Create a copy of Author /// with the given fields replaced by the non-null parameter values. @@ -1071,6 +1252,7 @@ mixin _$DailySchedule { DailyNutrition? get dailyNutrition => throw _privateConstructorUsedError; @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) String? get productNutritionalValueId => throw _privateConstructorUsedError; @@ -1100,6 +1282,7 @@ abstract class $DailyScheduleCopyWith<$Res> { DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId}); @@ -1189,6 +1372,7 @@ abstract class _$$DailyScheduleImplCopyWith<$Res> DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId}); @@ -1258,6 +1442,7 @@ class _$DailyScheduleImpl implements _DailySchedule { this.dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) this.productNutritionalValueId}); @@ -1283,6 +1468,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @override @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) final String? productNutritionalValueId; @@ -1340,6 +1526,7 @@ abstract class _DailySchedule implements DailySchedule { final DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) final String? productNutritionalValueId}) = _$DailyScheduleImpl; @@ -1365,6 +1552,7 @@ abstract class _DailySchedule implements DailySchedule { @override @JsonKey( name: 'product_nutritional_value_id', + includeToJson: true, includeFromJson: false, includeIfNull: false) String? get productNutritionalValueId; @@ -1511,12 +1699,13 @@ class __$$DietTypeImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$DietTypeImpl implements _DietType { +class _$DietTypeImpl extends _DietType { const _$DietTypeImpl( {@JsonKey(name: "light") this.light, @JsonKey(name: "veg") this.veg, @JsonKey(name: "carb") this.carb, - @JsonKey(name: "meat") this.meat}); + @JsonKey(name: "meat") this.meat}) + : super._(); factory _$DietTypeImpl.fromJson(Map json) => _$$DietTypeImplFromJson(json); @@ -1570,12 +1759,13 @@ class _$DietTypeImpl implements _DietType { } } -abstract class _DietType implements DietType { +abstract class _DietType extends DietType { const factory _DietType( {@JsonKey(name: "light") final bool? light, @JsonKey(name: "veg") final bool? veg, @JsonKey(name: "carb") final bool? carb, @JsonKey(name: "meat") final bool? meat}) = _$DietTypeImpl; + const _DietType._() : super._(); factory _DietType.fromJson(Map json) = _$DietTypeImpl.fromJson; diff --git a/lib/features/feed/models/diets_pagination_model.g.dart b/lib/features/feed/models/diets_pagination_model.g.dart index 776b52e5..7a54baa0 100644 --- a/lib/features/feed/models/diets_pagination_model.g.dart +++ b/lib/features/feed/models/diets_pagination_model.g.dart @@ -69,12 +69,23 @@ Map _$$DietImplToJson(_$DietImpl instance) => 'daily_nutrition': instance.dailyNutrition, }; +_$CreatedDietImpl _$$CreatedDietImplFromJson(Map json) => + _$CreatedDietImpl( + id: (json['id'] as num).toInt(), + ); + +Map _$$CreatedDietImplToJson(_$CreatedDietImpl instance) => + { + 'id': instance.id, + }; + _$AuthorImpl _$$AuthorImplFromJson(Map json) => _$AuthorImpl( authorSlug: json['author_slug'] as String, authorName: json['author_name'] as String, authorProfileIcon: json['author_profile_icon'] as String, authorIsMe: json['author_is_me'] as bool, - isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool, + isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool?, + authorPlaceInRating: (json['author_place_in_rating'] as num?)?.toInt(), ); Map _$$AuthorImplToJson(_$AuthorImpl instance) => @@ -84,6 +95,7 @@ Map _$$AuthorImplToJson(_$AuthorImpl instance) => 'author_profile_icon': instance.authorProfileIcon, 'author_is_me': instance.authorIsMe, 'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe, + 'author_place_in_rating': instance.authorPlaceInRating, }; _$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => @@ -99,6 +111,8 @@ Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => 'name': instance.name, 'portion': _$PortionEnumMap[instance.portion]!, 'quantity': instance.quantity, + if (instance.productNutritionalValueId case final value?) + 'product_nutritional_value_id': value, }; const _$PortionEnumMap = { diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 7abb842e..13d57c32 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -21,6 +21,7 @@ class PlanPaginationModel with _$PlanPaginationModel { class PlanModel with _$PlanModel { const factory PlanModel({ @JsonKey(name: "id") required int id, + @JsonKey(name: "slug") required String slug, @JsonKey(name: "name") required String name, @JsonKey(name: "image") required String image, @JsonKey(name: "duration") required int duration, @@ -28,13 +29,14 @@ class PlanModel with _$PlanModel { @JsonKey(name: "type") required PlanType type, @JsonKey(name: "details") required String details, @JsonKey(name: "packets_count") required int packetsCount, - @JsonKey(name: "packet1_options") dynamic packet1Options, - @JsonKey(name: "packet2_options") dynamic packet2Options, - @JsonKey(name: "packet3_options") dynamic packet3Options, + @JsonKey(name: "packet1_options") PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") PacketOptions? packet3Options, @JsonKey(name: "author") required Author author, @JsonKey(name: "total_likes") required int totalLikes, @JsonKey(name: "total_views") required int totalViews, - @JsonKey(name: "total_comments") required int totalComments, + @JsonKey(name: "total_comments") required int totalComments, + @JsonKey(name: "is_favorite") required bool isFavorite, }) = _PlanModel; factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); @@ -56,12 +58,17 @@ class PacketOptions with _$PacketOptions { @freezed class PlanType with _$PlanType { + const PlanType._(); const factory PlanType({ - @JsonKey(name: "fit") required bool fit, - @JsonKey(name: "slim") required bool slim, - @JsonKey(name: "behavior") required bool behavior, - @JsonKey(name: "wellness") required bool wellness, + @JsonKey(name: "fit") required bool? fit, + @JsonKey(name: "slim") required bool? slim, + @JsonKey(name: "behavior") required bool? behavior, + @JsonKey(name: "wellness") required bool? wellness, }) = _PlanType; factory PlanType.fromJson(Map json) => _$PlanTypeFromJson(json); + + bool hasAtLeastOneType() { + return (fit ?? false) || (slim ?? false) || (behavior ?? false) || (wellness ?? false); + } } diff --git a/lib/features/feed/models/plan_pagination_model.freezed.dart b/lib/features/feed/models/plan_pagination_model.freezed.dart index caa2ef10..a3348859 100644 --- a/lib/features/feed/models/plan_pagination_model.freezed.dart +++ b/lib/features/feed/models/plan_pagination_model.freezed.dart @@ -262,6 +262,8 @@ PlanModel _$PlanModelFromJson(Map json) { mixin _$PlanModel { @JsonKey(name: "id") int get id => throw _privateConstructorUsedError; + @JsonKey(name: "slug") + String get slug => throw _privateConstructorUsedError; @JsonKey(name: "name") String get name => throw _privateConstructorUsedError; @JsonKey(name: "image") @@ -277,11 +279,11 @@ mixin _$PlanModel { @JsonKey(name: "packets_count") int get packetsCount => throw _privateConstructorUsedError; @JsonKey(name: "packet1_options") - dynamic get packet1Options => throw _privateConstructorUsedError; + PacketOptions? get packet1Options => throw _privateConstructorUsedError; @JsonKey(name: "packet2_options") - dynamic get packet2Options => throw _privateConstructorUsedError; + PacketOptions? get packet2Options => throw _privateConstructorUsedError; @JsonKey(name: "packet3_options") - dynamic get packet3Options => throw _privateConstructorUsedError; + PacketOptions? get packet3Options => throw _privateConstructorUsedError; @JsonKey(name: "author") Author get author => throw _privateConstructorUsedError; @JsonKey(name: "total_likes") @@ -290,6 +292,8 @@ mixin _$PlanModel { int get totalViews => throw _privateConstructorUsedError; @JsonKey(name: "total_comments") int get totalComments => throw _privateConstructorUsedError; + @JsonKey(name: "is_favorite") + bool get isFavorite => throw _privateConstructorUsedError; /// Serializes this PlanModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -308,6 +312,7 @@ abstract class $PlanModelCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: "id") int id, + @JsonKey(name: "slug") String slug, @JsonKey(name: "name") String name, @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @@ -315,15 +320,19 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "type") PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, - @JsonKey(name: "packet1_options") dynamic packet1Options, - @JsonKey(name: "packet2_options") dynamic packet2Options, - @JsonKey(name: "packet3_options") dynamic packet3Options, + @JsonKey(name: "packet1_options") PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") PacketOptions? packet3Options, @JsonKey(name: "author") Author author, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, - @JsonKey(name: "total_comments") int totalComments}); + @JsonKey(name: "total_comments") int totalComments, + @JsonKey(name: "is_favorite") bool isFavorite}); $PlanTypeCopyWith<$Res> get type; + $PacketOptionsCopyWith<$Res>? get packet1Options; + $PacketOptionsCopyWith<$Res>? get packet2Options; + $PacketOptionsCopyWith<$Res>? get packet3Options; $AuthorCopyWith<$Res> get author; } @@ -343,6 +352,7 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> @override $Res call({ Object? id = null, + Object? slug = null, Object? name = null, Object? image = null, Object? duration = null, @@ -357,12 +367,17 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> Object? totalLikes = null, Object? totalViews = null, Object? totalComments = null, + Object? isFavorite = null, }) { return _then(_value.copyWith( id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -394,15 +409,15 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> packet1Options: freezed == packet1Options ? _value.packet1Options : packet1Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, packet2Options: freezed == packet2Options ? _value.packet2Options : packet2Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, packet3Options: freezed == packet3Options ? _value.packet3Options : packet3Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, author: null == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -419,6 +434,10 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> ? _value.totalComments : totalComments // ignore: cast_nullable_to_non_nullable as int, + isFavorite: null == isFavorite + ? _value.isFavorite + : isFavorite // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -432,6 +451,48 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> }); } + /// Create a copy of PlanModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PacketOptionsCopyWith<$Res>? get packet1Options { + if (_value.packet1Options == null) { + return null; + } + + return $PacketOptionsCopyWith<$Res>(_value.packet1Options!, (value) { + return _then(_value.copyWith(packet1Options: value) as $Val); + }); + } + + /// Create a copy of PlanModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PacketOptionsCopyWith<$Res>? get packet2Options { + if (_value.packet2Options == null) { + return null; + } + + return $PacketOptionsCopyWith<$Res>(_value.packet2Options!, (value) { + return _then(_value.copyWith(packet2Options: value) as $Val); + }); + } + + /// Create a copy of PlanModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PacketOptionsCopyWith<$Res>? get packet3Options { + if (_value.packet3Options == null) { + return null; + } + + return $PacketOptionsCopyWith<$Res>(_value.packet3Options!, (value) { + return _then(_value.copyWith(packet3Options: value) as $Val); + }); + } + /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @override @@ -453,6 +514,7 @@ abstract class _$$PlanModelImplCopyWith<$Res> @useResult $Res call( {@JsonKey(name: "id") int id, + @JsonKey(name: "slug") String slug, @JsonKey(name: "name") String name, @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @@ -460,17 +522,24 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "type") PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, - @JsonKey(name: "packet1_options") dynamic packet1Options, - @JsonKey(name: "packet2_options") dynamic packet2Options, - @JsonKey(name: "packet3_options") dynamic packet3Options, + @JsonKey(name: "packet1_options") PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") PacketOptions? packet3Options, @JsonKey(name: "author") Author author, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, - @JsonKey(name: "total_comments") int totalComments}); + @JsonKey(name: "total_comments") int totalComments, + @JsonKey(name: "is_favorite") bool isFavorite}); @override $PlanTypeCopyWith<$Res> get type; @override + $PacketOptionsCopyWith<$Res>? get packet1Options; + @override + $PacketOptionsCopyWith<$Res>? get packet2Options; + @override + $PacketOptionsCopyWith<$Res>? get packet3Options; + @override $AuthorCopyWith<$Res> get author; } @@ -488,6 +557,7 @@ class __$$PlanModelImplCopyWithImpl<$Res> @override $Res call({ Object? id = null, + Object? slug = null, Object? name = null, Object? image = null, Object? duration = null, @@ -502,12 +572,17 @@ class __$$PlanModelImplCopyWithImpl<$Res> Object? totalLikes = null, Object? totalViews = null, Object? totalComments = null, + Object? isFavorite = null, }) { return _then(_$PlanModelImpl( id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable as int, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -539,15 +614,15 @@ class __$$PlanModelImplCopyWithImpl<$Res> packet1Options: freezed == packet1Options ? _value.packet1Options : packet1Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, packet2Options: freezed == packet2Options ? _value.packet2Options : packet2Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, packet3Options: freezed == packet3Options ? _value.packet3Options : packet3Options // ignore: cast_nullable_to_non_nullable - as dynamic, + as PacketOptions?, author: null == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -564,6 +639,10 @@ class __$$PlanModelImplCopyWithImpl<$Res> ? _value.totalComments : totalComments // ignore: cast_nullable_to_non_nullable as int, + isFavorite: null == isFavorite + ? _value.isFavorite + : isFavorite // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -573,6 +652,7 @@ class __$$PlanModelImplCopyWithImpl<$Res> class _$PlanModelImpl implements _PlanModel { const _$PlanModelImpl( {@JsonKey(name: "id") required this.id, + @JsonKey(name: "slug") required this.slug, @JsonKey(name: "name") required this.name, @JsonKey(name: "image") required this.image, @JsonKey(name: "duration") required this.duration, @@ -586,7 +666,8 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "author") required this.author, @JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_views") required this.totalViews, - @JsonKey(name: "total_comments") required this.totalComments}); + @JsonKey(name: "total_comments") required this.totalComments, + @JsonKey(name: "is_favorite") required this.isFavorite}); factory _$PlanModelImpl.fromJson(Map json) => _$$PlanModelImplFromJson(json); @@ -595,6 +676,9 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "id") final int id; @override + @JsonKey(name: "slug") + final String slug; + @override @JsonKey(name: "name") final String name; @override @@ -617,13 +701,13 @@ class _$PlanModelImpl implements _PlanModel { final int packetsCount; @override @JsonKey(name: "packet1_options") - final dynamic packet1Options; + final PacketOptions? packet1Options; @override @JsonKey(name: "packet2_options") - final dynamic packet2Options; + final PacketOptions? packet2Options; @override @JsonKey(name: "packet3_options") - final dynamic packet3Options; + final PacketOptions? packet3Options; @override @JsonKey(name: "author") final Author author; @@ -636,10 +720,13 @@ class _$PlanModelImpl implements _PlanModel { @override @JsonKey(name: "total_comments") final int totalComments; + @override + @JsonKey(name: "is_favorite") + final bool isFavorite; @override String toString() { - return 'PlanModel(id: $id, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments)'; + return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite)'; } @override @@ -648,6 +735,7 @@ class _$PlanModelImpl implements _PlanModel { (other.runtimeType == runtimeType && other is _$PlanModelImpl && (identical(other.id, id) || other.id == id) && + (identical(other.slug, slug) || other.slug == slug) && (identical(other.name, name) || other.name == name) && (identical(other.image, image) || other.image == image) && (identical(other.duration, duration) || @@ -657,19 +745,21 @@ class _$PlanModelImpl implements _PlanModel { (identical(other.details, details) || other.details == details) && (identical(other.packetsCount, packetsCount) || other.packetsCount == packetsCount) && - const DeepCollectionEquality() - .equals(other.packet1Options, packet1Options) && - const DeepCollectionEquality() - .equals(other.packet2Options, packet2Options) && - const DeepCollectionEquality() - .equals(other.packet3Options, packet3Options) && + (identical(other.packet1Options, packet1Options) || + other.packet1Options == packet1Options) && + (identical(other.packet2Options, packet2Options) || + other.packet2Options == packet2Options) && + (identical(other.packet3Options, packet3Options) || + other.packet3Options == packet3Options) && (identical(other.author, author) || other.author == author) && (identical(other.totalLikes, totalLikes) || other.totalLikes == totalLikes) && (identical(other.totalViews, totalViews) || other.totalViews == totalViews) && (identical(other.totalComments, totalComments) || - other.totalComments == totalComments)); + other.totalComments == totalComments) && + (identical(other.isFavorite, isFavorite) || + other.isFavorite == isFavorite)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -677,6 +767,7 @@ class _$PlanModelImpl implements _PlanModel { int get hashCode => Object.hash( runtimeType, id, + slug, name, image, duration, @@ -684,13 +775,14 @@ class _$PlanModelImpl implements _PlanModel { type, details, packetsCount, - const DeepCollectionEquality().hash(packet1Options), - const DeepCollectionEquality().hash(packet2Options), - const DeepCollectionEquality().hash(packet3Options), + packet1Options, + packet2Options, + packet3Options, author, totalLikes, totalViews, - totalComments); + totalComments, + isFavorite); /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @@ -711,6 +803,7 @@ class _$PlanModelImpl implements _PlanModel { abstract class _PlanModel implements PlanModel { const factory _PlanModel( {@JsonKey(name: "id") required final int id, + @JsonKey(name: "slug") required final String slug, @JsonKey(name: "name") required final String name, @JsonKey(name: "image") required final String image, @JsonKey(name: "duration") required final int duration, @@ -718,13 +811,14 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "type") required final PlanType type, @JsonKey(name: "details") required final String details, @JsonKey(name: "packets_count") required final int packetsCount, - @JsonKey(name: "packet1_options") final dynamic packet1Options, - @JsonKey(name: "packet2_options") final dynamic packet2Options, - @JsonKey(name: "packet3_options") final dynamic packet3Options, + @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") final PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") final PacketOptions? packet3Options, @JsonKey(name: "author") required final Author author, @JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_views") required final int totalViews, - @JsonKey(name: "total_comments") required final int totalComments}) = + @JsonKey(name: "total_comments") required final int totalComments, + @JsonKey(name: "is_favorite") required final bool isFavorite}) = _$PlanModelImpl; factory _PlanModel.fromJson(Map json) = @@ -734,6 +828,9 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "id") int get id; @override + @JsonKey(name: "slug") + String get slug; + @override @JsonKey(name: "name") String get name; @override @@ -756,13 +853,13 @@ abstract class _PlanModel implements PlanModel { int get packetsCount; @override @JsonKey(name: "packet1_options") - dynamic get packet1Options; + PacketOptions? get packet1Options; @override @JsonKey(name: "packet2_options") - dynamic get packet2Options; + PacketOptions? get packet2Options; @override @JsonKey(name: "packet3_options") - dynamic get packet3Options; + PacketOptions? get packet3Options; @override @JsonKey(name: "author") Author get author; @@ -775,6 +872,9 @@ abstract class _PlanModel implements PlanModel { @override @JsonKey(name: "total_comments") int get totalComments; + @override + @JsonKey(name: "is_favorite") + bool get isFavorite; /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @@ -1030,13 +1130,13 @@ PlanType _$PlanTypeFromJson(Map json) { /// @nodoc mixin _$PlanType { @JsonKey(name: "fit") - bool get fit => throw _privateConstructorUsedError; + bool? get fit => throw _privateConstructorUsedError; @JsonKey(name: "slim") - bool get slim => throw _privateConstructorUsedError; + bool? get slim => throw _privateConstructorUsedError; @JsonKey(name: "behavior") - bool get behavior => throw _privateConstructorUsedError; + bool? get behavior => throw _privateConstructorUsedError; @JsonKey(name: "wellness") - bool get wellness => throw _privateConstructorUsedError; + bool? get wellness => throw _privateConstructorUsedError; /// Serializes this PlanType to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1054,10 +1154,10 @@ abstract class $PlanTypeCopyWith<$Res> { _$PlanTypeCopyWithImpl<$Res, PlanType>; @useResult $Res call( - {@JsonKey(name: "fit") bool fit, - @JsonKey(name: "slim") bool slim, - @JsonKey(name: "behavior") bool behavior, - @JsonKey(name: "wellness") bool wellness}); + {@JsonKey(name: "fit") bool? fit, + @JsonKey(name: "slim") bool? slim, + @JsonKey(name: "behavior") bool? behavior, + @JsonKey(name: "wellness") bool? wellness}); } /// @nodoc @@ -1075,28 +1175,28 @@ class _$PlanTypeCopyWithImpl<$Res, $Val extends PlanType> @pragma('vm:prefer-inline') @override $Res call({ - Object? fit = null, - Object? slim = null, - Object? behavior = null, - Object? wellness = null, + Object? fit = freezed, + Object? slim = freezed, + Object? behavior = freezed, + Object? wellness = freezed, }) { return _then(_value.copyWith( - fit: null == fit + fit: freezed == fit ? _value.fit : fit // ignore: cast_nullable_to_non_nullable - as bool, - slim: null == slim + as bool?, + slim: freezed == slim ? _value.slim : slim // ignore: cast_nullable_to_non_nullable - as bool, - behavior: null == behavior + as bool?, + behavior: freezed == behavior ? _value.behavior : behavior // ignore: cast_nullable_to_non_nullable - as bool, - wellness: null == wellness + as bool?, + wellness: freezed == wellness ? _value.wellness : wellness // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, ) as $Val); } } @@ -1110,10 +1210,10 @@ abstract class _$$PlanTypeImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: "fit") bool fit, - @JsonKey(name: "slim") bool slim, - @JsonKey(name: "behavior") bool behavior, - @JsonKey(name: "wellness") bool wellness}); + {@JsonKey(name: "fit") bool? fit, + @JsonKey(name: "slim") bool? slim, + @JsonKey(name: "behavior") bool? behavior, + @JsonKey(name: "wellness") bool? wellness}); } /// @nodoc @@ -1129,56 +1229,57 @@ class __$$PlanTypeImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? fit = null, - Object? slim = null, - Object? behavior = null, - Object? wellness = null, + Object? fit = freezed, + Object? slim = freezed, + Object? behavior = freezed, + Object? wellness = freezed, }) { return _then(_$PlanTypeImpl( - fit: null == fit + fit: freezed == fit ? _value.fit : fit // ignore: cast_nullable_to_non_nullable - as bool, - slim: null == slim + as bool?, + slim: freezed == slim ? _value.slim : slim // ignore: cast_nullable_to_non_nullable - as bool, - behavior: null == behavior + as bool?, + behavior: freezed == behavior ? _value.behavior : behavior // ignore: cast_nullable_to_non_nullable - as bool, - wellness: null == wellness + as bool?, + wellness: freezed == wellness ? _value.wellness : wellness // ignore: cast_nullable_to_non_nullable - as bool, + as bool?, )); } } /// @nodoc @JsonSerializable() -class _$PlanTypeImpl implements _PlanType { +class _$PlanTypeImpl extends _PlanType { const _$PlanTypeImpl( {@JsonKey(name: "fit") required this.fit, @JsonKey(name: "slim") required this.slim, @JsonKey(name: "behavior") required this.behavior, - @JsonKey(name: "wellness") required this.wellness}); + @JsonKey(name: "wellness") required this.wellness}) + : super._(); factory _$PlanTypeImpl.fromJson(Map json) => _$$PlanTypeImplFromJson(json); @override @JsonKey(name: "fit") - final bool fit; + final bool? fit; @override @JsonKey(name: "slim") - final bool slim; + final bool? slim; @override @JsonKey(name: "behavior") - final bool behavior; + final bool? behavior; @override @JsonKey(name: "wellness") - final bool wellness; + final bool? wellness; @override String toString() { @@ -1218,29 +1319,30 @@ class _$PlanTypeImpl implements _PlanType { } } -abstract class _PlanType implements PlanType { +abstract class _PlanType extends PlanType { const factory _PlanType( - {@JsonKey(name: "fit") required final bool fit, - @JsonKey(name: "slim") required final bool slim, - @JsonKey(name: "behavior") required final bool behavior, - @JsonKey(name: "wellness") required final bool wellness}) = + {@JsonKey(name: "fit") required final bool? fit, + @JsonKey(name: "slim") required final bool? slim, + @JsonKey(name: "behavior") required final bool? behavior, + @JsonKey(name: "wellness") required final bool? wellness}) = _$PlanTypeImpl; + const _PlanType._() : super._(); factory _PlanType.fromJson(Map json) = _$PlanTypeImpl.fromJson; @override @JsonKey(name: "fit") - bool get fit; + bool? get fit; @override @JsonKey(name: "slim") - bool get slim; + bool? get slim; @override @JsonKey(name: "behavior") - bool get behavior; + bool? get behavior; @override @JsonKey(name: "wellness") - bool get wellness; + bool? get wellness; /// Create a copy of PlanType /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/feed/models/plan_pagination_model.g.dart b/lib/features/feed/models/plan_pagination_model.g.dart index 8f203fa8..b1fb11b1 100644 --- a/lib/features/feed/models/plan_pagination_model.g.dart +++ b/lib/features/feed/models/plan_pagination_model.g.dart @@ -27,6 +27,7 @@ Map _$$PlanPaginationModelImplToJson( _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => _$PlanModelImpl( id: (json['id'] as num).toInt(), + slug: json['slug'] as String, name: json['name'] as String, image: json['image'] as String, duration: (json['duration'] as num).toInt(), @@ -34,18 +35,29 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => type: PlanType.fromJson(json['type'] as Map), details: json['details'] as String, packetsCount: (json['packets_count'] as num).toInt(), - packet1Options: json['packet1_options'], - packet2Options: json['packet2_options'], - packet3Options: json['packet3_options'], + packet1Options: json['packet1_options'] == null + ? null + : PacketOptions.fromJson( + json['packet1_options'] as Map), + packet2Options: json['packet2_options'] == null + ? null + : PacketOptions.fromJson( + json['packet2_options'] as Map), + packet3Options: json['packet3_options'] == null + ? null + : PacketOptions.fromJson( + json['packet3_options'] as Map), author: Author.fromJson(json['author'] as Map), totalLikes: (json['total_likes'] as num).toInt(), totalViews: (json['total_views'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(), + isFavorite: json['is_favorite'] as bool, ); Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => { 'id': instance.id, + 'slug': instance.slug, 'name': instance.name, 'image': instance.image, 'duration': instance.duration, @@ -60,6 +72,7 @@ Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => 'total_likes': instance.totalLikes, 'total_views': instance.totalViews, 'total_comments': instance.totalComments, + 'is_favorite': instance.isFavorite, }; _$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map json) => @@ -80,10 +93,10 @@ Map _$$PacketOptionsImplToJson(_$PacketOptionsImpl instance) => _$PlanTypeImpl _$$PlanTypeImplFromJson(Map json) => _$PlanTypeImpl( - fit: json['fit'] as bool, - slim: json['slim'] as bool, - behavior: json['behavior'] as bool, - wellness: json['wellness'] as bool, + fit: json['fit'] as bool?, + slim: json['slim'] as bool?, + behavior: json['behavior'] as bool?, + wellness: json['wellness'] as bool?, ); Map _$$PlanTypeImplToJson(_$PlanTypeImpl instance) => diff --git a/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart b/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart index bf5be7e1..3553c27d 100644 --- a/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart +++ b/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart @@ -27,6 +27,9 @@ class CommentsCubit extends Cubit { case CommentSourceType.recipe: emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id))); break; + case CommentSourceType.plan: + emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id))); + break; } } catch (e) { emit(CommentsState.error(errorMessage: e.toString())); @@ -62,6 +65,13 @@ class CommentsCubit extends Cubit { parentCommentId: parentCommentId, ); break; + case CommentSourceType.plan: + await feedRepository.sendPlansComment( + slug: id, + commentText: commentText, + parentCommentId: parentCommentId, + ); + break; } getComments(id, commentSourceType); } catch (e) { diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart index 10afc3ea..352d04fb 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; @@ -30,7 +32,9 @@ class PlansCubit extends Cubit { isLoading: false, ), ); - } catch (e) { + } catch (e, stackTrace) { + log(e.toString()); + log(stackTrace.toString()); emit(state.copyWith(errorMessage: e.toString())); } } diff --git a/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart new file mode 100644 index 00000000..2136624b --- /dev/null +++ b/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart @@ -0,0 +1,19 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FeedProfileCubit extends Cubit { + final ProfileRepository profileRepository; + FeedProfileCubit({required this.profileRepository}) : super(ProfileState(profile: null)); + + Future getProfile() async { + try { + final profile = await profileRepository.getMyProfile(); + emit(state.copyWith(profile: profile)); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + } + } +} diff --git a/lib/features/feed/presentation/bloc/profile_cubit/profile_state.dart b/lib/features/feed/presentation/bloc/profile_cubit/profile_state.dart new file mode 100644 index 00000000..08fb0e3f --- /dev/null +++ b/lib/features/feed/presentation/bloc/profile_cubit/profile_state.dart @@ -0,0 +1,13 @@ +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'profile_state.freezed.dart'; + +@freezed +class ProfileState with _$ProfileState { + + factory ProfileState({ + ProfileModel? profile, + }) = _ProfileState; + +} \ No newline at end of file diff --git a/lib/features/feed/presentation/bloc/profile_cubit/profile_state.freezed.dart b/lib/features/feed/presentation/bloc/profile_cubit/profile_state.freezed.dart new file mode 100644 index 00000000..05007d71 --- /dev/null +++ b/lib/features/feed/presentation/bloc/profile_cubit/profile_state.freezed.dart @@ -0,0 +1,162 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'profile_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ProfileState { + ProfileModel? get profile => throw _privateConstructorUsedError; + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProfileStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfileStateCopyWith<$Res> { + factory $ProfileStateCopyWith( + ProfileState value, $Res Function(ProfileState) then) = + _$ProfileStateCopyWithImpl<$Res, ProfileState>; + @useResult + $Res call({ProfileModel? profile}); + + $ProfileModelCopyWith<$Res>? get profile; +} + +/// @nodoc +class _$ProfileStateCopyWithImpl<$Res, $Val extends ProfileState> + implements $ProfileStateCopyWith<$Res> { + _$ProfileStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profile = freezed, + }) { + return _then(_value.copyWith( + profile: freezed == profile + ? _value.profile + : profile // ignore: cast_nullable_to_non_nullable + as ProfileModel?, + ) as $Val); + } + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ProfileModelCopyWith<$Res>? get profile { + if (_value.profile == null) { + return null; + } + + return $ProfileModelCopyWith<$Res>(_value.profile!, (value) { + return _then(_value.copyWith(profile: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ProfileStateImplCopyWith<$Res> + implements $ProfileStateCopyWith<$Res> { + factory _$$ProfileStateImplCopyWith( + _$ProfileStateImpl value, $Res Function(_$ProfileStateImpl) then) = + __$$ProfileStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ProfileModel? profile}); + + @override + $ProfileModelCopyWith<$Res>? get profile; +} + +/// @nodoc +class __$$ProfileStateImplCopyWithImpl<$Res> + extends _$ProfileStateCopyWithImpl<$Res, _$ProfileStateImpl> + implements _$$ProfileStateImplCopyWith<$Res> { + __$$ProfileStateImplCopyWithImpl( + _$ProfileStateImpl _value, $Res Function(_$ProfileStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profile = freezed, + }) { + return _then(_$ProfileStateImpl( + profile: freezed == profile + ? _value.profile + : profile // ignore: cast_nullable_to_non_nullable + as ProfileModel?, + )); + } +} + +/// @nodoc + +class _$ProfileStateImpl implements _ProfileState { + _$ProfileStateImpl({this.profile}); + + @override + final ProfileModel? profile; + + @override + String toString() { + return 'ProfileState(profile: $profile)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProfileStateImpl && + (identical(other.profile, profile) || other.profile == profile)); + } + + @override + int get hashCode => Object.hash(runtimeType, profile); + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProfileStateImplCopyWith<_$ProfileStateImpl> get copyWith => + __$$ProfileStateImplCopyWithImpl<_$ProfileStateImpl>(this, _$identity); +} + +abstract class _ProfileState implements ProfileState { + factory _ProfileState({final ProfileModel? profile}) = _$ProfileStateImpl; + + @override + ProfileModel? get profile; + + /// Create a copy of ProfileState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProfileStateImplCopyWith<_$ProfileStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 82463215..ba2c8993 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -6,6 +6,8 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cub import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; @@ -92,79 +94,94 @@ class _FeedScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: _selectedChipIndex == 1 - ? FloatingActionButton.small( - heroTag: 'recipesTagsFilter', - backgroundColor: appTheme.blueA400, - shape: CircleBorder(), - child: Icon( - Icons.filter_list_rounded, - color: Colors.white, - ), - onPressed: () async { - await showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) => TagsModalWidget( - recipesCubit: recipesCubit, - ), - ); - recipesCubit.loadRecipesList(); - }, - ) - : null, - appBar: AppBar( - backgroundColor: Colors.white, - toolbarHeight: 0, - ), - backgroundColor: Colors.grey.shade200, - body: NestedScrollView( - controller: switch (_selectedChipIndex) { - 0 => blogPostsScrollController, - 1 => recipesScrollController, - 2 => dietsScrollController, - 3 => plansScrollController, - _ => blogPostsScrollController - }, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverAppBar( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), - backgroundColor: Colors.white, - foregroundColor: Colors.white, - automaticallyImplyLeading: false, - stretchTriggerOffset: 1, - floating: true, - snap: true, - scrolledUnderElevation: 0, - bottom: _chipGroup(), - title: Row( - children: [ - _searchBar(), - SizedBox(width: 12), - _addItemButton(feedCubit), - ], - ), - ), - ]; - }, - body: Builder( - builder: (BuildContext context) { - if (_selectedChipIndex == 0) { - return _buildAllBlogPosts(); - } else if (_selectedChipIndex == 1) { - return _buildAllRecipes(); - } else if (_selectedChipIndex == 2) { - return _buildDiets(); - } else if (_selectedChipIndex == 3) { - return _buildPlans(); - } else { - return Center(child: Text(chipsLabelList[_selectedChipIndex])); - } + return BlocProvider( + create: (context) => sl()..getProfile(), + child: Scaffold( + floatingActionButton: _selectedChipIndex == 1 + ? FloatingActionButton.small( + heroTag: 'recipesTagsFilter', + backgroundColor: appTheme.blueA400, + shape: CircleBorder(), + child: Icon( + Icons.filter_list_rounded, + color: Colors.white, + ), + onPressed: () async { + await showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => TagsModalWidget( + recipesCubit: recipesCubit, + ), + ); + recipesCubit.loadRecipesList(); + }, + ) + : null, + appBar: AppBar( + backgroundColor: Colors.white, + toolbarHeight: 0, + ), + backgroundColor: Colors.grey.shade200, + body: NestedScrollView( + controller: switch (_selectedChipIndex) { + 0 => blogPostsScrollController, + 1 => recipesScrollController, + 2 => dietsScrollController, + 3 => plansScrollController, + _ => blogPostsScrollController }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + BlocBuilder( + builder: (context, state) { + return SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: Colors.white, + foregroundColor: Colors.white, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + snap: true, + scrolledUnderElevation: 0, + bottom: _chipGroup(), + title: Row( + children: [ + _searchBar(), + SizedBox(width: 12), + switch (_selectedChipIndex) { + 2 => _addItemButton( + onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null, + ), + 3 => _addItemButton( + onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, + ), + _ => _addItemButton() + }, + ], + ), + ); + }, + ), + ]; + }, + body: Builder( + builder: (BuildContext context) { + if (_selectedChipIndex == 0) { + return _buildAllBlogPosts(); + } else if (_selectedChipIndex == 1) { + return _buildAllRecipes(); + } else if (_selectedChipIndex == 2) { + return _buildDiets(); + } else if (_selectedChipIndex == 3) { + return _buildPlans(); + } else { + return Center(child: Text(chipsLabelList[_selectedChipIndex])); + } + }, + ), ), ), ); @@ -435,20 +452,12 @@ class _FeedScreenState extends State { ); } - _addItemButton(FeedCubit feedCubit) { + _addItemButton({void Function()? onTap}) { return InkWell( - onTap: () { - switch (_selectedChipIndex) { - case 2: - GoRouter.of(context).pushNamed(CreateDietScreen.name); - break; - case 3: - GoRouter.of(context).pushNamed(CreatePlanScreen.name); - break; - default: + onTap: onTap ?? + () { GoRouter.of(context).pushNamed('blogPostCreateScreen'); - } - }, + }, borderRadius: BorderRadius.circular(64), child: Ink( decoration: BoxDecoration( diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 9d796fa6..2adab6ff 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -88,7 +88,7 @@ class _BlogPostWidgetState extends State { ], ), const Spacer(), - if (!state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe && !state.feedBlogPostList[widget.index].author.authorIsMe) + if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && !state.feedBlogPostList[widget.index].author.authorIsMe) SubscribeButtonWidget( slug: state.feedBlogPostList[widget.index].author.authorSlug, ), diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index fd931541..f2ee43f8 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -6,7 +6,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -enum CommentSourceType { blogPost, recipe, diet } +enum CommentSourceType { blogPost, recipe, diet, plan } class CommentsWidget extends StatefulWidget { final String id; diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 7b51f7f4..f18c61f7 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -75,7 +75,7 @@ class _DietsWidgetState extends State { bottom: 16, child: SizedBox( width: MediaQuery.sizeOf(context).width, - child: DietInfoRow( + child: InfoRow( diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 7036ee50..5d844029 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -19,7 +19,10 @@ class PlanWidget extends StatelessWidget { borderRadius: BorderRadius.circular(12), onTap: () => GoRouter.of(context).pushNamed( 'aboutPlanScreen', - extra: planModel, + extra: { + 'plan': planModel, + 'navigateToComments': false, + }, ), child: Ink( decoration: BoxDecoration( diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index d11f56f7..6f858ac7 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -157,6 +157,32 @@ class FeedRepository { } } + Future getPlansComments({ + required String slug, + }) async { + try { + return (await feedService.getPlansComments(slug: slug)); + } catch (e) { + rethrow; + } + } + + Future sendPlansComment({ + required String slug, + required String commentText, + int? parentCommentId, + }) async { + try { + await feedService.sendPlansComment( + slug: slug, + body: commentText, + parentCommentId: parentCommentId, + ); + } catch (e) { + rethrow; + } + } + Future likeBlogPost({ required int id, }) async { diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 683cd8fd..7643fa49 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -63,6 +63,18 @@ abstract class FeedService { @Part() int? parentCommentId, }); + @GET('/api/plans/{slug}/comments/show') + Future getPlansComments({ + @Path() required String slug, + }); + + @POST('/api/plans/{slug}/comment/create') + Future sendPlansComment({ + @Path() required String slug, + @Part() required String body, + @Part() int? parentCommentId, + }); + @GET('/api/recipes/{id}/comments/show') Future getRecipesComments({ @Path() required String id, diff --git a/lib/features/feed/services/feed_service.g.dart b/lib/features/feed/services/feed_service.g.dart index 18d3ad18..13c72ded 100644 --- a/lib/features/feed/services/feed_service.g.dart +++ b/lib/features/feed/services/feed_service.g.dart @@ -328,6 +328,79 @@ class _FeedService implements FeedService { await _dio.fetch(_options); } + @override + Future getPlansComments({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/${slug}/comments/show', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late CommentModel _value; + try { + _value = CommentModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future sendPlansComment({ + required String slug, + required String body, + int? parentCommentId, + }) async { + final _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = FormData(); + _data.fields.add(MapEntry( + 'body', + body, + )); + if (parentCommentId != null) { + _data.fields.add(MapEntry( + 'parentCommentId', + parentCommentId.toString(), + )); + } + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/${slug}/comment/create', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + @override Future getRecipesComments({required String id}) async { final _extra = {}; diff --git a/lib/features/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index 95933f66..ee244be5 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -1,11 +1,72 @@ +import 'dart:developer'; + import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class AboutPlanCubit extends Cubit { - AboutPlanCubit() : super(AboutPlanState.initial()); + final PlanRepository planRepository; + AboutPlanCubit({required this.planRepository}) : super(AboutPlanState.initial()); + Future getPlan(String slug) async { + try { + final plan = await planRepository.getPlan(slug); + emit(state.copyWith(planModel: plan, error: null)); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString())); + } + } - Future getPlan() async{ + Future getPlanWithLoading(String slug) async { + try { + emit(state.copyWith(isLoading: true)); + final plan = await planRepository.getPlan(slug); + emit(state.copyWith( + planModel: plan, + error: null, + isLoading: false, + ),); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString(), isLoading: false)); + } + } + void choseOption(int index) { + emit(state.copyWith(chosenOptionIndex: index)); + } + + Future likePlan(String slug) async { + try { + await planRepository.likePlan(slug); + final plan = await planRepository.getPlan(slug); + emit(state.copyWith(planModel: plan, error: null)); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString())); + } + } + + Future addToFavorite(String slug) async { + try { + await planRepository.addToFavorite(slug); + final plan = await planRepository.getPlan(slug); + emit(state.copyWith(planModel: plan, error: null)); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString())); + } + } + + Future removeFromFavorite(String slug) async { + try { + await planRepository.removeFromFavorite(slug); + final plan = await planRepository.getPlan(slug); + emit(state.copyWith(planModel: plan, error: null)); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString())); + } } } diff --git a/lib/features/plans/presentation/cubit/about_plan_state.dart b/lib/features/plans/presentation/cubit/about_plan_state.dart index 19859f4b..4e236f67 100644 --- a/lib/features/plans/presentation/cubit/about_plan_state.dart +++ b/lib/features/plans/presentation/cubit/about_plan_state.dart @@ -12,7 +12,8 @@ class AboutPlanState with _$AboutPlanState { required bool isLoading, required String? error, required PlanModel? planModel, + required int chosenOptionIndex, }) = _AboutPlanState; - factory AboutPlanState.initial() => const AboutPlanState(isLoading: false, error: null, planModel: null); + factory AboutPlanState.initial() => const AboutPlanState(chosenOptionIndex: 0, isLoading: false, error: null, planModel: null); } \ No newline at end of file diff --git a/lib/features/plans/presentation/cubit/about_plan_state.freezed.dart b/lib/features/plans/presentation/cubit/about_plan_state.freezed.dart index 98a97a2f..fb0613ca 100644 --- a/lib/features/plans/presentation/cubit/about_plan_state.freezed.dart +++ b/lib/features/plans/presentation/cubit/about_plan_state.freezed.dart @@ -19,6 +19,7 @@ mixin _$AboutPlanState { bool get isLoading => throw _privateConstructorUsedError; String? get error => throw _privateConstructorUsedError; PlanModel? get planModel => throw _privateConstructorUsedError; + int get chosenOptionIndex => throw _privateConstructorUsedError; /// Create a copy of AboutPlanState /// with the given fields replaced by the non-null parameter values. @@ -33,7 +34,11 @@ abstract class $AboutPlanStateCopyWith<$Res> { AboutPlanState value, $Res Function(AboutPlanState) then) = _$AboutPlanStateCopyWithImpl<$Res, AboutPlanState>; @useResult - $Res call({bool isLoading, String? error, PlanModel? planModel}); + $Res call( + {bool isLoading, + String? error, + PlanModel? planModel, + int chosenOptionIndex}); $PlanModelCopyWith<$Res>? get planModel; } @@ -56,6 +61,7 @@ class _$AboutPlanStateCopyWithImpl<$Res, $Val extends AboutPlanState> Object? isLoading = null, Object? error = freezed, Object? planModel = freezed, + Object? chosenOptionIndex = null, }) { return _then(_value.copyWith( isLoading: null == isLoading @@ -70,6 +76,10 @@ class _$AboutPlanStateCopyWithImpl<$Res, $Val extends AboutPlanState> ? _value.planModel : planModel // ignore: cast_nullable_to_non_nullable as PlanModel?, + chosenOptionIndex: null == chosenOptionIndex + ? _value.chosenOptionIndex + : chosenOptionIndex // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } @@ -96,7 +106,11 @@ abstract class _$$AboutPlanStateImplCopyWith<$Res> __$$AboutPlanStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isLoading, String? error, PlanModel? planModel}); + $Res call( + {bool isLoading, + String? error, + PlanModel? planModel, + int chosenOptionIndex}); @override $PlanModelCopyWith<$Res>? get planModel; @@ -118,6 +132,7 @@ class __$$AboutPlanStateImplCopyWithImpl<$Res> Object? isLoading = null, Object? error = freezed, Object? planModel = freezed, + Object? chosenOptionIndex = null, }) { return _then(_$AboutPlanStateImpl( isLoading: null == isLoading @@ -132,6 +147,10 @@ class __$$AboutPlanStateImplCopyWithImpl<$Res> ? _value.planModel : planModel // ignore: cast_nullable_to_non_nullable as PlanModel?, + chosenOptionIndex: null == chosenOptionIndex + ? _value.chosenOptionIndex + : chosenOptionIndex // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -140,7 +159,10 @@ class __$$AboutPlanStateImplCopyWithImpl<$Res> class _$AboutPlanStateImpl implements _AboutPlanState { const _$AboutPlanStateImpl( - {required this.isLoading, required this.error, required this.planModel}); + {required this.isLoading, + required this.error, + required this.planModel, + required this.chosenOptionIndex}); @override final bool isLoading; @@ -148,10 +170,12 @@ class _$AboutPlanStateImpl implements _AboutPlanState { final String? error; @override final PlanModel? planModel; + @override + final int chosenOptionIndex; @override String toString() { - return 'AboutPlanState(isLoading: $isLoading, error: $error, planModel: $planModel)'; + return 'AboutPlanState(isLoading: $isLoading, error: $error, planModel: $planModel, chosenOptionIndex: $chosenOptionIndex)'; } @override @@ -163,11 +187,14 @@ class _$AboutPlanStateImpl implements _AboutPlanState { other.isLoading == isLoading) && (identical(other.error, error) || other.error == error) && (identical(other.planModel, planModel) || - other.planModel == planModel)); + other.planModel == planModel) && + (identical(other.chosenOptionIndex, chosenOptionIndex) || + other.chosenOptionIndex == chosenOptionIndex)); } @override - int get hashCode => Object.hash(runtimeType, isLoading, error, planModel); + int get hashCode => + Object.hash(runtimeType, isLoading, error, planModel, chosenOptionIndex); /// Create a copy of AboutPlanState /// with the given fields replaced by the non-null parameter values. @@ -183,7 +210,8 @@ abstract class _AboutPlanState implements AboutPlanState { const factory _AboutPlanState( {required final bool isLoading, required final String? error, - required final PlanModel? planModel}) = _$AboutPlanStateImpl; + required final PlanModel? planModel, + required final int chosenOptionIndex}) = _$AboutPlanStateImpl; @override bool get isLoading; @@ -191,6 +219,8 @@ abstract class _AboutPlanState implements AboutPlanState { String? get error; @override PlanModel? get planModel; + @override + int get chosenOptionIndex; /// Create a copy of AboutPlanState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 97070101..1f2aa1cf 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -1,19 +1,480 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/string_extentions.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; +import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; +import 'package:eatvio_app/features/plans/presentation/widgets/options_tab.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_svg/svg.dart'; -class AboutPlanScreen extends StatelessWidget { - final PlanModel planModel; +import '../widgets/option_widget.dart'; - const AboutPlanScreen({ - super.key, - required this.planModel, - }); +class AboutPlanScreen extends StatefulWidget { + AboutPlanScreen({super.key, required this.plan, required this.navigateToComments}); + final PlanModel plan; + final bool navigateToComments; + @override + State createState() => _AboutPlanScreenState(); +} + +class _AboutPlanScreenState extends State { + final CommentsCubit commentsCubit = sl(); + int? replyId; + final ScrollController scrollController = ScrollController(); + final textEditingController = TextEditingController(); + bool isMoreOptionsVisible = false; + bool _jumped = false; + @override + void initState() { + super.initState(); + scrollController.addListener(() { + if (isMoreOptionsVisible) { + setState(() { + isMoreOptionsVisible = false; + }); + } + }); + } + + void _scrollToBottom() { + _jumped = true; + scrollController.jumpTo( + scrollController.position.maxScrollExtent, + ); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(planModel.name), + final s = S.of(context); + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => commentsCubit + ..getComments( + widget.plan.slug, + CommentSourceType.plan, + true, + ), + ), + BlocProvider( + create: (context) => sl()..getPlanWithLoading(widget.plan.slug), + ), + ], + child: BlocConsumer( + listener: (BuildContext context, CommentsState commentsState) { + if (commentsState is CommentsStateLoaded) { + setState(() { + replyId = commentsState.replyId; + }); + } + }, + builder: (BuildContext context, CommentsState commentsState) { + Future.delayed(Duration(milliseconds: 500), () { + if (widget.navigateToComments && !_jumped) { + _scrollToBottom(); + } + }); + return BlocBuilder( + builder: (context, state) { + if (state.isLoading) return Scaffold(body: Loader()); + return Scaffold( + backgroundColor: appTheme.gray100, + resizeToAvoidBottomInset: true, + bottomSheet: Container( + padding: EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide( + color: Colors.black.withOpacity(0.1), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: SizedBox( + child: FloatingActionButton.extended( + heroTag: 'nextButtonRegistration', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: appTheme.greenA700, + onPressed: () { + /// TODO: добавить обработку покупки плана + // showModalBottomSheet( + // context: context, + // builder: (context) => StartDietBottomSheet( + // aboutDietCubit: context.read(), + // ), + // ); + }, + label: Text( + s.startDiet, + style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + ), + ), + ), + ), + SizedBox(width: 8), + BookmarkButton( + isFavorite: state.planModel?.isFavorite ?? false, + onTap: () { + final planModel = state.planModel; + if (planModel == null) return; + if (planModel.isFavorite) { + context.read().removeFromFavorite(planModel.slug); + } else { + context.read().addToFavorite(planModel.slug); + } + }, + width: 52, + height: 52, + size: 24, + shape: BoxShape.circle, + color: appTheme.gray50, + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (replyId != null) + GestureDetector( + onTap: () { + context.read().setReplyId(null); + setState(() { + replyId = null; + textEditingController.clear(); + }); + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Container( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + height: 32, + width: 32, + child: Icon( + Icons.close_rounded, + size: 18, + color: Colors.white, + ), + ), + ), + if (replyId != null) SizedBox(width: 8), + Expanded( + child: SizedBox( + child: TextFormField( + onChanged: (_) { + setState(() {}); + }, + controller: textEditingController, + maxLines: 5, + minLines: 1, + keyboardType: TextInputType.multiline, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + hintText: s.comment.capitalize(), + hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + ), + ), + ), + ), + SizedBox(width: 8), + GestureDetector( + onTap: () { + if (textEditingController.text.length > 2) { + commentsCubit.sendComment( + id: widget.plan.slug, + commentText: textEditingController.text, + parentCommentId: replyId, + commentSourceType: CommentSourceType.plan, + ); + context.read().setReplyId(null); + setState(() { + textEditingController.clear(); + replyId = null; + }); + + Future.delayed(Duration(milliseconds: 500), () { + context.read().getPlan(widget.plan.slug); + }); + FocusManager.instance.primaryFocus?.unfocus(); + } + }, + child: Container( + decoration: BoxDecoration( + color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, + shape: BoxShape.circle, + ), + height: 32, + width: 32, + child: Icon( + Icons.send_rounded, + size: 18, + color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black, + ), + ), + ), + ], + ), + ], + ), + ), + ), + appBar: AppBar( + centerTitle: true, + title: Text( + widget.plan.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions(id: widget.plan.id), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: SingleChildScrollView( + controller: scrollController, + padding: EdgeInsets.only(bottom: 160), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.plan.image, + height: 300, + width: MediaQuery.sizeOf(context).width, + fit: BoxFit.fill, + ), + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12), + SizedBox(width: 8), + Text( + widget.plan.totalViews.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + SizedBox(width: 12), + GestureDetector( + onTap: () { + context.read().likePlan(widget.plan.slug); + }, + child: InfoBouble( + children: [ + SvgPicture.asset( + 'assets/icons/heart.svg', + width: 14, + height: 12, + + /// TODO задача для бека на добавление wasLokedByMe? + colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn), + ), + SizedBox(width: 8), + Text( + widget.plan.totalLikes.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ), + SizedBox(width: 12), + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), + SizedBox(width: 8), + Text( + widget.plan.totalComments.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + s.about_plan, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 12), + + /// TODO глянуть как заменить + InfoRow(plan: state.planModel), + ExpandableText( + text: widget.plan.details, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AuthorCard( + author: widget.plan.author, + title: s.plan_author, + ), + ), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 16), + if (state.planModel?.packetsCount == 3) ...[ + OptionsTab( + cost: [ + state.planModel?.packet1Options?.price ?? 0, + state.planModel?.packet2Options?.price ?? 0, + state.planModel?.packet3Options?.price ?? 0, + ], + ), + SizedBox(height: 12), + ], + switch (state.chosenOptionIndex) { + 1 => OptionWidget( + option: state.planModel?.packet2Options, + ), + 2 => OptionWidget( + option: state.planModel?.packet3Options, + ), + _ => OptionWidget( + option: state.planModel?.packet1Options, + ), + }, + SizedBox( + height: 16, + ), + MainBackgroundBouble( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Text( + '${state.planModel?.totalComments ?? 0} ${getCommentForm(state.planModel?.totalComments ?? 0, s)}', + style: TextStyles.textMMedium.copyWith( + color: appTheme.gray500, + ), + ), + ), + SizedBox(height: 16), + if (commentsState is CommentsStateLoading) const Loader(), + if (commentsState is CommentsStateLoaded) + CommentsWidget( + id: widget.plan.id.toString(), + commentSourceType: CommentSourceType.plan, + comment: commentsState.comment, + replyId: replyId, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, ), ); } diff --git a/lib/features/plans/presentation/widgets/item_widget.dart b/lib/features/plans/presentation/widgets/item_widget.dart new file mode 100644 index 00000000..4cf41e70 --- /dev/null +++ b/lib/features/plans/presentation/widgets/item_widget.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ItemWidget extends StatelessWidget { + const ItemWidget({super.key, required this.text}); + final String text; + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + text, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + Spacer(), + SvgPicture.asset( + 'assets/icons/ok.svg', + width: 20, + height: 20, + ), + ], + ); + } +} diff --git a/lib/features/plans/presentation/widgets/option_duration_widget.dart b/lib/features/plans/presentation/widgets/option_duration_widget.dart new file mode 100644 index 00000000..e31b37b4 --- /dev/null +++ b/lib/features/plans/presentation/widgets/option_duration_widget.dart @@ -0,0 +1,45 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +class OptionDurationWidget extends StatelessWidget { + const OptionDurationWidget({super.key, required this.duration}); + final int duration; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.cardBackground), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.only( + topRight: Radius.circular(100), + bottomRight: Radius.circular(100), + ), + ), + ), + ), + SizedBox( + width: 12, + ), + Text( + '${s.option_duration} $duration ${getDayForm(duration, s)}', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox( + width: 12, + ), + ], + ), + ); + } +} diff --git a/lib/features/plans/presentation/widgets/option_widget.dart b/lib/features/plans/presentation/widgets/option_widget.dart new file mode 100644 index 00000000..96849dfb --- /dev/null +++ b/lib/features/plans/presentation/widgets/option_widget.dart @@ -0,0 +1,49 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/plans/presentation/widgets/item_widget.dart'; +import 'package:eatvio_app/features/plans/presentation/widgets/option_duration_widget.dart'; +import 'package:flutter/material.dart'; + +class OptionWidget extends StatelessWidget { + const OptionWidget({super.key, required this.option}); + final PacketOptions? option; + @override + Widget build(BuildContext context) { + if (option == null) { + return SizedBox.shrink(); + } + return MainBackgroundBouble( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16), + Text( + option?.short ?? '', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 12), + Text( + option?.description ?? '', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 12), + OptionDurationWidget(duration: 7), + SizedBox(height: 16), + CustomDivider(), + SizedBox(height: 16), + ListView.separated( + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 12), + itemBuilder: (context, index) => ItemWidget(text: option?.items[index] ?? ''), + itemCount: option?.items.length ?? 0, + shrinkWrap: true, + ), + ], + ), + ); + } +} diff --git a/lib/features/plans/presentation/widgets/options_tab.dart b/lib/features/plans/presentation/widgets/options_tab.dart new file mode 100644 index 00000000..8f230b71 --- /dev/null +++ b/lib/features/plans/presentation/widgets/options_tab.dart @@ -0,0 +1,43 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OptionsTab extends StatelessWidget { + const OptionsTab({super.key, required this.cost}); + final List cost; + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final selectedOption = state.chosenOptionIndex; + return Container( + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 3, + (index) => Expanded( + child: AnimatedOptionButton( + isSelected: selectedOption == index, + text: '${cost[index]}₽', + onTap: () { + context.read().choseOption(index); + }, + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/plans/repositories/plan_repository.dart b/lib/features/plans/repositories/plan_repository.dart new file mode 100644 index 00000000..5c1df29a --- /dev/null +++ b/lib/features/plans/repositories/plan_repository.dart @@ -0,0 +1,43 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; + +class PlanRepository { + final PlanService planService; + + PlanRepository({required this.planService}); + + Future getPlan(String slug) async { + try { + return planService.getPlan(slug); + } catch (e) { + rethrow; + } + } + + Future likePlan(String slug) async { + try { + await planService.likePlan(slug: slug); + } catch (e) { + rethrow; + } + } + + Future addToFavorite(String slug) async { + try { + await planService.addToFavorite(slug: slug); + } catch (e) { + rethrow; + } + } + + Future removeFromFavorite(String slug) async { + try { + await planService.removeFromFavorite(slug: slug); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + rethrow; + } + } +} diff --git a/lib/features/plans/services/plan_service.dart b/lib/features/plans/services/plan_service.dart new file mode 100644 index 00000000..d8a49a90 --- /dev/null +++ b/lib/features/plans/services/plan_service.dart @@ -0,0 +1,31 @@ + + +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'plan_service.g.dart'; + +@RestApi() +abstract class PlanService { + + factory PlanService(Dio dio, {String baseUrl}) = _PlanService; + + @GET('/api/plans/show/{slug}') + Future getPlan(@Path('slug') String slug); + + @POST('/api/plans/{slug}/like') + Future likePlan({ + @Path() required String slug, + }); + + @POST('/api/plans/{slug}/add_to_favorite') + Future addToFavorite({ + @Path() required String slug, + }); + + @POST('/api/plans/{slug}/remove_from_favorite') + Future removeFromFavorite({ + @Path() required String slug, + }); +} \ No newline at end of file diff --git a/lib/features/plans/services/plan_service.g.dart b/lib/features/plans/services/plan_service.g.dart new file mode 100644 index 00000000..71abb5af --- /dev/null +++ b/lib/features/plans/services/plan_service.g.dart @@ -0,0 +1,161 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'plan_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _PlanService implements PlanService { + _PlanService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getPlan(String slug) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/show/${slug}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late PlanModel _value; + try { + _value = PlanModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future likePlan({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/${slug}/like', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future addToFavorite({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/${slug}/add_to_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future removeFromFavorite({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/${slug}/remove_from_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 08eef1e1..0dc19f85 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -25,6 +25,7 @@ class MessageLookup extends MessageLookupByLibrary { "Moscow": MessageLookupByLibrary.simpleMessage("Москва"), "Russia": MessageLookupByLibrary.simpleMessage("Россия"), "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), + "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), "active": MessageLookupByLibrary.simpleMessage("Активен"), "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), "add": MessageLookupByLibrary.simpleMessage("Добавить"), @@ -104,6 +105,10 @@ class MessageLookup extends MessageLookupByLibrary { "draft": MessageLookupByLibrary.simpleMessage("Черновик"), "eating_behavior": MessageLookupByLibrary.simpleMessage("Пищевое поведение"), + "eating_behavior_type": MessageLookupByLibrary.simpleMessage( + "Формирование пищевого поведения"), + "eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage( + "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"), "economy": MessageLookupByLibrary.simpleMessage("Эконом"), "email": MessageLookupByLibrary.simpleMessage("Элетронная почта"), "email_empty": MessageLookupByLibrary.simpleMessage( @@ -134,6 +139,9 @@ class MessageLookup extends MessageLookupByLibrary { "Имя обязательно для заполнения"), "first_name_short": MessageLookupByLibrary.simpleMessage( "В имени должен быть минимум 1 символ"), + "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), + "fit_type_desc": MessageLookupByLibrary.simpleMessage( + "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), "fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), "from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), "from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), @@ -197,6 +205,8 @@ class MessageLookup extends MessageLookupByLibrary { "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( "Этот план питания для похудения на 7 дней сбалансированный и разнообразный"), + "option_duration": + MessageLookupByLibrary.simpleMessage("Продолжительность опции"), "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( "Опция подразумевает создание индивидуальной диеты"), "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), @@ -216,6 +226,7 @@ class MessageLookup extends MessageLookupByLibrary { "photo": MessageLookupByLibrary.simpleMessage("Фото"), "place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"), + "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), "plan_desc_example": MessageLookupByLibrary.simpleMessage( "Большое подробное, привлекательное описание плана"), @@ -243,6 +254,9 @@ class MessageLookup extends MessageLookupByLibrary { "share": MessageLookupByLibrary.simpleMessage("Поделиться"), "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), "slim": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type_desc": MessageLookupByLibrary.simpleMessage( + "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), "standard": MessageLookupByLibrary.simpleMessage("Стандарт"), "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), "tell_your_schedule": MessageLookupByLibrary.simpleMessage( @@ -271,6 +285,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Замер веса"), "wellness": MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), "what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"), "wrong_credentials": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 1c19a2b3..6eb67d1f 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1929,6 +1929,116 @@ class S { args: [], ); } + + /// `Об этом плане` + String get about_plan { + return Intl.message( + 'Об этом плане', + name: 'about_plan', + desc: '', + args: [], + ); + } + + /// `Автор плана` + String get plan_author { + return Intl.message( + 'Автор плана', + name: 'plan_author', + desc: '', + args: [], + ); + } + + /// `Продолжительность опции` + String get option_duration { + return Intl.message( + 'Продолжительность опции', + name: 'option_duration', + desc: '', + args: [], + ); + } + + /// `Поддержание формы` + String get fit_type { + return Intl.message( + 'Поддержание формы', + name: 'fit_type', + desc: '', + args: [], + ); + } + + /// `План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;` + String get fit_type_desc { + return Intl.message( + 'План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;', + name: 'fit_type_desc', + desc: '', + args: [], + ); + } + + /// `Похудение` + String get slim_type { + return Intl.message( + 'Похудение', + name: 'slim_type', + desc: '', + args: [], + ); + } + + /// `План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;` + String get slim_type_desc { + return Intl.message( + 'План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;', + name: 'slim_type_desc', + desc: '', + args: [], + ); + } + + /// `Улучшение самочувствия` + String get wellness_type { + return Intl.message( + 'Улучшение самочувствия', + name: 'wellness_type', + desc: '', + args: [], + ); + } + + /// `Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.` + String get wellness_type_desc { + return Intl.message( + 'Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.', + name: 'wellness_type_desc', + desc: '', + args: [], + ); + } + + /// `Формирование пищевого поведения` + String get eating_behavior_type { + return Intl.message( + 'Формирование пищевого поведения', + name: 'eating_behavior_type', + desc: '', + args: [], + ); + } + + /// `Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;` + String get eating_behavior_type_desc { + return Intl.message( + 'Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;', + name: 'eating_behavior_type_desc', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d42aec15..b5f2d8a3 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -186,5 +186,16 @@ "price_explanation": "Разница в ценах, это комиссия Итвио", "option_content": "Содержание опции", "option_content_hint_text": "На связи 7 дней в неделю", - "option_personal_diet_desc": "Опция подразумевает создание индивидуальной диеты" + "option_personal_diet_desc": "Опция подразумевает создание индивидуальной диеты", + "about_plan": "Об этом плане", + "plan_author": "Автор плана", + "option_duration": "Продолжительность опции", + "fit_type": "Поддержание формы", + "fit_type_desc": "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;", + "slim_type": "Похудение", + "slim_type_desc": "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;", + "wellness_type": "Улучшение самочувствия", + "wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", + "eating_behavior_type": "Формирование пищевого поведения", + "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;" }