mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: Migrate to Nuxt 3 framework (#5184)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
		| @@ -8,6 +8,7 @@ | ||||
|       :title="$t('recipe.delete-recipe')" | ||||
|       color="error" | ||||
|       :icon="$globals.icons.alertCircle" | ||||
|       can-confirm | ||||
|       @confirm="deleteRecipe()" | ||||
|     > | ||||
|       <v-card-text> | ||||
| @@ -19,16 +20,17 @@ | ||||
|       :title="$t('recipe.duplicate')" | ||||
|       color="primary" | ||||
|       :icon="$globals.icons.duplicate" | ||||
|       can-confirm | ||||
|       @confirm="duplicateRecipe()" | ||||
|     > | ||||
|       <v-card-text> | ||||
|         <v-text-field | ||||
|           v-model="recipeName" | ||||
|           dense | ||||
|           density="compact" | ||||
|           :label="$t('recipe.recipe-name')" | ||||
|           autofocus | ||||
|           @keyup.enter="duplicateRecipe()" | ||||
|         ></v-text-field> | ||||
|         /> | ||||
|       </v-card-text> | ||||
|     </BaseDialog> | ||||
|     <BaseDialog | ||||
| @@ -36,6 +38,7 @@ | ||||
|       :title="$t('recipe.add-recipe-to-mealplan')" | ||||
|       color="primary" | ||||
|       :icon="$globals.icons.calendar" | ||||
|       can-confirm | ||||
|       @confirm="addRecipeToPlan()" | ||||
|     > | ||||
|       <v-card-text> | ||||
| @@ -47,22 +50,21 @@ | ||||
|           max-width="290px" | ||||
|           min-width="auto" | ||||
|         > | ||||
|           <template #activator="{ on, attrs }"> | ||||
|           <template #activator="{ props }"> | ||||
|             <v-text-field | ||||
|               v-model="newMealdate" | ||||
|               v-model="newMealdateString" | ||||
|               :label="$t('general.date')" | ||||
|               :prepend-icon="$globals.icons.calendar" | ||||
|               v-bind="attrs" | ||||
|               v-bind="props" | ||||
|               readonly | ||||
|               v-on="on" | ||||
|             ></v-text-field> | ||||
|             /> | ||||
|           </template> | ||||
|           <v-date-picker | ||||
|             v-model="newMealdate" | ||||
|             no-title | ||||
|             hide-header | ||||
|             :first-day-of-week="firstDayOfWeek" | ||||
|             :local="$i18n.locale" | ||||
|             @input="pickerMenu = false" | ||||
|              @update:model-value="pickerMenu = false" | ||||
|           /> | ||||
|         </v-menu> | ||||
|         <v-select | ||||
| @@ -70,7 +72,9 @@ | ||||
|           :return-object="false" | ||||
|           :items="planTypeOptions" | ||||
|           :label="$t('recipe.entry-type')" | ||||
|         ></v-select> | ||||
|           item-title="text" | ||||
|           item-value="value" | ||||
|         /> | ||||
|       </v-card-text> | ||||
|     </BaseDialog> | ||||
|     <RecipeDialogAddToShoppingList | ||||
| @@ -81,35 +85,53 @@ | ||||
|     /> | ||||
|     <v-menu | ||||
|       offset-y | ||||
|       left | ||||
|       start | ||||
|       :bottom="!menuTop" | ||||
|       :nudge-bottom="!menuTop ? '5' : '0'" | ||||
|       :top="menuTop" | ||||
|       :nudge-top="menuTop ? '5' : '0'" | ||||
|       allow-overflow | ||||
|       close-delay="125" | ||||
|       :open-on-hover="$vuetify.breakpoint.mdAndUp" | ||||
|       :open-on-hover="$vuetify.display.mdAndUp" | ||||
|       content-class="d-print-none" | ||||
|     > | ||||
|       <template #activator="{ on, attrs }"> | ||||
|         <v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent> | ||||
|           <v-icon>{{ icon }}</v-icon> | ||||
|       <template #activator="{ props }"> | ||||
|         <v-btn | ||||
|           icon | ||||
|           :variant="fab ? 'flat' : undefined" | ||||
|           :rounded="fab ? 'circle' : undefined" | ||||
|           :size="fab ? 'small' : undefined" | ||||
|           :color="fab ? 'info' : 'secondary'" | ||||
|           :fab="fab" | ||||
|           v-bind="props" | ||||
|           @click.prevent | ||||
|         > | ||||
|           <v-icon | ||||
|           :size="!fab ? undefined : 'x-large'" | ||||
|           :color="fab ? 'white' : 'secondary'" | ||||
|         > | ||||
|           {{ icon }} | ||||
|         </v-icon> | ||||
|         </v-btn> | ||||
|       </template> | ||||
|       <v-list dense> | ||||
|       <v-list density="compact"> | ||||
|         <v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)"> | ||||
|           <v-list-item-icon> | ||||
|             <v-icon :color="item.color"> {{ item.icon }} </v-icon> | ||||
|           </v-list-item-icon> | ||||
|           <template #prepend> | ||||
|             <v-icon :color="item.color"> | ||||
|               {{ item.icon }} | ||||
|             </v-icon> | ||||
|           </template> | ||||
|           <v-list-item-title>{{ item.title }}</v-list-item-title> | ||||
|         </v-list-item> | ||||
|         <div v-if="useItems.recipeActions && recipeActions && recipeActions.length"> | ||||
|           <v-divider /> | ||||
|           <v-list-group @click.stop> | ||||
|             <template #activator> | ||||
|               <v-list-item-title>{{ $tc("recipe.recipe-actions") }}</v-list-item-title> | ||||
|             <template #activator="{ props }"> | ||||
|               <v-list-item-title v-bind="props"> | ||||
|                 {{ $t("recipe.recipe-actions") }} | ||||
|               </v-list-item-title> | ||||
|             </template> | ||||
|             <v-list dense class="ma-0 pa-0"> | ||||
|             <v-list density="compact" class="ma-0 pa-0"> | ||||
|               <v-list-item | ||||
|                 v-for="(action, index) in recipeActions" | ||||
|                 :key="index" | ||||
| @@ -129,7 +151,6 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, reactive, toRefs, useContext, useRoute, useRouter, ref } from "@nuxtjs/composition-api"; | ||||
| import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue"; | ||||
| import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue"; | ||||
| import RecipeDialogShare from "./RecipeDialogShare.vue"; | ||||
| @@ -139,15 +160,16 @@ import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions"; | ||||
| import { useHouseholdSelf } from "~/composables/use-households"; | ||||
| import { alert } from "~/composables/use-toast"; | ||||
| import { usePlanTypeOptions } from "~/composables/use-group-mealplan"; | ||||
| import { Recipe } from "~/lib/api/types/recipe"; | ||||
| import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/household"; | ||||
| import { PlanEntryType } from "~/lib/api/types/meal-plan"; | ||||
| import { useAxiosDownloader } from "~/composables/api/use-axios-download"; | ||||
| import type { Recipe } from "~/lib/api/types/recipe"; | ||||
| import type { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/household"; | ||||
| import type { PlanEntryType } from "~/lib/api/types/meal-plan"; | ||||
| import { useDownloader } from "~/composables/api/use-downloader"; | ||||
|  | ||||
| export interface ContextMenuIncludes { | ||||
|   delete: boolean; | ||||
|   edit: boolean; | ||||
|   download: boolean; | ||||
|   duplicate: boolean; | ||||
|   mealplanner: boolean; | ||||
|   shoppingList: boolean; | ||||
|   print: boolean; | ||||
| @@ -164,12 +186,12 @@ export interface ContextMenuItem { | ||||
|   isPublic: boolean; | ||||
| } | ||||
|  | ||||
| export default defineComponent({ | ||||
| export default defineNuxtComponent({ | ||||
|   components: { | ||||
|     RecipeDialogAddToShoppingList, | ||||
|     RecipeDialogPrintPreferences, | ||||
|     RecipeDialogShare, | ||||
| }, | ||||
|   }, | ||||
|   props: { | ||||
|     useItems: { | ||||
|       type: Object as () => ContextMenuIncludes, | ||||
| @@ -233,6 +255,7 @@ export default defineComponent({ | ||||
|       default: 1, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ["delete"], | ||||
|   setup(props, context) { | ||||
|     const api = useUserApi(); | ||||
|  | ||||
| @@ -246,17 +269,23 @@ export default defineComponent({ | ||||
|       recipeName: props.name, | ||||
|       loading: false, | ||||
|       menuItems: [] as ContextMenuItem[], | ||||
|       newMealdate: "", | ||||
|       newMealdate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000), | ||||
|       newMealType: "dinner" as PlanEntryType, | ||||
|       pickerMenu: false, | ||||
|     }); | ||||
|  | ||||
|     const { i18n, $auth, $globals } = useContext(); | ||||
|     const newMealdateString = computed(() => { | ||||
|       return state.newMealdate.toISOString().substring(0, 10); | ||||
|     }); | ||||
|  | ||||
|     const i18n = useI18n(); | ||||
|     const $auth = useMealieAuth(); | ||||
|     const { $globals } = useNuxtApp(); | ||||
|     const { household } = useHouseholdSelf(); | ||||
|     const { isOwnGroup } = useLoggedInState(); | ||||
|  | ||||
|     const route = useRoute(); | ||||
|     const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || ""); | ||||
|     const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || ""); | ||||
|  | ||||
|     const firstDayOfWeek = computed(() => { | ||||
|       return household.value?.preferences?.firstDayOfWeek || 0; | ||||
| @@ -267,63 +296,63 @@ export default defineComponent({ | ||||
|  | ||||
|     const defaultItems: { [key: string]: ContextMenuItem } = { | ||||
|       edit: { | ||||
|         title: i18n.tc("general.edit"), | ||||
|         title: i18n.t("general.edit"), | ||||
|         icon: $globals.icons.edit, | ||||
|         color: undefined, | ||||
|         event: "edit", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       delete: { | ||||
|         title: i18n.tc("general.delete"), | ||||
|         title: i18n.t("general.delete"), | ||||
|         icon: $globals.icons.delete, | ||||
|         color: undefined, | ||||
|         event: "delete", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       download: { | ||||
|         title: i18n.tc("general.download"), | ||||
|         title: i18n.t("general.download"), | ||||
|         icon: $globals.icons.download, | ||||
|         color: undefined, | ||||
|         event: "download", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       duplicate: { | ||||
|         title: i18n.tc("general.duplicate"), | ||||
|         title: i18n.t("general.duplicate"), | ||||
|         icon: $globals.icons.duplicate, | ||||
|         color: undefined, | ||||
|         event: "duplicate", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       mealplanner: { | ||||
|         title: i18n.tc("recipe.add-to-plan"), | ||||
|         title: i18n.t("recipe.add-to-plan"), | ||||
|         icon: $globals.icons.calendar, | ||||
|         color: undefined, | ||||
|         event: "mealplanner", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       shoppingList: { | ||||
|         title: i18n.tc("recipe.add-to-list"), | ||||
|         title: i18n.t("recipe.add-to-list"), | ||||
|         icon: $globals.icons.cartCheck, | ||||
|         color: undefined, | ||||
|         event: "shoppingList", | ||||
|         isPublic: false, | ||||
|       }, | ||||
|       print: { | ||||
|         title: i18n.tc("general.print"), | ||||
|         title: i18n.t("general.print"), | ||||
|         icon: $globals.icons.printer, | ||||
|         color: undefined, | ||||
|         event: "print", | ||||
|         isPublic: true, | ||||
|       }, | ||||
|       printPreferences: { | ||||
|         title: i18n.tc("general.print-preferences"), | ||||
|         title: i18n.t("general.print-preferences"), | ||||
|         icon: $globals.icons.printerSettings, | ||||
|         color: undefined, | ||||
|         event: "printPreferences", | ||||
|         isPublic: true, | ||||
|       }, | ||||
|       share: { | ||||
|         title: i18n.tc("general.share"), | ||||
|         title: i18n.t("general.share"), | ||||
|         icon: $globals.icons.shareVariant, | ||||
|         color: undefined, | ||||
|         event: "share", | ||||
| @@ -350,8 +379,10 @@ export default defineComponent({ | ||||
|     // Context Menu Event Handler | ||||
|  | ||||
|     const shoppingLists = ref<ShoppingListSummary[]>(); | ||||
|     const recipeRef = ref<Recipe>(props.recipe); | ||||
|     const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined); | ||||
|     const recipeRef = ref<Recipe | undefined>(props.recipe); | ||||
|     const recipeRefWithScale = computed(() => | ||||
|       recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined, | ||||
|     ); | ||||
|  | ||||
|     async function getShoppingLists() { | ||||
|       const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); | ||||
| @@ -371,13 +402,15 @@ export default defineComponent({ | ||||
|     const groupRecipeActionsStore = useGroupRecipeActions(); | ||||
|  | ||||
|     async function executeRecipeAction(action: GroupRecipeActionOut) { | ||||
|       if (!props.recipe) return; | ||||
|       const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale); | ||||
|  | ||||
|       if (action.actionType === "post") { | ||||
|         if (!response?.error) { | ||||
|           alert.success(i18n.tc("events.message-sent")); | ||||
|         } else { | ||||
|           alert.error(i18n.tc("events.something-went-wrong")); | ||||
|           alert.success(i18n.t("events.message-sent")); | ||||
|         } | ||||
|         else { | ||||
|           alert.error(i18n.t("events.something-went-wrong")); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -390,7 +423,7 @@ export default defineComponent({ | ||||
|       context.emit("delete", props.slug); | ||||
|     } | ||||
|  | ||||
|     const download = useAxiosDownloader(); | ||||
|     const download = useDownloader(); | ||||
|  | ||||
|     async function handleDownloadEvent() { | ||||
|       const { data } = await api.recipes.getZipToken(props.slug); | ||||
| @@ -402,7 +435,7 @@ export default defineComponent({ | ||||
|  | ||||
|     async function addRecipeToPlan() { | ||||
|       const { response } = await api.mealplans.createOne({ | ||||
|         date: state.newMealdate, | ||||
|         date: newMealdateString.value, | ||||
|         entryType: state.newMealType, | ||||
|         title: "", | ||||
|         text: "", | ||||
| @@ -411,7 +444,8 @@ export default defineComponent({ | ||||
|  | ||||
|       if (response?.status === 201) { | ||||
|         alert.success(i18n.t("recipe.recipe-added-to-mealplan") as string); | ||||
|       } else { | ||||
|       } | ||||
|       else { | ||||
|         alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string); | ||||
|       } | ||||
|     } | ||||
| @@ -424,6 +458,7 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     // Note: Print is handled as an event in the parent component | ||||
|     // eslint-disable-next-line @typescript-eslint/no-invalid-void-type | ||||
|     const eventHandlers: { [key: string]: () => void | Promise<any> } = { | ||||
|       delete: () => { | ||||
|         state.recipeDeleteDialog = true; | ||||
| @@ -448,7 +483,9 @@ export default defineComponent({ | ||||
|           promises.push(refreshRecipe()); | ||||
|         } | ||||
|  | ||||
|         Promise.allSettled(promises).then(() => { state.shoppingListDialog = true }); | ||||
|         Promise.allSettled(promises).then(() => { | ||||
|           state.shoppingListDialog = true; | ||||
|         }); | ||||
|       }, | ||||
|       share: () => { | ||||
|         state.shareDialog = true; | ||||
| @@ -472,6 +509,7 @@ export default defineComponent({ | ||||
|  | ||||
|     return { | ||||
|       ...toRefs(state), | ||||
|       newMealdateString, | ||||
|       recipeRef, | ||||
|       recipeRefWithScale, | ||||
|       executeRecipeAction, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user