ingredient editing
This commit is contained in:
parent
1154da1fd1
commit
a07f037b8f
@ -13,6 +13,7 @@
|
||||
import LockLocked from "open-iconic/svg/lock-locked";
|
||||
import LockUnlocked from "open-iconic/svg/lock-unlocked";
|
||||
import Person from "open-iconic/svg/person";
|
||||
import Star from "open-iconic/svg/star";
|
||||
import X from "open-iconic/svg/x";
|
||||
|
||||
const iconMap = {
|
||||
@ -23,6 +24,7 @@
|
||||
'lock-locked': LockLocked,
|
||||
'lock-unlocked': LockUnlocked,
|
||||
person: Person,
|
||||
star: Star,
|
||||
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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import AppAutocomplete from "./AppAutocomplete";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
ingredient: {
|
||||
@ -17,10 +74,52 @@
|
||||
type: String,
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.unit-label {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
</style>
|
@ -64,7 +64,7 @@
|
||||
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) {
|
||||
if (str === "-") {
|
||||
@ -80,22 +80,11 @@
|
||||
for (let line of lines) {
|
||||
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);
|
||||
|
||||
if (match) {
|
||||
let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(match[3]), preparation: magicFunc(match[4])};
|
||||
if (afterBar) {
|
||||
item.name = item.name + ", " + item.preparation;
|
||||
item.preparation = afterBar;
|
||||
}
|
||||
const matchedName = match[3].replace(/\|\s*$/, "");
|
||||
let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(matchedName), preparation: magicFunc(match[4]), id: match[5] || null};
|
||||
parsed.push(item);
|
||||
} else {
|
||||
parsed.push(null);
|
||||
@ -111,15 +100,20 @@
|
||||
},
|
||||
|
||||
methods: {
|
||||
addIngredient() {
|
||||
this.ingredients.push({
|
||||
createIngredient() {
|
||||
return {
|
||||
id: null,
|
||||
quantity: null,
|
||||
units: null,
|
||||
name: null,
|
||||
preparation: null,
|
||||
ingredient_id: null,
|
||||
sort_order: Math.max([0].concat(this.ingredients.map(i => i.sort_order))) + 5
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
addIngredient() {
|
||||
this.ingredients.push(this.createIngredient());
|
||||
},
|
||||
|
||||
deleteIngredient(ingredient) {
|
||||
@ -140,8 +134,9 @@
|
||||
text.push(
|
||||
item.quantity + " " +
|
||||
(item.units || "-") + " " +
|
||||
item.name +
|
||||
(item.preparation ? (", " + item.preparation) : "")
|
||||
(item.name.indexOf(",") >= 0 ? item.name + "|" : item.name) +
|
||||
(item.preparation ? (", " + item.preparation) : "") +
|
||||
(item.id ? (" [" + item.id + "]") : "")
|
||||
);
|
||||
}
|
||||
|
||||
@ -153,6 +148,47 @@
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
@ -42,10 +42,13 @@
|
||||
<span class="tag" v-for="tag in r.tags" :key="tag">{{tag}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{r.rating}}</td>
|
||||
<td>{{r.yields}}</td>
|
||||
<td>{{r.total_time}} ({{r.active_time}})</td>
|
||||
<td>{{r.created_at}}</td>
|
||||
<td>
|
||||
<app-rating v-if="r.rating !== null" :rating="r.rating" ></app-rating>
|
||||
<span v-else>--</span>
|
||||
</td>
|
||||
<td>{{ r.yields }}</td>
|
||||
<td>{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
|
||||
<td>{{ formatDate(r.updated_at) }}</td>
|
||||
<td>
|
||||
<router-link :to="{name: 'edit_recipe', params: { id: r.id } }" class="button">Edit</router-link>
|
||||
<button type="button" class="button">Delete</button>
|
||||
@ -62,6 +65,7 @@
|
||||
import api from "../lib/Api";
|
||||
import debounce from "lodash/debounce";
|
||||
import AppIcon from "./AppIcon";
|
||||
import AppRating from "./AppRating";
|
||||
|
||||
export default {
|
||||
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)
|
||||
.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() {
|
||||
@ -128,6 +159,7 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
AppRating,
|
||||
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