forked from eatvio/eatvio_app
added plan pagination and updated plans creation screen
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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_plan/presentation/cubit/create_plan_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class AddItemWidget extends StatefulWidget {
|
||||
AddItemWidget({
|
||||
super.key,
|
||||
required this.canBeDeleted,
|
||||
required this.enableHint,
|
||||
required this.index,
|
||||
required this.items,
|
||||
});
|
||||
final int index;
|
||||
final bool canBeDeleted;
|
||||
final bool enableHint;
|
||||
final List<String> items;
|
||||
@override
|
||||
State<AddItemWidget> createState() => _AddItemWidgetState();
|
||||
}
|
||||
|
||||
class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!widget.canBeDeleted && widget.items.length >= widget.index) {
|
||||
controller.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
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) ...[
|
||||
SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.lightGray0,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,320 @@
|
||||
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/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.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';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OptionWidget extends StatelessWidget {
|
||||
const OptionWidget({super.key});
|
||||
class OptionWidget extends StatefulWidget {
|
||||
OptionWidget({super.key});
|
||||
|
||||
@override
|
||||
State<OptionWidget> createState() => _OptionWidgetState();
|
||||
}
|
||||
|
||||
class _OptionWidgetState extends State<OptionWidget> {
|
||||
final customerPriceController = TextEditingController()..text = '1200₽';
|
||||
final creatorPriceController = TextEditingController()..text = '1000₽';
|
||||
final optionTitleController = TextEditingController();
|
||||
final optionDescriptionController = TextEditingController();
|
||||
bool firstExplanationVisible = false;
|
||||
bool isChecked = false;
|
||||
bool secondExplanationVisible = false;
|
||||
@override
|
||||
void initState() {
|
||||
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 = 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 = TextEditingValue(
|
||||
text: "1000₽",
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
builder: (context, state) {
|
||||
final optionNameHintText = switch (state.selectedOptionIndex ?? 1) {
|
||||
0 => s.economy,
|
||||
2 => s.comfort,
|
||||
_ => s.standard,
|
||||
};
|
||||
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
|
||||
|
||||
optionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
optionDescriptionController.text = state.planData.options[selectedOptionIndex].description;
|
||||
return MainBackgroundBouble(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
s.option_title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: optionNameHintText,
|
||||
controller: optionTitleController,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionTitle(value);
|
||||
},
|
||||
maxLength: 150,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
s.option_desc,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: CustomTextFormField(
|
||||
controller: optionDescriptionController,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
maxLength: 1000,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
hintText: s.option_desc_hint_text,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionDescription(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.you_will_get,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
_PriceExplanationWidget(),
|
||||
],
|
||||
),
|
||||
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);
|
||||
}
|
||||
},
|
||||
maxLength: 20,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.customer_price,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
_PriceExplanationWidget(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: customerPriceController,
|
||||
textInputType: TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (value) {
|
||||
final price = double.tryParse(value.replaceAll('₽', ''));
|
||||
if (price != null) {
|
||||
creatorPriceController.text = (price * 0.8).toStringAsFixed(2);
|
||||
context.read<CreatePlanCubit>().setPrice(price);
|
||||
}
|
||||
},
|
||||
maxLength: 20,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
s.minimal_customer_price,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CustomDivider(),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
s.option_content,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
reverse: false,
|
||||
itemBuilder: (context, index) {
|
||||
final items = state.planData.options[selectedOptionIndex].items;
|
||||
|
||||
if (items.isEmpty) {
|
||||
return AddItemWidget(
|
||||
index: index,
|
||||
canBeDeleted: false,
|
||||
enableHint: true,
|
||||
items: items,
|
||||
);
|
||||
} else {
|
||||
if (index < items.length) {
|
||||
return AddItemWidget(
|
||||
canBeDeleted: true,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
);
|
||||
} else {
|
||||
return AddItemWidget(
|
||||
canBeDeleted: false,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemCount: state.planData.options[selectedOptionIndex].items.length + 1,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CustomDivider(),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
activeColor: appTheme.gray,
|
||||
checkColor: appTheme.gray,
|
||||
fillColor: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PriceExplanationWidget extends StatefulWidget {
|
||||
const _PriceExplanationWidget();
|
||||
|
||||
@override
|
||||
State<_PriceExplanationWidget> createState() => _PriceExplanationWidgetState();
|
||||
}
|
||||
|
||||
class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
bool visible = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return PortalTarget(
|
||||
closeDuration: Duration(milliseconds: 300),
|
||||
portalFollower: AnimatedContainer(
|
||||
padding: EdgeInsets.all(10),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: visible ? appTheme.black : Colors.transparent,
|
||||
),
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: TextStyles.textMMediumAuto.copyWith(
|
||||
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
s.price_explanation,
|
||||
),
|
||||
),
|
||||
),
|
||||
visible: visible,
|
||||
anchor: Aligned(
|
||||
shiftToWithinBound: AxisFlag(x: true, y: true),
|
||||
follower: Alignment.bottomLeft,
|
||||
target: Alignment.topRight,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
visible = !visible;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset('assets/icons/question_mark.svg', height: 12, width: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class _AnimatedOptionButton extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
@@ -69,7 +69,7 @@ class _AnimatedOptionButton extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
style: TextStyles.textMSemibold.copyWith(
|
||||
color: isSelected ? appTheme.whiteA700 : appTheme.black,
|
||||
),
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SelectExactOptionWidget extends StatelessWidget {
|
||||
class SelectExactOptionWidget extends StatefulWidget {
|
||||
const SelectExactOptionWidget({super.key});
|
||||
|
||||
@override
|
||||
State<SelectExactOptionWidget> createState() => _SelectExactOptionWidgetState();
|
||||
}
|
||||
|
||||
class _SelectExactOptionWidgetState extends State<SelectExactOptionWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 140,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.black,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_OptionButton(isSelected: true, text: '1'),
|
||||
_OptionButton(isSelected: true, text: '2'),
|
||||
_OptionButton(isSelected: true, text: '3'),
|
||||
],
|
||||
),
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
builder: (context, state) {
|
||||
final selectedIndex = state.selectedOptionIndex;
|
||||
return Container(
|
||||
padding: EdgeInsets.all(4),
|
||||
width: 140,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.black,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
3,
|
||||
(index) => Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreatePlanCubit>().setSelectedOptionIndex(index);
|
||||
},
|
||||
child: _OptionButton(isSelected: selectedIndex == index, text: '${index + 1}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,13 +52,21 @@ class _OptionButton extends StatelessWidget {
|
||||
final String text;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
color: isSelected ? appTheme.whiteA700 : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(text),
|
||||
child: Center(
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
style: TextStyle(color: isSelected ? appTheme.black : appTheme.whiteA700),
|
||||
child: Text(
|
||||
text,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user