Feature/shopping list (#172)

* API Endpoint

* shopping list added to the UI

* fixed category sidebar on mobile

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-02-18 21:26:02 -09:00 committed by GitHub
commit 6ee9a893ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 16 deletions

View file

@ -5585,10 +5585,17 @@
"integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=" "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM="
}, },
"fast-levenshtein": { "fast-levenshtein": {
"version": "2.0.6", "version": "3.0.0",
"resolved": "https://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==",
"dev": true "requires": {
"fastest-levenshtein": "^1.0.7"
}
},
"fastest-levenshtein": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
"integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow=="
}, },
"faye-websocket": { "faye-websocket": {
"version": "0.11.3", "version": "0.11.3",
@ -8334,6 +8341,14 @@
"prelude-ls": "~1.1.2", "prelude-ls": "~1.1.2",
"type-check": "~0.3.2", "type-check": "~0.3.2",
"word-wrap": "~1.2.3" "word-wrap": "~1.2.3"
},
"dependencies": {
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
}
} }
}, },
"ora": { "ora": {

View file

@ -13,6 +13,7 @@
"@smartweb/vue-flash-message": "^0.6.10", "@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.8.2", "core-js": "^3.8.2",
"fast-levenshtein": "^3.0.0",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"qs": "^6.9.6", "qs": "^6.9.6",
"v-jsoneditor": "^1.4.2", "v-jsoneditor": "^1.4.2",

View file

@ -8,9 +8,10 @@ const mealPlanURLs = {
all: `${prefix}all`, all: `${prefix}all`,
create: `${prefix}create`, create: `${prefix}create`,
thisWeek: `${prefix}this-week`, thisWeek: `${prefix}this-week`,
update: (planID) => `${prefix}${planID}`, update: planID => `${prefix}${planID}`,
delete: (planID) => `${prefix}${planID}`, delete: planID => `${prefix}${planID}`,
today: `${prefix}today`, today: `${prefix}today`,
shopping: planID => `${prefix}${planID}/shopping-list`,
}; };
export default { export default {
@ -43,4 +44,9 @@ export default {
let response = await apiReq.put(mealPlanURLs.update(id), body); let response = await apiReq.put(mealPlanURLs.update(id), body);
return response; return response;
}, },
async shoppingList(id) {
let response = await apiReq.get(mealPlanURLs.shopping(id));
return response.data;
},
}; };

View file

@ -0,0 +1,112 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="650">
<v-card>
<v-card-title class="headline">
Shopping List
<v-spacer></v-spacer>
<v-btn text color="accent" @click="group = !group">
Group (Beta)
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text v-if="group == false">
<v-list
dense
v-for="(recipe, index) in ingredients"
:key="`${index}-recipe`"
>
<v-subheader>{{ recipe.name }} </v-subheader>
<v-divider></v-divider>
<v-list-item-group color="primary">
<v-list-item
v-for="(item, i) in recipe.recipeIngredient"
:key="i"
>
<v-list-item-content>
<v-list-item-title v-text="item"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-card-text v-else>
<v-list dense>
<v-list-item-group color="primary">
<v-list-item v-for="(item, i) in rawIngredients" :key="i">
<v-list-item-content>
<v-list-item-title v-text="item"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-divider></v-divider>
</v-card>
</v-dialog>
</div>
</template>
<script>
import api from "@/api";
const levenshtein = require("fast-levenshtein");
export default {
data() {
return {
dialog: false,
planID: 0,
ingredients: [],
rawIngredients: [],
group: false,
};
},
methods: {
openDialog: function(id) {
this.dialog = true;
this.planID = id;
this.getIngredients();
},
async getIngredients() {
this.ingredients = await api.mealPlans.shoppingList(this.planID);
console.log(this.ingredients);
this.getRawIngredients();
},
getRawIngredients() {
this.ingredients.forEach(element => {
this.rawIngredients.push(element.recipeIngredient);
});
this.rawIngredients = this.rawIngredients.flat();
this.rawIngredients = this.levenshteinFilter(this.rawIngredients);
console.log(this.rawIngredients);
},
levenshteinFilter(source, maximum = 5) {
let _source, matches, x, y;
_source = source.slice();
matches = [];
for (x = _source.length - 1; x >= 0; x--) {
let output = _source.splice(x, 1);
for (y = _source.length - 1; y >= 0; y--) {
if (levenshtein.get(output[0], _source[y]) <= maximum) {
output.push(_source[y]);
_source.splice(y, 1);
x--;
}
}
matches.push(output);
}
return matches.flat();
},
},
};
</script>
<style>
</style>

