Populate shopping list from meals
This commit is contained in:
parent
7123124aa6
commit
51348d8c1d
4 changed files with 123 additions and 92 deletions
|
|
@ -4,9 +4,9 @@
|
||||||
<li v-for="meal in meals" :key="meal.id">
|
<li v-for="meal in meals" :key="meal.id">
|
||||||
<!-- Have a checkbox and card for each meal, show the image and name -->
|
<!-- Have a checkbox and card for each meal, show the image and name -->
|
||||||
<!-- Emit meal-selected event on checked and meal-unselected on unchecked -->
|
<!-- Emit meal-selected event on checked and meal-unselected on unchecked -->
|
||||||
<input type="checkbox" :id="meal.id" @change="$emit('meal-selected', meal)" />
|
<input type="checkbox" :id="meal.id" @change="mealCheckChanged" />
|
||||||
<label :for="meal.id" :style="getImageStyling(meal)">
|
<label :for="meal.id" :style="getImageStyling(meal)">
|
||||||
{{ meal.meal_date.toLocaleDateString('en-AU', { weekday: 'long', day: 'numeric', month: 'long' }) }}
|
{{ formatDate(meal.meal_date) }}
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -37,17 +37,21 @@ input[type="checkbox"] {
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1em;
|
padding: 0.1vh 0.3em;
|
||||||
width: 10em;
|
/* Help the visibility of the text over the image */
|
||||||
height: 10em;
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Give the card a thick pastel green stroke when checked */
|
|
||||||
input[type="checkbox"]:checked + label {
|
input[type="checkbox"]:checked + label {
|
||||||
border: 0.5em solid #8FBC8F;
|
border: 3px solid #3d5447;
|
||||||
/* Reduce the size to account for the border */
|
color: #3d5447;
|
||||||
width: 9em;
|
text-shadow: #ccc 0 0 0.1em;
|
||||||
height: 9em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show the image as the background image of the card */
|
/* Show the image as the background image of the card */
|
||||||
|
|
@ -72,6 +76,44 @@ label {
|
||||||
|
|
||||||
import data from '@/data.js'
|
import data from '@/data.js'
|
||||||
|
|
||||||
|
const nth = (d) => {
|
||||||
|
if (d > 3 && d < 21) return 'th';
|
||||||
|
switch (d % 10) {
|
||||||
|
case 1: return "st";
|
||||||
|
case 2: return "nd";
|
||||||
|
case 3: return "rd";
|
||||||
|
default: return "th";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return `${date.toLocaleDateString('en-AU', { weekday: 'short' })} ${date.getDate()}${nth(date.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUrl = (meal) => {
|
||||||
|
// First non empty value in meal.recipe/image_urls
|
||||||
|
for (const recipe of meal.recipes) {
|
||||||
|
if (recipe.image_urls.length && recipe.image_urls[0]) {
|
||||||
|
return recipe.image_urls[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First meal.extra_ingredient with a product with an image
|
||||||
|
for (const ingredient of meal.extra_ingredients) {
|
||||||
|
if (ingredient.product) {
|
||||||
|
if (ingredient.product.img_large) {
|
||||||
|
return ingredient.product.img_large;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingredient.product.img_small) {
|
||||||
|
return ingredient.product.img_small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MealSelectionList',
|
name: 'MealSelectionList',
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -88,38 +130,24 @@ export default {
|
||||||
this.meals = meals;
|
this.meals = meals;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate,
|
||||||
getImageStyling(meal) {
|
getImageStyling(meal) {
|
||||||
let image = null;
|
const imageUrl = getUrl(meal);
|
||||||
// First non empty value in meal.recipe/image_urls
|
if (!imageUrl) {
|
||||||
for (const recipe of meal.recipes) {
|
|
||||||
if (recipe.image_urls.length && recipe.image_urls[0]) {
|
|
||||||
image = recipe.image_urls[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
// First meal.extra_ingredient with a product with an image
|
|
||||||
for (const ingredient of meal.extra_ingredients) {
|
|
||||||
if (ingredient.product) {
|
|
||||||
if (ingredient.product.img_large) {
|
|
||||||
image = ingredient.product.img_large;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ingredient.product.img_small) {
|
|
||||||
image = ingredient.product.img_small;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {background: `linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0) 100%), url('${image}') center/cover no-repeat` }
|
const opacity = 0.7;
|
||||||
|
return {background: `linear-gradient(to bottom, rgba(255, 255, 255, ${ opacity }) 0%, rgba(255, 255, 255, ${ opacity }) 100%), url('${imageUrl}') center/cover no-repeat`};
|
||||||
|
},
|
||||||
|
mealCheckChanged(event) {
|
||||||
|
const mealId = parseInt(event.target.id);
|
||||||
|
const meal = this.meals.find(m => m.id === mealId);
|
||||||
|
if (event.target.checked) {
|
||||||
|
this.$emit('meal-selected', meal);
|
||||||
|
} else {
|
||||||
|
this.$emit('meal-unselected', meal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,20 @@
|
||||||
<img :src="product.img_small" class="product-image" />
|
<img :src="product.img_small" class="product-image" />
|
||||||
<div class="product-details">
|
<div class="product-details">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<strong><a :href="product.link">{{ product.name }}</a></strong>, <small>{{ quantityRequired }} of {{ product.size }} {{ product.unit }} required.</small>
|
<strong><a :href="product.link">{{ product.name }}</a></strong>, <small>{{ quantityRequired }} required.</small>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="sources">
|
<p class="sources">
|
||||||
<span v-for="(source, index) in sources" :key="source.id">
|
<span v-for="(source, index) in sources" :key="source.id">
|
||||||
<span v-if="index">, and </span>
|
<span v-if="index">, and </span>
|
||||||
<span v-if="source.type === 'person'">
|
<span v-if="source.person">
|
||||||
{{ source.quantity.value }} {{ source.quantity.unit }} for {{ source.person.name }}
|
{{ source.ingredient.quantity }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.type === 'meal_recipe'">
|
<span v-else-if="source.recipe">
|
||||||
{{ source.quantity.value }} {{ source.quantity.unit }}
|
{{ source.ingredient.quantity }} {{ source.ingredient.unit }}
|
||||||
in <router-link :to="`/recipes/${ source.recipe.id }/`">{{ source.recipe.name }}</router-link>
|
in <router-link :to="`/recipes/${ source.recipe.id }/`">{{ source.recipe.name }}</router-link>
|
||||||
for <router-link :to="`/meals/${ source.meal.id }/`">{{ source.meal.meal_date.toLocaleDateString("en-AU", { weekday: 'long', month: 'long', day: 'numeric' }) }}</router-link>
|
for <router-link :to="`/meals/${ source.meal.id }/`">{{ source.meal.meal_date.toLocaleDateString("en-AU", { weekday: 'long', month: 'long', day: 'numeric' }) }}</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="source.type === 'meal_extra_ingredient'">
|
<span v-else-if="source.meal">
|
||||||
{{ source.ingredient.line }} for <router-link :to="`/meals/${ source.meal.id }/`">{{ source.meal.meal_date.toLocaleDateString("en-AU", { weekday: 'long', month: 'long', day: 'numeric' }) }}</router-link>
|
{{ source.ingredient.line }} for <router-link :to="`/meals/${ source.meal.id }/`">{{ source.meal.meal_date.toLocaleDateString("en-AU", { weekday: 'long', month: 'long', day: 'numeric' }) }}</router-link>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Meals</h1>
|
|
||||||
<meal-selection-list />
|
|
||||||
<h1>Shopping List</h1>
|
<h1>Shopping List</h1>
|
||||||
|
<h4>Included Meals</h4>
|
||||||
|
<meal-selection-list @meal-selected="mealSelected" @meal-unselected="mealUnselected" />
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="item in shoppingList" :key="item.id">
|
<li v-for="item in shoppingList" :key="item.id">
|
||||||
<shopping-list-item :product="item.product" :sources="item.sources" />
|
<shopping-list-item :product="item.product" :sources="item.sources" />
|
||||||
|
|
@ -55,6 +55,7 @@ ul {
|
||||||
<script>
|
<script>
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
import MealSelectionList from './MealSelectionList.vue'
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
|
import { mealToShoppingListSources, removeMealFromShoppingListSources, groupByProduct } from './shopping.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShoppingPage',
|
name: 'ShoppingPage',
|
||||||
|
|
@ -62,51 +63,19 @@ export default {
|
||||||
ShoppingListItem, MealSelectionList
|
ShoppingListItem, MealSelectionList
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { shoppingList: [], sources: [] }
|
||||||
shoppingList: [
|
|
||||||
{
|
|
||||||
product: { name: 'Apples', link: "#", img_small: "https://freshsensations.com.au/cdn/shop/products/Apple_Red_Delicious_db92e9ab-4239-4138-97af-065285ccfc83.png?v=1579222082", unit: "kg", size: 1},
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
type: "person",
|
|
||||||
person: { id: 1, name: "Jacob" },
|
|
||||||
quantity: { value: 3, unit: "kg" }
|
|
||||||
},
|
},
|
||||||
{
|
watch: {
|
||||||
type: "meal_extra_ingredient",
|
sources() {
|
||||||
meal: { id: 1, meal_date: new Date() },
|
this.shoppingList = groupByProduct(this.sources);
|
||||||
ingredient: { id: 1, name: "Apples", line: "sum extra applez" },
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
methods: {
|
||||||
product: { name: 'Bananas', link: "#", img_small: "https://freshsensations.com.au/cdn/shop/products/Banana_Cavendish_1b7b3b7b-4b3b-4b3b-4b3b-4b3b4b3b4b3b.png?v=1579222082", unit: "kg", size: 1},
|
mealSelected(meal) {
|
||||||
sources: [
|
this.sources = this.sources.concat(mealToShoppingListSources(meal));
|
||||||
{
|
|
||||||
type: "meal_recipe",
|
|
||||||
meal: { id: 1, meal_date: new Date()},
|
|
||||||
recipe: { id: 1, name: "Banana Bread" },
|
|
||||||
quantity: { value: 2, unit: "kg" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
mealUnselected(meal) {
|
||||||
product: { name: 'Milk', link: "#", img_small: "https://freshsensations.com.au/cdn/shop/products/Milk_1L_1b7b3b7b-4b3b-4b3b-4b3b-4b3b4b3b4b3b.png?v=1579222082", unit: "l", size: 1},
|
this.sources = removeMealFromShoppingListSources(this.sources, meal);
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
type: "meal_recipe",
|
|
||||||
meal: { id: 1, meal_date: new Date() },
|
|
||||||
recipe: { id: 1, name: "Banana Bread" },
|
|
||||||
quantity: { value: 1, unit: "l" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "person",
|
|
||||||
person: { id: 1, name: "Jacob" },
|
|
||||||
quantity: { value: 4, unit: "litres" }
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/components/shopping/shopping.js
Normal file
34
src/components/shopping/shopping.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export function mealToShoppingListSources(meal) {
|
||||||
|
const sources = [];
|
||||||
|
for (const recipe of meal.recipes) {
|
||||||
|
for (const ingredient of recipe.ingredients) {
|
||||||
|
sources.push({ ingredient, meal, recipe, });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ingredient of meal.extra_ingredients) {
|
||||||
|
sources.push({ ingredient, meal, });
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeMealFromShoppingListSources(sources, meal) {
|
||||||
|
return sources.filter(source => source.meal.id !== meal.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupByProduct(sources) {
|
||||||
|
const grouped = {};
|
||||||
|
for (const source of sources) {
|
||||||
|
if (!grouped[source.ingredient.product.id]) {
|
||||||
|
grouped[source.ingredient.product.id] = {
|
||||||
|
product: source.ingredient.product,
|
||||||
|
sources: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped[source.ingredient.product.id].sources.push(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(grouped);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue