ingredient editing

This commit is contained in:
Dan Elbert 2018-04-03 10:29:57 -05:00
parent 1154da1fd1
commit a07f037b8f
6 changed files with 324 additions and 26 deletions

View File

@ -13,6 +13,7 @@
import LockLocked from "open-iconic/svg/lock-locked"; import LockLocked from "open-iconic/svg/lock-locked";
import LockUnlocked from "open-iconic/svg/lock-unlocked"; import LockUnlocked from "open-iconic/svg/lock-unlocked";
import Person from "open-iconic/svg/person"; import Person from "open-iconic/svg/person";
import Star from "open-iconic/svg/star";
import X from "open-iconic/svg/x"; import X from "open-iconic/svg/x";
const iconMap = { const iconMap = {
@ -23,6 +24,7 @@
'lock-locked': LockLocked, 'lock-locked': LockLocked,
'lock-unlocked': LockUnlocked, 'lock-unlocked': LockUnlocked,
person: Person, person: Person,
star: Star,
x: X x: X
}; };

View File

@ -0,0 +1,99 @@
<template>
<span ref="wrapper" class="rating" @click="handleClick" @mousemove="handleMousemove" @mouseleave="handleMouseleave">
<span class="set empty-set">
<app-icon v-for="i in starCount" icon="star"></app-icon>
</span>
<span class="set filled-set" :style="filledStyle">
<app-icon v-for="i in starCount" icon="star"></app-icon>
</span>
</span>
</template>
<script>
import AppIcon from "./AppIcon";
export default {
props: {
starCount: {
required: false,
type: Number,
default: 5
},
rating: {
required: false,
type: Number,
default: 0
}
},
data() {
return {
temporaryWidth: null
};
},
computed: {
ratingPercent() {
return (this.rating / this.starCount) * 100.0;
},
filledStyle() {
const width = this.temporaryWidth === null ? this.ratingPercent : this.temporaryWidth;
return {
width: width + "%"
};
}
},
methods: {
handleClick(evt) {
console.log("click");
},
handleMousemove(evt) {
const wrapperBox = this.$refs.wrapper.getBoundingClientRect();
const wrapperWidth = wrapperBox.right - wrapperBox.left;
const mousePosition = evt.clientX;
if (mousePosition > wrapperBox.left && mousePosition < wrapperBox.right) {
this.temporaryWidth = ((mousePosition - wrapperBox.left) / wrapperWidth) * 100.0;
}
},
handleMouseleave(evt) {
this.temporaryWidth = null;
}
},
components: {
AppIcon
}
}
</script>
<style lang="scss" scoped>
span.rating {
position: relative;
display: inline-block;
.set {
white-space: nowrap;
}
.empty-set {
color: gray;
}
.filled-set {
color: gold;
position: absolute;
top: 0;
left: 0;
overflow-x: hidden;
}
}
</style>

View File