View file

@ -1,20 +1,42 @@
<template> <template>
<v-navigation-drawer width="175px" clipped app :expand-on-hover="!mobile"> <div>
<v-list nav dense> <v-btn
<v-list-item v-for="nav in links" :key="nav.title" link :to="nav.to"> class="mt-9 ml-n1"
<v-list-item-icon> fixed
<v-icon>{{ nav.icon }}</v-icon> left
</v-list-item-icon> bottom
<v-list-item-title>{{ nav.title | titleCase }}</v-list-item-title> fab
</v-list-item> small
</v-list> color="primary"
</v-navigation-drawer> @click="showSidebar = !showSidebar"
>
<v-icon>mdi-tag</v-icon></v-btn
>
<v-navigation-drawer
:value="mobile ? showSidebar : true"
v-model="showSidebar"
width="175px"
clipped
app
>
<v-list nav dense>
<v-list-item v-for="nav in links" :key="nav.title" link :to="nav.to">
<v-list-item-icon>
<v-icon>{{ nav.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ nav.title | titleCase }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
</div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
showSidebar: false,
links: [], links: [],
baseLinks: [ baseLinks: [
{ {

View file

@ -44,6 +44,7 @@
"sign-up": "Sign up" "sign-up": "Sign up"
}, },
"meal-plan": { "meal-plan": {
"shopping-list": "Shopping List",
"dinner-this-week": "Dinner This Week", "dinner-this-week": "Dinner This Week",
"meal-planner": "Meal Planner", "meal-planner": "Meal Planner",
"dinner-today": "Dinner Today", "dinner-today": "Dinner Today",

View file

@ -6,6 +6,7 @@
@updated="planUpdated" @updated="planUpdated"
/> />
<NewMeal v-else @created="requestMeals" class="mb-5" /> <NewMeal v-else @created="requestMeals" class="mb-5" />
<ShoppingListDialog ref="shoppingList" />
<v-card class="my-2"> <v-card class="my-2">
<v-card-title class="headline"> <v-card-title class="headline">
@ -49,6 +50,14 @@
</v-list-item-group> </v-list-item-group>
</v-list> </v-list>
<v-card-actions class="mt-n5"> <v-card-actions class="mt-n5">
<v-btn
color="accent lighten-2"
class="mx-0"
text
@click="openShoppingList(mealplan.uid)"
>
{{ $t("meal-plan.shopping-list") }}
</v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
color="accent lighten-2" color="accent lighten-2"
@ -78,11 +87,13 @@ import api from "@/api";
import utils from "@/utils"; import utils from "@/utils";
import NewMeal from "../components/MealPlan/MealPlanNew"; import NewMeal from "../components/MealPlan/MealPlanNew";
import EditPlan from "../components/MealPlan/MealPlanEditor"; import EditPlan from "../components/MealPlan/MealPlanEditor";
import ShoppingListDialog from "../components/MealPlan/ShoppingListDialog";
export default { export default {
components: { components: {
NewMeal, NewMeal,
EditPlan, EditPlan,
ShoppingListDialog,
}, },
data: () => ({ data: () => ({
plannedMeals: [], plannedMeals: [],
@ -122,6 +133,10 @@ export default {
api.mealPlans.delete(id); api.mealPlans.delete(id);
this.requestMeals(); this.requestMeals();
}, },
openShoppingList(id) {
console.log(this.$refs.shoppingList.openDialog);
this.$refs.shoppingList.openDialog(id);
},
}, },
}; };
</script> </script>

View file

@ -1,5 +1,6 @@
from typing import List from typing import List
from db.database import db
from db.db_setup import generate_session from db.db_setup import generate_session
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from services.meal_services import MealPlan from services.meal_services import MealPlan
@ -16,6 +17,21 @@ def get_all_meals(session: Session = Depends(generate_session)):
return MealPlan.get_all(session) return MealPlan.get_all(session)
@router.get("/{id}/shopping-list")
def get_shopping_list(id: str, session: Session = Depends(generate_session)):
#! Refactor into Single Database Call
mealplan = db.meals.get(session, id)
slugs = [x.get("slug") for x in mealplan.get("meals")]
recipes = [db.recipes.get(session, x) for x in slugs]
ingredients = [
{"name": x.get("name"), "recipeIngredient": x.get("recipeIngredient")}
for x in recipes
]
return ingredients
@router.post("/create") @router.post("/create")
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)): def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
""" Creates a meal plan database entry """ """ Creates a meal plan database entry """