mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
moved many compoents to composition API
This commit is contained in:
@@ -17,11 +17,12 @@
|
||||
"mavon-editor": "^3.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.15",
|
||||
"vue-i18n": "9",
|
||||
"vue-multiselect": "^3.0.0-beta.3",
|
||||
"vue-router": "4",
|
||||
"vue-simple-calendar": "^7.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.5.10"
|
||||
"vuetify": "^3.5.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
|
||||
@@ -28,8 +28,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
// 4. Provide the history implementation to use. We
|
||||
// are using the hash history for simplicity here.
|
||||
// TODO configure proper history mode
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
<template>
|
||||
<v-table density="compact" v-if="ingredients.length > 0">
|
||||
<v-table density="compact" v-if="props.ingredients.length > 0">
|
||||
|
||||
<tbody>
|
||||
<IngredientsTableRow v-for="i in ingredients" :ingredient="i" :key="i.id" :show-notes="showNotes" :draggable="draggable"></IngredientsTableRow>
|
||||
<IngredientsTableRow v-for="i in props.ingredients" :ingredient="i" :key="i.id" :show-notes="props.showNotes" :draggable="props.draggable"></IngredientsTableRow>
|
||||
</tbody>
|
||||
|
||||
</v-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
import {Ingredient, Step} from "@/openapi";
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, PropType, ref} from 'vue'
|
||||
import {Ingredient} from "@/openapi";
|
||||
import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue";
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default defineComponent({
|
||||
name: "IngredientsTable",
|
||||
components: {IngredientsTableRow, draggable},
|
||||
props: {
|
||||
ingredients: {
|
||||
type: Array as PropType<Array<Ingredient>>,
|
||||
default: [],
|
||||
},
|
||||
showNotes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
},
|
||||
const props = defineProps({
|
||||
ingredients: {
|
||||
type: Array as PropType<Array<Ingredient>>,
|
||||
default: [],
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mutable_ingredients: [] as Ingredient[]
|
||||
}
|
||||
showNotes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
},
|
||||
mounted() {
|
||||
this.mutable_ingredients = this.ingredients
|
||||
}
|
||||
})
|
||||
|
||||
const mutable_ingredients = ref([] as Ingredient[])
|
||||
|
||||
onMounted(() => {
|
||||
mutable_ingredients.value = props.ingredients
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<tr>
|
||||
<template v-if="ingredient.isHeader">
|
||||
<td colspan="4"><b>{{ ingredient.note }}</b></td>
|
||||
<template v-if="props.ingredient.isHeader">
|
||||
<td colspan="4"><b>{{ props.ingredient.note }}</b></td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td>{{ ingredient.amount }}</td>
|
||||
<td><span v-if="ingredient.unit != null">{{ ingredient.unit.name }}</span></td>
|
||||
<td><span v-if="ingredient.food != null">{{ ingredient.food.name }}</span></td>
|
||||
<td v-if="showNotes">
|
||||
<v-icon class="far fa-comment float-right" v-if="ingredient.note != '' && ingredient.note != undefined" @click="show_tooltip = !show_tooltip">
|
||||
<v-tooltip v-model="show_tooltip" activator="parent" location="start">{{ ingredient.note }}</v-tooltip>
|
||||
<td>{{ props.ingredient.amount }}</td>
|
||||
<td><span v-if="props.ingredient.unit != null">{{ props.ingredient.unit.name }}</span></td>
|
||||
<td><span v-if="props.ingredient.food != null">{{ props.ingredient.food.name }}</span></td>
|
||||
<td v-if="props.showNotes">
|
||||
<v-icon class="far fa-comment float-right" v-if="props.ingredient.note != '' && props.ingredient.note != undefined" @click="showTooltip = !showTooltip">
|
||||
<v-tooltip v-model="showTooltip" activator="parent" location="start">{{ props.ingredient.note }}</v-tooltip>
|
||||
</v-icon>
|
||||
</td>
|
||||
<td v-if="draggable">
|
||||
<td v-if="props.draggable">
|
||||
<i class="fas fa-grip-lines drag-handle cursor-move"></i>
|
||||
</td>
|
||||
</template>
|
||||
@@ -20,32 +20,26 @@
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {PropType, ref} from 'vue'
|
||||
import {Ingredient} from "@/openapi";
|
||||
|
||||
export default defineComponent({
|
||||
name: "IngredientsTableRow",
|
||||
components: {},
|
||||
props: {
|
||||
ingredient: {
|
||||
type: {} as PropType<Ingredient>,
|
||||
required: true
|
||||
},
|
||||
showNotes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
},
|
||||
const props = defineProps({
|
||||
ingredient: {
|
||||
type: {} as PropType<Ingredient>,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show_tooltip: false,
|
||||
}
|
||||
showNotes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
const showTooltip = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
<template>
|
||||
<div v-if="keywords">
|
||||
<v-chip class="ms-1" :color="color" :size="size" :variant="variant" v-for="k in keywords"> {{ k.label }}</v-chip>
|
||||
<div v-if="props.keywords">
|
||||
<v-chip class="ms-1" :color="props.color" :size="props.size" :variant="props.variant" v-for="k in props.keywords"> {{ k.label }}</v-chip>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Keyword, KeywordLabel} from "@/openapi";
|
||||
import {PropType} from "vue";
|
||||
|
||||
export default {
|
||||
name: 'KeywordsBar',
|
||||
mixins: [],
|
||||
props: {
|
||||
keywords: Array as PropType<Array<Keyword> | Array<KeywordLabel> | undefined>,
|
||||
size: {type: String, default: 'x-small'},
|
||||
color: {type: String, default: ''},
|
||||
variant: {type: String as PropType<NonNullable<"tonal" | "flat" | "text" | "elevated" | "outlined" | "plain"> | undefined>, default: 'tonal'},
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
const props = defineProps({
|
||||
keywords: Array as PropType<Array<Keyword> | Array<KeywordLabel> | undefined>,
|
||||
size: {type: String, default: 'x-small'},
|
||||
color: {type: String, default: ''},
|
||||
variant: {type: String as PropType<NonNullable<"tonal" | "flat" | "text" | "elevated" | "outlined" | "plain"> | undefined>, default: 'tonal'},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
<template>
|
||||
<template v-if="!loading">
|
||||
<v-card :to="`/recipe/${recipe.id}`" :style="{'height': height}">
|
||||
<template v-if="!componentProps.loading">
|
||||
<v-card :to="`/recipe/${componentProps.recipe.id}`" :style="{'height': componentProps.height}">
|
||||
<v-tooltip
|
||||
class="align-center justify-center"
|
||||
location="top center" origin="overlap"
|
||||
no-click-animation
|
||||
:open-on-hover="recipe.description != null && recipe.description != ''"
|
||||
:open-on-hover="componentProps.recipe.description != null && componentProps.recipe.description != ''"
|
||||
contained
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-img cover
|
||||
height="60%"
|
||||
:src="recipeImageUrl"
|
||||
<recipe-image
|
||||
height="60%"
|
||||
width="100%"
|
||||
:recipe="componentProps.recipe"
|
||||
>
|
||||
<v-chip size="x-small" prepend-icon="fa fa-clock" label color="light" variant="elevated"
|
||||
class="float-start ms-1 mt-1" v-if="recipe.workingTime != undefined && recipe.workingTime > 0">
|
||||
{{ recipe.workingTime }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" prepend-icon="fa fa-pause" label color="secondary" variant="elevated"
|
||||
class="float-start ms-1 mt-1" v-if="recipe.waitingTime != undefined && recipe.waitingTime > 0">
|
||||
{{ recipe.waitingTime }}
|
||||
</v-chip>
|
||||
<template #overlay>
|
||||
<v-chip size="x-small" prepend-icon="fa fa-clock" label color="light" variant="elevated"
|
||||
class="float-start ms-1 mt-1" v-if="componentProps.recipe.workingTime != undefined && componentProps.recipe.workingTime > 0">
|
||||
{{ recipe.workingTime }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" prepend-icon="fa fa-pause" label color="secondary" variant="elevated"
|
||||
class="float-start ms-1 mt-1" v-if="componentProps.recipe.waitingTime != undefined && componentProps.recipe.waitingTime > 0">
|
||||
{{ recipe.waitingTime }}
|
||||
</v-chip>
|
||||
|
||||
<keywords-component variant="flat" :keywords="recipe.keywords"></keywords-component>
|
||||
</v-img>
|
||||
<v-divider class="p-0" v-if="recipe.image == null"></v-divider>
|
||||
<keywords-component variant="flat" :keywords="componentProps.recipe.keywords"></keywords-component>
|
||||
</template>
|
||||
</recipe-image>
|
||||
|
||||
<v-divider class="p-0" v-if="componentProps.recipe.image == null"></v-divider>
|
||||
|
||||
</template>
|
||||
<div v-if="recipe.description != null && recipe.description != ''">
|
||||
{{ recipe.description }}
|
||||
<div v-if="componentProps.recipe.description != null && componentProps.recipe.description != ''">
|
||||
{{ componentProps.recipe.description }}
|
||||
</div>
|
||||
</v-tooltip>
|
||||
|
||||
<v-card-item>
|
||||
<v-card-title>
|
||||
{{ recipe.name }}
|
||||
<recipe-context-menu class="float-end" :recipe="recipe"></recipe-context-menu>
|
||||
{{ componentProps.recipe.name }}
|
||||
<recipe-context-menu class="float-end" :recipe="recipe"></recipe-context-menu>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>by {{ recipe.createdBy}}</v-card-subtitle>
|
||||
<v-card-subtitle>by {{ componentProps.recipe.createdBy }}</v-card-subtitle>
|
||||
|
||||
|
||||
<!-- <v-card-subtitle v-if="show_keywords">-->
|
||||
<!-- <keywords-component :keywords="recipe.keywords"></keywords-component>-->
|
||||
<!-- </v-card-subtitle>-->
|
||||
<!-- <v-rating-->
|
||||
<!-- v-if="recipe.rating != null"-->
|
||||
<!-- v-model="recipe.rating"-->
|
||||
<!-- color="amber"-->
|
||||
<!-- density="comfortable"-->
|
||||
<!-- half-increments-->
|
||||
<!-- readonly-->
|
||||
<!-- size="x-small"-->
|
||||
<!-- ></v-rating>-->
|
||||
<!-- <v-card-subtitle v-if="show_keywords">-->
|
||||
<!-- <keywords-component :keywords="recipe.keywords"></keywords-component>-->
|
||||
<!-- </v-card-subtitle>-->
|
||||
<!-- <v-rating-->
|
||||
<!-- v-if="recipe.rating != null"-->
|
||||
<!-- v-model="recipe.rating"-->
|
||||
<!-- color="amber"-->
|
||||
<!-- density="comfortable"-->
|
||||
<!-- half-increments-->
|
||||
<!-- readonly-->
|
||||
<!-- size="x-small"-->
|
||||
<!-- ></v-rating>-->
|
||||
|
||||
</v-card-item>
|
||||
|
||||
</v-card>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-card :style="{'height': height}">
|
||||
<v-card :style="{'height': componentProps.height}">
|
||||
<v-img src="../../assets/recipe_no_image.svg" cover height="60%"></v-img>
|
||||
<v-card-title>
|
||||
<v-skeleton-loader type="heading"></v-skeleton-loader>
|
||||
@@ -72,29 +76,22 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {PropType} from 'vue'
|
||||
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
|
||||
import {Recipe, RecipeOverview} from "@/openapi";
|
||||
import recipeNoImage from '@/assets/recipe_no_image.svg';
|
||||
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RecipeCard",
|
||||
components: {RecipeContextMenu, KeywordsComponent},
|
||||
props: {
|
||||
recipe: {type: {} as PropType<Recipe | RecipeOverview>, required: true,},
|
||||
loading: {type: Boolean, required: false},
|
||||
show_keywords: {type: Boolean, required: false},
|
||||
show_description: {type: Boolean, required: false},
|
||||
height: {type: String, required: false, default: '25vh'},
|
||||
},
|
||||
computed: {
|
||||
recipeImageUrl: function () {
|
||||
return (this.recipe.image != null) ? this.recipe.image : recipeNoImage
|
||||
}
|
||||
}
|
||||
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
|
||||
import RecipeImage from "@/components/display/RecipeImage.vue";
|
||||
|
||||
const componentProps = defineProps({
|
||||
recipe: {type: {} as PropType<Recipe | RecipeOverview>, required: true,},
|
||||
loading: {type: Boolean, required: false},
|
||||
show_keywords: {type: Boolean, required: false},
|
||||
show_description: {type: Boolean, required: false},
|
||||
height: {type: String, required: false, default: '25vh'},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
<template>
|
||||
<v-img :cover="cover" :style="{'height': height, 'width': width,}" :src="image" alt="Recipe Image"/>
|
||||
<v-img :cover="cover" :style="{'height': height, 'width': width,}" :src="image" alt="Recipe Image">
|
||||
<slot name="overlay">
|
||||
|
||||
</slot>
|
||||
</v-img>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, PropType} from "vue";
|
||||
import {computed, PropType, watch} from "vue";
|
||||
import {Recipe, RecipeOverview} from "@/openapi";
|
||||
import recipeDefaultImage from '../../assets/recipe_no_image.svg'
|
||||
|
||||
const props = defineProps({
|
||||
recipe: {type: {} as PropType<Recipe|RecipeOverview|undefined>, required: false, default: undefined},
|
||||
recipe: {type: {} as PropType<Recipe | RecipeOverview | undefined>, required: false, default: undefined},
|
||||
height: {type: String},
|
||||
width: {type: String},
|
||||
cover: {type: Boolean, default: true}
|
||||
})
|
||||
|
||||
const image = computed(() => {
|
||||
if(props.recipe != undefined && props.recipe.image != undefined){
|
||||
|
||||
if (props.recipe != undefined && props.recipe.image != undefined) {
|
||||
return props.recipe.image
|
||||
} else {
|
||||
return recipeDefaultImage
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.recipe, () => {
|
||||
console.log('changed')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
|
||||
<template v-if="recipe.name != undefined">
|
||||
<template v-if="props.recipe.name != undefined">
|
||||
|
||||
<v-card class="mt-md-4 rounded-0">
|
||||
<v-img max-height="25vh" cover lazy :src="recipe.image" v-if="recipe.image != undefined" class="align-end">
|
||||
<v-chip class="ms-2" color="primary" variant="flat" size="x-small">by {{ recipe.createdBy}}</v-chip>
|
||||
<KeywordsComponent variant="flat" class="ms-1 mb-2" :keywords="recipe.keywords"></KeywordsComponent>
|
||||
</v-img>
|
||||
<recipe-image
|
||||
max-height="25vh"
|
||||
:recipe="props.recipe"
|
||||
>
|
||||
<template #overlay>
|
||||
<v-chip class="ms-2" color="primary" variant="flat" size="x-small">by {{ props.recipe.createdBy }}</v-chip>
|
||||
<KeywordsComponent variant="flat" class="ms-1 mb-2" :keywords="props.recipe.keywords"></KeywordsComponent>
|
||||
</template>
|
||||
</recipe-image>
|
||||
|
||||
|
||||
<v-card>
|
||||
<v-sheet class="d-flex align-center">
|
||||
<span class="ps-2 text-h5 flex-grow-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName">{{ recipe.name }}</span>
|
||||
<span class="ps-2 text-h5 flex-grow-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName">{{ props.recipe.name }}</span>
|
||||
<recipe-context-menu :recipe="recipe"></recipe-context-menu>
|
||||
</v-sheet>
|
||||
</v-card>
|
||||
@@ -20,11 +26,11 @@
|
||||
<v-container>
|
||||
<v-row class="text-center text-body-2">
|
||||
<v-col class="pt-1 pb-1">
|
||||
<i class="fas fa-cogs fa-fw mr-1"></i> {{ recipe.workingTime }} min<br/>
|
||||
<i class="fas fa-cogs fa-fw mr-1"></i> {{ props.recipe.workingTime }} min<br/>
|
||||
<div class="text-grey">Working Time</div>
|
||||
</v-col>
|
||||
<v-col class="pt-1 pb-1">
|
||||
<div><i class="fas fa-hourglass-half fa-fw mr-1"></i> {{ recipe.waitingTime }} min</div>
|
||||
<div><i class="fas fa-hourglass-half fa-fw mr-1"></i> {{ props.recipe.waitingTime }} min</div>
|
||||
<div class="text-grey">Waiting Time</div>
|
||||
</v-col>
|
||||
<v-col class="pt-1 pb-1">
|
||||
@@ -32,7 +38,7 @@
|
||||
<template #activator>
|
||||
<div class="cursor-pointer">
|
||||
<i class="fas fa-sort-numeric-up fa-fw mr-1"></i> {{ servings }} <br/>
|
||||
<div class="text-grey"><span v-if="recipe?.servingsText">{{ recipe.servingsText }}</span><span v-else>Servings</span></div>
|
||||
<div class="text-grey"><span v-if="props.recipe?.servingsText">{{ props.recipe.servingsText }}</span><span v-else>Servings</span></div>
|
||||
</div>
|
||||
</template>
|
||||
</NumberScalerDialog>
|
||||
@@ -41,21 +47,21 @@
|
||||
</v-container>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-1" v-if="recipe.steps.length > 1">
|
||||
<StepsOverview :steps="recipe.steps"></StepsOverview>
|
||||
<v-card class="mt-1" v-if="props.recipe.steps.length > 1">
|
||||
<StepsOverview :steps="props.recipe.steps"></StepsOverview>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-1" v-for="(step, index) in recipe.steps" :key="step.id">
|
||||
<Step :step="step" :step-number="index+1" :ingredient_factor="ingredient_factor"></Step>
|
||||
<v-card class="mt-1" v-for="(step, index) in props.recipe.steps" :key="step.id">
|
||||
<Step :step="step" :step-number="index+1" :ingredient_factor="ingredientFactor"></Step>
|
||||
</v-card>
|
||||
|
||||
<recipe-activity :recipe="recipe"></recipe-activity>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
import {computed, defineComponent, PropType, ref, watch} from 'vue'
|
||||
import {ApiApi, Ingredient, Recipe} from "@/openapi"
|
||||
import KeywordsBar from "@/components/display/KeywordsBar.vue"
|
||||
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
|
||||
@@ -65,39 +71,28 @@ import Step from "@/components/display/Step.vue";
|
||||
import RecipeActivity from "@/components/display/RecipeActivity.vue";
|
||||
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
|
||||
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
|
||||
import RecipeImage from "@/components/display/RecipeImage.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RecipeView",
|
||||
components: {KeywordsComponent, RecipeContextMenu, RecipeActivity, Step, StepsOverview, IngredientsTable, NumberScalerDialog, KeywordsBar},
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
return this.servings / this.recipe.servings
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
servings: 1,
|
||||
showFullRecipeName: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'recipe.servings': function () {
|
||||
if (this.recipe.servings) {
|
||||
this.servings = this.recipe.servings
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as PropType<Recipe>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {}
|
||||
const props = defineProps({
|
||||
recipe: {
|
||||
type: Object as PropType<Recipe>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const servings = ref(1)
|
||||
const showFullRecipeName = ref(false)
|
||||
|
||||
const ingredientFactor = computed(() => {
|
||||
return servings.value / ((props.recipe.servings != undefined) ? props.recipe.servings : 1)
|
||||
})
|
||||
|
||||
watch(() => props.recipe.servings, () => {
|
||||
if (props.recipe.servings) {
|
||||
servings.value = props.recipe.servings
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,71 +2,59 @@
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col><span v-if="step.name">{{ step.name }}</span><span v-else>Step {{ stepNumber}}</span></v-col>
|
||||
<v-col><span v-if="props.step.name">{{ props.step.name }}</span><span v-else>Step {{ props.stepNumber }}</span></v-col>
|
||||
<v-col class="text-right">
|
||||
<v-btn-group density="compact" variant="tonal">
|
||||
<v-btn size="small" color="info" v-if="step.time != undefined && step.time > 0" @click="timerRunning = true"><i class="fas fa-stopwatch mr-1 fa-fw"></i> {{ step.time }}</v-btn>
|
||||
<v-btn size="small" color="info" v-if="props.step.time != undefined && props.step.time > 0" @click="timerRunning = true"><i class="fas fa-stopwatch mr-1 fa-fw"></i> {{ props.step.time }}</v-btn>
|
||||
<v-btn size="small" color="success" v-if="hasDetails" @click="stepChecked = !stepChecked"><i class="fas fa-fw" :class="{'fa-check': !stepChecked, 'fa-times': stepChecked}"></i></v-btn>
|
||||
</v-btn-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<template v-if="!stepChecked">
|
||||
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
|
||||
<timer :seconds="props.step.time != undefined ? props.step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
|
||||
|
||||
<IngredientsTable :ingredients="step.ingredients"></IngredientsTable>
|
||||
<IngredientsTable :ingredients="props.step.ingredients"></IngredientsTable>
|
||||
|
||||
<v-card-text v-if="step.instructionsMarkdown.length > 0">
|
||||
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredient_factor"></instructions>
|
||||
<v-card-text v-if="props.step.instructionsMarkdown.length > 0">
|
||||
<instructions :instructions_html="props.step.instructionsMarkdown" :ingredient_factor="ingredient_factor"></instructions>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {computed, defineComponent, PropType, ref} from 'vue'
|
||||
import IngredientsTable from "@/components/display/IngredientsTable.vue";
|
||||
import {Step} from "@/openapi";
|
||||
import {DateTime, Duration, Interval} from "luxon";
|
||||
|
||||
import Instructions from "@/components/display/Instructions.vue";
|
||||
import Timer from "@/components/display/Timer.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Step",
|
||||
components: {Timer, Instructions, IngredientsTable},
|
||||
props: {
|
||||
step: {
|
||||
type: {} as PropType<Step>,
|
||||
required: true,
|
||||
},
|
||||
stepNumber: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
ingredient_factor: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
const props = defineProps({
|
||||
step: {
|
||||
type: {} as PropType<Step>,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
hasDetails: function () {
|
||||
return this.step.ingredients.length > 0 || (this.step.instruction != undefined && this.step.instruction.length > 0) || this.step.stepRecipeData != undefined || this.step.file != undefined
|
||||
}
|
||||
stepNumber: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timerRunning: false,
|
||||
stepChecked: false,
|
||||
}
|
||||
ingredient_factor: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {}
|
||||
})
|
||||
|
||||
const timerRunning = ref(false)
|
||||
const stepChecked = ref(false)
|
||||
|
||||
const hasDetails = computed(() => {
|
||||
return props.step.ingredients.length > 0 || (props.step.instruction != undefined && props.step.instruction.length > 0) || props.step.stepRecipeData != undefined || props.step.file != undefined
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-expansion-panel-title><i class="far fa-list-alt fa-fw me-2"></i> Steps Overview</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-container>
|
||||
<v-row v-for="(s, i) in steps">
|
||||
<v-row v-for="(s, i) in props.steps">
|
||||
<v-col class="pa-1">
|
||||
<b v-if="s.showAsHeader">{{ i + 1 }}. {{ s.name }} </b>
|
||||
<IngredientsTable :ingredients="s.ingredients"></IngredientsTable>
|
||||
@@ -18,22 +18,18 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {PropType} from 'vue'
|
||||
import {Step} from "@/openapi";
|
||||
import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue";
|
||||
import IngredientsTable from "@/components/display/IngredientsTable.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "StepsOverview",
|
||||
components: {IngredientsTable, IngredientsTableRow},
|
||||
props: {
|
||||
steps: {
|
||||
type: Array as PropType<Array<Step>>,
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
const props = defineProps({
|
||||
steps: {
|
||||
type: Array as PropType<Array<Step>>,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<v-card-text class="pb-0">
|
||||
<v-text-field
|
||||
id="id_global_search_input"
|
||||
v-model="search_query"
|
||||
v-model="searchQuery"
|
||||
autocomplete="off"
|
||||
clearable
|
||||
placeholder="Search"
|
||||
@@ -27,7 +27,7 @@
|
||||
<v-divider></v-divider>
|
||||
<!-- search results -->
|
||||
<v-card-text>
|
||||
<v-card :variant="cardVariant(index)" v-for="(item, index) in search_results" hover class="mt-1" @click="selected_result = index" :key="index">
|
||||
<v-card :variant="cardVariant(index)" v-for="(item, index) in searchResults" hover class="mt-1" @click="selectedResult = index" :key="index">
|
||||
<v-card-title @click="goToSelectedRecipe()">
|
||||
<v-avatar v-if="item.image" :image="item.image"></v-avatar>
|
||||
<v-avatar v-else-if="item.recipe_id !== undefined" color="tandoor">{{ item.name.charAt(0) }}</v-avatar>
|
||||
@@ -56,124 +56,120 @@
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineComponent} from 'vue'
|
||||
import {computed, defineComponent, onMounted, ref, watch} from 'vue'
|
||||
import {SearchResult} from "@/types/SearchTypes";
|
||||
import {ApiApi, Recipe, RecipeFlat} from "@/openapi";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "GlobalSearchDialog",
|
||||
props: {},
|
||||
watch: {
|
||||
dialog: function (newValue) {
|
||||
/**
|
||||
* since dialog has no opened event watch the variable and focus input after delay (nextTick/directly does not work)
|
||||
*/
|
||||
this.search_query = ""
|
||||
setTimeout(() => {
|
||||
if (newValue) {
|
||||
let search = document.getElementById('id_global_search_input')
|
||||
if (search != null) {
|
||||
search.focus()
|
||||
}
|
||||
}
|
||||
}, 20)
|
||||
},
|
||||
search_query: function (newValue) {
|
||||
/**
|
||||
* update selected result if search result length changes due to search_query changes
|
||||
*/
|
||||
if (this.selected_result >= this.search_results.length) {
|
||||
this.selected_result = this.search_results.length - 1
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
recipes: [] as Recipe[],
|
||||
flat_recipes: [] as RecipeFlat[],
|
||||
search_query: null as string|null,
|
||||
selected_result: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* build array of search results
|
||||
* uses custom type to be able to incorporate recent items, plans, books, ... at a later stage
|
||||
*/
|
||||
search_results: function () {
|
||||
let search_results = [] as Array<SearchResult>
|
||||
const router = useRouter()
|
||||
|
||||
if (this.search_query != '' && this.search_query != null) {
|
||||
search_results.push({name: this.search_query, icon: 'fas fa-search', suffix: 'Advanced Search'} as SearchResult)
|
||||
const dialog = ref(false)
|
||||
const recipes = ref([] as Recipe[])
|
||||
const flatRecipes = ref([] as RecipeFlat[])
|
||||
const searchQuery = ref(null as string | null)
|
||||
const selectedResult = ref(0)
|
||||
|
||||
this.flat_recipes.filter(fr => fr.name.toLowerCase().includes(this.search_query.toLowerCase())).slice(0, 10).forEach(r => {
|
||||
search_results.push({name: r.name, image: r.image, recipe_id: r.id} as SearchResult)
|
||||
})
|
||||
} else {
|
||||
// search_results.push({name: 'Recent 1', icon: 'fas fa-history',} as SearchResult)
|
||||
// search_results.push({name: 'Recent 2', icon: 'fas fa-history',} as SearchResult)
|
||||
// search_results.push({name: 'Recent 3', icon: 'fas fa-history',} as SearchResult)
|
||||
/**
|
||||
* build array of search results
|
||||
* uses custom type to be able to incorporate recent items, plans, books, ... at a later stage
|
||||
*/
|
||||
const searchResults = computed(() => {
|
||||
let search_results = [] as Array<SearchResult>
|
||||
|
||||
this.flat_recipes.slice(0, 5).forEach(r => {
|
||||
search_results.push({name: r.name, image: r.image, recipe_id: r.id} as SearchResult)
|
||||
})
|
||||
}
|
||||
if (searchQuery.value != '' && searchQuery.value != null) {
|
||||
search_results.push({name: searchQuery.value, icon: 'fas fa-search', suffix: 'Advanced Search'} as SearchResult)
|
||||
|
||||
return search_results
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// add keyhandlers
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (this.dialog) {
|
||||
if (e.key == 'ArrowUp') {
|
||||
this.selected_result = Math.max(0, this.selected_result - 1)
|
||||
}
|
||||
if (e.key == 'ArrowDown') {
|
||||
this.selected_result = Math.min(this.search_results.length, this.selected_result + 1)
|
||||
}
|
||||
if (e.key == 'Enter') {
|
||||
this.goToSelectedRecipe()
|
||||
}
|
||||
if (e.key == 'k' && e.ctrlKey) {
|
||||
this.dialog = true
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
flatRecipes.value.filter(fr => fr.name.toLowerCase().includes(searchQuery.value.toLowerCase())).slice(0, 10).forEach(r => {
|
||||
search_results.push({name: r.name, image: r.image, recipe_id: r.id} as SearchResult)
|
||||
})
|
||||
} else {
|
||||
// search_results.push({name: 'Recent 1', icon: 'fas fa-history',} as SearchResult)
|
||||
// search_results.push({name: 'Recent 2', icon: 'fas fa-history',} as SearchResult)
|
||||
// search_results.push({name: 'Recent 3', icon: 'fas fa-history',} as SearchResult)
|
||||
|
||||
const api = new ApiApi()
|
||||
api.apiRecipeFlatList().then(r => {
|
||||
this.flat_recipes = r
|
||||
flatRecipes.value.slice(0, 5).forEach(r => {
|
||||
search_results.push({name: r.name, image: r.image, recipe_id: r.id} as SearchResult)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* determines the style for selected elements
|
||||
* @param index index of card to determine style for
|
||||
*/
|
||||
cardVariant(index: number) {
|
||||
if (this.selected_result == index) {
|
||||
return 'tonal'
|
||||
} else {
|
||||
return 'elevated'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* open selected recipe
|
||||
*/
|
||||
goToSelectedRecipe() {
|
||||
this.dialog = false
|
||||
let searchResult = this.search_results[this.selected_result]
|
||||
if (searchResult.recipe_id != null) {
|
||||
this.$router.push({name: 'view_recipe', params: {'id': searchResult.recipe_id}})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return search_results
|
||||
|
||||
})
|
||||
|
||||
watch(dialog, (newValue) => {
|
||||
/**
|
||||
* since dialog has no opened event watch the variable and focus input after delay (nextTick/directly does not work)
|
||||
*/
|
||||
searchQuery.value = ""
|
||||
setTimeout(() => {
|
||||
if (newValue) {
|
||||
let search = document.getElementById('id_global_search_input')
|
||||
if (search != null) {
|
||||
search.focus()
|
||||
}
|
||||
}
|
||||
}, 20)
|
||||
})
|
||||
|
||||
watch(searchQuery, () => {
|
||||
/**
|
||||
* update selected result if search result length changes due to search_query changes
|
||||
*/
|
||||
if (selectedResult.value >= searchResults.value.length) {
|
||||
selectedResult.value = searchResults.value.length - 1
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (dialog.value) {
|
||||
if (e.key == 'ArrowUp') {
|
||||
selectedResult.value = Math.max(0, selectedResult.value - 1)
|
||||
}
|
||||
if (e.key == 'ArrowDown') {
|
||||
selectedResult.value = Math.min(searchResults.value.length, selectedResult.value + 1)
|
||||
}
|
||||
if (e.key == 'Enter') {
|
||||
goToSelectedRecipe()
|
||||
}
|
||||
} else {
|
||||
if (e.key == 'k' && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
dialog.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const api = new ApiApi()
|
||||
api.apiRecipeFlatList().then(r => {
|
||||
flatRecipes.value = r
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* determines the style for selected elements
|
||||
* @param index index of card to determine style for
|
||||
*/
|
||||
function cardVariant(index: number) {
|
||||
if (selectedResult.value == index) {
|
||||
return 'tonal'
|
||||
} else {
|
||||
return 'elevated'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* open selected recipe
|
||||
*/
|
||||
function goToSelectedRecipe() {
|
||||
dialog.value = false
|
||||
let searchResult = searchResults.value[selectedResult.value]
|
||||
if (searchResult.recipe_id != null) {
|
||||
router.push({name: 'view_recipe', params: {'id': searchResult.recipe_id}})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<v-number-input v-model="mutable_number" @update:modelValue="updateNumber('set')" control-variant="split" :min="0" >
|
||||
<v-number-input v-model="mutable_number" @update:modelValue="updateNumber('set')" control-variant="split" :min="0" >
|
||||
</v-number-input>
|
||||
|
||||
<v-btn-group divided class="d-flex">
|
||||
@@ -53,7 +53,8 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
mutable_number: 0
|
||||
mutable_number: 0,
|
||||
someNumber: 12
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -82,7 +83,7 @@ export default defineComponent({
|
||||
if (operation === 'sub') {
|
||||
this.mutable_number = this.number - 1
|
||||
}
|
||||
|
||||
console.log(operation, this.mutable_number)
|
||||
this.$emit('change', {number: this.mutable_number})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,6 +127,27 @@
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258"
|
||||
integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==
|
||||
|
||||
"@intlify/core-base@9.13.1":
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.13.1.tgz#bd1f38e665095993ef9b67aeeb794f3cabcb515d"
|
||||
integrity sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==
|
||||
dependencies:
|
||||
"@intlify/message-compiler" "9.13.1"
|
||||
"@intlify/shared" "9.13.1"
|
||||
|
||||
"@intlify/message-compiler@9.13.1":
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.13.1.tgz#ff8129badf77db3fb648b8d3cceee87c8033ed0a"
|
||||
integrity sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==
|
||||
dependencies:
|
||||
"@intlify/shared" "9.13.1"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@intlify/shared@9.13.1":
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.13.1.tgz#202741d11ece1a9c7480bfd3f27afcf9cb8f72e4"
|
||||
integrity sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.15":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
@@ -1128,6 +1149,15 @@ vue-demi@>=0.14.5, vue-demi@>=0.14.7:
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
|
||||
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
|
||||
|
||||
vue-i18n@9:
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.13.1.tgz#a292c8021b7be604ebfca5609ae1f8fafe5c36d7"
|
||||
integrity sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==
|
||||
dependencies:
|
||||
"@intlify/core-base" "9.13.1"
|
||||
"@intlify/shared" "9.13.1"
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-multiselect@^3.0.0-beta.3:
|
||||
version "3.0.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-3.0.0-beta.3.tgz#b1348238a84c435582c3f46f2a9c045b29bb976c"
|
||||
@@ -1182,10 +1212,10 @@ vuedraggable@^4.1.0:
|
||||
dependencies:
|
||||
sortablejs "1.14.0"
|
||||
|
||||
vuetify@^3.5.10:
|
||||
version "3.5.11"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.11.tgz#9e5b628544e736de0b7f236b704539d544588152"
|
||||
integrity sha512-us5I0jyFwIQYG4v41PFmVMkoc/oJddVT4C2RFjJTI99ttigbQ92gsTeG5SB8BPfmfnUS4paR5BedZwk6W3KlJw==
|
||||
vuetify@^3.5.16:
|
||||
version "3.5.16"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.16.tgz#5046aab39bfa536f0d99c5be4f9d91a7245c3246"
|
||||
integrity sha512-jyApfATreFMkgjvK0bL7ntZnr+p9TU73+4E3kX6fIvUitdAP9fltG7yj+v3k14HLqZRSNhTL1GhQ95DFx631zw==
|
||||
|
||||
w3c-xmlserializer@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user