meal planner refactor progress

This commit is contained in:
Kaibu 2021-09-14 23:16:33 +02:00
parent b7fe3e38e6
commit d9300a9a90
16 changed files with 536 additions and 90 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.touchable[data-v-18b1d8a0]{padding-right:2em;padding-left:2em;margin-right:-2em;margin-left:-2em}.meal-plan-card[data-v-6ae25baa]{background-color:#fff}.theme-default .cv-day.draghover[data-v-6ae25baa]{box-shadow:inset 0 0 .2em .2em grey}.calender-parent{display:flex;flex-direction:column;flex-grow:1;overflow-x:hidden;overflow-y:hidden;max-height:80vh;min-height:40rem}.cv-item{white-space:inherit!important}.isHovered{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.cv-day.draghover{box-shadow:inset 0 0 .2em .2em grey!important}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/meal_plan_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/meal_plan_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
{% block content_fluid %}
<div id="app">
<meal-plan-view></meal-plan-view>
</div>
{% 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.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'meal_plan_view' %}
{% endblock %}

File diff suppressed because one or more lines are too long

View File

@ -57,6 +57,7 @@ urlpatterns = [
path('search/v2/', views.search_v2, name='view_search_v2'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan_new/', views.meal_plan_new, name='view_plan_new'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),

View File

@ -220,6 +220,9 @@ def books(request):
def meal_plan(request):
return render(request, 'meal_plan.html', {})
@group_required('user')
def meal_plan_new(request):
return render(request, 'meal_plan_new.html', {})
@group_required('user')
def supermarket(request):

View File

@ -26,6 +26,7 @@
"vue-infinite-loading": "^2.4.5",
"vue-multiselect": "^2.1.6",
"vue-property-decorator": "^9.1.2",
"vue-simple-calendar": "^5.0.1",
"vue-template-compiler": "^2.6.14",
"vue2-touch-events": "^3.2.2",
"vuedraggable": "^2.24.3",

View File

@ -0,0 +1,238 @@
<template>
<div>
<div class="row">
<div class="col-md-2 calender-options">
<b-form>
<b-form-group id="UomInput"
:label="$t('Period')"
:description="$t('PeriodToShow')"
label-for="UomInput">
<b-form-select
id="UomInput"
v-model="settings.displayPeriodUom"
:options="options.displayPeriodUom"
></b-form-select>
</b-form-group>
<b-form-group id="PeriodInput"
:label="$t('PeriodCount')"
:description="$t('ShowHowManyPeriods')"
label-for="PeriodInput">
<b-form-select
id="PeriodInput"
v-model="settings.displayPeriodCount"
:options="options.displayPeriodCount"
></b-form-select>
</b-form-group>
<b-form-group id="DaysInput"
:label="$t('StartingDay')"
:description="$t('StartingDay')"
label-for="DaysInput">
<b-form-select
id="DaysInput"
v-model="settings.startingDayOfWeek"
:options="dayNames"
></b-form-select>
</b-form-group>
</b-form>
<recipe-card :recipe="recipe_viewed" v-if="false"></recipe-card>
</div>
<div class="col-md-10 calender-parent">
<calendar-view
:show-date="showDate" :enable-date-selection="true" class="theme-default"
@date-selection-finish="createEntryRange" :items="plan_items"
:display-period-uom="settings.displayPeriodUom"
:period-changed-callback="refreshData" :enable-drag-drop="true" :item-content-height="item_height"
@click-item="entryClick" @click-date="createEntryClick" @drop-on-date="moveEntry"
:display-period-count="settings.displayPeriodCount"
:starting-day-of-week="settings.startingDayOfWeek"
:display-week-numbers="settings.displayWeekNumbers">
<template #item="{ value, weekStartDate, top }">
<meal-plan-card :value="value" :week-start-date="weekStartDate" :top="top" :detailed="detailed_items"
:item_height="item_height"
@move-left="moveLeft(value)" @move-right="moveRight(value)"/>
</template>
<template #header="{ headerProps }">
<calendar-view-header
:header-props="headerProps"
@input="setShowDate"/>
</template>
</calendar-view>
</div>
</div>
</div>
</template>
<script>
import "vue-simple-calendar/static/css/default.css"
import {CalendarView, CalendarViewHeader, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle";
import Vue from "vue";
import {BootstrapVue} from "bootstrap-vue";
import {ApiApiFactory} from "../../utils/openapi/api";
import RecipeCard from "../../components/RecipeCard";
import MealPlanCard from "../../components/MealPlanCard";
import moment from 'moment'
import {StandardToasts} from "../../utils/utils";
Vue.prototype.moment = moment
Vue.use(BootstrapVue)
export default {
name: "MealPlanView",
components: {
MealPlanCard,
RecipeCard,
CalendarView,
CalendarViewHeader
},
mixins: [CalendarMathMixin],
data: function () {
return {
showDate: new Date(),
plan_entries: [],
recipe_viewed: {},
settings: {
displayPeriodUom: 'week',
displayPeriodCount: 2,
startingDayOfWeek: 1,
displayWeekNumbers: true
},
meal_types: [],
options: {
displayPeriodUom: [{text: this.$t('Week'), value: 'week'}, {
text: this.$t('Month'),
value: 'month'
}, {text: this.$t('Year'), value: 'year'}],
displayPeriodCount: [1, 2, 3],
}
}
},
computed: {
plan_items: function () {
let items = []
this.plan_entries.forEach((entry) => {
items.push(this.buildItem(entry))
})
return items
},
detailed_items: function () {
return this.settings.displayPeriodUom === 'week';
},
dayNames: function () {
let options = []
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
options.push({text: day, value: index})
})
return options
},
userLocale: function () {
return this.getDefaultBrowserLocale
},
item_height: function () {
if (this.settings.displayPeriodUom === 'week') {
return "10rem"
} else {
return "1.6rem"
}
},
},
methods: {
setShowDate(d) {
this.showDate = d;
},
createEntryRange(data) {
console.log(data)
},
createEntryClick(data) {
console.log(data)
},
findEntry(id) {
return this.plan_entries.filter(entry => {
return entry.id === id
})[0]
},
moveEntry(data, target_date) {
this.plan_entries.forEach((entry) => {
if (entry.id === data.id) {
entry.date = target_date
this.saveEntry(entry)
}
})
},
moveLeft(data) {
this.plan_entries.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).subtract(1, 'd')
this.saveEntry(entry)
}
})
},
moveRight(data) {
this.plan_entries.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).add(1, 'd')
this.saveEntry(entry)
}
})
},
entryClick(data) {
console.log(data)
let entry = this.findEntry(data.id)
this.recipe_viewed = entry.recipe
},
refreshData() {
let apiClient = new ApiApiFactory()
apiClient.listMealPlans().then(result => {
this.plan_entries = result.data
})
apiClient.listMealTypes().then(result => {
this.meal_types = result.data
})
},
saveEntry(entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")
let apiClient = new ApiApiFactory()
apiClient.updateMealPlan(entry.id, entry).catch(error => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
},
buildItem(plan_entry) {
return {
id: plan_entry.id,
startDate: plan_entry.date,
endDate: plan_entry.date,
entry: plan_entry
}
}
}
}
</script>
<style>
.calender-parent {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-x: hidden;
overflow-y: hidden;
max-height: 80vh;
min-height: 40rem;
}
.cv-item {
white-space: inherit !important;
}
.isHovered {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.cv-day.draghover {
box-shadow: inset 0 0 0.2em 0.2em grey !important;
}
</style>

View File

@ -0,0 +1,10 @@
import Vue from 'vue'
import App from './MealPlanView.vue'
import i18n from '@/i18n'
Vue.config.productionTip = false
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -0,0 +1,103 @@
<template>
<div v-hover class="card cv-item meal-plan-card p-0" :key="value.id" :draggable="true"
:style="`top:${top};height:${item_height}`"
@dragstart="onDragItemStart(value, $event)"
@click.stop="onClickItem(value, $event)"
:aria-grabbed="value == currentDragItem"
:class="value.classes" :title="title">
<div class="card-header p-1 text-center text-primary border-bottom-0" v-if="detailed">
<span class="font-light">{{ entry.entry.meal_type_name }}</span>
</div>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right p-0"
v-if="detailed">
<a>
<meal-plan-card-context-menu :entry="entry.entry" @move-left="$emit('move-left')"
@move-right="$emit('move-right')"></meal-plan-card-context-menu>
</a>
</div>
<div class="card-header p-1 text-center" v-if="detailed">
<span class="font-light">{{ title }}</span>
</div>
<b-img fluid class="card-img-bottom" :src="entry.entry.recipe.image" v-if="isRecipe && detailed"></b-img>
<div class="card-body p-1" v-if="!isRecipe && detailed">
{{ entry.entry.note }}
</div>
<div class="row p-1 flex-nowrap" v-if="!detailed">
<div class="col-2">
<span class="font-light text-center">🍔</span>
</div>
<div class="col-10 d-inline-block text-truncate" :style="`max-height:${item_height}`">
<span class="font-light">{{ title }}</span>
</div>
</div>
</div>
</template>
<script>
import MealPlanCardContextMenu from "./MealPlanCardContextMenu";
export default {
name: "MealPlanCard.vue",
components: {MealPlanCardContextMenu},
props: {
value: Object,
weekStartDate: Date,
top: String,
detailed: Boolean,
item_height: String
},
data: function () {
return {
dateSelectionOrigin: null,
currentDragItem: null
}
},
computed: {
entry: function () {
return this.value.originalItem
},
title: function () {
if (this.isRecipe) {
return this.entry.entry.recipe_name
} else {
return this.entry.entry.title
}
},
isRecipe: function () {
return ('recipe_name' in this.entry.entry)
},
},
methods: {
onClickItem(calendarItem, windowEvent) {
this.$root.$emit("click-item", calendarItem, windowEvent)
},
onDragItemStart(calendarItem, windowEvent) {
windowEvent.dataTransfer.setData("text", calendarItem.id.toString())
this.$root.$emit("dragUpdate", calendarItem, windowEvent)
return true
},
},
directives: {
hover: {
inserted: (el) => {
el.addEventListener('mouseenter', () => {
el.classList.add("shadow")
});
el.addEventListener('mouseleave', () => {
el.classList.remove("shadow")
});
}
}
}
}
</script>
<style scoped>
.meal-plan-card {
background-color: #fff;
}
.theme-default .cv-day.draghover {
box-shadow: inset 0 0 0.2em 0.2em grey;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div>
<b-dropdown variant="link" toggle-class="text-decoration-none text-body pr-1" right no-caret>
<template #button-content>
<i class="fas fa-ellipsis-v fa-lg"></i>
</template>
<b-dropdown-form class="p-1">
<b-button variant="primary" size="sm" @click="moveLeft" class="float-left"><i
class="fas fa-arrow-left fa-lg"></i></b-button>
<b-button variant="primary" size="sm" @click="moveRight" class="float-right"><i
class="fas fa-arrow-right fa-lg"></i></b-button>
</b-dropdown-form>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'MealPlanCardContextMenu',
props: {
entry: Object
},
methods: {
moveLeft: function () {
this.$emit('move-left')
},
moveRight: function () {
this.$emit('move-right')
}
}
}
</script>

View File

@ -36,6 +36,10 @@ const pages = {
'cookbook_view': {
entry: './src/apps/CookbookView/main.js',
chunks: ['chunk-vendors']
},
'meal_plan_view': {
entry: './src/apps/MealPlanView/main.js',
chunks: ['chunk-vendors']
}
}

View File

@ -33,6 +33,14 @@
"name": "js/import_response_view.js",
"path": "js\\import_response_view.js"
},
"css/meal_plan_view.css": {
"name": "css/meal_plan_view.css",
"path": "css\\meal_plan_view.css"
},
"js/meal_plan_view.js": {
"name": "js/meal_plan_view.js",
"path": "js\\meal_plan_view.js"
},
"css/model_list_view.css": {
"name": "css/model_list_view.css",
"path": "css\\model_list_view.css"
@ -105,6 +113,10 @@
"name": "cookbook_view.html",
"path": "cookbook_view.html"
},
"meal_plan_view.html": {
"name": "meal_plan_view.html",
"path": "meal_plan_view.html"
},
"manifest.json": {
"name": "manifest.json",
"path": "manifest.json"
@ -160,6 +172,12 @@
"js/chunk-vendors.js",
"css/cookbook_view.css",
"js/cookbook_view.js"
],
"meal_plan_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/meal_plan_view.css",
"js/meal_plan_view.js"
]
}
}