meal planner refactor progress
This commit is contained in:
parent
b7fe3e38e6
commit
d9300a9a90
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/css/meal_plan_view.css
Normal file
1
cookbook/static/vue/css/meal_plan_view.css
Normal 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
1
cookbook/static/vue/js/meal_plan_view.js
Normal file
1
cookbook/static/vue/js/meal_plan_view.js
Normal file
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/meal_plan_view.html
Normal file
1
cookbook/static/vue/meal_plan_view.html
Normal 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>
|
33
cookbook/templates/meal_plan_new.html
Normal file
33
cookbook/templates/meal_plan_new.html
Normal 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
@ -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'),
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
238
vue/src/apps/MealPlanView/MealPlanView.vue
Normal file
238
vue/src/apps/MealPlanView/MealPlanView.vue
Normal 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>
|
10
vue/src/apps/MealPlanView/main.js
Normal file
10
vue/src/apps/MealPlanView/main.js
Normal 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')
|
103
vue/src/components/MealPlanCard.vue
Normal file
103
vue/src/components/MealPlanCard.vue
Normal 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>
|
32
vue/src/components/MealPlanCardContextMenu.vue
Normal file
32
vue/src/components/MealPlanCardContextMenu.vue
Normal 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>
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user