This commit is contained in:
Dan Elbert 2018-09-13 14:51:41 -05:00
parent 0c4c5b899b
commit 532c9372ea
19 changed files with 369 additions and 196 deletions

View File

@ -16,20 +16,43 @@ class CalculatorController < ApplicationController
ingredient = Ingredient.find_by_ingredient_id(ingredient_id)
end
data = {errors: [], output: ''}
data = {errors: Hash.new { |h, k| h[k] = [] }, output: ''}
begin
UnitConversion::with_custom_units(ingredient ? ingredient.custom_units : []) do
unit = UnitConversion.parse(input)
if output_unit.present?
unit = unit.convert(output_unit, density)
data[:output] = unit.to_s
else
data[:output] = unit.auto_unit.to_s
density_unit = nil
begin
if density
density_unit = UnitConversion.parse(density)
unless density_unit.density?
data[:errors][:density] << 'not a density unit'
density_unit = nil
end
end
rescue UnitConversion::UnparseableUnitError => e
data[:errors] << e.message
data[:errors][:density] << 'invalid string'
end
begin
input_unit = UnitConversion.parse(input)
rescue UnitConversion::UnparseableUnitError => e
data[:errors][:input] << 'invalid string'
end
if !input_unit.nil?
if output_unit.present?
begin
input_unit = input_unit.convert(output_unit, density_unit)
rescue UnitConversion::UnparseableUnitError => e
data[:errors][:output_unit] << e.message
end
else
input_unit = input_unit.auto_unit
end
end
if data[:errors].empty?
data[:output] = input_unit.to_s
end
end
render json: data

View File

@ -18,6 +18,7 @@
import { mapMutations, mapState } from "vuex";
import api from "../lib/Api";
import TWEEN from '@tweenjs/tween.js';
export default {
data() {
@ -44,9 +45,15 @@
},
created() {
// Setup global animation loop
function animate () {
TWEEN.update();
requestAnimationFrame(animate);
}
animate();
if (this.user === null && this.authChecked === false) {
this.checkAuthentication();
}
},

View File

@ -1,55 +1,98 @@
<template>
<transition
name="expand"
:duration="500"
@enter="enter"
@after-enter="afterEnter"
@leave="leave">
@leave="leave"
@enter-cancel="cancel"
@leave-cancel="cancel">
<slot></slot>
</transition>
</template>
<script>
import TWEEN from '@tweenjs/tween.js';
export default {
props: {
expandTime: {
type: Number,
default: 250
}
},
data() {
return {
animation: null
}
},
methods: {
cancel () {
if (this.animation) {
this.animation.stop();
this.animation = null;
}
},
enter(element, done) {
const width = getComputedStyle(element).width;
const width = parseInt(getComputedStyle(element).width);
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
element.style.width = width;
element.style.position = 'absolute';
element.style.visibility = 'hidden';
element.style.height = 'auto';
const height = getComputedStyle(element).height;
const height = parseInt(getComputedStyle(element).height);
element.style.width = null;
element.style.position = null;
element.style.visibility = null;
element.style.overflow = 'hidden';
element.style.height = 0;
// Trigger the animation.
// We use `setTimeout` because we need
// to make sure the browser has finished
// painting after setting the `height`
// to `0` in the line above.
this.animation = new TWEEN.Tween({height: 0, paddingTop: 0, paddingBottom: 0})
.to({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom}, this.expandTime)
.onUpdate(obj => {
element.style.height = obj.height + "px";
element.style.paddingBottom = obj.paddingBottom + "px";
element.style.paddingTop = obj.paddingTop + "px";
})
.onComplete(() => {
this.animation = null;
element.removeAttribute('style');
element.style.opacity = 0.99;
setTimeout(() => {
element.style.height = height;
});
},
afterEnter(element) {
element.style.height = 'auto';
// Fixes odd drawing bug in Chrome
element.style.opacity = 1.0;
}, 1000);
done();
})
.start();
},
leave(element, done) {
const height = getComputedStyle(element).height;
const height = parseInt(getComputedStyle(element).height);
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
element.style.height = height;
element.style.overflow = 'hidden';
setTimeout(() => {
element.style.height = 0;
});
this.animation = new TWEEN.Tween({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom})
.to({height: 0, paddingTop: 0, paddingBottom: 0}, this.expandTime)
.onUpdate(obj => {
element.style.height = obj.height + "px";
element.style.paddingBottom = obj.paddingBottom + "px";
element.style.paddingTop = obj.paddingTop + "px";
})
.onComplete(() => {
this.animation = null;
element.removeAttribute('style');
done();
})
.start();
}
}
}

