1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-19 13:19:41 +02: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:
Hoa (Kyle) Trinh 2025-06-20 00:09:12 +07:00 committed by GitHub
parent 89ab7fac25
commit c24d532608
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
403 changed files with 23959 additions and 19557 deletions

View file

@ -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,