@ -1,11 +1,68 @@
<template> <template>
<div> <div>
{{ingredient.name}} <h1 class="title">{{action}} {{ingredient.name || "[Unnamed Ingredient]"}}</h1>
<div class="field">
<label class="label is-small-mobile">Name</label>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="ingredient.name">
</div>
</div>
<div class="field">
<label class="label is-small-mobile">Nutrient Databank Number</label>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="ingredient.ndbn">
</div>
</div>
<div class="field">
<label class="label is-small-mobile">Density</label>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="ingredient.density">
</div>
</div>
<fieldset>
<legent>Ingredient Units</legent>
<button class="button" type="button">Add Unit</button>
</fieldset>
<div class="field">
<label class="label is-small-mobile">Notes</label>
<div class="control">
<textarea type="text" class="textarea is-small-mobile" v-model="ingredient.notes"></textarea>
</div>
</div>
<fieldset>
<legend>Nutrition per 100 grams</legend>
<div class="columns is-mobile is-multiline">
<div v-for="(nutrient, name) in nutrients" :key="name" class="column is-half-mobile is-one-third-tablet">
<label class="label is-small-mobile">{{nutrient.label}}</label>
<div class="field has-addons">
<div class="control">
<input type="text" class="input is-small-mobile" v-model="ingredient[name]">
</div>
<div class="control">
<button type="button" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
</div>
</div>
</div>
</div>
</fieldset>
</div> </div>
</template> </template>
<script> <script>
import AppAutocomplete from "./AppAutocomplete";
export default { export default {
props: { props: {
ingredient: { ingredient: {
@ -17,10 +74,52 @@
type: String, type: String,
default: "Editing" default: "Editing"
} }
},
data() {
return {
nutrients: {
kcal: { label: "Calories", unit: "kcal" },
protein: { label: "Protein", unit: "g" },
lipids: { label: "Fat", unit: "g" },
carbohydrates: { label: "Carbohydrates", unit: "g" },
water: { label: "Water", unit: "g" },
sugar: { label: "Sugar", unit: "g" },
fiber: { label: "Fiber", unit: "g" },
cholesterol: { label: "Cholesterol", unit: "mg" },
sodium: { label: "Sodium", unit: "mg" },
calcium: { label: "Calcium", unit: "mg" },
iron: { label: "Iron", unit: "mg" },
magnesium: { label: "Magnesium", unit: "mg" },
phosphorus: { label: "Phosphorus", unit: "mg" },
potassium: { label: "Potassium", unit: "mg" },
zinc: { label: "Zinc", unit: "mg" },
copper: { label: "Copper", unit: "mg" },
manganese: { label: "Manganese", unit: "mg" },
vit_a: { label: "Vitamin A", unit: "μg" },
vit_b6: { label: "Vitamin B6", unit: "mg" },
vit_b12: { label: "Vitamin B12", unit: "μg" },
vit_c: { label: "Vitamin C", unit: "mg" },
vit_d: { label: "Vitamin D", unit: "μg" },
vit_e: { label: "Vitamin E", unit: "mg" },
vit_k: { label: "Vitamin K", unit: "μg" },
ash: { label: "ash", unit: "g" }
}
};
},
components: {
AppAutocomplete
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.unit-label {
width: 3em;
}
</style> </style>

View File

@ -64,7 +64,7 @@
return []; return [];
} }
const regex = /^(?:([\d\/.]+(?:\s+[\d\/]+)?)\s+)?(?:([\w-]+)(?:\s+of)?\s+)?([^,]*)(?:,\s*(.*))?$/i; const regex = /^(?:([\d\/.]+(?:\s+[\d\/]+)?)\s+)?(?:([\w-]+)(?:\s+of)?\s+)?([^,|]+?|.+\|)(?:,\s*([^|]*?))?(?:\s*\[(\d+)\]\s*)?$/i;
const magicFunc = function(str) { const magicFunc = function(str) {
if (str === "-") { if (str === "-") {
@ -80,22 +80,11 @@
for (let line of lines) { for (let line of lines) {
if (line.length === 0) { continue; } if (line.length === 0) { continue; }
const barIndex = line.lastIndexOf("|");
let afterBar = null;
if (barIndex >= 0) {
afterBar = line.slice(barIndex + 1);
line = line.slice(0, barIndex);
}
const match = line.match(regex); const match = line.match(regex);
if (match) { if (match) {
let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(match[3]), preparation: magicFunc(match[4])}; const matchedName = match[3].replace(/\|\s*$/, "");
if (afterBar) { let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(matchedName), preparation: magicFunc(match[4]), id: match[5] || null};
item.name = item.name + ", " + item.preparation;
item.preparation = afterBar;
}
parsed.push(item); parsed.push(item);
} else { } else {
parsed.push(null); parsed.push(null);
@ -111,15 +100,20 @@
}, },
methods: { methods: {
addIngredient() { createIngredient() {
this.ingredients.push({ return {
id: null,
quantity: null, quantity: null,
units: null, units: null,
name: null, name: null,
preparation: null, preparation: null,
ingredient_id: null, ingredient_id: null,
sort_order: Math.max([0].concat(this.ingredients.map(i => i.sort_order))) + 5 sort_order: Math.max([0].concat(this.ingredients.map(i => i.sort_order))) + 5
}); };
},
addIngredient() {
this.ingredients.push(this.createIngredient());
}, },
deleteIngredient(ingredient) { deleteIngredient(ingredient) {
@ -140,8 +134,9 @@
text.push( text.push(
item.quantity + " " + item.quantity + " " +
(item.units || "-") + " " + (item.units || "-") + " " +
item.name + (item.name.indexOf(",") >= 0 ? item.name + "|" : item.name) +
(item.preparation ? (", " + item.preparation) : "") (item.preparation ? (", " + item.preparation) : "") +
(item.id ? (" [" + item.id + "]") : "")
); );
} }
@ -153,6 +148,47 @@
}, },
saveBulkEditing() { saveBulkEditing() {
const parsed = this.bulkIngredientPreview.filter(i => i !== null);
const existing = [...this.ingredients];
const newList = [];
for (let parsedIngredient of parsed) {
let newIngredient = null;
if (parsedIngredient.id !== null) {
let intId = parseInt(parsedIngredient.id);
let exIdx = existing.findIndex(i => i.id === intId);
if (exIdx >= 0) {
let ex = existing[exIdx];
if (ex.name === parsedIngredient.name) {
newIngredient = ex;
existing.splice(exIdx, 1);
}
}
}
if (newIngredient === null) {
newIngredient = this.createIngredient();
}
newIngredient.quantity = parsedIngredient.quantity;
newIngredient.units = parsedIngredient.units;
newIngredient.name = parsedIngredient.name;
newIngredient.preparation = parsedIngredient.preparation;
newList.push(newIngredient);
}
for (let oldExisting of existing.filter(i => i.id !== null)) {
newList.push({id: oldExisting.id, _destroy: true});
}
this.ingredients.splice(0);
let sortIdx = 0;
for (let n of newList) {
n.sort_order = sortIdx++;
this.ingredients.push(n);
}
this.isBulkEditing = false; this.isBulkEditing = false;
} }
}, },

View File

@ -42,10 +42,13 @@
<span class="tag" v-for="tag in r.tags" :key="tag">{{tag}}</span> <span class="tag" v-for="tag in r.tags" :key="tag">{{tag}}</span>
</div> </div>
</td> </td>
<td>{{r.rating}}</td> <td>
<td>{{r.yields}}</td> <app-rating v-if="r.rating !== null" :rating="r.rating" ></app-rating>
<td>{{r.total_time}} ({{r.active_time}})</td> <span v-else>--</span>
<td>{{r.created_at}}</td> </td>
<td>{{ r.yields }}</td>
<td>{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
<td>{{ formatDate(r.updated_at) }}</td>
<td> <td>
<router-link :to="{name: 'edit_recipe', params: { id: r.id } }" class="button">Edit</router-link> <router-link :to="{name: 'edit_recipe', params: { id: r.id } }" class="button">Edit</router-link>
<button type="button" class="button">Delete</button> <button type="button" class="button">Delete</button>
@ -62,6 +65,7 @@
import api from "../lib/Api"; import api from "../lib/Api";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import AppIcon from "./AppIcon"; import AppIcon from "./AppIcon";
import AppRating from "./AppRating";
export default { export default {
data() { data() {
@ -114,7 +118,34 @@
api.getRecipeList(this.search.page, this.search.per, this.search.sortColumn, this.search.sortDirection, this.search.name, this.search.tags) api.getRecipeList(this.search.page, this.search.per, this.search.sortColumn, this.search.sortDirection, this.search.name, this.search.tags)
.then(data => this.recipeData = data) .then(data => this.recipeData = data)
); );
}, 500, {leading: true, trailing: true}) }, 500, {leading: true, trailing: true}),
formatDate(dateStr) {
if (dateStr && dateStr.length) {
const date = new Date(dateStr);
return [date.getMonth() + 1, date.getDate(), date.getFullYear() % 100].join("/");
} else {
return "";
}
},
formatRecipeTime(total, active) {
let str = "";
if (total && total > 0) {
str += total;
}
if (active && active > 0) {
if (str.length) {
str += " (" + active + ")";
} else {
str += active;
}
}
return str;
}
}, },
created() { created() {
@ -128,6 +159,7 @@
}, },
components: { components: {
AppRating,
AppIcon AppIcon
} }
} }

View File

@ -1,2 +1,32 @@
json.extract! @ingredient, :id, :name, :ndbn, :density json.extract! @ingredient,
:id,
:name,
:ndbn,
:notes,
:density,
:water,
:ash,
:protein,
:kcal,
:fiber,
:sugar,
:carbohydrates,
:calcium,
:iron,
:magnesium,
:phosphorus,
:potassium,
:sodium,
:zinc,
:copper,
:manganese,
:vit_c,
:vit_b6,
:vit_b12,
:vit_a,
:vit_e,
:vit_d,
:vit_k,
:cholesterol,
:lipids