View File

@ -35,6 +35,7 @@
person: new IconData('person'),
star: new IconData('star'),
'star-empty': new IconData('star-empty'),
warning: new IconData('warning'),
x: new IconData('x')
};

View File

@ -14,6 +14,7 @@
import Pencil from "../iconic/svg/smart/pencil";
import Star from "../iconic/svg/smart/star";
import StarEmpty from "../iconic/svg/smart/star-empty";
import Warning from "../iconic/svg/smart/warning";
import X from "../iconic/svg/smart/x";
const APIS = {};
@ -36,6 +37,7 @@
person: Person,
star: Star,
'star-empty': StarEmpty,
warning: Warning,
x: X
};

View File

@ -1,10 +1,14 @@
<template>
<div class="field">
<label v-if="label.length" class="label is-small-mobile">{{ label }}</label>
<div class="control">
<textarea v-if="isTextarea" class="textarea is-small-mobile" :value="value" @input="input"></textarea>
<input v-else :type="type" class="input is-small-mobile" :value="value" @input="input">
<div :class="controlClasses">
<textarea v-if="isTextarea" :class="inputClasses" :value="value" @input="input" :disabled="disabled"></textarea>
<input v-else :type="type" :class="inputClasses" :value="value" @input="input" :disabled="disabled">
<app-icon class="is-right" icon="warning" v-if="validationError !== null"></app-icon>
</div>
<p v-if="helpMessage !== null" :class="helpClasses">
{{ helpMessage }}
</p>
</div>
</template>
@ -26,12 +30,54 @@
required: false,
type: String,
default: "text"
},
disabled: {
type: Boolean,
default: false
},
validationError: {
required: false,
type: String,
default: null
}
},
computed: {
isTextarea() {
return this.type === "textarea";
},
controlClasses() {
return [
"control",
{
"has-icons-right": this.validationError !== null
}
]
},
inputClasses() {
return [
"is-small-mobile",
{
"textarea": this.isTextarea,
"input": !this.isTextarea,
"is-danger": this.validationError !== null
}
]
},
helpMessage() {
return this.validationError;
},
helpClasses() {
return [
"help",
{
"is-danger": this.validationError !== null
}
];
}
},

View File

@ -1,6 +1,10 @@
<template>
<div>
<h3 class="title">
{{food.name}}
</h3>
</div>
</template>

View File

@ -1,21 +1,21 @@
<template>
<div>
<div class="columns task-item-edit">
<div class="field">
<div class="field column">
<label class="label is-small">Name</label>
<div class="control">
<input class="input is-small" type="text" placeholder="Name" v-model="taskItem.name" @keydown="inputKeydown" ref="nameInput">
</div>
</div>
<div class="field">
<div class="field column">
<label class="label is-small">Quantity</label>
<div class="control">
<input class="input is-small" type="text" placeholder="Qty" v-model="taskItem.quantity" @keydown="inputKeydown">
</div>
</div>
<div class="field">
<div class="field column">
<div class="control">
<button class="button is-primary" @click="save">Add</button>
</div>
@ -61,4 +61,8 @@
<style lang="scss" scoped>
.task-item-edit {
}
</style>

View File

