Merge branch 'feature/books_refactor' into develop
# Conflicts: # vue/webpack-stats.json
This commit is contained in:
@ -548,7 +548,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
|
||||
|
||||
def get_recipe_content(self, obj):
|
||||
return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
|
||||
return RecipeSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
|
||||
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
|
127
cookbook/static/assets/book.svg
Normal file
127
cookbook/static/assets/book.svg
Normal file
@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="200mm"
|
||||
height="155mm"
|
||||
viewBox="0 0 200 155"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="book.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<defs
|
||||
id="defs2608" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="250.5725"
|
||||
inkscape:cy="265.89318"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="1072"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata2611">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m 178.43105,2.5084522 h -0.0553 c -8.32303,0.04154 -50.03147,-4.552088 -75.88885,9.0177618 -1.39985,0.732762 -3.570481,0.732762 -4.970321,0 C 71.655739,-2.0436358 29.947282,2.5499292 21.627712,2.5084522 h -0.0553 c -11.6446421,0 -21.11519213,9.1491068 -21.11519213,20.3962508 V 125.45629 c 0,10.85658 8.78272603,19.7983 19.99531613,20.35823 12.017921,0.60833 42.178527,-1.29269 62.868573,6.34943 1.82153,0.674 3.67762,1.04383 5.61666,1.04383 h 22.121021 c 1.9425,0 3.79859,-0.37329 5.62012,-1.04383 20.6935,-7.64212 50.85065,-5.7411 62.87894,-6.34943 11.20221,-0.55993 19.98493,-9.4982 19.98493,-20.35477 V 22.904703 C 199.54678,11.657559 190.0757,2.5084522 178.43105,2.5084522 Z"
|
||||
id="path835"
|
||||
style="fill:none;fill-opacity:1;stroke:#ddbf86;stroke-width:0.91445;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="sccccsssccssccscs" />
|
||||
<g
|
||||
id="g1062-7"
|
||||
transform="matrix(0.29031165,0.23248306,-0.29632355,0.23741836,21.63667,1.1307691)"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1">
|
||||
<g
|
||||
id="g1005-6"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1">
|
||||
<g
|
||||
id="g1003-5"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1">
|
||||
<path
|
||||
d="M 15.875,42.952 H 4.847 L 22.627,60.731 40.409,42.952 H 29.244 C 30.824,22.001 41.734,5.075 55.884,0.96 53.726,0.332 51.49,0 49.2,0 31.919,0 17.694,18.8 15.875,42.952 Z"
|
||||
id="path1001-2"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g1007-2"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1009-3"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1011-2"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1013-4"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1015-3"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1017-6"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1019-6"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1021-8"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1023-0"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1025-1"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1027-2"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1029-1"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1031-6"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1033-7"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
<g
|
||||
id="g1035-8"
|
||||
style="fill:none;stroke:#ddbf86;stroke-opacity:1" />
|
||||
</g>
|
||||
<path
|
||||
d="m 186.65756,15.01934 3.20156,-2.563823 0.10659,8.35461 -10.43065,-0.08705 3.24132,-2.595673 c -6.66696,-4.606829 -14.84983,-6.088982 -20.17712,-3.776323 0.44041,-0.650797 0.99116,-1.249452 1.65598,-1.781838 5.01687,-4.0175396 14.71744,-2.8611469 22.40232,2.450095 z"
|
||||
id="path1001"
|
||||
style="fill:none;stroke:#ddbf86;stroke-width:0.371235;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
@ -1,81 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Recipe Books' %}{% endblock %}
|
||||
|
||||
{% block content_fluid %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
<h2>{% trans 'Recipe Books' %}</h2>
|
||||
</div>
|
||||
<div class="col col-md-3" style="text-align: right">
|
||||
<a href="{% url 'new_recipe_book' %}" class="btn btn-success"><i
|
||||
class="fas fa-plus-circle"></i> {% trans 'New Book' %}</a>
|
||||
</div>
|
||||
<div id="app" >
|
||||
<cookbook-view></cookbook-view>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
{% for b in book_list %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card" style="margin-top: 2px">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{% if b.book.icon %}{{ b.book.icon }} {% endif %}{{ b.book.name }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{% if b.book.created_by != request.user %}
|
||||
{% trans 'by' %} {{ b.book.created_by.get_user_name }}
|
||||
{% endif %}</h6>
|
||||
|
||||
{% if b.book.description %}
|
||||
<p class="card-text">{{ b.book.description }}</p>
|
||||
{% endif %}
|
||||
<a data-toggle="collapse" href="#collapse_{{ b.book.pk }}" role="button" aria-expanded="false"
|
||||
aria-controls="collapse_{{ b.book.pk }}" class="card-link">{% trans 'Toggle Recipes' %}</a>
|
||||
{% if b.book.created_by == request.user or request.user.is_superuser %}
|
||||
<a href="{% url 'edit_recipe_book' b.book.pk %}" class="card-link">{% trans 'Edit' %}</a>
|
||||
<a href="{% url 'delete_recipe_book' b.book.pk %}"
|
||||
class="card-link">{% trans 'Delete' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="collapse" id="collapse_{{ b.book.pk }}">
|
||||
{% if b.recipes %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for r in b.recipes %}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
{% recipe_last r.recipe request.user as last_cooked %}
|
||||
<a href="{% url 'view_recipe' r.recipe.pk %}">{{ r.recipe.name }}</a>
|
||||
{% recipe_rating r.recipe request.user as rating %}
|
||||
{{ rating|safe }}
|
||||
{% if last_cooked %}
|
||||
|
||||
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if b.book.created_by == request.user or request.user.is_superuser %}
|
||||
<div class="col-2" style="text-align: right">
|
||||
<a href="{% url 'delete_recipe_book_entry' r.pk %}"
|
||||
class="pull-right"><i class="fas fa-trash-alt"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{% trans 'There are no recipes in this book yet.' %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
||||
window.IMAGE_BOOK = "{% static 'assets/book.svg' %}"
|
||||
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'cookbook_view' %}
|
||||
{% endblock %}
|
@ -213,20 +213,7 @@ def recipe_view(request, pk, share=None):
|
||||
|
||||
@group_required('user')
|
||||
def books(request):
|
||||
book_list = []
|
||||
|
||||
recipe_books = RecipeBook.objects.filter(Q(created_by=request.user) | Q(shared=request.user),
|
||||
space=request.space).distinct().all()
|
||||
|
||||
for b in recipe_books:
|
||||
book_list.append(
|
||||
{
|
||||
'book': b,
|
||||
'recipes': RecipeBookEntry.objects.filter(book=b).all()
|
||||
}
|
||||
)
|
||||
|
||||
return render(request, 'books.html', {'book_list': book_list})
|
||||
return render(request, 'books.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
@ -25,6 +25,7 @@
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vue2-touch-events": "^3.2.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.0",
|
||||
"workbox-webpack-plugin": "^6.1.5"
|
||||
|
114
vue/src/apps/CookbookView/CookbookView.vue
Normal file
114
vue/src/apps/CookbookView/CookbookView.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div id="app" class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1 offset">
|
||||
<div class="mb-3" v-for="book in cookbooks" v-bind:key="book.id">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<b-card class="d-flex flex-column" v-hover
|
||||
v-on:click="openBook(book.id)">
|
||||
<b-row no-gutters style="height:inherit;">
|
||||
<b-col no-gutters md="2" style="height:inherit;">
|
||||
<h3>{{book.icon}}</h3>
|
||||
</b-col>
|
||||
<b-col no-gutters md="10" style="height:inherit;">
|
||||
<b-card-body class="m-0 py-0" style="height:inherit;">
|
||||
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
|
||||
<h5 class="m-0 mt-1 text-truncate">{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span> </h5>
|
||||
<div class="m-0 text-truncate">{{ book.description }}</div>
|
||||
<div class="mt-auto mb-1 d-flex flex-row justify-content-end">
|
||||
</div>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading-spinner v-if="current_book === book.id && loading" ></loading-spinner>
|
||||
<transition name="slide-fade">
|
||||
<cookbook-slider :recipes="recipes" :book="book" v-if="current_book === book.id && !loading"></cookbook-slider>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import {ApiApiFactory} from "../../utils/openapi/api";
|
||||
import CookbookSlider from "../../components/CookbookSlider";
|
||||
import LoadingSpinner from "../../components/LoadingSpinner";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'CookbookView',
|
||||
mixins: [],
|
||||
components: {LoadingSpinner, CookbookSlider},
|
||||
data() {
|
||||
return {
|
||||
cookbooks: [],
|
||||
book_background: window.IMAGE_BOOK,
|
||||
recipes: [],
|
||||
current_book: undefined,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.refreshData()
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
methods: {
|
||||
refreshData: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.listRecipeBooks().then(result => {
|
||||
this.cookbooks = result.data
|
||||
})
|
||||
},
|
||||
openBook: function (book) {
|
||||
if(book === this.current_book) {
|
||||
this.current_book = undefined
|
||||
this.recipes = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
this.current_book = book
|
||||
apiClient.listRecipeBookEntrys({query: {book: book}}).then(result => {
|
||||
this.recipes = result.data
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
inserted: function (el) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
el.classList.add("shadow")
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.classList.remove("shadow")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.slide-fade-enter-active {
|
||||
transition: all .6s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter, .slide-fade-leave-to
|
||||
/* .slide-fade-leave-active below version 2.1.8 */
|
||||
{
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
10
vue/src/apps/CookbookView/main.js
Normal file
10
vue/src/apps/CookbookView/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './CookbookView.vue'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
@ -3,15 +3,11 @@
|
||||
<div>
|
||||
<b-modal class="modal" :id="`id_modal_add_book_${modal_id}`" :title="$t('Manage_Books')" :ok-title="$t('Add')"
|
||||
:cancel-title="$t('Close')" @ok="addToBook()" @shown="loadBookEntries">
|
||||
|
||||
<table>
|
||||
<tr v-for="be in this.recipe_book_list" v-bind:key="be.id">
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" @click="removeFromBook(be)"><i class="fa fa-trash-alt"></i></button>
|
||||
</td>
|
||||
<td> {{ be.book_content.name }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="be in this.recipe_book_list" v-bind:key="be.id">
|
||||
{{ be.book_content.name }} <span class="btn btn-sm btn-danger" @click="removeFromBook(be)"><i class="fa fa-trash-alt"></i></span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<multiselect
|
||||
style="margin-top: 1vh"
|
||||
|
107
vue/src/components/CookbookEditCard.vue
Normal file
107
vue/src/components/CookbookEditCard.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<b-card no-body v-hover>
|
||||
<b-card-header class="p-4">
|
||||
<h5>{{ book_copy.icon }} {{ book_copy.name }}
|
||||
<span class="float-right" @click="editOrSave"><i
|
||||
class="fa" v-bind:class="{ 'fa-pen': !editing, 'fa-save': editing }"
|
||||
aria-hidden="true"></i></span></h5>
|
||||
</b-card-header>
|
||||
<b-card-body class="p-4">
|
||||
<div class="form-group" v-if="editing">
|
||||
<label for="inputName1">{{ $t('Name') }}</label>
|
||||
<input class="form-control" id="inputName1"
|
||||
placeholder="Name" v-model="book_copy.name">
|
||||
</div>
|
||||
<div class="form-group" v-if="editing">
|
||||
<label for="inputIcon1">{{ $t('Icon') }}</label>
|
||||
<input class="form-control" id="inputIcon1"
|
||||
placeholder="Name" v-model="book_copy.icon">
|
||||
<emoji-input :field="'icon'" :label="'icon'" :value="book_copy.icon"></emoji-input>
|
||||
</div>
|
||||
<div class="form-group" v-if="editing">
|
||||
<label for="inputDesc1">{{ $t('Description') }}</label>
|
||||
<textarea class="form-control" id="inputDesc1" rows="3" v-model="book_copy.description">
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
<b-card-text style="text-overflow: ellipsis;" v-if="!editing">
|
||||
{{ book_copy.description }}
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiApiFactory} from "../utils/openapi/api";
|
||||
import {StandardToasts} from "../utils/utils";
|
||||
import EmojiInput from "./Modals/EmojiInput";
|
||||
|
||||
export default {
|
||||
name: "CookbookEditCard",
|
||||
components: {EmojiInput},
|
||||
props: {
|
||||
book: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
book_copy: {},
|
||||
users: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.book_copy = this.book
|
||||
this.$root.$on('change', this.updateEmoji);
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
inserted: function (el) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
el.classList.add("shadow")
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.classList.remove("shadow")
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editOrSave: function () {
|
||||
if (!this.editing) {
|
||||
this.editing = true
|
||||
this.$emit("editing", true)
|
||||
} else {
|
||||
this.editing = false
|
||||
this.saveData()
|
||||
this.$emit("editing", false)
|
||||
}
|
||||
},
|
||||
updateEmoji: function (item, value) {
|
||||
if (item === 'icon') {
|
||||
this.book_copy.icon = value
|
||||
}
|
||||
},
|
||||
saveData: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.updateRecipeBook(this.book_copy.id, this.book_copy).then(result => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch(error => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
},
|
||||
refreshData: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.listUsers().then(result => {
|
||||
this.users = result.data
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
220
vue/src/components/CookbookSlider.vue
Normal file
220
vue/src/components/CookbookSlider.vue
Normal file
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div v-bind:class="{ bounceright: bounce_right, bounceleft: bounce_left }">
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-center pt-2 pb-4">
|
||||
<b-pagination pills
|
||||
v-model="current_page"
|
||||
:total-rows="page_count_pagination"
|
||||
:per-page="per_page_count"
|
||||
@change="pageChange"
|
||||
first-text="📖"
|
||||
align="fill">
|
||||
|
||||
</b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-touch:swipe.left="swipeLeft" v-touch:swipe.right="swipeRight">
|
||||
<div class="col-md-1" @click="swipeRight" style="cursor: pointer;">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<transition name="flip" mode="out-in">
|
||||
<cookbook-edit-card :book="book" v-if="current_page === 1"
|
||||
v-on:editing="cookbook_editing = $event"></cookbook-edit-card>
|
||||
</transition>
|
||||
<transition name="flip" mode="out-in">
|
||||
<recipe-card :recipe="display_recipes[0].recipe_content"
|
||||
v-if="current_page > 1" :key="display_recipes[0]"></recipe-card>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<transition name="flip" mode="out-in">
|
||||
<cookbook-toc :recipes="recipes" v-if="current_page === 1"
|
||||
v-on:switchRecipe="switchRecipe($event)"></cookbook-toc>
|
||||
</transition>
|
||||
<transition name="flip">
|
||||
<recipe-card :recipe="display_recipes[1].recipe_content"
|
||||
v-if="current_page > 1 && display_recipes.length === 2" :key="display_recipes[1]"></recipe-card>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="col-md-1" @click="swipeLeft" style="cursor: pointer;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import RecipeCard from "./RecipeCard";
|
||||
import CookbookEditCard from "./CookbookEditCard";
|
||||
import CookbookToc from "./CookbookToc";
|
||||
import Vue2TouchEvents from "vue2-touch-events"
|
||||
import Vue from "vue";
|
||||
|
||||
Vue.use(Vue2TouchEvents)
|
||||
|
||||
export default {
|
||||
name: "CookbookSlider.vue",
|
||||
components: {CookbookToc, CookbookEditCard, RecipeCard},
|
||||
props: {
|
||||
recipes: Array,
|
||||
book: Object
|
||||
},
|
||||
computed: {
|
||||
page_count_pagination: function () {
|
||||
return this.recipes.length + 2
|
||||
},
|
||||
page_count: function () {
|
||||
return Math.ceil(this.page_count_pagination / this.per_page_count)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
display_recipes: [],
|
||||
current_page: 1,
|
||||
per_page_count: 2,
|
||||
bounce_left: false,
|
||||
bounce_right: false,
|
||||
cookbook_editing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageChange: function (page) {
|
||||
this.current_page = page
|
||||
this.display_recipes = this.recipes.slice(((this.current_page - 1) - 1) * 2, (this.current_page - 1) * 2)
|
||||
},
|
||||
swipeLeft: function () {
|
||||
if (this.cookbook_editing) {
|
||||
return
|
||||
}
|
||||
if (this.current_page < this.page_count) {
|
||||
this.pageChange(this.current_page + 1)
|
||||
} else {
|
||||
this.bounce_left = true
|
||||
setTimeout(() => this.bounce_left = false, 500);
|
||||
}
|
||||
},
|
||||
swipeRight: function () {
|
||||
if (this.cookbook_editing) {
|
||||
return
|
||||
}
|
||||
if (this.current_page > 1) {
|
||||
this.pageChange(this.current_page - 1)
|
||||
} else {
|
||||
this.bounce_right = true
|
||||
setTimeout(() => this.bounce_right = false, 500);
|
||||
}
|
||||
},
|
||||
switchRecipe: function (index) {
|
||||
this.pageChange(Math.ceil((index + 1) / this.per_page_count) + 1)
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
inserted: function (el) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
el.classList.add("shadow")
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.classList.remove("shadow")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flip-enter-active {
|
||||
-webkit-animation-name: bounceUp;
|
||||
animation-name: bounceUp;
|
||||
-webkit-animation-duration: .5s;
|
||||
animation-duration: .5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.bounceleft {
|
||||
-webkit-animation-name: bounceLeft;
|
||||
animation-name: bounceLeft;
|
||||
-webkit-animation-duration: .5s;
|
||||
animation-duration: .5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: 1;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
.bounceright {
|
||||
-webkit-animation-name: bounceRight;
|
||||
animation-name: bounceRight;
|
||||
-webkit-animation-duration: .5s;
|
||||
animation-duration: .5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: 1;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounceUp {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateY(-7px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceUp {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-7px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounceLeft {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceLeft {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounceRight {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceRight {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
</style>
|
44
vue/src/components/CookbookToc.vue
Normal file
44
vue/src/components/CookbookToc.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<b-card no-body v-hover>
|
||||
<b-card-header class="p-4">
|
||||
<h5>{{ $t('TableOfContents') }}</h5>
|
||||
</b-card-header>
|
||||
<b-card-body class="p-4">
|
||||
<ol style="max-height: 60vh;overflow-y:auto;-webkit-overflow-scrolling: touch;" class="mb-1">
|
||||
<li v-for="(recipe, index) in recipes" v-bind:key="index" v-on:click="$emit('switchRecipe', index)">
|
||||
<a href="#">{{ recipe.recipe_content.name }} <recipe-rating :recipe="recipe"></recipe-rating> </a>
|
||||
</li>
|
||||
</ol>
|
||||
<b-card-text v-if="recipes.length === 0">
|
||||
{{ $t('Empty')}}
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeRating from "./RecipeRating";
|
||||
export default {
|
||||
name: "CookbookToc",
|
||||
components: {RecipeRating},
|
||||
props: {
|
||||
recipes: Array
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
inserted: function (el) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
el.classList.add("shadow")
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.classList.remove("shadow")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -2,41 +2,41 @@
|
||||
|
||||
<tr @click="$emit('checked-state-changed', ingredient)">
|
||||
<template v-if="ingredient.is_header">
|
||||
<td colspan="5">
|
||||
<b>{{ ingredient.note }}</b>
|
||||
</td>
|
||||
<td colspan="5">
|
||||
<b>{{ ingredient.note }}</b>
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td class="d-print-none">
|
||||
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
|
||||
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="ingredient.amount !== 0" v-html="calculateAmount(ingredient.amount)"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="ingredient.unit !== null && !ingredient.no_amount">{{ ingredient.unit.name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="ingredient.food !== null">
|
||||
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe)" v-if="ingredient.food.recipe !== null"
|
||||
target="_blank" rel="noopener noreferrer">{{ ingredient.food.name }}</a>
|
||||
<span v-if="ingredient.food.recipe === null">{{ ingredient.food.name }}</span>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="ingredient.note">
|
||||
<td class="d-print-non" v-if="detailed">
|
||||
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
|
||||
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="ingredient.amount !== 0" v-html="calculateAmount(ingredient.amount)"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="ingredient.unit !== null && !ingredient.no_amount">{{ ingredient.unit.name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="ingredient.food !== null">
|
||||
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe)" v-if="ingredient.food.recipe !== null"
|
||||
target="_blank" rel="noopener noreferrer">{{ ingredient.food.name }}</a>
|
||||
<span v-if="ingredient.food.recipe === null">{{ ingredient.food.name }}</span>
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="detailed">
|
||||
<div v-if="ingredient.note">
|
||||
<span v-b-popover.hover="ingredient.note"
|
||||
class="d-print-none"> <i class="far fa-comment"></i>
|
||||
</span>
|
||||
|
||||
<div class="d-none d-print-block">
|
||||
<i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}
|
||||
</div>
|
||||
<div class="d-none d-print-block">
|
||||
<i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tr>
|
||||
|
||||
</template>
|
||||
|
||||
@ -51,6 +51,10 @@ export default {
|
||||
ingredient_factor: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="recipe.keywords.length > 0">
|
||||
<span :key="k.id" v-for="k in recipe.keywords" style="padding: 2px">
|
||||
<b-badge pill variant="light">{{k.label}}</b-badge>
|
||||
<span :key="k.id" v-for="k in recipe.keywords" class="pl-1">
|
||||
<b-badge pill variant="light" class="font-weight-normal">{{k.label}}</b-badge>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span>
|
||||
<b-badge pill variant="primary" v-if="recipe.last_cooked !== null"><i class="fas fa-utensils"></i> {{
|
||||
<span class="pl-1">
|
||||
<b-badge pill variant="primary" v-if="recipe.last_cooked !== null" class="font-weight-normal"><i class="fas fa-utensils"></i> {{
|
||||
formatDate(recipe.last_cooked)
|
||||
}}</b-badge>
|
||||
</span>
|
||||
|
@ -6,12 +6,20 @@
|
||||
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src=recipe_image
|
||||
v-bind:alt="$t('Recipe_Image')"
|
||||
top></b-card-img-lazy>
|
||||
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
|
||||
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
|
||||
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right pt-2 pr-1">
|
||||
<a>
|
||||
<recipe-context-menu :recipe="recipe" style="float:right" v-if="recipe !== null"></recipe-context-menu>
|
||||
<recipe-context-menu :recipe="recipe" class="float-right" v-if="recipe !== null"></recipe-context-menu>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-img-overlay w-50 d-flex flex-column justify-content-left float-left text-left pt-2"
|
||||
v-if="recipe.waiting_time !== 0">
|
||||
<b-badge pill variant="light" class="mt-1 font-weight-normal"><i class="fa fa-clock"></i>
|
||||
{{ recipe.working_time }} {{ $t('min') }}
|
||||
</b-badge>
|
||||
<b-badge pill variant="secondary" class="mt-1 font-weight-normal"><i class="fa fa-pause"></i>
|
||||
{{ recipe.waiting_time }} {{ $t('min') }}
|
||||
</b-badge>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
@ -25,18 +33,31 @@
|
||||
<template v-if="recipe !== null">
|
||||
<recipe-rating :recipe="recipe"></recipe-rating>
|
||||
<template v-if="recipe.description !== null">
|
||||
<span v-if="recipe.description.length > 120">
|
||||
{{ recipe.description.substr(0, 120) + "\u2026" }}
|
||||
<span v-if="recipe.description.length > text_length">
|
||||
{{ recipe.description.substr(0, text_length) + "\u2026" }}
|
||||
</span>
|
||||
<span v-if="recipe.description.length <= 120">
|
||||
<span v-if="recipe.description.length <= text_length">
|
||||
{{ recipe.description }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<br/> <!-- TODO UGLY! -->
|
||||
<last-cooked :recipe="recipe"></last-cooked>
|
||||
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
|
||||
|
||||
<p class="mt-1">
|
||||
<last-cooked :recipe="recipe"></last-cooked>
|
||||
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
|
||||
</p>
|
||||
<div class="row mt-3" v-if="detailed">
|
||||
<div class="col-md-12">
|
||||
<h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t('Ingredients') }}</h6>
|
||||
<table class="table table-sm text-wrap">
|
||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||
<template v-for="s in recipe.steps">
|
||||
<template v-for="i in s.ingredients">
|
||||
<Ingredient :detailed="false" :ingredient="i" :ingredient_factor="1" :key="i.id"></Ingredient>
|
||||
</template>
|
||||
</template>
|
||||
<!-- eslint-enable vue/no-v-for-template-key-on-child -->
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t('External') }}</b-badge>
|
||||
<!-- <b-badge pill variant="success"
|
||||
@ -65,6 +86,7 @@ import RecipeRating from "@/components/RecipeRating";
|
||||
import moment from "moment/moment";
|
||||
import Vue from "vue";
|
||||
import LastCooked from "@/components/LastCooked";
|
||||
import Ingredient from "./Ingredient";
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
@ -73,18 +95,30 @@ export default {
|
||||
mixins: [
|
||||
ResolveUrlMixin,
|
||||
],
|
||||
components: {LastCooked, RecipeRating, Keywords, RecipeContextMenu},
|
||||
components: {LastCooked, RecipeRating, Keywords, RecipeContextMenu, Ingredient},
|
||||
props: {
|
||||
recipe: Object,
|
||||
meal_plan: Object,
|
||||
footer_text: String,
|
||||
footer_icon: String,
|
||||
footer_icon: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recipe_image: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
detailed: function () {
|
||||
return this.recipe.steps !== undefined;
|
||||
},
|
||||
text_length: function () {
|
||||
if (this.detailed) {
|
||||
return 200
|
||||
} else {
|
||||
return 120
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
if (this.recipe == null || this.recipe.image === null) {
|
||||
|
@ -124,6 +124,9 @@ export function resolveDjangoUrl(url, params = null) {
|
||||
* */
|
||||
|
||||
export function getUserPreference(pref) {
|
||||
if(window.USER_PREF === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return window.USER_PREF[pref]
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,10 @@ const pages = {
|
||||
entry: './src/apps/ModelListView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
},
|
||||
'cookbook_view': {
|
||||
entry: './src/apps/CookbookView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
Reference in New Issue
Block a user