ingredients

This commit is contained in:
Dan Elbert 2018-04-02 00:10:06 -05:00
parent 4c1e0929f1
commit d587ed58b7
17 changed files with 497 additions and 11 deletions

View File

@ -1,13 +1,20 @@
class IngredientsController < ApplicationController class IngredientsController < ApplicationController
before_action :set_ingredient, only: [:edit, :update, :destroy] before_action :set_ingredient, only: [:show, :edit, :update, :destroy]
before_action :ensure_valid_user, except: [:index] before_action :ensure_valid_user, except: [:index]
# GET /ingredients # GET /ingredients
# GET /ingredients.json # GET /ingredients.json
def index def index
@ingredients = Ingredient.all.order(:name) @ingredients = Ingredient.all.order(:name).page(params[:page]).per(params[:per])
if params[:name].present?
@ingredients = @ingredients.matches_tokens(:name, params[:name].split.take(4))
end
end
def show
end end
# GET /ingredients/new # GET /ingredients/new

View File

@ -8,6 +8,8 @@
import CaretBottom from "open-iconic/svg/caret-bottom"; import CaretBottom from "open-iconic/svg/caret-bottom";
import CaretTop from "open-iconic/svg/caret-top"; 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 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";
@ -16,6 +18,8 @@
const iconMap = { const iconMap = {
'caret-bottom': CaretBottom, 'caret-bottom': CaretBottom,
'caret-top': CaretTop, 'caret-top': CaretTop,
check: Check,
'circle-check': CircleCheck,
'lock-locked': LockLocked, 'lock-locked': LockLocked,
'lock-unlocked': LockUnlocked, 'lock-unlocked': LockUnlocked,
person: Person, person: Person,

View File

@ -0,0 +1,26 @@
<template>
<div>
{{ingredient.name}}
</div>
</template>
<script>
export default {
props: {
ingredient: {
required: true,
type: Object
},
action: {
required: false,
type: String,
default: "Editing"
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,21 @@
<template>
<div>
{{ingredient.name}}
</div>
</template>
<script>
export default {
props: {
ingredient: {
required: true,
type: Object
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,11 +1,126 @@
<template> <template>
<div>
<h1 class="title">Calculator</h1>
<div v-for="err in errors" :key="err" class="notification is-warning">
{{err}}
</div>
<div class="columns">
<div class="field column">
<label class="label">Input</label>
<div class="control">
<input class="input" type="text" placeholder="input" v-model="input">
</div>
</div>
<div class="field column">
<label class="label">Output Unit</label>
<div class="control">
<input class="input" type="text" placeholder="unit" v-model="outputUnit">
</div>
</div>
</div>
<div class="columns">
<div class="field column">
<label class="label">Ingredient</label>
<div class="control">
<app-autocomplete
:inputClass="{'is-success': ingredient !== null}"
ref="autocomplete"
v-model="ingredient_name"
:minLength="2"
valueAttribute="name"
labelAttribute="name"
placeholder=""
@optionSelected="searchItemSelected"
:onGetOptions="updateSearchItems"
>
</app-autocomplete>
</div>
</div>
<div class="field column">
<label class="label">Density</label>
<div class="control">
<input class="input" type="text" placeholder="8.345 lb/gallon" v-model="density">
</div>
</div>
</div>
<div class="field">
<label class="label">Output</label>
<div class="control">
<input class="input" type="text" disabled="disabled" v-model="output">
</div>
</div>
</div>
</template> </template>
<script> <script>
export default { import api from "../lib/Api";
import debounce from "lodash/debounce";
import AppAutocomplete from "./AppAutocomplete";
export default {
data() {
return {
input: '',
outputUnit: '',
ingredient_name: '',
ingredient: null,
density: '',
output: '',
errors: []
};
},
methods: {
updateSearchItems(text) {
return api.getSearchIngredients(text);
},
searchItemSelected(ingredient) {
this.ingredient = ingredient;
this.ingredient_name = ingredient.name;
this.density = ingredient.density;
},
updateOutput: debounce(function() {
this.loadResource(api.getCalculate(this.input, this.outputUnit, this.density)
.then(data => {
this.output = data.output;
this.errors = data.errors;
})
);
}, 500)
},
watch: {
'ingredient_name': function(val) {
if (this.ingredient && this.ingredient.name !== val) {
this.ingredient = null;
}
}
},
created() {
this.$watch(
function() {
return this.input + this.outputUnit + this.density;
},
function() {
this.updateOutput();
}
)
},
components: {
AppAutocomplete
}
} }
</script> </script>

View File

@ -0,0 +1,49 @@
<template>
<div>
<div v-if="ingredient === null">
Loading...
</div>
<div v-else>
<ingredient-show :ingredient="ingredient"></ingredient-show>
</div>
<router-link class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
<router-link class="button" to="/ingredients">Back</router-link>
</div>
</template>
<script>
import IngredientShow from "./IngredientShow";
import { mapState } from "vuex";
import api from "../lib/Api";
export default {
data: function () {
return {
ingredient: null
}
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
})
},
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
);
},
components: {
IngredientShow
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,47 @@
<template>
<div>
<ingredient-edit :ingredient="ingredient" action="Creating"></ingredient-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
</div>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
ingredient: {
name: null
},
validationErrors: null
}
},
methods: {
save() {
this.loadResource(
api.postIngredient(this.ingredient)
.then(() => this.$router.push('/ingredients'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
components: {
IngredientEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,63 @@
<template>
<div>
<div v-if="recipe === null">
Loading...
</div>
<div v-else>
<ingredient-edit :ingredient="ingredient"></ingredient-edit>
</div>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
</div>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data: function () {
return {
ingredient: null,
validationErrors: null
};
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
})
},
methods: {
save() {
this.loadResource(
api.patchIngredient(this.ingredient)
.then(() => this.$router.push({name: 'ingredient', params: {id: this.ingredientId }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
);
},
components: {
IngredientEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,14 +1,96 @@
<template> <template>
<div>
<h1 class="title">Ingredients</h1>
<router-link :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>USDA</th>
<th>KCal per 100g</th>
<th>Density</th>
<th></th>
</tr>
<tr>
<th>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="search names" v-model="search.name">
</div>
</div>
</th>
<th colspan="4"></th>
</tr>
</thead>
<tbody>
<tr v-for="i in ingredients" :key="i.id">
<td><router-link :to="{name: 'ingredient', params: { id: i.id } }">{{i.name}}</router-link></td>
<td><app-icon v-if="i.usda" icon="check"></app-icon></td>
<td>{{i.kcal}}</td>
<td>{{i.density}}</td>
<td>
<button type="button" class="button is-danger">X</button>
</td>
</tr>
</tbody>
</table>
</div>
</template> </template>
<script> <script>
export default { import api from "../lib/Api";
import debounce from "lodash/debounce";
import AppIcon from "./AppIcon";
export default {
data() {
return {
ingredientData: null,
search: {
page: 1,
per: 25,
name: null
}
};
},
computed: {
ingredients() {
if (this.ingredientData) {
return this.ingredientData.ingredients;
} else {
return [];
}
}
},
methods: {
getList: debounce(function() {
this.loadResource(
api.getIngredientList(this.search.page, this.search.per, this.search.name)
.then(data => this.ingredientData = data)
);
}, 500, {leading: true, trailing: true})
},
created() {
this.$watch("search",
() => this.getList(),
{
deep: true,
immediate: true
}
);
},
components: {
AppIcon
}
} }
</script> </script>
<style lang="scss" scoped>
</style>

View File

@ -158,6 +158,29 @@ class Api {
return this.get("/ingredients/search", params); return this.get("/ingredients/search", params);
} }
getCalculate(input, output_unit, density) {
const params = {
input,
output_unit,
density
};
return this.get("/calculator/calculate", params);
}
getIngredientList(page, per, name) {
const params = {
page,
per,
name
};
return this.get("/ingredients/", params);
}
getIngredient(id) {
return this.get("/ingredients/" + id);
}
postLogin(username, password) { postLogin(username, password) {
const params = { const params = {
username: username, username: username,

View File

@ -5,6 +5,9 @@ import The404Page from './components/The404Page';
import TheAboutPage from './components/TheAboutPage'; import TheAboutPage from './components/TheAboutPage';
import TheCalculator from './components/TheCalculator'; import TheCalculator from './components/TheCalculator';
import TheIngredientList from './components/TheIngredientList'; import TheIngredientList from './components/TheIngredientList';
import TheIngredient from "./components/TheIngredient";
import TheIngredientEditor from "./components/TheIngredientEditor";
import TheIngredientCreator from "./components/TheIngredientCreator";
import TheNotesList from './components/TheNotesList'; import TheNotesList from './components/TheNotesList';
import TheRecipe from './components/TheRecipe'; import TheRecipe from './components/TheRecipe';
import TheRecipeEditor from './components/TheRecipeEditor'; import TheRecipeEditor from './components/TheRecipeEditor';
@ -58,6 +61,21 @@ router.addRoutes(
name: "ingredients", name: "ingredients",
component: TheIngredientList component: TheIngredientList
}, },
{
path: "/ingredients/new",
name: "new_ingredient",
component: TheIngredientCreator
},
{
path: "/ingredients/:id/edit",
name: "edit_ingredient",
component: TheIngredientEditor
},
{
path: "/ingredients/:id",
name: "ingredient",
component: TheIngredient
},
{ {
path: "/notes", path: "/notes",
name: "notes", name: "notes",

View File

@ -12,7 +12,11 @@
@import "./responsive_controls"; @import "./responsive_controls";
html, body { html {
height: 100%;
}
body {
min-height: 100%; min-height: 100%;
} }

View File

@ -0,0 +1,17 @@
json.extract! @ingredients, :total_count, :total_pages, :current_page
json.page_size @ingredients.limit_value
json.ingredients @ingredients do |i|
json.extract! i, :id, :name, :ndbn, :kcal
json.usda i.ndbn.present?
if i.density.present?
value = UnitConversion::parse(i.density)
json.density value.convert('oz/cup').change_formatter(UnitConversion::DecimalFormatter.new).pretty_value
else
json.density nil
end
end

View File

@ -0,0 +1,2 @@
json.extract! @ingredient, :id, :name, :ndbn, :density

View File

@ -10,7 +10,7 @@ Rails.application.routes.draw do
resources :logs, except: [:new, :create] resources :logs, except: [:new, :create]
resources :ingredients, except: [:show] do resources :ingredients, except: [] do
collection do collection do
get :usda_food_search get :usda_food_search

View File

@ -34,7 +34,12 @@ module UnitConversion
input = input / @density.unitwise input = input / @density.unitwise
end end
input = input.convert_to @target_unit.unit begin
input = input.convert_to @target_unit.unit
rescue Unitwise::ConversionError => err
raise ConversionError, err.message
end
formatter = @target_unit.metric? ? DecimalFormatter.new : value_unit.formatter formatter = @target_unit.metric? ? DecimalFormatter.new : value_unit.formatter

View File

@ -7,4 +7,7 @@ module UnitConversion
class MissingDensityError < UnparseableUnitError class MissingDensityError < UnparseableUnitError
end end
class ConversionError < UnparseableUnitError
end
end end