Tag editor; ingredient editor
This commit is contained in:
parent
a07f037b8f
commit
c13b3a09bd
@ -2,7 +2,7 @@ class IngredientsController < ApplicationController
|
||||
|
||||
before_action :set_ingredient, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
before_action :ensure_valid_user, except: [:index]
|
||||
before_action :ensure_valid_user, except: [:index, :show]
|
||||
|
||||
# GET /ingredients
|
||||
# GET /ingredients.json
|
||||
@ -39,7 +39,7 @@ class IngredientsController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @ingredient.save
|
||||
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully created.' }
|
||||
format.json { render :show, status: :created, location: @ingredient }
|
||||
format.json { render json: { success: true }, status: :created, location: @ingredient }
|
||||
else
|
||||
format.html { render :new }
|
||||
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
|
||||
@ -58,7 +58,7 @@ class IngredientsController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @ingredient.save
|
||||
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully updated.' }
|
||||
format.json { render :show, status: :ok, location: @ingredient }
|
||||
format.json { render json: { success: true }, status: :ok, location: @ingredient }
|
||||
else
|
||||
format.html { render :edit }
|
||||
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
|
||||
@ -92,9 +92,7 @@ class IngredientsController < ApplicationController
|
||||
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js {}
|
||||
end
|
||||
render :show
|
||||
end
|
||||
|
||||
def prefetch
|
||||
@ -134,7 +132,7 @@ class IngredientsController < ApplicationController
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def ingredient_params
|
||||
params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy])
|
||||
params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ash, :iron, :magnesium, :phosphorus, :potassium, :zinc, :copper, :manganese, :vit_c, :vit_b6, :vit_b12, :vit_a, :vit_e, :vit_d, :cholesterol, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy])
|
||||
end
|
||||
|
||||
def conversion_params
|
||||
|
@ -60,8 +60,6 @@ class RecipesController < ApplicationController
|
||||
if @recipe.update(recipe_params)
|
||||
render json: { success: true }
|
||||
else
|
||||
puts '===='
|
||||
puts @recipe.recipe_ingredients.map { |ri| ri.ingredient_id.class }.join("|")
|
||||
render json: @recipe.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
94
app/javascript/components/AppDateTime.vue
Normal file
94
app/javascript/components/AppDateTime.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<span :title="fullString">{{ friendlyString }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
dateTime: {
|
||||
required: true,
|
||||
type: [Date, String]
|
||||
},
|
||||
|
||||
showDate: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
showTime: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dateObj() {
|
||||
if (this.dateTime instanceof Date) {
|
||||
return this.dateTime;
|
||||
} else if (this.dateTime === null || this.dateTime.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return new Date(this.dateTime);
|
||||
}
|
||||
},
|
||||
|
||||
friendlyString() {
|
||||
const parts = [];
|
||||
if (this.showDate) {
|
||||
parts.push(this.formatDate());
|
||||
}
|
||||
if (this.showTime) {
|
||||
parts.push(this.formatTime(true));
|
||||
}
|
||||
return parts.join(" ");
|
||||
},
|
||||
|
||||
fullString() {
|
||||
return this.formatDate() + " " + this.formatTime(false);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate() {
|
||||
if (this.dateObj) {
|
||||
return [this.dateObj.getMonth() + 1, this.dateObj.getDate(), this.dateObj.getFullYear() % 100].join("/");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
formatTime(use12hour) {
|
||||
if (this.dateObj) {
|
||||
let h = this.dateObj.getHours();
|
||||
const m = this.dateObj.getMinutes().toString().padStart(2, "0");
|
||||
let meridiem = "";
|
||||
|
||||
if (use12hour) {
|
||||
meridiem = " am";
|
||||
|
||||
if (h === 0) {
|
||||
h = 12;
|
||||
} else if (h > 12) {
|
||||
h = h - 12;
|
||||
meridiem = " pm";
|
||||
}
|
||||
} else {
|
||||
h = h.toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
return h + ":" + m + " " + meridiem;
|
||||
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
@ -10,9 +10,12 @@
|
||||
import CaretTop from "open-iconic/svg/caret-top";
|
||||
import Check from "open-iconic/svg/check";
|
||||
import CircleCheck from "open-iconic/svg/circle-check.svg";
|
||||
import LinkBroken from "open-iconic/svg/link-broken";
|
||||
import LinkIntact from "open-iconic/svg/link-intact";
|
||||
import LockLocked from "open-iconic/svg/lock-locked";
|
||||
import LockUnlocked from "open-iconic/svg/lock-unlocked";
|
||||
import Person from "open-iconic/svg/person";
|
||||
import Pencil from "open-iconic/svg/pencil";
|
||||
import Star from "open-iconic/svg/star";
|
||||
import X from "open-iconic/svg/x";
|
||||
|
||||
@ -21,8 +24,11 @@
|
||||
'caret-top': CaretTop,
|
||||
check: Check,
|
||||
'circle-check': CircleCheck,
|
||||
'link-broken': LinkBroken,
|
||||
'link-intact': LinkIntact,
|
||||
'lock-locked': LockLocked,
|
||||
'lock-unlocked': LockUnlocked,
|
||||
pencil: Pencil,
|
||||
person: Person,
|
||||
star: Star,
|
||||
x: X
|
||||
@ -85,24 +91,29 @@
|
||||
fill: currentColor;
|
||||
|
||||
&.sm {
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
}
|
||||
|
||||
&.md {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&.md {
|
||||
&.lg {
|
||||
width: 1.33em;
|
||||
height: 1.33em;
|
||||
}
|
||||
|
||||
&.lg {
|
||||
&.xl {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
&.xl {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
}
|
||||
/*&.xl {*/
|
||||
/*width: 3em;*/
|
||||
/*height: 3em;*/
|
||||
/*}*/
|
||||
}
|
||||
|
||||
}
|
||||
|
100
app/javascript/components/AppPager.vue
Normal file
100
app/javascript/components/AppPager.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<nav v-show="totalPages > 1" class="pagination" role="navigation" :aria-label="pagedItemName + ' page navigation'">
|
||||
<a class="pagination-previous" :title="isFirstPage ? 'This is the first page' : ''" :disabled="isFirstPage" @click.prevent="changePage(currentPage - 1)">Previous</a>
|
||||
<a class="pagination-next" :title="isLastPage ? 'This is the last page' : ''" :disabled="isLastPage" @click.prevent="changePage(currentPage + 1)">Next page</a>
|
||||
<ul class="pagination-list">
|
||||
<li v-for="page in pageItems" :key="page">
|
||||
<a v-if="page > 0" class="pagination-link" :class="{'is-current': page === currentPage}" href="#" @click.prevent="changePage(page)">{{page}}</a>
|
||||
<span v-else class="pagination-ellipsis">…</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pagedItemName: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
currentPage: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
|
||||
totalPages: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
|
||||
pageWindow: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: 4
|
||||
},
|
||||
|
||||
pageOuterWindow: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageItems() {
|
||||
const items = new Set();
|
||||
|
||||
for (let x = 0; x < this.pageOuterWindow; x++) {
|
||||
items.add(x + 1);
|
||||
items.add(this.totalPages - x);
|
||||
}
|
||||
|
||||
const start = this.currentPage - Math.ceil(this.pageWindow / 2);
|
||||
const end = this.currentPage + Math.floor(this.pageWindow / 2);
|
||||
|
||||
for (let x = start; x <= end; x++) {
|
||||
items.add(x);
|
||||
}
|
||||
|
||||
let emptySpace = -1;
|
||||
const finalList = [];
|
||||
|
||||
[...items.values()].filter(p => p > 0 && p <= this.totalPages).sort((a, b) => a - b).forEach((p, idx, list) => {
|
||||
finalList.push(p);
|
||||
if (list[idx + 1] && list[idx + 1] !== p + 1) {
|
||||
finalList.push(emptySpace--);
|
||||
}
|
||||
});
|
||||
|
||||
return finalList;
|
||||
},
|
||||
|
||||
isLastPage() {
|
||||
return this.currentPage === this.totalPages;
|
||||
},
|
||||
|
||||
isFirstPage() {
|
||||
return this.currentPage === 1;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(idx) {
|
||||
this.$emit("changePage", idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
ul.pagination {
|
||||
|
||||
}
|
||||
|
||||
</style>
|
64
app/javascript/components/AppTagEditor.vue
Normal file
64
app/javascript/components/AppTagEditor.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="tag-editor control">
|
||||
<input type="text" class="input" v-model="tagText">
|
||||
<div class="tags">
|
||||
<span v-for="t in value" :key="t" class="tag">{{t}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
|
||||
// data() {
|
||||
// return {
|
||||
// valueCopy: []
|
||||
// };
|
||||
// },
|
||||
|
||||
computed: {
|
||||
tagText: {
|
||||
get: function() {
|
||||
return this.value.join(" ");
|
||||
},
|
||||
set: function(str) {
|
||||
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
|
||||
if (!this.arraysEqual(newTags, this.value)) {
|
||||
this.$emit("input", newTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// value(newVal) {
|
||||
// console.log(newVal);
|
||||
// if (!this.arraysEqual(newVal, this.valueCopy)) {
|
||||
// console.log("diff");
|
||||
// this.valueCopy = newVal;
|
||||
// }
|
||||
// }
|
||||
},
|
||||
|
||||
methods: {
|
||||
arraysEqual(arr1, arr2) {
|
||||
if(arr1.length !== arr2.length)
|
||||
return false;
|
||||
for(let i = arr1.length; i--;) {
|
||||
if(arr1[i] !== arr2[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
@ -9,10 +9,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small-mobile">Nutrient Databank Number</label>
|
||||
|
||||
<label class="label is-small-mobile">Nutrient Databank Number</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input type="text" class="input is-small-mobile" v-model="ingredient.ndbn">
|
||||
<button type="button" class="button" :class="{'is-primary': hasNdbn}"><app-icon :icon="hasNdbn ? 'link-intact' : 'link-broken'" size="sm"></app-icon><span>{{ingredient.ndbn}}</span></button>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<app-autocomplete
|
||||
:inputClass="'is-small-mobile'"
|
||||
ref="autocomplete"
|
||||
v-model="ingredient.usda_food_name"
|
||||
:minLength="2"
|
||||
valueAttribute="name"
|
||||
labelAttribute="description"
|
||||
placeholder=""
|
||||
@optionSelected="searchItemSelected"
|
||||
:onGetOptions="updateSearchItems"
|
||||
>
|
||||
</app-autocomplete>
|
||||
</div>
|
||||
<div v-if="hasNdbn" class="control">
|
||||
<button type="button" class="button is-danger" @click="removeNdbn">X</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -23,13 +41,6 @@
|
||||
</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">
|
||||
@ -37,31 +48,96 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Nutrition per 100 grams</legend>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
Custom Units
|
||||
</div>
|
||||
|
||||
<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 class="message-body">
|
||||
<button class="button" type="button" @click="addUnit">Add Unit</button>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Grams</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr v-for="unit in visibleIngredientUnits" :key="unit.id">
|
||||
<td>
|
||||
<div class="control">
|
||||
<input type="text" class="input is-small-mobile" v-model="unit.name">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="control">
|
||||
<input type="text" class="input is-small-mobile" v-model="unit.gram_weight">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="button is-danger" @click="removeUnit(unit)">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
<div class="column">
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
NDBN Units
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Grams</th>
|
||||
</tr>
|
||||
<tr v-for="unit in ingredient.ndbn_units">
|
||||
<td>{{unit.description}}</td>
|
||||
<td>{{unit.gram_weight}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
Nutrition per 100 grams
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
<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 is-expanded">
|
||||
<input type="text" class="input is-small-mobile" :disabled="hasNdbn" v-model="ingredient[name]">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import api from "../lib/Api";
|
||||
import AppAutocomplete from "./AppAutocomplete";
|
||||
import AppIcon from "./AppIcon";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -109,8 +185,67 @@
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
visibleIngredientUnits() {
|
||||
return this.ingredient.ingredient_units.filter(iu => iu._destroy !== true);
|
||||
},
|
||||
|
||||
hasNdbn() {
|
||||
return this.ingredient.ndbn !== null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addUnit() {
|
||||
this.ingredient.ingredient_units.push({
|
||||
id: null,
|
||||
name: null,
|
||||
gram_weight: null
|
||||
});
|
||||
},
|
||||
|
||||
removeUnit(unit) {
|
||||
if (unit.id) {
|
||||
unit._destroy = true;
|
||||
} else {
|
||||
const idx = this.ingredient.ingredient_units.findIndex(i => i === unit);
|
||||
this.ingredient.ingredient_units.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
removeNdbn() {
|
||||
this.ingredient.ndbn = null;
|
||||
this.ingredient.usda_food_name = null;
|
||||
this.ingredient.ndbn_units = [];
|
||||
},
|
||||
|
||||
updateSearchItems(text) {
|
||||
return api.getUsdaFoodSearch(text)
|
||||
.then(data => data.map(f => {
|
||||
return {
|
||||
name: f.name,
|
||||
ndbn: f.ndbn,
|
||||
description: ["#", f.ndbn, ", Cal:", f.kcal, ", Carbs:", f.carbohydrates, ", Fat:", f.lipid, ", Protein:", f.protein].join("")
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
searchItemSelected(food) {
|
||||
this.ingredient.ndbn = food.ndbn;
|
||||
this.ingredient.usda_food_name = food.name;
|
||||
this.ingredient.ndbn_units = [];
|
||||
|
||||
this.loadResource(
|
||||
api.postIngredientSelectNdbn(this.ingredient)
|
||||
.then(i => Object.assign(this.ingredient, i))
|
||||
);
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
AppAutocomplete
|
||||
AppAutocomplete,
|
||||
AppIcon
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,9 +31,7 @@
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small-mobile">Tags</label>
|
||||
<div class="control">
|
||||
<input class="input is-small-mobile" type="text" placeholder="tags">
|
||||
</div>
|
||||
<app-tag-editor v-model="recipe.tags"></app-tag-editor>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
@ -93,6 +91,7 @@
|
||||
import api from "../lib/Api";
|
||||
|
||||
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
|
||||
import AppTagEditor from "./AppTagEditor";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -143,6 +142,7 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
AppTagEditor,
|
||||
RecipeEditIngredientEditor
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,9 @@
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<span class="label is-small-mobile" v-if="showLabels"> </span>
|
||||
<button type="button" class="button is-danger is-small-mobile" @click="deleteIngredient(ingredient)">X</button>
|
||||
<button type="button" class="button is-danger is-small-mobile" @click="deleteIngredient(ingredient)">
|
||||
<app-icon icon="x"></app-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -45,6 +47,7 @@
|
||||
<script>
|
||||
|
||||
import AppAutocomplete from "./AppAutocomplete";
|
||||
import AppIcon from "./AppIcon";
|
||||
import api from "../lib/Api";
|
||||
|
||||
export default {
|
||||
@ -92,7 +95,8 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
AppAutocomplete
|
||||
AppAutocomplete,
|
||||
AppIcon
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<ingredient-show :ingredient="ingredient"></ingredient-show>
|
||||
</div>
|
||||
|
||||
<router-link class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
|
||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
|
||||
<router-link class="button" to="/ingredients">Back</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -20,7 +20,36 @@
|
||||
data() {
|
||||
return {
|
||||
ingredient: {
|
||||
name: null
|
||||
name: null,
|
||||
notes: null,
|
||||
ndbn: null,
|
||||
density: null,
|
||||
water: null,
|
||||
ash: null,
|
||||
protein: null,
|
||||
kcal: null,
|
||||
fiber: null,
|
||||
sugar: null,
|
||||
carbohydrates: null,
|
||||
calcium: null,
|
||||
iron: null,
|
||||
magnesium: null,
|
||||
phosphorus: null,
|
||||
potassium: null,
|
||||
sodium: null,
|
||||
zinc: null,
|
||||
copper: null,
|
||||
manganese: null,
|
||||
vit_c: null,
|
||||
vit_b6: null,
|
||||
vit_b12: null,
|
||||
vit_a: null,
|
||||
vit_e: null,
|
||||
vit_d: null,
|
||||
vit_k: null,
|
||||
cholesterol: null,
|
||||
lipids: null,
|
||||
ingredient_units: []
|
||||
},
|
||||
validationErrors: null
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div v-if="recipe === null">
|
||||
<div v-if="ingredient === null">
|
||||
Loading...
|
||||
</div>
|
||||
<div v-else>
|
||||
|
@ -2,7 +2,9 @@
|
||||
<div>
|
||||
<h1 class="title">Ingredients</h1>
|
||||
|
||||
<router-link :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
|
||||
<router-link v-if="isLoggedIn" :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
|
||||
|
||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
@ -10,7 +12,7 @@
|
||||
<th>Name</th>
|
||||
<th>USDA</th>
|
||||
<th>KCal per 100g</th>
|
||||
<th>Density</th>
|
||||
<th>Density (oz/cup)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -31,12 +33,19 @@
|
||||
<td>{{i.kcal}}</td>
|
||||
<td>{{i.density}}</td>
|
||||
<td>
|
||||
<button type="button" class="button is-danger">X</button>
|
||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: i.id } }">
|
||||
<app-icon icon="pencil"></app-icon>
|
||||
</router-link>
|
||||
<button v-if="isLoggedIn" type="button" class="button is-danger">
|
||||
<app-icon icon="x"></app-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -45,6 +54,7 @@
|
||||
import api from "../lib/Api";
|
||||
import debounce from "lodash/debounce";
|
||||
import AppIcon from "./AppIcon";
|
||||
import AppPager from "./AppPager";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@ -65,10 +75,27 @@
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
totalPages() {
|
||||
if (this.ingredientData) {
|
||||
return this.ingredientData.total_pages
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
currentPage() {
|
||||
if (this.ingredientData) {
|
||||
return this.ingredientData.current_page
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(idx) {
|
||||
this.search.page = idx;
|
||||
},
|
||||
|
||||
getList: debounce(function() {
|
||||
this.loadResource(
|
||||
@ -89,7 +116,8 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
AppIcon
|
||||
AppIcon,
|
||||
AppPager
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<recipe-show :recipe="recipe"></recipe-show>
|
||||
</div>
|
||||
|
||||
<router-link class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
|
||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
|
||||
<router-link class="button" to="/">Back</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,7 +2,9 @@
|
||||
<div>
|
||||
<h1 class="title">Recipes</h1>
|
||||
|
||||
<router-link :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
|
||||
<router-link v-if="isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
|
||||
|
||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
@ -48,15 +50,21 @@
|
||||
</td>
|
||||
<td>{{ r.yields }}</td>
|
||||
<td>{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
|
||||
<td>{{ formatDate(r.updated_at) }}</td>
|
||||
<td><app-date-time :date-time="r.updated_at" :show-time="false"></app-date-time></td>
|
||||
<td>
|
||||
<router-link :to="{name: 'edit_recipe', params: { id: r.id } }" class="button">Edit</router-link>
|
||||
<button type="button" class="button">Delete</button>
|
||||
<router-link v-if="isLoggedIn" :to="{name: 'edit_recipe', params: { id: r.id } }" class="button">
|
||||
<app-icon icon="pencil" size="md"></app-icon>
|
||||
</router-link>
|
||||
<button v-if="isLoggedIn" type="button" class="button is-danger">
|
||||
<app-icon icon="x" size="md"></app-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -64,7 +72,9 @@
|
||||
|
||||
import api from "../lib/Api";
|
||||
import debounce from "lodash/debounce";
|
||||
import AppDateTime from "./AppDateTime";
|
||||
import AppIcon from "./AppIcon";
|
||||
import AppPager from "./AppPager";
|
||||
import AppRating from "./AppRating";
|
||||
|
||||
export default {
|
||||
@ -100,10 +110,28 @@
|
||||
{name: 'total_time', label: 'Time', sort: true},
|
||||
{name: 'created_at', label: 'Created', sort: true}
|
||||
]
|
||||
},
|
||||
|
||||
totalPages() {
|
||||
if (this.recipeData) {
|
||||
return this.recipeData.total_pages;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
currentPage() {
|
||||
if (this.recipeData) {
|
||||
return this.recipeData.current_page;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(idx) {
|
||||
this.search.page = idx;
|
||||
},
|
||||
|
||||
setSort(col) {
|
||||
if (this.search.sortColumn === col) {
|
||||
this.search.sortDirection = this.search.sortDirection === "desc" ? "asc" : "desc";
|
||||
@ -120,15 +148,6 @@
|
||||
);
|
||||
}, 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 = "";
|
||||
|
||||
@ -160,7 +179,9 @@
|
||||
|
||||
components: {
|
||||
AppRating,
|
||||
AppIcon
|
||||
AppIcon,
|
||||
AppDateTime,
|
||||
AppPager
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,6 +181,76 @@ class Api {
|
||||
return this.get("/ingredients/" + id);
|
||||
}
|
||||
|
||||
buildIngredientParams(ingredient) {
|
||||
return {
|
||||
ingredient: {
|
||||
name: ingredient.name,
|
||||
notes: ingredient.notes,
|
||||
ndbn: ingredient.ndbn,
|
||||
density: ingredient.density,
|
||||
|
||||
water: ingredient.water,
|
||||
ash: ingredient.ash,
|
||||
protein: ingredient.protein,
|
||||
kcal: ingredient.kcal,
|
||||
fiber: ingredient.fiber,
|
||||
sugar: ingredient.sugar,
|
||||
carbohydrates: ingredient.carbohydrates,
|
||||
calcium: ingredient.calcium,
|
||||
iron: ingredient.iron,
|
||||
magnesium: ingredient.magnesium,
|
||||
phosphorus: ingredient.phosphorus,
|
||||
potassium: ingredient.potassium,
|
||||
sodium: ingredient.sodium,
|
||||
zinc: ingredient.zinc,
|
||||
copper: ingredient.copper,
|
||||
manganese: ingredient.manganese,
|
||||
vit_c: ingredient.vit_c,
|
||||
vit_b6: ingredient.vit_b6,
|
||||
vit_b12: ingredient.vit_b12,
|
||||
vit_a: ingredient.vit_a,
|
||||
vit_e: ingredient.vit_e,
|
||||
vit_d: ingredient.vit_d,
|
||||
vit_k: ingredient.vit_k,
|
||||
cholesterol: ingredient.cholesterol,
|
||||
lipids: ingredient.lipids,
|
||||
|
||||
|
||||
ingredient_units_attributes: ingredient.ingredient_units.map(iu => {
|
||||
if (iu._destroy) {
|
||||
return {
|
||||
id: iu.id,
|
||||
_destroy: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: iu.id,
|
||||
name: iu.name,
|
||||
gram_weight: iu.gram_weight
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postIngredient(ingredient) {
|
||||
return this.post("/ingredients/", this.buildIngredientParams(ingredient));
|
||||
}
|
||||
|
||||
patchIngredient(ingredient) {
|
||||
return this.patch("/ingredients/" + ingredient.id, this.buildIngredientParams(ingredient));
|
||||
}
|
||||
|
||||
postIngredientSelectNdbn(ingredient) {
|
||||
const url = ingredient.id ? "/ingredients/" + ingredient.id + "/select_ndbn" : "/ingredients/select_ndbn";
|
||||
return this.post(url, this.buildIngredientParams(ingredient));
|
||||
}
|
||||
|
||||
getUsdaFoodSearch(query) {
|
||||
return this.get("/ingredients/usda_food_search", {query: query});
|
||||
}
|
||||
|
||||
postLogin(username, password) {
|
||||
const params = {
|
||||
username: username,
|
||||
|
@ -1,6 +1,7 @@
|
||||
|
||||
// Bluma default overrides
|
||||
$green: #79A736;
|
||||
$red: #FF0000;
|
||||
$primary: $green;
|
||||
|
||||
$modal-content-width: 750px;
|
||||
|
@ -6,6 +6,7 @@
|
||||
@import "~bulma/sass/components/level";
|
||||
@import "~bulma/sass/components/message";
|
||||
@import "~bulma/sass/components/modal";
|
||||
@import "~bulma/sass/components/pagination";
|
||||
@import "~bulma/sass/elements/_all";
|
||||
@import "~bulma/sass/grid/columns";
|
||||
@import "~bulma/sass/layout/section";
|
||||
|
@ -3,6 +3,7 @@ json.extract! @ingredient,
|
||||
:id,
|
||||
:name,
|
||||
:ndbn,
|
||||
:usda_food_name,
|
||||
:notes,
|
||||
:density,
|
||||
:water,
|
||||
@ -30,3 +31,16 @@ json.extract! @ingredient,
|
||||
:vit_k,
|
||||
:cholesterol,
|
||||
:lipids
|
||||
|
||||
if @ingredient.ndbn.present?
|
||||
json.ndbn_units @ingredient.usda_food.usda_food_weights do |fw|
|
||||
json.extract! fw, :amount, :description, :gram_weight
|
||||
end
|
||||
else
|
||||
json.ndbn_units []
|
||||
end
|
||||
|
||||
json.ingredient_units @ingredient.ingredient_units do |iu|
|
||||
json.extract! iu, :id, :name, :gram_weight
|
||||
json._destroy false
|
||||
end
|
@ -1,7 +1,8 @@
|
||||
|
||||
json.array! @foods do |f|
|
||||
|
||||
json.extract! f, :ndbn
|
||||
json.extract! f, :ndbn, :kcal, :carbohydrates, :lipid, :protein
|
||||
json.name f.long_description
|
||||
|
||||
|
||||
end
|
@ -1,30 +1,36 @@
|
||||
Arguments:
|
||||
/home/dan/.nvm/versions/node/v8.7.0/bin/node /home/dan/.nvm/versions/node/v8.7.0/bin/yarn add svg-router
|
||||
/Users/delbert/.nvm/versions/node/v8.9.0/bin/node /usr/local/Cellar/yarn/1.5.1_1/libexec/bin/yarn.js check --integrity
|
||||
|
||||
PATH:
|
||||
/home/dan/.rvm/gems/ruby-2.5.1/bin:/home/dan/.rvm/gems/ruby-2.5.1@global/bin:/home/dan/.rvm/rubies/ruby-2.5.1/bin:/home/dan/.rvm/bin:/home/dan/.cargo/bin:/home/dan/.nvm/versions/node/v8.7.0/bin:/home/dan/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
|
||||
/Users/delbert/.rvm/gems/ruby-2.4.3/bin:/Users/delbert/.rvm/gems/ruby-2.4.3@global/bin:/Users/delbert/.rvm/rubies/ruby-2.4.3/bin:/Users/delbert/.rvm/bin:/Users/delbert/.cargo/bin:/Users/delbert/.nvm/versions/node/v8.9.0/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin
|
||||
|
||||
Yarn version:
|
||||
1.5.1
|
||||
|
||||
Node version:
|
||||
8.7.0
|
||||
8.9.0
|
||||
|
||||
Platform:
|
||||
linux x64
|
||||
darwin x64
|
||||
|
||||
npm manifest:
|
||||
{
|
||||
"dependencies": {
|
||||
"@rails/webpacker": "^3.4.1",
|
||||
"autosize": "^4.0.1",
|
||||
"bulma": "^0.6.2",
|
||||
"caniuse-lite": "^1.0.30000815",
|
||||
"css-loader": "^0.28.11",
|
||||
"lodash": "^4.17.5",
|
||||
"open-iconic": "^1.1.1",
|
||||
"svg-loader": "^0.0.2",
|
||||
"vue": "^2.5.16",
|
||||
"vue-loader": "^14.2.2",
|
||||
"vue-progressbar": "^0.7.4",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
"webpack": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -318,6 +324,10 @@ Lockfile:
|
||||
postcss "^6.0.17"
|
||||
postcss-value-parser "^3.2.3"
|
||||
|
||||
autosize@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.1.tgz#4e2f89d00e290dd98e1f95555a8ccb91e9c7a41a"
|
||||
|
||||
aws-sign2@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
||||
@ -3312,7 +3322,7 @@ Lockfile:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
||||
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4:
|
||||
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.4:
|
||||
version "4.17.5"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
|
||||
|
||||
@ -3817,6 +3827,10 @@ Lockfile:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6"
|
||||
|
||||
open-iconic@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/open-iconic/-/open-iconic-1.1.1.tgz#9dcfc8c7cd3c61cdb4a236b1a347894c97adc0c6"
|
||||
|
||||
opn@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
|
||||
@ -5554,6 +5568,10 @@ Lockfile:
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
svg-loader@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/svg-loader/-/svg-loader-0.0.2.tgz#601ab2fdaa1dadae3ca9975b550de92a07e1d92b"
|
||||
|
||||
svgo@^0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
|
||||
@ -5903,6 +5921,10 @@ Lockfile:
|
||||
vue-style-loader "^4.0.1"
|
||||
vue-template-es2015-compiler "^1.6.0"
|
||||
|
||||
vue-progressbar@^0.7.4:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-progressbar/-/vue-progressbar-0.7.4.tgz#435dd9cb3707e29a1b18063afed44aeff371e606"
|
||||
|
||||
vue-router@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
|
||||
@ -5929,6 +5951,10 @@ Lockfile:
|
||||
version "2.5.16"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
|
||||
|
||||
vuex-router-sync@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz#1a225c17a1dd9e2f74af0a1b2c62072e9492b305"
|
||||
|
||||
vuex@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
|
||||
@ -6188,11 +6214,10 @@ Lockfile:
|
||||
window-size "0.1.0"
|
||||
|
||||
Trace:
|
||||
Error: Couldn't find package "svg-router" on the "npm" registry.
|
||||
at new MessageError (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:186:110)
|
||||
at NpmResolver.<anonymous> (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:51625:15)
|
||||
Error: Found 1 errors.
|
||||
at new MessageError (/usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:186:110)
|
||||
at /usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:59470:13
|
||||
at Generator.next (<anonymous>)
|
||||
at step (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:98:30)
|
||||
at /home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:109:13
|
||||
at step (/usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:98:30)
|
||||
at /usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:109:13
|
||||
at <anonymous>
|
||||
at process._tickCallback (internal/process/next_tick.js:188:7)
|
||||
|
Loading…
Reference in New Issue
Block a user