Good progress on shopping pages
This commit is contained in:
parent
5100ff0206
commit
74e46eef11
5 changed files with 315 additions and 18 deletions
|
|
@ -1,17 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
Shopping <br>
|
|
||||||
<action-item title="Add to next shop" :image="require('@/assets/add-cart.svg')" @click="() => this.$router.push('/')" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import ActionItem from './ActionItem.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ShoppingPage',
|
|
||||||
components: {
|
|
||||||
ActionItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
127
src/components/shopping/MealSelectionList.vue
Normal file
127
src/components/shopping/MealSelectionList.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="meal in meals" :key="meal.id">
|
||||||
|
<!-- Have a checkbox and card for each meal, show the image and name -->
|
||||||
|
<!-- Emit meal-selected event on checked and meal-unselected on unchecked -->
|
||||||
|
<input type="checkbox" :id="meal.id" @change="$emit('meal-selected', meal)" />
|
||||||
|
<label :for="meal.id" :style="getImageStyling(meal)">
|
||||||
|
{{ meal.meal_date.toLocaleDateString('en-AU', { weekday: 'long', day: 'numeric', month: 'long' }) }}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
list-style-type: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the default checkbox formatting, and format the card instead */
|
||||||
|
input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1em;
|
||||||
|
width: 10em;
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Give the card a thick pastel green stroke when checked */
|
||||||
|
input[type="checkbox"]:checked + label {
|
||||||
|
border: 0.5em solid #8FBC8F;
|
||||||
|
/* Reduce the size to account for the border */
|
||||||
|
width: 9em;
|
||||||
|
height: 9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the image as the background image of the card */
|
||||||
|
.meal-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position the text in the center of the card */
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import data from '@/data.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MealSelectionList',
|
||||||
|
data() {
|
||||||
|
const from = new Date();
|
||||||
|
from.setTime(0);
|
||||||
|
|
||||||
|
const to = new Date();
|
||||||
|
to.setDate(to.getDate() + 7);
|
||||||
|
|
||||||
|
return { from, to, meals: [], }
|
||||||
|
},
|
||||||
|
async beforeMount() {
|
||||||
|
const meals = await data.getMeals(this.from, this.to)
|
||||||
|
this.meals = meals;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getImageStyling(meal) {
|
||||||
|
let image = null;
|
||||||
|
// First non empty value in meal.recipe/image_urls
|
||||||
|
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 {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` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
74
src/components/shopping/ShoppingListItem.vue
Normal file
74
src/components/shopping/ShoppingListItem.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<!-- Show a card of the product with the total, taking up the available space and have an expanding section to view sources -->
|
||||||
|
<div class="shopping-list-item">
|
||||||
|
<img :src="product.img_small" class="product-image" />
|
||||||
|
<div class="product-details">
|
||||||
|
<h3 class="header">
|
||||||
|
<strong><a :href="product.link">{{ product.name }}</a></strong>, <small>{{ quantityRequired }} of {{ product.size }} {{ product.unit }} required.</small>
|
||||||
|
</h3>
|
||||||
|
<p class="sources">
|
||||||
|
<span v-for="(source, index) in sources" :key="source.id">
|
||||||
|
<span v-if="index">, and </span>
|
||||||
|
<span v-if="source.type === 'person'">
|
||||||
|
{{ source.quantity.value }} {{ source.quantity.unit }} for {{ source.person.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="source.type === 'meal_recipe'">
|
||||||
|
{{ source.quantity.value }} {{ source.quantity.unit }}
|
||||||
|
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>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="source.type === 'meal_extra_ingredient'">
|
||||||
|
{{ 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>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
/* Show the product image to the left, then the product name and size to the right */
|
||||||
|
|
||||||
|
.shopping-list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
max-width: 5em;
|
||||||
|
max-height: 4em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* show sources as subtitles to the product name, with the quantity and source name */
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShoppingListItem',
|
||||||
|
props: ['product', 'sources'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
quantityRequired: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
113
src/components/shopping/ShoppingPage.vue
Normal file
113
src/components/shopping/ShoppingPage.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Meals</h1>
|
||||||
|
<meal-selection-list />
|
||||||
|
<h1>Shopping List</h1>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in shoppingList" :key="item.id">
|
||||||
|
<shopping-list-item :product="item.product" :sources="item.sources" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
# Functions
|
||||||
|
* Add a random item to next shop
|
||||||
|
* Add meals to next shop
|
||||||
|
* Show cards for the next 7 meals
|
||||||
|
* Slider / drawer for more meals
|
||||||
|
* Check meal to add
|
||||||
|
* Update shopping list automatically as meals change
|
||||||
|
* Visual indicator for meals that are already in the list
|
||||||
|
* Visual indicator for purchased meals
|
||||||
|
* Aggregate items from meals and random items into a single list of products
|
||||||
|
* For each product, need to have visibility of
|
||||||
|
* Name
|
||||||
|
* Link
|
||||||
|
* Image
|
||||||
|
* Quantity
|
||||||
|
* Source meal / person
|
||||||
|
* Date added / for
|
||||||
|
* Need to be able to mark list as done
|
||||||
|
* Keeps history - track purchased meals
|
||||||
|
* Clears list
|
||||||
|
|
||||||
|
* History view of purchased lists (seperate page ofc)
|
||||||
|
-->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShoppingPage',
|
||||||
|
components: {
|
||||||
|
ShoppingListItem, MealSelectionList
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
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" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "meal_extra_ingredient",
|
||||||
|
meal: { id: 1, meal_date: new Date() },
|
||||||
|
ingredient: { id: 1, name: "Apples", line: "sum extra applez" },
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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},
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
type: "meal_recipe",
|
||||||
|
meal: { id: 1, meal_date: new Date()},
|
||||||
|
recipe: { id: 1, name: "Banana Bread" },
|
||||||
|
quantity: { value: 2, unit: "kg" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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},
|
||||||
|
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" }
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -6,7 +6,7 @@ import App from './App.vue'
|
||||||
import LoginPage from './components/LoginPage.vue'
|
import LoginPage from './components/LoginPage.vue'
|
||||||
import RecipesPage from './components/recipes/RecipesPage.vue'
|
import RecipesPage from './components/recipes/RecipesPage.vue'
|
||||||
import MealPlanPage from './components/meals/MealPlanPage.vue'
|
import MealPlanPage from './components/meals/MealPlanPage.vue'
|
||||||
import ShoppingPage from './components/ShoppingPage.vue'
|
import ShoppingPage from './components/shopping/ShoppingPage.vue'
|
||||||
import EditMealPage from './components/meals/EditMealPage.vue'
|
import EditMealPage from './components/meals/EditMealPage.vue'
|
||||||
import EditRecipePage from './components/recipes/EditRecipePage.vue'
|
import EditRecipePage from './components/recipes/EditRecipePage.vue'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue