mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat(frontend): ✨ Add Recipe Editor to Assign Units and Foods for Recipe Scaling
This commit is contained in:
		| @@ -2,9 +2,9 @@ | ||||
|   <div class="text-center"> | ||||
|     <v-dialog v-model="dialog" width="600"> | ||||
|       <template #activator="{ on, attrs }"> | ||||
|         <v-btn color="secondary lighten-2" dark v-bind="attrs" v-on="on" @click="inputText = ''"> | ||||
|         <BaseButton v-bind="attrs" v-on="on" @click="inputText = ''"> | ||||
|           {{ $t("new-recipe.bulk-add") }} | ||||
|         </v-btn> | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|  | ||||
|       <v-card> | ||||
|   | ||||
							
								
								
									
										122
									
								
								frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-text-field | ||||
|       v-if="value.title || showTitle" | ||||
|       v-model="value.title" | ||||
|       dense | ||||
|       hide-details | ||||
|       class="mx-1 mb-4" | ||||
|       placeholder="Section Title" | ||||
|       style="max-width: 500px" | ||||
|     > | ||||
|     </v-text-field> | ||||
|     <v-row :no-gutters="$vuetify.breakpoint.mdAndUp" dense class="d-flex flex-wrap my-1"> | ||||
|       <v-col v-if="!disableAmount" sm="12" md="2" cols="12" class="flex-grow-0 flex-shrink-0"> | ||||
|         <v-text-field | ||||
|           v-model="value.quantity" | ||||
|           solo | ||||
|           hide-details | ||||
|           dense | ||||
|           class="mx-1" | ||||
|           type="number" | ||||
|           placeholder="Quantity" | ||||
|         > | ||||
|           <v-icon slot="prepend" class="mr-n1" color="error" @click="$emit('delete')"> | ||||
|             {{ $globals.icons.delete }} | ||||
|           </v-icon> | ||||
|         </v-text-field> | ||||
|       </v-col> | ||||
|       <v-col v-if="!disableAmount && units" sm="12" md="3" cols="12"> | ||||
|         <v-select | ||||
|           v-model="value.unit" | ||||
|           hide-details | ||||
|           dense | ||||
|           solo | ||||
|           return-object | ||||
|           :items="units" | ||||
|           item-text="name" | ||||
|           class="mx-1" | ||||
|           placeholder="Choose Unit" | ||||
|         > | ||||
|         </v-select> | ||||
|       </v-col> | ||||
|       <v-col v-if="!disableAmount && foods" m="12" md="3" cols="12" class=""> | ||||
|         <v-select | ||||
|           v-model="value.food" | ||||
|           hide-details | ||||
|           dense | ||||
|           solo | ||||
|           return-object | ||||
|           :items="foods" | ||||
|           item-text="name" | ||||
|           class="mx-1 py-0" | ||||
|           placeholder="Choose Food" | ||||
|         > | ||||
|         </v-select> | ||||
|       </v-col> | ||||
|       <v-col sm="12" md="" cols="12"> | ||||
|         <v-text-field v-model="value.note" hide-details dense solo class="mx-1" placeholder="Notes"> | ||||
|           <v-icon v-if="disableAmount" slot="prepend" class="mr-n1" color="error" @click="$emit('delete')"> | ||||
|             {{ $globals.icons.delete }} | ||||
|           </v-icon> | ||||
|           <template slot="append"> | ||||
|             <v-tooltip top nudge-right="10"> | ||||
|               <template #activator="{ on, attrs }"> | ||||
|                 <v-btn icon small class="mt-n1" v-bind="attrs" v-on="on" @click="toggleTitle()"> | ||||
|                   <v-icon>{{ showTitle || value.title ? $globals.icons.minus : $globals.icons.createAlt }}</v-icon> | ||||
|                 </v-btn> | ||||
|               </template> | ||||
|               <span>{{ showTitle ? $t("recipe.remove-section") : $t("recipe.insert-section") }}</span> | ||||
|             </v-tooltip> | ||||
|           </template> | ||||
|           <template slot="append-outer"> | ||||
|             <v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon> | ||||
|           </template> | ||||
|         </v-text-field> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|     <v-divider v-if="!$vuetify.breakpoint.mdAndUp" class="my-4"></v-divider> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs } from "@nuxtjs/composition-api"; | ||||
| import { useFoods } from "~/composables/use-recipe-foods"; | ||||
| import { useUnits } from "~/composables/use-recipe-units"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   props: { | ||||
|     value: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|     disableAmount: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   setup(props) { | ||||
|     const { value } = props; | ||||
|  | ||||
|     const { foods } = useFoods(); | ||||
|     const { units } = useUnits(); | ||||
|  | ||||
|     const state = reactive({ | ||||
|       showTitle: false, | ||||
|     }); | ||||
|  | ||||
|     function toggleTitle() { | ||||
|       if (value.title) { | ||||
|         state.showTitle = false; | ||||
|         value.title = ""; | ||||
|       } else { | ||||
|         state.showTitle = true; | ||||
|         value.title = "Section Title"; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return { foods, units, ...toRefs(state), toggleTitle }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| @@ -89,7 +89,7 @@ export default { | ||||
|  | ||||
|     edit: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|   | ||||
| @@ -32,7 +32,7 @@ | ||||
|               class="ma-1" | ||||
|               :class="[{ 'on-hover': hover }, isChecked(index)]" | ||||
|               :elevation="hover ? 12 : 2" | ||||
|               :ripple="!edit" | ||||
|               :ripple="false" | ||||
|               @click="toggleDisabled(index)" | ||||
|             > | ||||
|               <v-card-title :class="{ 'pb-0': !isChecked(index) }"> | ||||
|   | ||||
| @@ -96,30 +96,6 @@ export default defineComponent({ | ||||
|           restricted: false, | ||||
|           nav: "/user/login", | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.calendarWeek, | ||||
|           title: this.$t("meal-plan.dinner-this-week"), | ||||
|           nav: "/meal-plan/this-week", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.calendarToday, | ||||
|           title: this.$t("meal-plan.dinner-today"), | ||||
|           nav: "/meal-plan/today", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.calendarMultiselect, | ||||
|           title: this.$t("meal-plan.planner"), | ||||
|           nav: "/meal-plan/planner", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.formatListCheck, | ||||
|           title: this.$t("shopping-list.shopping-lists"), | ||||
|           nav: "/shopping-list", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.logout, | ||||
|           title: this.$t("user.logout"), | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <v-navigation-drawer :value="value" clipped app> | ||||
|   <v-navigation-drawer :value="value" clipped app width="240px"> | ||||
|     <!-- User Profile --> | ||||
|     <template v-if="$auth.user"> | ||||
|       <v-list-item two-line to="/user/profile"> | ||||
| @@ -16,16 +16,47 @@ | ||||
|     </template> | ||||
|  | ||||
|     <!-- Primary Links --> | ||||
|     <template v-if="topLink"> | ||||
|       <v-list nav dense> | ||||
|       <v-list-item-group v-model="topSelected" color="primary"> | ||||
|         <v-list-item v-for="nav in topLink" :key="nav.title" exact link :to="nav.to"> | ||||
|         <template v-for="nav in topLink"> | ||||
|           <!-- Multi Items --> | ||||
|           <v-list-group | ||||
|             v-if="nav.children && ($auth.loggedIn || !nav.restricted)" | ||||
|             :key="nav.title + 'multi-item'" | ||||
|             v-model="dropDowns[nav.title]" | ||||
|             color="primary" | ||||
|             :prepend-icon="nav.icon" | ||||
|           > | ||||
|             <template #activator> | ||||
|               <v-list-item-title>{{ nav.title }}</v-list-item-title> | ||||
|             </template> | ||||
|  | ||||
|             <v-list-item v-for="child in nav.children" :key="child.title" :to="child.to"> | ||||
|               <v-list-item-icon> | ||||
|                 <v-icon>{{ child.icon }}</v-icon> | ||||
|               </v-list-item-icon> | ||||
|               <v-list-item-title>{{ child.title }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
|             <v-divider class="mb-4"></v-divider> | ||||
|           </v-list-group> | ||||
|  | ||||
|           <!-- Single Item --> | ||||
|           <v-list-item-group | ||||
|             v-else-if="$auth.loggedIn || !nav.restricted" | ||||
|             :key="nav.title + 'single-item'" | ||||
|             v-model="secondarySelected" | ||||
|             color="primary" | ||||
|           > | ||||
|             <v-list-item link :to="nav.to"> | ||||
|               <v-list-item-icon> | ||||
|                 <v-icon>{{ nav.icon }}</v-icon> | ||||
|               </v-list-item-icon> | ||||
|               <v-list-item-title>{{ nav.title }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
|           </v-list-item-group> | ||||
|         </template> | ||||
|       </v-list> | ||||
|     </template> | ||||
|  | ||||
|     <!-- Secondary Links --> | ||||
|     <template v-if="secondaryLinks"> | ||||
| @@ -51,6 +82,7 @@ | ||||
|               </v-list-item-icon> | ||||
|               <v-list-item-title>{{ child.title }}</v-list-item-title> | ||||
|             </v-list-item> | ||||
|             <v-divider class="mb-4"></v-divider> | ||||
|           </v-list-group> | ||||
|  | ||||
|           <!-- Single Item --> | ||||
|   | ||||
| @@ -37,20 +37,46 @@ export default defineComponent({ | ||||
|       sidebar: null, | ||||
|       topLinks: [ | ||||
|         { | ||||
|           icon: this.$globals.icons.home, | ||||
|           to: "/", | ||||
|           title: this.$t("sidebar.home-page"), | ||||
|           icon: this.$globals.icons.calendar, | ||||
|           restricted: true, | ||||
|           title: this.$t("meal-plan.meal-planner"), | ||||
|           children: [ | ||||
|             { | ||||
|               icon: this.$globals.icons.calendarMultiselect, | ||||
|               title: this.$t("meal-plan.planner"), | ||||
|               to: "/meal-plan/planner", | ||||
|               restricted: true, | ||||
|             }, | ||||
|             { | ||||
|           icon: this.$globals.icons.search, | ||||
|           to: "/search", | ||||
|           title: this.$t("sidebar.search"), | ||||
|               icon: this.$globals.icons.calendarWeek, | ||||
|               title: this.$t("meal-plan.dinner-this-week"), | ||||
|               to: "/meal-plan/this-week", | ||||
|               restricted: true, | ||||
|             }, | ||||
|             { | ||||
|               icon: this.$globals.icons.calendarToday, | ||||
|               title: this.$t("meal-plan.dinner-today"), | ||||
|               to: "/meal-plan/today", | ||||
|               restricted: true, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.formatListCheck, | ||||
|           title: this.$t("shopping-list.shopping-lists"), | ||||
|           to: "/shopping-list", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.viewModule, | ||||
|           to: "/recipes/all", | ||||
|           title: this.$t("sidebar.all-recipes"), | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.search, | ||||
|           to: "/search", | ||||
|           title: this.$t("sidebar.search"), | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.tags, | ||||
|           to: "/recipes/categories", | ||||
|   | ||||
| @@ -1,13 +1,30 @@ | ||||
| <template> | ||||
|   <v-app dark> | ||||
|     <h1 v-if="error.statusCode === 404"> | ||||
|       {{ pageNotFound }} | ||||
|     </h1> | ||||
|     <h1 v-else> | ||||
|       {{ otherError }} | ||||
|     </h1> | ||||
|     <NuxtLink to="/"> Home page </NuxtLink> | ||||
|   </v-app> | ||||
|   <div> | ||||
|     <v-card-title> | ||||
|       <slot> | ||||
|         <h1 class="mx-auto">{{ $t("page.404-page-not-found") }}</h1> | ||||
|       </slot> | ||||
|     </v-card-title> | ||||
|     <div class="d-flex justify-space-around"> | ||||
|       <div class="d-flex align-center"> | ||||
|         <p class="primary--text">4</p> | ||||
|         <v-icon color="primary" class="mx-auto mb-0" size="200"> | ||||
|           {{ $globals.icons.primary }} | ||||
|         </v-icon> | ||||
|         <p class="primary--text">4</p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <v-card-actions> | ||||
|       <v-spacer></v-spacer> | ||||
|       <slot name="actions"> | ||||
|         <v-btn v-for="(button, index) in buttons" :key="index" nuxt :to="button.to" color="primary"> | ||||
|           <v-icon left> {{ button.icon }} </v-icon> | ||||
|           {{ button.text }} | ||||
|         </v-btn> | ||||
|       </slot> | ||||
|       <v-spacer></v-spacer> | ||||
|     </v-card-actions> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -31,6 +48,15 @@ export default { | ||||
|       title, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     buttons() { | ||||
|       return [ | ||||
|         { icon: this.$globals.icons.home, to: "/", text: this.$t("general.home") }, | ||||
|         { icon: this.$globals.icons.primary, to: "/recipes/all", text: this.$t("page.all-recipes") }, | ||||
|         { icon: this.$globals.icons.search, to: "/search", text: this.$t("search.search") }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @@ -38,4 +64,10 @@ export default { | ||||
| h1 { | ||||
|   font-size: 20px; | ||||
| } | ||||
| p { | ||||
|   padding-bottom: 0 !important; | ||||
|   margin-bottom: 0 !important; | ||||
|   font-size: 200px; | ||||
| } | ||||
| </style> | ||||
|  | ||||
|   | ||||
| @@ -97,6 +97,24 @@ | ||||
|             </v-textarea> | ||||
|           </template> | ||||
|  | ||||
|           <!-- Advanced Editor --> | ||||
|           <div v-if="form"> | ||||
|             <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> | ||||
|             <draggable v-model="recipe.recipeIngredient" handle=".handle"> | ||||
|               <RecipeIngredientEditor | ||||
|                 v-for="(ingredient, index) in recipe.recipeIngredient" | ||||
|                 :key="index + 'ing-editor'" | ||||
|                 v-model="recipe.recipeIngredient[index]" | ||||
|                 :disable-amount="recipe.settings.disableAmount" | ||||
|                 @delete="removeByIndex(recipe.recipeIngredient, index)" | ||||
|               /> | ||||
|             </draggable> | ||||
|             <div class="d-flex justify-end mt-2"> | ||||
|               <RecipeDialogBulkAdd class="mr-2" @bulk-data="addIngredient" /> | ||||
|               <BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <div class="d-flex justify-space-between align-center pb-3"> | ||||
|             <v-btn | ||||
|               v-if="recipe.recipeYield" | ||||
| @@ -121,24 +139,43 @@ | ||||
|           </div> | ||||
|           <v-row> | ||||
|             <v-col cols="12" sm="12" md="4" lg="4"> | ||||
|               <RecipeIngredients :value="recipe.recipeIngredient" :edit="form" /> | ||||
|               <div v-if="$vuetify.breakpoint.mdAndUp"> | ||||
|                 <v-card v-if="recipe.recipeCategory.length > 0" class="mt-2"> | ||||
|               <RecipeIngredients v-if="!form" :value="recipe.recipeIngredient" /> | ||||
|  | ||||
|               <!-- Recipe Categories --> | ||||
|               <div v-if="$vuetify.breakpoint.mdAndUp" class="mt-5"> | ||||
|                 <v-card v-if="recipe.recipeCategory.length > 0 || form" class="mt-2"> | ||||
|                   <v-card-title class="py-2"> | ||||
|                     {{ $t("recipe.categories") }} | ||||
|                   </v-card-title> | ||||
|                   <v-divider class="mx-2"></v-divider> | ||||
|                   <v-card-text> | ||||
|                     <RecipeChips :items="recipe.recipeCategory" /> | ||||
|                     <RecipeCategoryTagSelector | ||||
|                       v-if="form" | ||||
|                       v-model="recipe.recipeCategory" | ||||
|                       :return-object="false" | ||||
|                       :show-add="true" | ||||
|                       :show-label="false" | ||||
|                     /> | ||||
|                     <RecipeChips v-else :items="recipe.recipeCategory" /> | ||||
|                   </v-card-text> | ||||
|                 </v-card> | ||||
|                 <v-card v-if="recipe.tags.length > 0" class="mt-2"> | ||||
|  | ||||
|                 <!-- Recipe Tags --> | ||||
|                 <v-card v-if="recipe.tags.length > 0 || form" class="mt-2"> | ||||
|                   <v-card-title class="py-2"> | ||||
|                     {{ $t("tag.tags") }} | ||||
|                   </v-card-title> | ||||
|                   <v-divider class="mx-2"></v-divider> | ||||
|                   <v-card-text> | ||||
|                     <RecipeChips :items="recipe.tags" :is-category="false" /> | ||||
|                     <RecipeCategoryTagSelector | ||||
|                       v-if="form" | ||||
|                       v-model="recipe.tags" | ||||
|                       :return-object="false" | ||||
|                       :show-add="true" | ||||
|                       :tag-selector="true" | ||||
|                       :show-label="false" | ||||
|                     /> | ||||
|                     <RecipeChips v-else :items="recipe.tags" :is-category="false" /> | ||||
|                   </v-card-text> | ||||
|                 </v-card> | ||||
|  | ||||
| @@ -168,6 +205,9 @@ | ||||
| import { defineComponent, ref, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api"; | ||||
| // @ts-ignore | ||||
| import VueMarkdown from "@adapttive/vue-markdown"; | ||||
| import draggable from "vuedraggable"; | ||||
| import RecipeCategoryTagSelector from "@/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; | ||||
| import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
| import { useRecipeContext } from "~/composables/use-recipe-context"; | ||||
| @@ -182,23 +222,28 @@ import RecipeInstructions from "~/components/Domain/Recipe/RecipeInstructions.vu | ||||
| import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue"; | ||||
| import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue"; | ||||
| import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue"; | ||||
| import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue"; | ||||
| import { Recipe } from "~/types/api-types/admin"; | ||||
| import { useStaticRoutes } from "~/composables/api"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { | ||||
|     RecipeActionMenu, | ||||
|     RecipeDialogBulkAdd, | ||||
|     RecipeAssets, | ||||
|     RecipeCategoryTagSelector, | ||||
|     RecipeChips, | ||||
|     RecipeImageUploadBtn, | ||||
|     RecipeIngredients, | ||||
|     RecipeInstructions, | ||||
|     RecipeNotes, | ||||
|     RecipeNutrition, | ||||
|     RecipeRating, | ||||
|     RecipeTimeCard, | ||||
|     RecipeImageUploadBtn, | ||||
|     RecipeSettingsMenu, | ||||
|     RecipeIngredientEditor, | ||||
|     RecipeTimeCard, | ||||
|     VueMarkdown, | ||||
|     draggable, | ||||
|   }, | ||||
|   setup() { | ||||
|     const route = useRoute(); | ||||
| @@ -243,6 +288,38 @@ export default defineComponent({ | ||||
|       imageKey.value++; | ||||
|     } | ||||
|  | ||||
|     function removeByIndex(list: Array<any>, index: number) { | ||||
|       list.splice(index, 1); | ||||
|     } | ||||
|  | ||||
|     function addIngredient(ingredients: Array<string> | null = null) { | ||||
|       if (ingredients?.length) { | ||||
|         const newIngredients = ingredients.map((x) => { | ||||
|           return { | ||||
|             title: "", | ||||
|             note: x, | ||||
|             unit: {}, | ||||
|             food: {}, | ||||
|             disableAmount: true, | ||||
|             quantity: 1, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         if (newIngredients) { | ||||
|           recipe?.value?.recipeIngredient?.push(...newIngredients); | ||||
|         } | ||||
|       } else { | ||||
|         recipe?.value?.recipeIngredient?.push({ | ||||
|           title: "", | ||||
|           note: "", | ||||
|           unit: {}, | ||||
|           food: {}, | ||||
|           disableAmount: true, | ||||
|           quantity: 1, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       imageKey, | ||||
|       recipe, | ||||
| @@ -254,6 +331,8 @@ export default defineComponent({ | ||||
|       uploadImage, | ||||
|       validators, | ||||
|       recipeImage, | ||||
|       addIngredient, | ||||
|       removeByIndex, | ||||
|     }; | ||||
|   }, | ||||
|   data() { | ||||
|   | ||||
| @@ -56,15 +56,15 @@ export interface Recipe { | ||||
|   name: string; | ||||
|   slug: string; | ||||
|   image?: unknown; | ||||
|   description?: string; | ||||
|   recipeCategory?: string[]; | ||||
|   tags?: string[]; | ||||
|   rating?: number; | ||||
|   description: string; | ||||
|   recipeCategory: string[]; | ||||
|   tags: string[]; | ||||
|   rating: number; | ||||
|   dateAdded?: string; | ||||
|   dateUpdated?: string; | ||||
|   recipeYield?: string; | ||||
|   recipeIngredient?: RecipeIngredient[]; | ||||
|   recipeInstructions?: RecipeStep[]; | ||||
|   recipeIngredient: RecipeIngredient[]; | ||||
|   recipeInstructions: RecipeStep[]; | ||||
|   nutrition?: Nutrition; | ||||
|   tools?: string[]; | ||||
|   totalTime?: string; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user