feat(plans): plan creating has been completed

This commit is contained in:
Username1548
2025-04-29 14:44:33 +07:00
parent c8b828bfe1
commit 0fcb2c1a0d
43 changed files with 1602 additions and 635 deletions

View File

@@ -1,6 +1,8 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.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';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -39,34 +41,53 @@ class _AddItemWidgetState extends State<AddItemWidget> {
if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) {
controller.text = widget.items[widget.index];
}
return Row(
children: [
Expanded(
child: CustomTextFormField(
controller: controller,
hintText: widget.enableHint ? s.option_content_hint_text : null,
onChanged: (value) {
context.read<CreatePlanCubit>().setOptionContent(value, widget.index);
},
),
),
if (widget.canBeDeleted) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
},
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: appTheme.lightGray0,
borderRadius: BorderRadius.circular(20),
),
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
buildWhen: (previous, current) => previous.validationState != current.validationState,
builder: (context, state) {
final hasError = state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty ||
(state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false);
return Column(
children: [
Row(
children: [
Expanded(
child: CustomTextFormField(
controller: controller,
hasError: hasError,
hintText: widget.enableHint ? s.option_content_hint_text : null,
onChanged: (value) {
context.read<CreatePlanCubit>().setOptionContent(value, widget.index);
},
),
),
if (widget.canBeDeleted) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
},
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: appTheme.lightGray0,
borderRadius: BorderRadius.circular(20),
),
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
),
),
],
],
),
),
],
],
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.addAtLeastOneItem)
] else if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols)
]
],
);
},
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.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/features/create_plan/presentation/widgets/add_item_widget.dart';
@@ -32,35 +33,9 @@ class _OptionWidgetState extends State<OptionWidget> {
super.initState();
customerPriceController.addListener(() {
if (!customerPriceController.text.endsWith('₽')) {
final textWithoutCurrency = customerPriceController.text.replaceAll('₽', '');
customerPriceController.value = TextEditingValue(
text: "$textWithoutCurrency₽",
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
if (price != null && price < 1200) {
customerPriceController.value = const TextEditingValue(
text: "1200₽",
selection: TextSelection.collapsed(offset: 4),
);
}
});
creatorPriceController.addListener(() {
if (!creatorPriceController.text.endsWith('₽')) {
final textWithoutCurrency = creatorPriceController.text.replaceAll('₽', '');
creatorPriceController.value = TextEditingValue(
text: "$textWithoutCurrency₽",
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
final price = double.tryParse(creatorPriceController.text.replaceAll('₽', ''));
if (price != null && price < 1000) {
creatorPriceController.value = const TextEditingValue(
text: "1000₽",
selection: TextSelection.collapsed(offset: 4),
);
final text = customerPriceController.text.replaceAll('₽', '');
customerPriceController.text = '$text₽';
customerPriceController.selection = TextSelection.collapsed(offset: text.length);
}
});
}
@@ -94,12 +69,17 @@ class _OptionWidgetState extends State<OptionWidget> {
const SizedBox(height: 4),
CustomTextFormField(
hintText: optionNameHintText,
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError,
controller: optionTitleController,
onChanged: (value) {
context.read<CreatePlanCubit>().setOptionTitle(value);
},
maxLength: 150,
),
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12),
Text(
s.option_desc,
@@ -114,6 +94,7 @@ class _OptionWidgetState extends State<OptionWidget> {
child: CustomTextFormField(
controller: optionDescriptionController,
textAlignVertical: TextAlignVertical.top,
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError,
maxLength: 1000,
minLines: null,
maxLines: null,
@@ -124,6 +105,10 @@ class _OptionWidgetState extends State<OptionWidget> {
},
),
),
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12),
Row(
children: [
@@ -141,14 +126,8 @@ class _OptionWidgetState extends State<OptionWidget> {
const SizedBox(height: 4),
CustomTextFormField(
controller: creatorPriceController,
onChanged: (value) {
double? price = double.tryParse(value.replaceAll('₽', ''));
if (price != null) {
price = price * 1.2;
customerPriceController.text = price.toStringAsFixed(2);
context.read<CreatePlanCubit>().setPrice(price);
}
},
readOnly: true,
textStyle: TextStyle(color: appTheme.gray),
maxLength: 20,
),
const SizedBox(height: 12),
@@ -169,10 +148,11 @@ class _OptionWidgetState extends State<OptionWidget> {
CustomTextFormField(
controller: customerPriceController,
textInputType: const TextInputType.numberWithOptions(decimal: true),
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
onChanged: (value) {
final price = double.tryParse(value.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
if (price != null) {
creatorPriceController.text = (price * 0.8).toStringAsFixed(2);
context.read<CreatePlanCubit>().setPrice(price);
}
},
@@ -235,33 +215,33 @@ class _OptionWidgetState extends State<OptionWidget> {
),
const SizedBox(height: 16),
const CustomDivider(),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: isChecked,
activeColor: appTheme.gray,
checkColor: appTheme.gray,
fillColor: const WidgetStatePropertyAll(Colors.transparent),
side: WidgetStateBorderSide.resolveWith(
(Set<WidgetState> states) {
return BorderSide(width: 1, color: appTheme.gray);
},
),
onChanged: (bool? value) {
setState(() {
isChecked = value ?? false;
});
},
),
Expanded(
child: Text(
s.option_personal_diet_desc,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
],
),
// const SizedBox(height: 16),
// Row(
// children: [
// Checkbox(
// value: isChecked,
// activeColor: appTheme.gray,
// checkColor: appTheme.gray,
// fillColor: const WidgetStatePropertyAll(Colors.transparent),
// side: WidgetStateBorderSide.resolveWith(
// (Set<WidgetState> states) {
// return BorderSide(width: 1, color: appTheme.gray);
// },
// ),
// onChanged: (bool? value) {
// setState(() {
// isChecked = value ?? false;
// });
// },
// ),
// Expanded(
// child: Text(
// s.option_personal_diet_desc,
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
// ),
// ),
// ],
// ),
],
),
);

View File

@@ -3,6 +3,7 @@ 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/custom_drop_down_button.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.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';
@@ -36,28 +37,37 @@ class _PlanDurationDropDownWidgetState extends State<PlanDurationDropDownWidget>
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
return CustomDropDownButton(
hint: Text(
s.choose_plan_duration,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
onChanged: (value) {
if (value != null) {
context.read<CreatePlanCubit>().setDuration(durationDropDownTitles.indexOf(value));
}
},
items: durationDropDownTitles
.mapIndexed(
(index, value) => DropdownMenuItem(
value: durationDropDownTitles[index],
child: Text(
durationDropDownTitles[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0],
return Column(
children: [
CustomDropDownButton(
hasError: state.validationState.durationHasError,
hint: Text(
s.choose_plan_duration,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
onChanged: (value) {
if (value != null) {
context.read<CreatePlanCubit>().setDuration(durationDropDownTitles.indexOf(value));
}
},
items: durationDropDownTitles
.mapIndexed(
(index, value) => DropdownMenuItem(
value: durationDropDownTitles[index],
child: Text(
durationDropDownTitles[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0],
),
if (state.validationState.durationHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.choose_plan_duration),
],
],
);
},
);

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dotted_border/dotted_border.dart';
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/widgets/error_widget.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';
@@ -72,74 +73,79 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
return GestureDetector(
onTap: () {
_showImagePickerOptions(s, context.read<CreatePlanCubit>());
},
child: Container(
height: 240,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null,
color: appTheme.gray50,
return Column(
children: [
GestureDetector(
onTap: () {
_showImagePickerOptions(s, context.read<CreatePlanCubit>());
},
child: Container(
height: 240,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null,
color: appTheme.gray50,
),
width: MediaQuery.sizeOf(context).width,
padding: state.planData.image != null ? null : const EdgeInsets.all(8),
child: state.planData.image != null
? Padding(
padding: const EdgeInsets.only(top: 6, right: 6),
child: GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().setImage(null);
},
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appTheme.dividerColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close_rounded,
color: Colors.grey,
size: 16,
),
),
),
),
)
: Stack(
children: [
DottedBorder(
stackFit: StackFit.expand,
dashPattern: const [14, 14],
color: state.validationState.imageHasError ? appTheme.red : appTheme.dottedBorder,
borderType: BorderType.RRect,
radius: const Radius.circular(12),
child: const SizedBox.expand(),
),
Center(
child: Column(
children: [
const SizedBox(
height: 48,
),
SvgPicture.asset('assets/icons/eatvio_logo.svg'),
const SizedBox(
height: 24,
),
Text(
s.add_cover,
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
),
],
),
),
],
),
),
),
width: MediaQuery.sizeOf(context).width,
padding: state.planData.image != null ? null : const EdgeInsets.all(8),
child: state.planData.image != null
? Padding(
padding: const EdgeInsets.only(top: 6, right: 6),
child: GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().setImage(null);
},
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appTheme.dividerColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close_rounded,
color: Colors.grey,
size: 16,
),
),
),
),
)
: Stack(
children: [
DottedBorder(
stackFit: StackFit.expand,
dashPattern: const [14, 14],
color: appTheme.dottedBorder,
borderType: BorderType.RRect,
radius: const Radius.circular(12),
child: const SizedBox.expand(),
),
Center(
child: Column(
children: [
const SizedBox(
height: 48,
),
SvgPicture.asset('assets/icons/eatvio_logo.svg'),
const SizedBox(
height: 24,
),
Text(
s.add_cover,
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
),
],
),
),
],
),
),
if (state.validationState.imageHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.add_cover)],
],
);
},
);

