mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 14:48:02 -05:00
usable books page
This commit is contained in:
@@ -10,7 +10,7 @@ from treebeard.forms import movenodeform_factory
|
|||||||
|
|
||||||
from cookbook.managers import DICTIONARY
|
from cookbook.managers import DICTIONARY
|
||||||
|
|
||||||
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
|
from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, ImportLog, Ingredient, InviteLink,
|
||||||
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
|
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
|
||||||
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||||
@@ -103,6 +103,13 @@ class ConnectorConfigAdmin(admin.ModelAdmin):
|
|||||||
admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
|
admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFilterAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'type', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CustomFilter, CustomFilterAdmin)
|
||||||
|
|
||||||
|
|
||||||
class SyncAdmin(admin.ModelAdmin):
|
class SyncAdmin(admin.ModelAdmin):
|
||||||
list_display = ('storage', 'path', 'active', 'last_checked')
|
list_display = ('storage', 'path', 'active', 'last_checked')
|
||||||
search_fields = ('storage__name', 'path')
|
search_fields = ('storage__name', 'path')
|
||||||
|
|||||||
58
vue3/src/components/display/BookEntryCard.vue
Normal file
58
vue3/src/components/display/BookEntryCard.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :loading="loading">
|
||||||
|
<v-card-title>{{ props.recipeOverview.name }}</v-card-title>
|
||||||
|
<recipe-image height="25vh" :recipe="props.recipeOverview"></recipe-image>
|
||||||
|
<v-card-subtitle>{{ props.recipeOverview.description }}</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<keywords-bar :keywords="props.recipeOverview.keywords"></keywords-bar>
|
||||||
|
</v-card-text>
|
||||||
|
<ingredients-table :ingredient-factor="1" v-model="ingredients" :show-checkbox="false"></ingredients-table>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import RecipeImage from "@/components/display/RecipeImage.vue";
|
||||||
|
import {ApiApi, Ingredient, Recipe, RecipeOverview} from "@/openapi";
|
||||||
|
import {onMounted, PropType, ref} from "vue";
|
||||||
|
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||||
|
import IngredientsTable from "@/components/display/IngredientsTable.vue";
|
||||||
|
import {getRecipeIngredients} from "@/utils/model_utils";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import KeywordsBar from "@/components/display/KeywordsBar.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
recipeOverview: {type: {} as PropType<RecipeOverview>, required: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const recipe = ref({} as Recipe)
|
||||||
|
const ingredients = ref([] as Ingredient[])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadRecipe()
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadRecipe() {
|
||||||
|
let api = new ApiApi()
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
api.apiRecipeRetrieve({id: props.recipeOverview.id!}).then(r => {
|
||||||
|
recipe.value = r
|
||||||
|
ingredients.value = getRecipeIngredients(recipe.value, t,{showStepHeaders: true})
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<td colspan="5" class="font-weight-bold">{{ i.note }}</td>
|
<td colspan="5" class="font-weight-bold">{{ i.note }}</td>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<td style="width: 1%; text-wrap: nowrap" class="pa-0">
|
<td style="width: 1%; text-wrap: nowrap" class="pa-0" v-if="showCheckbox">
|
||||||
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
|
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"></td>
|
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"></td>
|
||||||
@@ -73,6 +73,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showCheckbox: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const tableHeaders = computed(() => {
|
const tableHeaders = computed(() => {
|
||||||
|
|||||||
@@ -3,31 +3,68 @@
|
|||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text class="pt-2 pb-2">
|
<v-card-title>{{ book.name }}
|
||||||
<v-btn variant="flat" @click="router.go(-1)" prepend-icon="fa-solid fa-arrow-left">{{ $t('Back') }}</v-btn>
|
<v-btn class="float-right" variant="flat" :to="{name: 'BooksPage'}" prepend-icon="$books" v-if="mdAndUp">{{ $t('Books') }}</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text v-if="book.shared && book.shared.length > 0">
|
||||||
|
<v-chip-group>
|
||||||
|
<v-label class="me-2">{{ $t('shared_with') }}</v-label>
|
||||||
|
<v-chip v-for="u in book.shared">{{ u.displayName }}</v-chip>
|
||||||
|
</v-chip-group>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<v-card-text class="text-disabled">
|
||||||
|
{{ book.description }}
|
||||||
|
</v-card-text>
|
||||||
|
<v-expansion-panels v-model="toc">
|
||||||
|
<v-expansion-panel>
|
||||||
|
<v-expansion-panel-title>{{ $t('Table_of_Contents') }}</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="(entry, i) in entries" :key="entry.id" @click="page = i; toc = false">
|
||||||
|
{{ entry.recipeContent.name }}
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col class="text-center">
|
||||||
<h2>{{ book.name }}</h2>
|
<v-pagination :model-value="page + 1" @update:model-value="value => page = value - 1" :length="totalItems" @next="page = page + (mdAndUp ? 2 : 1)"
|
||||||
<p class="text-disabled">{{ book.description }}</p>
|
@prev="page = page - (mdAndUp ? 2 : 1)"></v-pagination>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-data-iterator :items="entries" :items-per-page="recipesPerPage" :page="page" @update:page="loadRecipe">
|
|
||||||
<template #default="{items}">
|
|
||||||
<v-card v-for="i in items">
|
|
||||||
<v-card-title>{{i.raw.recipeContent.name}}</v-card-title>
|
|
||||||
<v-card-subtitle>{{i.raw.recipeContent.desciption}}</v-card-subtitle>
|
|
||||||
|
|
||||||
</v-card>
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-window v-model="page" show-arrows>
|
||||||
|
<template #next>
|
||||||
|
<v-btn icon="fa-solid fa-chevron-right" variant="plain" @click="page = page + (mdAndUp ? 2 : 1)"></v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-data-iterator>
|
<template #prev>
|
||||||
<v-pagination v-model="page" :length="totalItems / recipesPerPage"></v-pagination>
|
<v-btn icon="fa-solid fa-chevron-left" variant="plain" @click="page = page - (mdAndUp ? 2 : 1)"></v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-window-item v-for="(entry, i) in entries" :key="entry.id">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<book-entry-card :recipe-overview="entries[i].recipeContent"></book-entry-card>
|
||||||
|
<div class="text-center mt-1">
|
||||||
|
<span class="text-disabled">{{ i + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" v-if="mdAndUp && entries.length > i + 1">
|
||||||
|
<book-entry-card :recipe-overview="entries[i + 1].recipeContent"></book-entry-card>
|
||||||
|
<div class="text-center mt-1">
|
||||||
|
<span class="text-disabled">{{ i + 2 }}</span>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
@@ -39,18 +76,21 @@
|
|||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {ApiApi, RecipeBook, RecipeBookEntry} from "@/openapi";
|
import {ApiApi, RecipeBook, RecipeBookEntry} from "@/openapi";
|
||||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||||
import {VDataTableUpdateOptions} from "@/vuetify";
|
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
import RecipeImage from "@/components/display/RecipeImage.vue";
|
||||||
|
import {useDisplay} from "vuetify";
|
||||||
|
import BookEntryCard from "@/components/display/BookEntryCard.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bookId: {type: String, required: true},
|
bookId: {type: String, required: true},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {mdAndUp} = useDisplay()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const page = ref(1)
|
const toc = ref(false)
|
||||||
const recipesPerPage = ref(1)
|
const page = ref(0)
|
||||||
const totalItems = ref(0)
|
const totalItems = ref(0)
|
||||||
|
|
||||||
const book = ref({} as RecipeBook)
|
const book = ref({} as RecipeBook)
|
||||||
@@ -58,13 +98,15 @@ const entries = ref([] as RecipeBookEntry[])
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadBook()
|
loadBook()
|
||||||
loadEntries({page: 1})
|
|
||||||
|
entries.value = []
|
||||||
|
loadEntries(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the given book
|
* load the given book
|
||||||
*/
|
*/
|
||||||
function loadBook(){
|
function loadBook() {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
@@ -77,12 +119,15 @@ function loadBook(){
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadEntries(options: VDataTableUpdateOptions){
|
function loadEntries(page: number) {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
|
|
||||||
api.apiRecipeBookEntryList({book: props.bookId, page: options.page}).then(r => {
|
api.apiRecipeBookEntryList({book: props.bookId, page: page}).then(r => {
|
||||||
entries.value = r.results
|
entries.value = entries.value.concat(r.results)
|
||||||
totalItems.value = r.count
|
totalItems.value = r.count
|
||||||
|
if (r.next) {
|
||||||
|
loadEntries(page + 1)
|
||||||
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Ingredient} from "@/openapi";
|
import {Ingredient, Recipe} from "@/openapi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a string representing an ingredient
|
* returns a string representing an ingredient
|
||||||
@@ -7,21 +7,46 @@ import {Ingredient} from "@/openapi";
|
|||||||
export function ingredientToString(ingredient: Ingredient) {
|
export function ingredientToString(ingredient: Ingredient) {
|
||||||
let content = []
|
let content = []
|
||||||
|
|
||||||
if(ingredient == undefined){
|
if (ingredient == undefined) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ingredient.amount != 0){
|
if (ingredient.amount != 0) {
|
||||||
content.push(ingredient.amount)
|
content.push(ingredient.amount)
|
||||||
}
|
}
|
||||||
if(ingredient.unit){
|
if (ingredient.unit) {
|
||||||
content.push(ingredient.unit.name)
|
content.push(ingredient.unit.name)
|
||||||
}
|
}
|
||||||
if(ingredient.food){
|
if (ingredient.food) {
|
||||||
content.push(ingredient.food.name)
|
content.push(ingredient.food.name)
|
||||||
}
|
}
|
||||||
if(ingredient.note){
|
if (ingredient.note) {
|
||||||
content.push(`(${ingredient.note})`)
|
content.push(`(${ingredient.note})`)
|
||||||
}
|
}
|
||||||
return content.join(' ')
|
return content.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a list of all ingredients used by the given recipe
|
||||||
|
* @param recipe recipe to return ingredients for
|
||||||
|
* @param t useI18N object to use for translation
|
||||||
|
* @param options options object for list generation
|
||||||
|
* showStepHeaders - add steps as a header ingredient if it's configured on the step
|
||||||
|
*/
|
||||||
|
export function getRecipeIngredients(recipe: Recipe, t: any, options: { showStepHeaders: boolean } = {showStepHeaders: false}) {
|
||||||
|
let ingredients = [] as Ingredient[]
|
||||||
|
recipe.steps.forEach((step, index) => {
|
||||||
|
if (step.showAsHeader && options.showStepHeaders && recipe.steps.length > 1 && (step.ingredients.length > 0 || step.name != '')) {
|
||||||
|
ingredients.push({
|
||||||
|
amount: 0,
|
||||||
|
unit: null,
|
||||||
|
food: null,
|
||||||
|
note: (step.name !== '') ? step.name : t('Step') + ' ' + (index + 1),
|
||||||
|
isHeader: true
|
||||||
|
} as Ingredient)
|
||||||
|
}
|
||||||
|
ingredients = ingredients.concat(step.ingredients)
|
||||||
|
})
|
||||||
|
return ingredients
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user