@ -1,43 +1,61 @@
<template>
<div>
<app-expand-transition name="fade">
<task-item-edit @save="save" :task-item="newItem" v-if="showAddItem" ref="itemEdit"></task-item-edit>
<div class="panel">
<p class="panel-heading">
{{taskList.name}} ({{completedItemCount}} / {{taskList.task_items.length}})
</p>
<div class="panel-block">
<button class="button is-fullwidth is-primary" @click="toggleShowAddItem">{{ showAddItem ? 'Done' : 'New Item' }}</button>
</div>
<app-expand-transition>
<div class="panel-block" v-if="showAddItem">
<task-item-edit @save="save" :task-item="newItem" ref="itemEdit"></task-item-edit>
</div>
</app-expand-transition>
<table class="table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Quantity</th>
<th>
<button class="button" @click="toggleShowAddItem">{{ showAddItem ? 'Done' : 'New Item' }}</button>
</th>
</tr>
</thead>
<transition-group tag="tbody" name="list-item-move">
<tr v-for="i in taskItems" :key="i.id" @click="toggleItem(i)">
<td>
<transition-group tag="div" name="list-item-move">
<a v-for="i in taskItems" :key="i.id" @click="toggleItem(i)" class="panel-block">
<div class="check">
<app-icon v-if="i.completed" icon="check"></app-icon>
<span class="icon" v-else></span>
</div>
</td>
<td>{{ i.name }}</td>
<td>{{ i.quantity }}</td>
<td></td>
</tr>
<span>{{ i.quantity }} {{ i.name }}</span>
</a>
</transition-group>
<tbody v-if="taskItems.length === 0">
<tr>
<td colspan="4">
No Items
</td>
</tr>
</tbody>
</table>
<app-expand-transition>
<div class="panel-block" v-if="uncompletedItemCount > 0">
<button class="button is-fullwidth is-link" @click="completeAllItems">
<span class="check">
<app-icon icon="check"></app-icon>
</span>
<span>Check All</span>
</button>
</div>
</app-expand-transition>
<app-expand-transition>
<div class="panel-block" v-if="completedItemCount > 0">
<button class="button is-fullwidth is-link" @click="unCompleteAllItems">
<span class="check">
<span class="icon"></span>
</span>
<span>Uncheck All</span>
</button>
</div>
</app-expand-transition>
<app-expand-transition>
<div class="panel-block" v-if="completedItemCount > 0">
<button class="button is-fullwidth is-link" @click="deleteCompletedItems">
<app-icon icon="x" class="is-text-danger"></app-icon>
<span>Clear Completed</span>
</button>
</div>
</app-expand-transition>
</div>
</div>
</template>
@ -76,6 +94,13 @@
},
computed: {
completedItemCount() {
return this.taskList === null ? 0 : this.taskList.task_items.filter(i => i.completed).length;
},
uncompletedItemCount() {
return this.taskList === null ? 0 : this.taskList.task_items.filter(i => !i.completed).length;
},
completedTaskItems() {
return (this.taskList ? this.taskList.task_items : []).filter(i => i.completed);
},
@ -93,6 +118,7 @@
...mapActions([
'createTaskItem',
'updateTaskItem',
'deleteTaskItems',
'completeTaskItems'
]),
@ -120,7 +146,38 @@
toggleShowAddItem() {
this.newItem = newItemTemplate(this.taskList.id);
this.showAddItem = !this.showAddItem;
}
},
completeAllItems() {
const toComplete = this.taskList.task_items.filter(i => !i.completed);
this.loadResource(
this.completeTaskItems({
taskList: this.taskList,
taskItems: toComplete,
completed: true
})
)
},
unCompleteAllItems() {
const toUnComplete = this.taskList.task_items.filter(i => i.completed);
this.loadResource(
this.completeTaskItems({
taskList: this.taskList,
taskItems: toUnComplete,
completed: false
})
)
},
deleteCompletedItems() {
this.loadResource(
this.deleteTaskItems({
taskList: this.taskList,
taskItems: this.taskList.task_items.filter(i => i.completed)
})
);
},
},
@ -147,9 +204,22 @@
margin-bottom: 0;
}
div.check {
border: 2px solid $link;
display: flex;
.check {
display: inline-flex;
margin-right: 1.5rem;
.icon {
position: relative;
&:after {
content: '';
position: absolute;
top: -3px;
left: -3px;
bottom: -3px;
right: -3px;
border: 2px solid currentColor;
}
}
}
</style>

View File

@ -2,23 +2,11 @@
<div>
<h1 class="title">Calculator</h1>
<div v-for="err in errors" :key="err" class="notification is-warning">
{{err}}
</div>
<div class="box">
<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>
<app-text-field label="Input" v-model="input" class="column" :validation-error="inputErrors"></app-text-field>
<app-text-field label="Output Unit" v-model="outputUnit" class="column" :validation-error="outputUnitErrors"></app-text-field>
</div>
@ -41,20 +29,14 @@
</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" :disabled="ingredient !== null">
</div>
</div>
<app-text-field label="Density" v-model="density" class="column" :disabled="ingredient !== null" :validation-error="densityErrors"></app-text-field>
</div>
<div class="field">
<label class="label">Output</label>
<div class="control">
<input class="input" type="text" disabled="disabled" v-model="output">
</div>
<app-text-field label="Output" v-model="output" disabled></app-text-field>
</div>
</div>
</template>
@ -72,10 +54,36 @@
ingredient: null,
density: '',
output: '',
errors: []
errors: {}
};
},
computed: {
inputErrors() {
if (this.errors.input && this.errors.input.length > 0) {
return this.errors.input.join(", ");
} else {
return null;
}
},
outputUnitErrors() {
if (this.errors.output_unit && this.errors.output_unit.length > 0) {
return this.errors.output_unit.join(", ");
} else {
return null;
}
},
densityErrors() {
if (this.errors.density && this.errors.density.length > 0) {
return this.errors.density.join(", ");
} else {
return null;
}
}
},
methods: {
updateSearchItems(text) {
return api.getSearchIngredients(text);
@ -88,12 +96,14 @@
},
updateOutput: debounce(function() {
if (this.input && this.input.length > 0) {
this.loadResource(api.getCalculate(this.input, this.outputUnit, this.ingredient ? this.ingredient.ingredient_id : null, this.density)
.then(data => {
this.output = data.output;
this.errors = data.errors;
})
);
}
}, 500)
},