View File

@@ -1,5 +1,6 @@
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/widgets/error_widget.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';
@@ -50,49 +51,55 @@ class _PlanTypeChoiceWidgetState extends State<PlanTypeChoiceWidget> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
return Row(
return Column(
children: [
...List.generate(4, (index) {
return Expanded(
child: GestureDetector(
onTap: () {
final planTypes = [...state.planData.type];
planTypes[index] = !planTypes[index];
context.read<CreatePlanCubit>().setPlanType(planTypes);
},
child: Container(
margin: EdgeInsets.only(right: index == 3 ? 0 : 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700,
border: Border.all(
width: 1,
color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00,
Row(
children: [
...List.generate(4, (index) {
return Expanded(
child: GestureDetector(
onTap: () {
final planTypes = [...state.planData.type];
planTypes[index] = !planTypes[index];
context.read<CreatePlanCubit>().setPlanType(planTypes);
},
child: Container(
margin: EdgeInsets.only(right: index == 3 ? 0 : 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700,
border: Border.all(
width: 1,
color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00,
),
),
height: 104,
child: Column(
children: [
const SizedBox(height: 16),
Image.asset(
choiceContent[index].$1,
height: 30,
),
const SizedBox(height: 8),
Text(
choiceContent[index].$2,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
style: TextStyles.textFS10FW500.copyWith(color: appTheme.black),
),
],
),
),
),
height: 104,
child: Column(
children: [
const SizedBox(height: 16),
Image.asset(
choiceContent[index].$1,
height: 30,
),
const SizedBox(height: 8),
Text(
choiceContent[index].$2,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
style: TextStyles.textFS10FW500.copyWith(color: appTheme.black),
),
],
),
),
),
);
}),
);
}),
],
),
if (state.validationState.typeHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.chosePlanType)]
],
);
},