ingredient editing
This commit is contained in:
parent
1154da1fd1
commit
a07f037b8f
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
99
app/javascript/components/AppRating.vue
Normal file
99
app/javascript/components/AppRating.vue
Normal 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>
|
@ -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>
|
@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user