forked from eatvio/eatvio_app
feat: Introduce user-generated content features, including recipe creation and management, and blog post creation.
This commit is contained in:
@@ -568,6 +568,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
|
||||
const SizedBox(height: 80),
|
||||
_createBlogPostButton(),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -134,7 +134,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
||||
(widget.recipe != null && widget.recipe?.state != 'drafted') ? 'Обновить' : 'Опубликовать',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
@@ -180,7 +180,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Сохранить как черновик',
|
||||
widget.recipe != null ? 'Обновить черновик' : 'Сохранить как черновик',
|
||||
style: TextStyle(fontSize: 16, color: appTheme.blueA400, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/models/step.dart' as cr_step;
|
||||
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as cr_ingredient;
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -83,3 +84,26 @@ extension MyRecipeModelX on MyRecipeModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension RecipeToRecipeModelX on Recipe {
|
||||
RecipeModel toRecipeModel() {
|
||||
return RecipeModel(
|
||||
title: title,
|
||||
description: description,
|
||||
image: image,
|
||||
totalCookingTime: cookingTime,
|
||||
activeCookTime: activeCookingTime,
|
||||
energyPerServing: calories.toInt(),
|
||||
videoUrl: videoUrl,
|
||||
state: 'active',
|
||||
steps: steps?.map((e) => cr_step.Step(image: e.image, description: e.description)).toList(),
|
||||
ingredients: ingredients?.map((e) => cr_ingredient.Ingredient(
|
||||
name: e.name,
|
||||
portion: e.portion,
|
||||
unit: e.unit ?? '',
|
||||
productId: e.productId,
|
||||
)).toList() ?? [],
|
||||
tags: tags.map((e) => e.name).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,39 +33,40 @@ class MyRecipesScreen extends StatelessWidget {
|
||||
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
|
||||
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
|
||||
builder: (BuildContext context, MyRecipesState state) {
|
||||
return Column(
|
||||
return Stack(
|
||||
children: [
|
||||
MyRecipesTab(
|
||||
selectedIndex: state is MyRecipesStateLoaded
|
||||
? (state.selectedState == MyRecipesCubit.drafted ? 1 : 0)
|
||||
: 0,
|
||||
onTabChanged: (index) {
|
||||
if (index == 0) {
|
||||
context.read<MyRecipesCubit>().setActiveState();
|
||||
} else {
|
||||
context.read<MyRecipesCubit>().setDraftedState();
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
Positioned.fill(
|
||||
child: state is MyRecipesStateLoaded
|
||||
? state.recipes.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
state.selectedState == MyRecipesCubit.drafted
|
||||
? 'Нет черновиков'
|
||||
: 'Нет опубликованных рецептов',
|
||||
style: TextStyles.textLMedium.copyWith(color: appTheme.gray500),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.recipes.length,
|
||||
)
|
||||
? Center(
|
||||
child: Text(
|
||||
state.selectedState == MyRecipesCubit.drafted ? 'Нет черновиков' : 'Нет опубликованных рецептов',
|
||||
style: TextStyles.textLMedium.copyWith(color: appTheme.gray500),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.only(top: 80, bottom: 16),
|
||||
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.recipes.length,
|
||||
)
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: MyRecipesTab(
|
||||
selectedIndex: state is MyRecipesStateLoaded ? (state.selectedState == MyRecipesCubit.drafted ? 1 : 0) : 0,
|
||||
onTabChanged: (index) {
|
||||
if (index == 0) {
|
||||
context.read<MyRecipesCubit>().setActiveState();
|
||||
} else {
|
||||
context.read<MyRecipesCubit>().setDraftedState();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -145,8 +146,17 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
Expanded(
|
||||
child: isDraft
|
||||
? CustomOutlinedButton(
|
||||
onPressed: () {
|
||||
context.push(RecipeCreateScreen.path, extra: widget.recipe.toRecipe());
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black12,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
final fullRecipe = await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // close dialog
|
||||
context.push(RecipeCreateScreen.path, extra: fullRecipe);
|
||||
}
|
||||
},
|
||||
height: 52,
|
||||
buttonStyle: ButtonStyle(
|
||||
@@ -180,27 +190,6 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isDraft)
|
||||
PopupMenuItem(
|
||||
onTap: () async {
|
||||
final result = await context.read<MyRecipesCubit>().draftRecipe(widget.recipe.id);
|
||||
if (result) {
|
||||
showSuccessToast('Рецепт перенесен в черновики');
|
||||
if (context.mounted) {
|
||||
context.read<MyRecipesCubit>().getMyRecipes();
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Не удалось перенести в черновики');
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.archive_outlined, size: 20, color: appTheme.black),
|
||||
const SizedBox(width: 8),
|
||||
const Text('В черновик', style: TextStyles.textSMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () {},
|
||||
@@ -225,21 +214,36 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
width: double.infinity,
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
// Publish draft
|
||||
final recipeModel = widget.recipe.toRecipeModel();
|
||||
final success = await context.read<MyRecipesCubit>().publishDraft(recipeModel, widget.recipe.slug);
|
||||
if (success && context.mounted) {
|
||||
showSuccessToast('Черновик опубликован');
|
||||
context.read<MyRecipesCubit>().setDraftedState();
|
||||
} else {
|
||||
showErrorToast('Заполните все необходимые поля для публикации рецепта');
|
||||
if (context.mounted) {
|
||||
context.push(RecipeCreateScreen.path, extra: {
|
||||
'recipe': widget.recipe.toRecipe(),
|
||||
'autoValidate': true,
|
||||
});
|
||||
}
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black12,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
final fullRecipe = await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug);
|
||||
final createCubit = sl<CreateRecipeCubit>();
|
||||
await createCubit.recipeToEntity(fullRecipe);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
if (createCubit.validateAll()) {
|
||||
final success = await createCubit.publishRecipe(true, widget.recipe.slug);
|
||||
if (success && context.mounted) {
|
||||
showSuccessToast('Черновик опубликован');
|
||||
context.read<MyRecipesCubit>().setDraftedState();
|
||||
} else {
|
||||
showErrorToast('Не удалось опубликовать черновик');
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Заполните все необходимые поля для публикации рецепта');
|
||||
if (context.mounted) {
|
||||
context.push(RecipeCreateScreen.path, extra: {
|
||||
'recipe': fullRecipe,
|
||||
'autoValidate': true,
|
||||
});
|
||||
}
|
||||
}
|
||||
await createCubit.close();
|
||||
}
|
||||
},
|
||||
height: 52,
|
||||
buttonStyle: ButtonStyle(
|
||||
@@ -252,6 +256,35 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isDraft)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await context.read<MyRecipesCubit>().draftRecipe(widget.recipe.id);
|
||||
if (result) {
|
||||
showSuccessToast('Рецепт перенесен в черновики');
|
||||
if (context.mounted) {
|
||||
context.read<MyRecipesCubit>().getMyRecipes();
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Не удалось перенести в черновики');
|
||||
}
|
||||
},
|
||||
height: 52,
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: 'Снять с публикации',
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user