ingredients
This commit is contained in:
parent
4c1e0929f1
commit
d587ed58b7
@ -1,13 +1,20 @@
|
||||
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]
|
||||
|
||||
# GET /ingredients
|
||||
# GET /ingredients.json
|
||||
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
|
||||
|
||||
# GET /ingredients/new
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
import CaretBottom from "open-iconic/svg/caret-bottom";
|
||||
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 LockUnlocked from "open-iconic/svg/lock-unlocked";
|
||||
import Person from "open-iconic/svg/person";
|
||||
@ -16,6 +18,8 @@
|
||||
const iconMap = {
|
||||
'caret-bottom': CaretBottom,
|
||||
'caret-top': CaretTop,
|
||||
check: Check,
|
||||
'circle-check': CircleCheck,
|
||||
'lock-locked': LockLocked,
|
||||
'lock-unlocked': LockUnlocked,
|
||||
person: Person,
|
||||
|
26
app/javascript/components/IngredientEdit.vue
Normal file
26
app/javascript/components/IngredientEdit.vue
Normal 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>
|
21
app/javascript/components/IngredientShow.vue
Normal file
21
app/javascript/components/IngredientShow.vue
Normal 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>
|
@ -1,11 +1,126 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
49
app/javascript/components/TheIngredient.vue
Normal file
49
app/javascript/components/TheIngredient.vue
Normal 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>
|
47
app/javascript/components/TheIngredientCreator.vue
Normal file
47
app/javascript/components/TheIngredientCreator.vue
Normal 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>
|
63
app/javascript/components/TheIngredientEditor.vue
Normal file
63
app/javascript/components/TheIngredientEditor.vue
Normal 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>
|
@ -1,14 +1,96 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
@ -158,6 +158,29 @@ class Api {
|
||||
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) {
|
||||
const params = {
|
||||
username: username,
|
||||
|
@ -5,6 +5,9 @@ import The404Page from './components/The404Page';
|
||||
import TheAboutPage from './components/TheAboutPage';
|
||||
import TheCalculator from './components/TheCalculator';
|
||||
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 TheRecipe from './components/TheRecipe';
|
||||
import TheRecipeEditor from './components/TheRecipeEditor';
|
||||
@ -58,6 +61,21 @@ router.addRoutes(
|
||||
name: "ingredients",
|
||||
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",
|
||||
name: "notes",
|
||||
|
@ -12,7 +12,11 @@
|
||||
|
||||
@import "./responsive_controls";
|
||||
|
||||
html, body {
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
|
17
app/views/ingredients/index.json.jbuilder
Normal file
17
app/views/ingredients/index.json.jbuilder
Normal 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
|
||||
|
2
app/views/ingredients/show.json.jbuilder
Normal file
2
app/views/ingredients/show.json.jbuilder
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
json.extract! @ingredient, :id, :name, :ndbn, :density
|
@ -10,7 +10,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :logs, except: [:new, :create]
|
||||
|
||||
resources :ingredients, except: [:show] do
|
||||
resources :ingredients, except: [] do
|
||||
collection do
|
||||
get :usda_food_search
|
||||
|
||||
|
@ -34,7 +34,12 @@ module UnitConversion
|
||||
input = input / @density.unitwise
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
@ -7,4 +7,7 @@ module UnitConversion
|
||||
|
||||
class MissingDensityError < UnparseableUnitError
|
||||
end
|
||||
|
||||
class ConversionError < UnparseableUnitError
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user