View File

@ -1,7 +1,7 @@
<template>
<div>
<h1 class="title is-3">Tasks</h1>
<h1 class="title is-3">
Tasks
<app-dropdown button-class="is-primary" :open="showListDropdown" :label="listSelectLabel" @open="showListDropdown = true" @close="showListDropdown = false">
<task-list-dropdown-item v-for="l in taskLists" :key="l.id" :task-list="l" :active="currentTaskList !== null && currentTaskList.id === l.id" @select="selectList" @delete="deleteList"></task-list-dropdown-item>
@ -11,20 +11,12 @@
<task-list-mini-form :task-list="newList" :validation-errors="newListValidationErrors" @save="saveNewList"></task-list-mini-form>
</div>
</app-dropdown>
<br><br>
</h1>
<div v-if="currentTaskList !== null">
<div class="box">
<button class="button" @click="deleteCompletedItems" v-if="completedItemCount > 0">Clear Completed</button>
<button class="button" @click="completeAllItems" v-if="uncompletedItemCount > 0">Check All</button>
<button class="button" @click="unCompleteAllItems" v-if="completedItemCount > 0">Uncheck All</button>
</div>
<div class="box">
<div class="columns" v-if="currentTaskList !== null">
<div class="column is-6-widescreen is-8-desktop is-10-tablet">
<task-item-list :task-list="currentTaskList"></task-item-list>
</div>
</div>
</div>
@ -66,14 +58,6 @@
} else {
return this.currentTaskList.name;
}
},
completedItemCount() {
return this.currentTaskList === null ? 0 : this.currentTaskList.task_items.filter(i => i.completed).length;
},
uncompletedItemCount() {
return this.currentTaskList === null ? 0 : this.currentTaskList.task_items.filter(i => !i.completed).length;
}
},
@ -109,36 +93,7 @@
);
},
completeAllItems() {
const toComplete = this.currentTaskList.task_items.filter(i => !i.completed);
this.loadResource(
this.completeTaskItems({
taskList: this.currentTaskList,
taskItems: toComplete,
completed: true
})
)
},
unCompleteAllItems() {
const toUnComplete = this.currentTaskList.task_items.filter(i => i.completed);
this.loadResource(
this.completeTaskItems({
taskList: this.currentTaskList,
taskItems: toUnComplete,
completed: false
})
)
},
deleteCompletedItems() {
this.loadResource(
this.deleteTaskItems({
taskList: this.currentTaskList,
taskItems: this.currentTaskList.task_items.filter(i => i.completed)
})
);
},
deleteAllItems() {
this.loadResource(

View File

@ -11,8 +11,8 @@
.expand-enter-active,
.expand-leave-active {
transition: height .5s ease-in-out;
overflow: hidden;
// transition: height .5s ease-in-out;
// overflow: hidden;
}

View File

@ -8,6 +8,7 @@
@import "~bulma/sass/components/message";
@import "~bulma/sass/components/modal";
@import "~bulma/sass/components/pagination";
@import "~bulma/sass/components/panel";
@import "~bulma/sass/elements/_all";
@import "~bulma/sass/grid/columns";
@import "~bulma/sass/layout/section";
@ -36,6 +37,7 @@ body {
.container {
padding: 1rem;
background-color: $white;
min-height: 75vh;
}
}

View File

@ -4,13 +4,10 @@ class Ingredient < ApplicationRecord
class << self
def find_by_ingredient_id(ingredient_id)
puts "looking up |#{ingredient_id}|"
case ingredient_id
when /^R(\d+)$/
puts 'rec'
Recipe.find($1)
when /^F(\d+)$/
puts 'food'
Food.find($1)
else
raise ActiveRecord::RecordNotFound

View File

@ -24,7 +24,7 @@ module UnitConversion
def convert(value_unit)
input = value_unit.unitwise
input = value_unit.unitwise * 1.0
if value_unit.volume? && @target_unit.mass?
raise MissingDensityError, "Cannot convert #{value_unit.unit} to #{@target_unit} without density" unless @density
@ -36,7 +36,7 @@ module UnitConversion
begin
input = input.convert_to @target_unit.unit
rescue Unitwise::ConversionError => err
rescue Unitwise::ConversionError, Unitwise::ExpressionError => err
raise ConversionError, err.message
end

View File

@ -45,7 +45,7 @@ module UnitConversion
def compatible?(unit_str)
begin
unitwise.compatible_with? Unitwise(1, unit_str)
rescue UnknownUnitError
rescue TypeError, Unitwise::ConversionError, Unitwise::ExpressionError
false
end
end

View File

@ -19,6 +19,10 @@ module Unitwise
def self.with_custom_units(unit_list, &block)
if unit_list.empty?
return block.call
end
atoms = []
ret_val = nil

View File

@ -1,6 +1,7 @@
{
"dependencies": {
"@rails/webpacker": "^3.5.5",
"@tweenjs/tween.js": "^17.2.0",
"autosize": "^4.0.1",
"bulma": "^0.7.1",
"caniuse-lite": "^1.0.30000815",

View File

@ -32,6 +32,10 @@
webpack "^3.12.0"
webpack-manifest-plugin "^1.3.2"
"@tweenjs/tween.js@^17.2.0":
version "17.2.0"
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-17.2.0.tgz#21f89b709bafc4b303adae7a83b4f35a0d9e4796"
"@types/node@*":
version "10.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897"