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

@ -1,47 +1,54 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div :class="dense ? 'wrapper' : 'wrapper pa-3'">
<section>
<v-container class="ma-0 pa-0">
<v-row>
<v-col
v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden"
:order="preferences.imagePosition == ImagePosition.left ? -1 : 1"
cols="4"
align-self="center"
<v-col v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden"
:order="preferences.imagePosition == ImagePosition.left ? -1 : 1"
cols="4"
align-self="center"
>
<img :key="imageKey" :src="recipeImageUrl" style="min-height: 50; max-width: 100%;" />
<img :key="imageKey"
:src="recipeImageUrl"
style="min-height: 50; max-width: 100%;"
>
</v-col>
<v-col order=0>
<v-col order="0">
<v-card-title class="headline pl-0">
<v-icon left color="primary">
<v-icon start
color="primary"
>
{{ $globals.icons.primary }}
</v-icon>
{{ recipe.name }}
</v-card-title>
<div v-if="recipeYield" class="d-flex justify-space-between align-center px-4 pb-2">
<v-chip
:small="$vuetify.breakpoint.smAndDown"
label
<div v-if="recipeYield"
class="d-flex justify-space-between align-center px-4 pb-2"
>
<v-chip :size="$vuetify.display.smAndDown ? 'small' : undefined"
label
>
<v-icon left>
<v-icon start>
{{ $globals.icons.potSteam }}
</v-icon>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="recipeYield"></span>
<span v-html="recipeYield" />
</v-chip>
</div>
<v-row class="d-flex justify-start">
<RecipeTimeCard
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
small
color="white"
class="ml-4"
<RecipeTimeCard :prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
small
color="white"
class="ml-4"
/>
</v-row>
<v-card-text v-if="preferences.showDescription" class="px-0">
<v-card-text v-if="preferences.showDescription"
class="px-0"
>
<SafeMarkdown :source="recipe.description" />
</v-card-text>
</v-col>
@ -51,22 +58,28 @@
<!-- Ingredients -->
<section>
<v-card-title class="headline pl-0"> {{ $t("recipe.ingredients") }} </v-card-title>
<div
v-for="(ingredientSection, sectionIndex) in ingredientSections"
:key="`ingredient-section-${sectionIndex}`"
class="print-section"
<v-card-title class="headline pl-0">
{{ $t("recipe.ingredients") }}
</v-card-title>
<div v-for="(ingredientSection, sectionIndex) in ingredientSections"
:key="`ingredient-section-${sectionIndex}`"
class="print-section"
>
<h4 v-if="ingredientSection.ingredients[0].title" class="ingredient-title mt-2">
<h4 v-if="ingredientSection.ingredients[0].title"
class="ingredient-title mt-2"
>
{{ ingredientSection.ingredients[0].title }}
</h4>
<div
class="ingredient-grid"
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
<div class="ingredient-grid"
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
>
<template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients">
<template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients"
:key="`ingredient-${ingredientIndex}`"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<p :key="`ingredient-${ingredientIndex}`" class="ingredient-body" v-html="parseText(ingredient)" />
<p class="ingredient-body"
v-html="parseText(ingredient)"
/>
</template>
</div>
</div>
@ -74,19 +87,35 @@
<!-- Instructions -->
<section>
<div
v-for="(instructionSection, sectionIndex) in instructionSections"
:key="`instruction-section-${sectionIndex}`"
:class="{ 'print-section': instructionSection.sectionName }"
<div v-for="(instructionSection, sectionIndex) in instructionSections"
:key="`instruction-section-${sectionIndex}`"
:class="{ 'print-section': instructionSection.sectionName }"
>
<v-card-title v-if="!sectionIndex" class="headline pl-0">{{ $t("recipe.instructions") }}</v-card-title>
<div v-for="(step, stepIndex) in instructionSection.instructions" :key="`instruction-${stepIndex}`">
<v-card-title v-if="!sectionIndex"
class="headline pl-0"
>
{{ $t("recipe.instructions") }}
</v-card-title>
<div v-for="(step, stepIndex) in instructionSection.instructions"
:key="`instruction-${stepIndex}`"
>
<div class="print-section">
<h4 v-if="step.title" :key="`instruction-title-${stepIndex}`" class="instruction-title mb-2">
<h4 v-if="step.title"
:key="`instruction-title-${stepIndex}`"
class="instruction-title mb-2"
>
{{ step.title }}
</h4>
<h5>{{ step.summary ? step.summary : $t("recipe.step-index", { step: stepIndex + instructionSection.stepOffset + 1 }) }}</h5>
<SafeMarkdown :source="step.text" class="recipe-step-body" />
<h5>
{{ step.summary ? step.summary : $t("recipe.step-index", {
step: stepIndex
+ instructionSection.stepOffset
+ 1,
}) }}
</h5>
<SafeMarkdown :source="step.text"
class="recipe-step-body"
/>
</div>
</div>
</div>
@ -94,13 +123,19 @@
<!-- Notes -->
<div v-if="preferences.showNotes">
<v-divider v-if="hasNotes" class="grey my-4"></v-divider>
<v-divider v-if="hasNotes"
class="grey my-4"
/>
<section>
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
<div v-for="(note, index) in recipe.notes"
:key="index + 'note'"
>
<div class="print-section">
<h4>{{ note.title }}</h4>
<SafeMarkdown :source="note.text" class="note-body" />
<SafeMarkdown :source="note.text"
class="note-body"
/>
</div>
</div>
</section>
@ -108,13 +143,17 @@
<!-- Nutrition -->
<div v-if="preferences.showNutrition">
<v-card-title class="headline pl-0"> {{ $t("recipe.nutrition") }} </v-card-title>
<v-card-title class="headline pl-0">
{{ $t("recipe.nutrition") }}
</v-card-title>
<section>
<div class="print-section">
<table class="nutrition-table">
<tbody>
<tr v-for="(value, key) in recipe.nutrition" :key="key">
<tr v-for="(value, key) in recipe.nutrition"
:key="key"
>
<template v-if="value">
<td>{{ labels[key].label }}</td>
<td>{{ value ? (labels[key].suffix ? `${value} ${labels[key].suffix}` : value) : '-' }}</td>
@ -122,26 +161,23 @@
</tr>
</tbody>
</table>
</div>
</section>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import DOMPurify from "dompurify";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { useStaticRoutes } from "~/composables/api";
import { Recipe, RecipeIngredient, RecipeStep} from "~/lib/api/types/recipe";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
import { parseIngredientText, useNutritionLabels } from "~/composables/recipes";
import { usePageState } from "~/composables/recipe-page/shared-state";
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
type IngredientSection = {
sectionName: string;
ingredients: RecipeIngredient[];
@ -153,7 +189,7 @@ type InstructionSection = {
instructions: RecipeStep[];
};
export default defineComponent({
export default defineNuxtComponent({
components: {
RecipeTimeCard,
},
@ -168,15 +204,15 @@ export default defineComponent({
},
dense: {
type: Boolean,
default: false
}
default: false,
},
},
setup(props) {
const { i18n } = useContext();
const i18n = useI18n();
const preferences = useUserPrintPreferences();
const { recipeImage } = useStaticRoutes();
const { imageKey } = usePageState(props.recipe.slug);
const {labels} = useNutritionLabels();
const { labels } = useNutritionLabels();
function sanitizeHTML(rawHtml: string) {
return DOMPurify.sanitize(rawHtml, {
@ -187,11 +223,13 @@ export default defineComponent({
const servingsDisplay = computed(() => {
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
return scaledAmountDisplay ? i18n.t("recipe.yields-amount-with-text", {
amount: scaledAmountDisplay,
text: props.recipe.recipeYield,
}) as string : "";
})
return scaledAmountDisplay
? i18n.t("recipe.yields-amount-with-text", {
amount: scaledAmountDisplay,
text: props.recipe.recipeYield,
}) as string
: "";
});
const yieldDisplay = computed(() => {
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeServings, props.scale);
@ -201,10 +239,11 @@ export default defineComponent({
const recipeYield = computed(() => {
if (servingsDisplay.value && yieldDisplay.value) {
return sanitizeHTML(`${yieldDisplay.value}; ${servingsDisplay.value}`);
} else {
}
else {
return sanitizeHTML(yieldDisplay.value || servingsDisplay.value);
}
})
});
const recipeImageUrl = computed(() => {
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
@ -320,7 +359,7 @@ export default defineComponent({
}
.wrapper,
.wrapper >>> * {
.wrapper :deep(*) {
opacity: 1 !important;
color: black !important;
}
@ -396,10 +435,10 @@ li {
width: 30%;
text-align: right;
}
.nutrition-table td {
padding: 2px;
text-align: left;
font-size: 14px;
}
</style>