recipePage use v-model instead of prop and turn into script setup

This commit is contained in:
Kuchenpirat 2025-06-18 11:53:38 +00:00
commit 5fadd299dd
3 changed files with 152 additions and 196 deletions

View file

@ -149,7 +149,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { invoke, until } from "@vueuse/core"; import { invoke, until } from "@vueuse/core";
import RecipeIngredients from "../RecipeIngredients.vue"; import RecipeIngredients from "../RecipeIngredients.vue";
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue"; import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
@ -186,203 +186,159 @@ const EDITOR_OPTIONS = {
mainMenuBar: false, mainMenuBar: false,
}; };
export default defineNuxtComponent({ const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
components: {
RecipePageHeader,
RecipePrintContainer,
RecipePageComments,
RecipePageInfoEditor,
RecipePageEditorToolbar,
RecipePageIngredientEditor,
RecipePageOrganizers,
RecipePageScale,
RecipePageIngredientToolsView,
RecipeDialogBulkAdd,
RecipeNotes,
RecipePageInstructions,
RecipePageFooter,
RecipeIngredients,
},
props: {
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
required: true,
},
},
setup(props) {
const { $vuetify } = useNuxtApp();
const i18n = useI18n();
const $auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || ""); const { $vuetify } = useNuxtApp();
const { isOwnGroup } = useLoggedInState(); const i18n = useI18n();
const $auth = useMealieAuth();
const route = useRoute();
const router = useRouter(); const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
const api = useUserApi(); const { isOwnGroup } = useLoggedInState();
const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode }
= usePageState(props.recipe.slug);
const { deactivateNavigationWarning } = useNavigationWarning();
const notLinkedIngredients = computed(() => {
return props.recipe.recipeIngredient.filter((ingredient) => {
return !props.recipe.recipeInstructions.some(step =>
step.ingredientReferences?.map(ref => ref.referenceId).includes(ingredient.referenceId),
);
});
});
/** ============================================================= const router = useRouter();
* Recipe Snapshot on Mount const api = useUserApi();
* this is used to determine if the recipe has been changed since the last save const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode }
* and prompts the user to save if they have unsaved changes. = usePageState(recipe.value.slug);
*/ const { deactivateNavigationWarning } = useNavigationWarning();
const originalRecipe = ref<Recipe | null>(null); const notLinkedIngredients = computed(() => {
return recipe.value.recipeIngredient.filter((ingredient) => {
invoke(async () => { return !recipe.value.recipeInstructions.some(step =>
await until(props.recipe).not.toBeNull(); step.ingredientReferences?.map(ref => ref.referenceId).includes(ingredient.referenceId),
originalRecipe.value = deepCopy(props.recipe); );
}); });
onUnmounted(async () => {
const isSame = JSON.stringify(props.recipe) === JSON.stringify(originalRecipe.value);
if (isEditMode.value && !isSame && props.recipe?.slug !== undefined) {
const save = window.confirm(i18n.t("general.unsaved-changes"));
if (save) {
await api.recipes.updateOne(props.recipe.slug, props.recipe);
}
}
deactivateNavigationWarning();
toggleCookMode();
clearPageState(props.recipe.slug || "");
console.debug("reset RecipePage state during unmount");
});
const hasLinkedIngredients = computed(() => {
return props.recipe.recipeInstructions.some(
step => step.ingredientReferences && step.ingredientReferences.length > 0,
);
});
/** =============================================================
* Set State onMounted
*/
type BooleanString = "true" | "false" | "";
const edit = useRouteQuery<BooleanString>("edit", "");
onMounted(() => {
if (edit.value === "true") {
setMode(PageMode.EDIT);
}
});
/** =============================================================
* Recipe Save Delete
*/
async function saveRecipe() {
const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
setMode(PageMode.VIEW);
if (data?.slug) {
router.push(`/g/${groupSlug.value}/r/` + data.slug);
}
}
async function deleteRecipe() {
const { data } = await api.recipes.deleteOne(props.recipe.slug);
if (data?.slug) {
router.push(`/g/${groupSlug.value}`);
}
}
/** =============================================================
* View Preferences
*/
const landscape = computed(() => {
const preferLandscape = props.recipe.settings.landscapeView;
const smallScreen = !$vuetify.display.smAndUp.value;
if (preferLandscape) {
return true;
}
else if (smallScreen) {
return true;
}
return false;
});
/** =============================================================
* Bulk Step Editor
* TODO: Move to RecipePageInstructions component
*/
function addStep(steps: Array<string> | null = null) {
if (!props.recipe.recipeInstructions) {
return;
}
if (steps) {
const cleanedSteps = steps.map((step) => {
return { id: uuid4(), text: step, title: "", ingredientReferences: [] };
});
props.recipe.recipeInstructions.push(...cleanedSteps);
}
else {
props.recipe.recipeInstructions.push({
id: uuid4(),
text: "",
title: "",
summary: "",
ingredientReferences: [],
});
}
}
/** =============================================================
* Meta Tags
*/
const { user } = usePageUser();
/** =============================================================
* RecipeChip Clicked
*/
function chipClicked(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
if (!item.id) {
return;
}
router.push(`/g/${groupSlug.value}?${itemType}=${item.id}`);
}
return {
user,
isOwnGroup,
api,
scale: ref(1),
EDITOR_OPTIONS,
landscape,
pageMode,
editMode,
PageMode,
EditorMode,
isEditMode,
isEditForm,
isEditJSON,
isCookMode,
toggleCookMode,
saveRecipe,
deleteRecipe,
addStep,
hasLinkedIngredients,
notLinkedIngredients,
chipClicked,
};
},
}); });
/** =============================================================
* Recipe Snapshot on Mount
* this is used to determine if the recipe has been changed since the last save
* and prompts the user to save if they have unsaved changes.
*/
const originalRecipe = ref<Recipe | null>(null);
invoke(async () => {
await until(recipe.value).not.toBeNull();
originalRecipe.value = deepCopy(recipe.value);
});
onUnmounted(async () => {
const isSame = JSON.stringify(recipe.value) === JSON.stringify(originalRecipe.value);
if (isEditMode.value && !isSame && recipe.value?.slug !== undefined) {
const save = window.confirm(i18n.t("general.unsaved-changes"));
if (save) {
await api.recipes.updateOne(recipe.value.slug, recipe.value);
}
}
deactivateNavigationWarning();
toggleCookMode();
clearPageState(recipe.value.slug || "");
console.debug("reset RecipePage state during unmount");
});
const hasLinkedIngredients = computed(() => {
return recipe.value.recipeInstructions.some(
step => step.ingredientReferences && step.ingredientReferences.length > 0,
);
});
/** =============================================================
* Set State onMounted
*/
type BooleanString = "true" | "false" | "";
const edit = useRouteQuery<BooleanString>("edit", "");
onMounted(() => {
if (edit.value === "true") {
setMode(PageMode.EDIT);
}
});
/** =============================================================
* Recipe Save Delete
*/
async function saveRecipe() {
const { data } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
setMode(PageMode.VIEW);
if (data?.slug) {
router.push(`/g/${groupSlug.value}/r/` + data.slug);
}
}
async function deleteRecipe() {
const { data } = await api.recipes.deleteOne(recipe.value.slug);
if (data?.slug) {
router.push(`/g/${groupSlug.value}`);
}
}
/** =============================================================
* View Preferences
*/
const landscape = computed(() => {
const preferLandscape = recipe.value.settings.landscapeView;
const smallScreen = !$vuetify.display.smAndUp.value;
if (preferLandscape) {
return true;
}
else if (smallScreen) {
return true;
}
return false;
});
/** =============================================================
* Bulk Step Editor
* TODO: Move to RecipePageInstructions component
*/
function addStep(steps: Array<string> | null = null) {
if (!recipe.value.recipeInstructions) {
return;
}
if (steps) {
const cleanedSteps = steps.map((step) => {
return { id: uuid4(), text: step, title: "", ingredientReferences: [] };
});
recipe.value.recipeInstructions.push(...cleanedSteps);
}
else {
recipe.value.recipeInstructions.push({
id: uuid4(),
text: "",
title: "",
summary: "",
ingredientReferences: [],
});
}
}
/** =============================================================
* Meta Tags
*/
const { user } = usePageUser();
/** =============================================================
* RecipeChip Clicked
*/
function chipClicked(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
if (!item.id) {
return;
}
router.push(`/g/${groupSlug.value}?${itemType}=${item.id}`);
}
const scale = ref(1);
// expose to template
// (all variables used in template are top-level in <script setup>)
</script> </script>
<style lang="css"> <style lang="css">

View file

@ -2,7 +2,7 @@
<div> <div>
<RecipePage <RecipePage
v-if="recipe" v-if="recipe"
:recipe="recipe" v-model="recipe"
/> />
</div> </div>
</template> </template>

View file

@ -3,7 +3,7 @@
<client-only> <client-only>
<RecipePage <RecipePage
v-if="recipe" v-if="recipe"
:recipe="recipe" v-model="recipe"
/> />
</client-only> </client-only>
</div> </div>