2024-01-13 02:00:15 +00:00
|
|
|
<template>
|
2025-10-18 02:08:22 +00:00
|
|
|
<div>
|
2024-01-13 08:43:15 +00:00
|
|
|
<div v-if="!id && !recipe">
|
2025-10-18 03:16:34 +00:00
|
|
|
<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>
|
2024-01-13 02:00:15 +00:00
|
|
|
</div>
|
2024-01-13 08:43:15 +00:00
|
|
|
|
2024-01-13 03:22:10 +00:00
|
|
|
<div v-if="parse_failed">
|
2025-10-18 02:08:22 +00:00
|
|
|
<p>Recipe not found</p>
|
2024-01-13 02:00:15 +00:00
|
|
|
</div>
|
|
|
|
|
|
2024-01-13 03:22:10 +00:00
|
|
|
<div v-if="!parse_failed && recipe">
|
2025-10-18 03:16:34 +00:00
|
|
|
<div
|
|
|
|
|
v-if="image_styling"
|
|
|
|
|
class="image-container"
|
|
|
|
|
:style="image_styling"
|
|
|
|
|
/>
|
|
|
|
|
<h1>
|
|
|
|
|
<input
|
|
|
|
|
v-model="recipe.name"
|
|
|
|
|
class="recipe-name"
|
|
|
|
|
type="text"
|
|
|
|
|
>
|
|
|
|
|
</h1>
|
2025-10-18 02:08:22 +00:00
|
|
|
<label for="recipe-serves">Number of serves: </label>
|
2025-10-18 03:16:34 +00:00
|
|
|
<input
|
|
|
|
|
v-model="recipe.serves"
|
|
|
|
|
type="number"
|
|
|
|
|
>
|
|
|
|
|
<h3 class="recipe-link">
|
|
|
|
|
<a :href="recipe.link">View Recipe</a>
|
|
|
|
|
</h3>
|
2025-10-18 02:08:22 +00:00
|
|
|
<h2>Ingredients</h2>
|
|
|
|
|
<editable-ingredients-panel
|
|
|
|
|
:ingredients="recipe.ingredients"
|
|
|
|
|
:edit-only="true"
|
|
|
|
|
@on-add="addIngredient"
|
|
|
|
|
@on-delete="deleteIngredient"
|
|
|
|
|
@on-update-ingredient="updateIngredient"
|
|
|
|
|
/>
|
2024-05-04 02:00:05 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
<div>
|
2025-10-18 03:16:34 +00:00
|
|
|
<button
|
|
|
|
|
v-if="recipe.id"
|
|
|
|
|
class="delete-btn"
|
|
|
|
|
@click="deleteRecipe"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="submit-btn"
|
|
|
|
|
@click="saveRecipe"
|
|
|
|
|
>
|
|
|
|
|
{{ recipe.id ? 'Save' : 'Create' }}
|
|
|
|
|
</button>
|
2025-10-18 02:08:22 +00:00
|
|
|
</div>
|
2024-01-13 02:00:15 +00:00
|
|
|
</div>
|
2025-10-18 02:08:22 +00:00
|
|
|
</div>
|
2024-01-13 02:00:15 +00:00
|
|
|
</template>
|
|
|
|
|
|
2025-10-18 02:57:40 +00:00
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
2025-10-18 03:12:27 +00:00
|
|
|
import { useAlert } from '@/composables/useAlert'
|
2025-10-18 02:57:40 +00:00
|
|
|
import { getRecipe, parseRecipe, saveRecipe as saveRecipeApi, deleteRecipe as deleteRecipeApi } from '@/api/recipes'
|
2024-05-12 07:00:26 +00:00
|
|
|
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
|
2024-01-13 02:00:15 +00:00
|
|
|
|
2025-10-18 02:57:40 +00:00
|
|
|
const props = defineProps({
|
|
|
|
|
id: { type: Number, required: false },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute()
|
2025-10-18 03:12:27 +00:00
|
|
|
const { show: showAlert } = useAlert()
|
2025-10-18 02:57:40 +00:00
|
|
|
|
|
|
|
|
const link = ref(route.query.url ?? '')
|
|
|
|
|
const parse_failed = ref(false)
|
|
|
|
|
const recipe = ref(null)
|
|
|
|
|
|
|
|
|
|
const image_styling = computed(() => {
|
|
|
|
|
if (recipe.value?.image_urls && recipe.value.image_urls[0]) {
|
|
|
|
|
const image = recipe.value.image_urls[0]
|
2025-10-18 02:08:22 +00:00
|
|
|
return {
|
2025-10-18 02:57:40 +00:00
|
|
|
background: `linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%), url('${image}') center/cover no-repeat`,
|
2025-10-18 02:08:22 +00:00
|
|
|
}
|
2025-10-18 02:57:40 +00:00
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function parseLink() {
|
|
|
|
|
router.push({ path: '/recipes/add', query: { url: link.value } })
|
|
|
|
|
refreshRecipe()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshRecipe() {
|
|
|
|
|
if (props.id >= 0) {
|
|
|
|
|
recipe.value = await getRecipe(props.id)
|
|
|
|
|
link.value = recipe.value.link
|
|
|
|
|
return
|
|
|
|
|
} else if (link.value) {
|
|
|
|
|
recipe.value = await parseRecipe(link.value)
|
|
|
|
|
parse_failed.value = !recipe.value
|
|
|
|
|
} else {
|
|
|
|
|
recipe.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateIngredient(ingredient, newIngredient) {
|
|
|
|
|
recipe.value.ingredients = recipe.value.ingredients.map((i) =>
|
|
|
|
|
i == ingredient ? newIngredient : i
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteIngredient(ingredient) {
|
|
|
|
|
recipe.value.ingredients = recipe.value.ingredients.filter((i) => i != ingredient)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveRecipe() {
|
|
|
|
|
const saved = await saveRecipeApi(recipe.value)
|
|
|
|
|
if (saved?.id >= 0) {
|
2025-10-18 03:12:27 +00:00
|
|
|
showAlert({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
|
2025-10-18 02:57:40 +00:00
|
|
|
router.push(`/recipes/${saved.id}`)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-10-18 03:12:27 +00:00
|
|
|
showAlert({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
|
2025-10-18 02:57:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createFromScratch() {
|
|
|
|
|
recipe.value = {
|
|
|
|
|
id: -1,
|
|
|
|
|
name: 'My new recipe',
|
|
|
|
|
created_by_id: -1,
|
|
|
|
|
link: '',
|
|
|
|
|
ingredients: [],
|
|
|
|
|
image_urls: [],
|
|
|
|
|
}
|
2024-01-13 02:00:15 +00:00
|
|
|
}
|
2025-10-18 02:57:40 +00:00
|
|
|
|
|
|
|
|
function addIngredient() {
|
|
|
|
|
recipe.value.ingredients = [{ line: '', product: null }, ...recipe.value.ingredients]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteRecipe() {
|
|
|
|
|
if (confirm('Are you sure you want to delete this recipe?')) {
|
|
|
|
|
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) => {
|
|
|
|
|
if (typeof newUrl === 'string') {
|
|
|
|
|
link.value = newUrl
|
|
|
|
|
refreshRecipe()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// expose functions for template binding names (automatic in <script setup>)
|
2025-10-18 02:08:22 +00:00
|
|
|
</script>
|
2025-10-18 03:16:34 +00:00
|
|
|
|
|
|
|
|
<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>
|