Some checks failed
CI / build-test (push) Has been cancelled
Reviewed-on: #3 Co-authored-by: jableader <jacobdunk@gmail.com> Co-committed-by: jableader <jacobdunk@gmail.com>
386 lines
9.6 KiB
Vue
386 lines
9.6 KiB
Vue
<template>
|
|
<div class="container">
|
|
<div class="fields">
|
|
<date-picker
|
|
:date="meal.suggestedDate ?? new Date()"
|
|
@date-selected="selectDate"
|
|
/>
|
|
<div class="persons-list">
|
|
Cooked by
|
|
<person-list
|
|
:people="meal.chefs"
|
|
@remove-person="(p) => removePerson('chefs', p)"
|
|
@add-person="(p) => addPerson('chefs', p)"
|
|
/>
|
|
for
|
|
<person-list
|
|
:people="meal.consumers"
|
|
@remove-person="(p) => removePerson('consumers', p)"
|
|
@add-person="(p) => addPerson('consumers', p)"
|
|
/>, with
|
|
<person-list
|
|
:people="meal.cleanup"
|
|
@remove-person="(p) => removePerson('cleanup', p)"
|
|
@add-person="(p) => addPerson('cleanup', p)"
|
|
/>
|
|
on cleanup.
|
|
</div>
|
|
</div>
|
|
<div class="recipes">
|
|
<h2>Recipes</h2>
|
|
<ul v-if="meal.recipes.length">
|
|
<li
|
|
v-for="mealRecipe in meal.recipes"
|
|
:key="mealRecipe.recipe?.id ?? mealRecipe.recipeId"
|
|
>
|
|
<div
|
|
v-if="mealRecipe.recipe"
|
|
class="saved-recipe"
|
|
>
|
|
<p class="recipe-card">
|
|
<recipe-card :recipe="mealRecipe.recipe" />
|
|
</p>
|
|
|
|
<p class="servings">
|
|
<input
|
|
v-model="mealRecipe.servings"
|
|
type="number"
|
|
min="1"
|
|
>
|
|
<small><em>servings</em></small>
|
|
</p>
|
|
|
|
<input
|
|
type="checkbox"
|
|
class="show-ingredient-checkbox"
|
|
:checked="showIngredient(mealRecipe)"
|
|
>
|
|
<label
|
|
for="show-ingredients"
|
|
@click="showIngredient(mealRecipe, !showIngredient(mealRecipe))"
|
|
>
|
|
<img
|
|
class="icon"
|
|
:src="showIngredientsIcon"
|
|
>
|
|
</label>
|
|
<button
|
|
class="icon-button"
|
|
@click="removeRecipe(mealRecipe)"
|
|
>
|
|
<img
|
|
class="icon"
|
|
:src="trash"
|
|
>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="showIngredient(mealRecipe)">
|
|
<ul>
|
|
<li
|
|
v-for="ingredient in scaleIngredients(mealRecipe)"
|
|
:key="ingredient.id"
|
|
class="saved-ingredient"
|
|
>
|
|
<CompactParsedIngredient :ingredient="ingredient" />
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-else>
|
|
<p>Add some recipes using the search box</p>
|
|
</div>
|
|
<div class="fields">
|
|
<recipe-search-box @select-recipe="selectRecipe" />
|
|
</div>
|
|
</div>
|
|
<div class="ingredients">
|
|
<h2>Sides & Additional Ingredients</h2>
|
|
<editable-ingredients-panel
|
|
:ingredients="meal.extraIngredients"
|
|
@on-add="addIngredient"
|
|
@on-delete="deleteIngredient"
|
|
@on-update-ingredient="updateIngredient"
|
|
@on-editing="onEditAdditionalIngredients"
|
|
/>
|
|
</div>
|
|
|
|
<button @click="onSaveMeal">
|
|
Save
|
|
</button>
|
|
<p v-if="meal.purchaseDate">
|
|
<em>Purchased {{ ago(meal.purchaseDate) }}</em>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, onBeforeMount } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { getMeal, saveMeal, getRecipe } from '@/api/sdk'
|
|
import { toMealInput } from '@/domain/decoders'
|
|
import { currentUser } from '@/api/auth'
|
|
import { useAlert } from '@/composables/useAlert'
|
|
import { parseRouteId } from '@/router/helpers'
|
|
import type { Person, Ingredient, Meal, MealRecipe } from '@/domain/types'
|
|
|
|
import { ago } from '@/dateformats'
|
|
|
|
import RecipeSearchBox from '@/components/recipes/RecipeSearchBox.vue'
|
|
import RecipeCard from '@/components/recipes/RecipeCard.vue'
|
|
import DatePicker from './DatePicker.vue'
|
|
import EditableIngredientsPanel from '../ingredients/EditableIngredientsPanel.vue'
|
|
import PersonList from './PersonList.vue'
|
|
import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
|
|
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
|
|
const trash = new URL('@/assets/trash.svg', import.meta.url).toString()
|
|
|
|
function addPersonIfNotExists(list: Person[], person: Person | null | undefined) {
|
|
if (!person) return
|
|
if (!list.find((p) => p.id === person.id)) {
|
|
list.push(person)
|
|
}
|
|
}
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { show: showAlert } = useAlert()
|
|
|
|
type PeopleKey = 'chefs' | 'consumers' | 'cleanup'
|
|
|
|
const meal = reactive<Meal>({
|
|
id: -1,
|
|
suggestedDate: new Date(),
|
|
consumedDate: null,
|
|
purchaseDate: null,
|
|
recipes: [],
|
|
extraIngredients: [],
|
|
chefs: [],
|
|
consumers: [],
|
|
cleanup: [],
|
|
})
|
|
|
|
onBeforeMount(async () => {
|
|
const id = parseRouteId(route.params.id)
|
|
if (id !== null) {
|
|
const loaded = await getMeal(id)
|
|
Object.assign(meal, loaded)
|
|
} else {
|
|
const self = await currentUser()
|
|
if (self) {
|
|
meal.chefs = [self]
|
|
meal.consumers = [self]
|
|
meal.cleanup = [self]
|
|
}
|
|
}
|
|
})
|
|
|
|
function selectDate(date: Date) {
|
|
meal.suggestedDate = date
|
|
}
|
|
|
|
function removeRecipe(mealRecipe: MealRecipe) {
|
|
if (
|
|
confirm(
|
|
`Are you sure you want to remove ${mealRecipe.servings} servings of ${mealRecipe.recipe?.name ?? 'this recipe'} from this meal?`
|
|
)
|
|
) {
|
|
meal.recipes = meal.recipes.filter((r) => r !== mealRecipe)
|
|
}
|
|
}
|
|
|
|
function addIngredient() {
|
|
meal.extraIngredients = [{ id: -1, name: '', line: '', unit: 'Items', quantity: 0, preparation: '', productId: null, recipeId: null, mealId: null, product: null }, ...meal.extraIngredients]
|
|
}
|
|
|
|
function deleteIngredient(ingredient: Ingredient) {
|
|
meal.extraIngredients = meal.extraIngredients.filter((i) => i !== ingredient)
|
|
}
|
|
|
|
function updateIngredient(ingredient: Ingredient, newIngredient: Ingredient) {
|
|
meal.extraIngredients = meal.extraIngredients.map((i) => (i === ingredient ? newIngredient : i))
|
|
}
|
|
|
|
function removePerson(list: PeopleKey, person: Person) {
|
|
meal[list] = meal[list].filter((p) => p.id !== person.id)
|
|
}
|
|
|
|
function addPerson(list: PeopleKey, person: Person) {
|
|
addPersonIfNotExists(meal[list], person)
|
|
}
|
|
|
|
async function selectRecipe(recipe: { id: number | string }) {
|
|
// Refetch to get additional details
|
|
const r = await getRecipe(recipe.id)
|
|
|
|
if (r.createdBy) {
|
|
addPersonIfNotExists(meal.chefs, r.createdBy)
|
|
addPersonIfNotExists(meal.consumers, r.createdBy)
|
|
|
|
if (meal.cleanup.length === 0) {
|
|
addPersonIfNotExists(meal.cleanup, r.createdBy)
|
|
}
|
|
}
|
|
|
|
meal.recipes.push({ recipe: r, recipeId: r.id, mealId: meal.id, servings: r.serves })
|
|
}
|
|
|
|
async function onEditAdditionalIngredients(editing: boolean) {
|
|
if (editing && meal.extraIngredients.length === 0) {
|
|
addIngredient()
|
|
} else {
|
|
meal.extraIngredients = meal.extraIngredients.filter((i) => !!i.line)
|
|
}
|
|
}
|
|
|
|
const showMap = reactive<Record<string, boolean>>({})
|
|
function showIngredient(mealRecipe: MealRecipe, value?: boolean): boolean {
|
|
const index = meal.recipes.indexOf(mealRecipe)
|
|
const key = `${mealRecipe.recipe?.id ?? 'unknown'}-${index}`
|
|
if (value === undefined) {
|
|
return !!showMap[key]
|
|
}
|
|
showMap[key] = value
|
|
return value
|
|
}
|
|
|
|
function scaleIngredients(mealRecipe: MealRecipe) {
|
|
const ing = mealRecipe.recipe?.ingredients ?? []
|
|
const serves = mealRecipe.recipe?.serves ?? 1
|
|
return ing.map((i) => {
|
|
return {
|
|
...i,
|
|
quantity: (i.quantity * mealRecipe.servings) / serves,
|
|
}
|
|
})
|
|
}
|
|
|
|
async function onSaveMeal() {
|
|
const saved = await saveMeal(toMealInput(meal))
|
|
if (saved && saved.id >= 0) {
|
|
Object.assign(meal, saved)
|
|
router.push(`/meals/${saved.id}`)
|
|
showAlert({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
|
return
|
|
}
|
|
|
|
showAlert({
|
|
heading: 'Error saving meal',
|
|
message: 'An error occurred while saving the meal',
|
|
type: 'error',
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
img.icon {
|
|
width: 2em;
|
|
height: 2em;
|
|
}
|
|
|
|
.persons-list {
|
|
text-align: left;
|
|
padding: 1ex 2em;
|
|
}
|
|
|
|
.persons-list p {
|
|
margin: 0;
|
|
padding-bottom: 1em;
|
|
}
|
|
|
|
.person-list li {
|
|
display: inline-block;
|
|
padding-right: 1em;
|
|
}
|
|
|
|
.container {
|
|
margin: 1em auto;
|
|
}
|
|
|
|
ul {
|
|
list-style-type: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
li {
|
|
padding-bottom: 0.5vh;
|
|
}
|
|
|
|
.saved-ingredient {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.saved-recipe {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.saved-ingredient p {
|
|
flex: 1;
|
|
margin: 0;
|
|
margin-right: 1em;
|
|
}
|
|
|
|
.saved-recipe button:hover {
|
|
background: #eee;
|
|
}
|
|
|
|
.saved-recipe .recipe-card {
|
|
margin: 0;
|
|
}
|
|
|
|
.recipe-card {
|
|
flex: 1;
|
|
}
|
|
|
|
.servings {
|
|
display: inline-block;
|
|
margin-right: 1em;
|
|
}
|
|
|
|
.servings input {
|
|
width: 3em;
|
|
border: none;
|
|
border-bottom: 1px solid #000;
|
|
text-align: center;
|
|
font-size: large;
|
|
font-style: italic;
|
|
}
|
|
|
|
.icon-button {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-radius: 1em;
|
|
padding: 0.5em;
|
|
}
|
|
|
|
.icon-button:hover {
|
|
/* Invert the colors of the trash icon */
|
|
filter: invert(1);
|
|
}
|
|
|
|
.show-ingredient-checkbox {
|
|
display: none;
|
|
}
|
|
|
|
.show-ingredient-checkbox+label {
|
|
cursor: pointer;
|
|
background-color: #fff;
|
|
padding: 0.5em;
|
|
border-radius: 1em;
|
|
}
|
|
|
|
.show-ingredient-checkbox+label:hover {
|
|
background-color: #eee;
|
|
}
|
|
|
|
.show-ingredient-checkbox:checked+label {
|
|
filter: invert(1);
|
|
}
|
|
</style>
|