munch-ease-frontend/src/components/recipes/EditRecipePage.vue
jableader 211489ca55
Some checks failed
CI / build-test (push) Has been cancelled
pruning (#3)
Reviewed-on: #3
Co-authored-by: jableader <jacobdunk@gmail.com>
Co-committed-by: jableader <jacobdunk@gmail.com>
2025-10-25 02:18:30 +00:00

239 lines
6 KiB
Vue

<template>
<div>
<div v-if="!id && !recipe">
<input
v-model="link"
class="recipe-link"
type="text"
placeholder="Link to Recipe"
> <br>
<button @click="parseLink">
Parse
</button>
<button @click="createFromScratch">
Create from Scratch
</button>
</div>
<div v-if="parse_failed">
<p>Recipe not found</p>
</div>
<div v-if="!parse_failed && recipe">
<div
v-if="image_styling"
class="image-container"
:style="image_styling"
/>
<h1>
<input
v-model="recipe.name"
class="recipe-name"
type="text"
>
</h1>
<label for="recipe-serves">Number of serves: </label>
<input
v-model="recipe.serves"
type="number"
>
<h3 class="recipe-link">
<a :href="recipe.link">View Recipe</a>
</h3>
<h2>Ingredients</h2>
<editable-ingredients-panel
:ingredients="recipe.ingredients"
:edit-only="true"
@on-add="addIngredient"
@on-delete="deleteIngredient"
@on-update-ingredient="updateIngredient"
/>
<div>
<button
v-if="recipe.id"
class="delete-btn"
@click="deleteRecipe"
>
Delete
</button>
<button
class="submit-btn"
@click="saveRecipe"
>
{{ recipe.id ? 'Save' : 'Create' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAlert } from '@/composables/useAlert'
import { parseQueryString } from '@/router/helpers'
import { getRecipe, parseRecipe, saveRecipe as saveRecipeApi, deleteRecipe as deleteRecipeApi } from '@/api/sdk'
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
import type { Recipe as DomainRecipe, Ingredient, RecipeInput } from '@/domain/types'
const props = defineProps({
id: { type: String, required: false, default: undefined },
})
const router = useRouter()
const route = useRoute()
const { show: showAlert } = useAlert()
const link = ref<string>(parseQueryString(route.query.url))
const parse_failed = ref(false)
const recipe = ref<DomainRecipe | null>(null)
const image_styling = computed(() => {
const urls = recipe.value?.imageUrls ?? []
if (urls.length && urls[0]) {
const image = urls[0]!
return {
background: `linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%), url('${image}') center/cover no-repeat`,
}
}
return null
})
function parseLink() {
router.push({ path: '/recipes/add', query: { url: link.value } })
refreshRecipe()
}
async function refreshRecipe() {
const id = props.id ? parseInt(props.id) : null
if (id !== null && id >= 0) {
const r = await getRecipe(id)
recipe.value = r
link.value = r.link ?? ''
return
} else if (link.value) {
const r = await parseRecipe(link.value)
recipe.value = r
parse_failed.value = !r
} else {
recipe.value = null
}
}
async function updateIngredient(ingredient: Ingredient, newIngredient: Ingredient) {
if (!recipe.value) return
const list = recipe.value.ingredients ?? []
recipe.value = { ...recipe.value, ingredients: list.map((i) => (i === ingredient ? newIngredient : i)) }
}
function deleteIngredient(ingredient: Ingredient) {
if (!recipe.value) return
const list = recipe.value.ingredients ?? []
recipe.value = { ...recipe.value, ingredients: list.filter((i) => i !== ingredient) }
}
async function saveRecipe() {
const saved = recipe.value ? await saveRecipeApi(toRecipeInput(recipe.value)) : null
if (saved && saved.id >= 0) {
showAlert({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
router.push(`/recipes/${saved.id}`)
return
}
showAlert({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
}
function createFromScratch() {
recipe.value = {
id: -1,
name: 'My new recipe',
createdById: -1,
link: '',
ingredients: [],
imageUrls: [],
serves: 1,
dateCreated: new Date(),
dateHidden: null,
}
}
function addIngredient() {
if (!recipe.value) return
const list = recipe.value.ingredients ?? []
const draft: Ingredient = { id: -1, name: '', line: '', unit: 'Items', quantity: 0, preparation: '', productId: null, recipeId: null, mealId: null, product: null }
recipe.value = { ...recipe.value, ingredients: [draft, ...list] }
}
async function deleteRecipe() {
if (confirm('Are you sure you want to delete this recipe?')) {
if (!recipe.value) return
await deleteRecipeApi(recipe.value.id)
router.push('/recipes')
}
}
onMounted(() => {
refreshRecipe()
})
// Keep recipe in sync if link query changes while on page
watch(
() => route.query.url,
(newUrl) => {
const parsed = parseQueryString(newUrl)
if (parsed) {
link.value = parsed
refreshRecipe()
}
}
)
// expose functions for template binding names (automatic in <script setup>)
function toRecipeInput(r: DomainRecipe): RecipeInput {
return {
id: r.id,
name: r.name,
link: r.link,
serves: r.serves,
imageUrls: r.imageUrls ?? [],
ingredients: r.ingredients ?? [],
basedOnRecipe: r.basedOnRecipe ?? null,
// let backend set created/hidden dates
createdById: r.createdById,
createdBy: r.createdBy ?? null,
// dateHidden omitted
hiddenById: r.hiddenById ?? null,
hiddenBy: r.hiddenBy ?? null,
}
}
</script>
<style scoped>
input {
border: 0;
border-bottom: 1px solid #ccc;
font-size: large;
}
input.recipe-link {
width: 80%;
}
.image-container {
max-height: 20vh;
min-height: 20vh;
display: flex;
flex-direction: column;
}
input.recipe-name {
width: 100%;
font-weight: bold;
font-size: larger;
}
.recipe-link {
color: #0000ee;
text-decoration: none;
}
</style>