This commit is contained in:
Dan Elbert 2018-04-13 10:25:18 -05:00
parent 2da16e334e
commit 46072422d4
18 changed files with 398 additions and 68 deletions

View File

@ -2,12 +2,11 @@ class LogsController < ApplicationController
before_action :ensure_valid_user
before_action :set_log, only: [:show, :edit, :update, :destroy]
before_action :set_recipe, only: [:new, :create]
before_action :set_log, only: [:show, :update, :destroy]
before_action :require_recipe, only: [:new, :create]
def index
@logs = Log.for_user(current_user).order(:date)
@logs = Log.for_user(current_user).order(:date).page(params[:page]).per(params[:per])
end
def show
@ -45,9 +44,9 @@ class LogsController < ApplicationController
@log.source_recipe = @recipe
if @log.save
redirect_to logs_path, notice: 'Log Entry was successfully created.'
render json: { success: true }
else
render :new
render json: @log.errors, status: :unprocessable_entity
end
end

View File

@ -79,7 +79,7 @@
h = h.toString().padStart(2, "0");
}
return h + ":" + m + " " + meridiem;
return h + ":" + m + meridiem;
} else {
return "";

View File

@ -16,8 +16,9 @@
<router-link to="/" class="navbar-item">Recipes</router-link>
<router-link to="/ingredients" class="navbar-item">Ingredients</router-link>
<router-link to="/calculator" class="navbar-item">Calculator</router-link>
<router-link to="/about" class="navbar-item">About</router-link>
<router-link v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>
<router-link to="/about" class="navbar-item">About</router-link>
<a v-if="isAdmin" class="navbar-item" href="/admin/users">Admin</a>
</div>

View File

@ -0,0 +1,48 @@
<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>
</div>
</template>
<script>
export default {
props: {
label: {
required: false,
type: String,
default: ""
},
value: {
required: false,
type: [String, Number],
default: ""
},
type: {
required: false,
type: String,
default: "text"
}
},
computed: {
isTextarea() {
return this.type === "textarea";
}
},
methods: {
input(evt) {
this.$emit("input", evt.target.value);
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,39 @@
<template>
<div>
<h1 class="title">Creating Log for {{ log.recipe.name }}</h1>
<p>Edit Rating</p>
<p>Edit Date</p>
<app-text-field label="Notes" :value="log.notes" type="textarea"></app-text-field>
<recipe-edit :recipe="log.recipe" :for-logging="true"></recipe-edit>
</div>
</template>
<script>
import RecipeEdit from "./RecipeEdit";
export default {
props: {
log: {
required: true,
type: Object
}
},
components: {
RecipeEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,65 +1,37 @@
<template>
<div>
<h1 class="title">{{ action }} {{ recipe.name || "[Unamed Recipe]" }}</h1>
<template v-if="!forLogging">
<div class="columns">
<div class="column">
<div class="field">
<label class="label is-small-mobile">Name</label>
<div class="control">
<input class="input is-small-mobile" type="text" placeholder="name" v-model="recipe.name">
</div>
</div>
<app-text-field label="Name" v-model="recipe.name"></app-text-field>
</div>
<div class="column">
<div class="field">
<label class="label is-small-mobile">Source</label>
<div class="control">
<input class="input is-small-mobile" type="text" placeholder="source" v-model="recipe.source">
</div>
</div>
<app-text-field label="Source" v-model="recipe.source"></app-text-field>
</div>
</div>
<div class="field">
<label class="label is-small-mobile">Description</label>
<div class="control">
<textarea class="textarea is-small-mobile" placeholder="description" v-model="recipe.description"></textarea>
</div>
</div>
<app-text-field label="Description" type="textarea" v-model="recipe.description"></app-text-field>
<div class="field">
<label class="label is-small-mobile">Tags</label>
<app-tag-editor v-model="recipe.tags"></app-tag-editor>
</div>
</template>
<div class="columns">
<div class="column">
<div class="field">
<label class="label is-small-mobile">Yields</label>
<div class="control">
<input class="input is-small-mobile" type="text" placeholder="servings" v-model="recipe.yields">
</div>
</div>
<app-text-field label="Yields" v-model="recipe.yields"></app-text-field>
</div>
<div class="column">
<div class="field">
<label class="label is-small-mobile">Total Time</label>
<div class="control">
<input class="input is-small-mobile" type="number" placeholder="minutes" v-model="recipe.total_time">
</div>
</div>
<app-text-field label="Total Time" type="number" v-model="recipe.total_time"></app-text-field>
</div>
<div class="column">
<div class="field">
<label class="label is-small-mobile">Active Time</label>
<div class="control">
<input class="input is-small-mobile" type="number" placeholder="minutes" v-model="recipe.active_time">
</div>
</div>
<app-text-field label="Active Time" v-model="recipe.active_time"></app-text-field>
</div>
</div>
@ -98,10 +70,10 @@
required: true,
type: Object
},
action: {
forLogging: {
required: false,
type: String,
default: "Editing"
type: Boolean,
default: false
}
},

View File

@ -0,0 +1,14 @@
<template>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,63 @@
<template>
<div>
<log-edit v-if="log.recipe !== null" :log="log"></log-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
</div>
</template>
<script>
import LogEdit from "./LogEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
validationErrors: [],
log: {
date: null,
rating: null,
notes: null,
recipe: null
}
}
},
computed: {
...mapState({
recipeId: state => state.route.params.recipeId,
})
},
methods: {
save() {
this.loadResource(
api.postLog(this.log)
.then(() => this.$router.push('/'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
created() {
this.loadResource(
api.getRecipe(this.recipeId)
.then(data => { this.log.recipe = data; return data; })
);
},
components: {
LogEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,93 @@
<template>
<div>
<h1 class="title">
Log Entries
</h1>
<table class="table">
<tr>
<th>Recipe</th>
<th>Date</th>
<th>Rating</th>
<th>Notes</th>
</tr>
<tr v-for="l in logs" :key="l.id">
<td>{{l.recipe.name}}</td>
<td>l.date</td>
<td>l.rating</td>
<td>l.notes</td>
</tr>
</table>
</div>
</template>
<script>
import api from "../lib/Api";
import debounce from "lodash/debounce";
export default {
data() {
return {
logData: null,
search: {
page: 1,
per: 25
}
};
},
computed: {
logs() {
if (this.logData) {
return this.logData.logs;
} else {
return [];
}
},
totalPages() {
if (this.logData) {
return this.logData.total_pages
}
return 0;
},
currentPage() {
if (this.logData) {
return this.logData.current_page
}
return 0;
}
},
methods: {
changePage(idx) {
this.search.page = idx;
},
getList: debounce(function() {
this.loadResource(
api.getLogList(this.search.page, this.search.per)
.then(data => this.logData = data)
);
}, 500, {leading: true, trailing: true})
},
created() {
this.$watch("search",
() => this.getList(),
{
deep: true,
immediate: true
}
);
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,6 +1,8 @@
<template>
<div>
<h1 class="title">Creating {{ recipe.name || "[Unamed Recipe]" }}</h1>
<recipe-edit :recipe="recipe" action="Creating"></recipe-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
@ -12,13 +14,13 @@
<script>
import RecipeEdit from "./RecipeEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
validationErrors: [],
recipe: {
name: null,
source: null,
@ -33,12 +35,6 @@
}
},
computed: {
...mapState({
recipeId: state => state.route.params.id,
})
},
methods: {
save() {
this.loadResource(

View File

@ -5,6 +5,7 @@
Loading...
</div>
<div v-else>
<h1 class="title">Editing {{ recipe.name || "[Unamed Recipe]" }}</h1>
<recipe-edit :recipe="recipe"></recipe-edit>
</div>

View File

@ -52,6 +52,9 @@
<td>{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
<td><app-date-time :date-time="r.created_at" :show-time="false"></app-date-time></td>
<td>
<router-link v-if="isLoggedIn" :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary">
<app-icon icon="star" size="md"></app-icon>
</router-link>
<router-link v-if="isLoggedIn" :to="{name: 'edit_recipe', params: { id: r.id } }" class="button is-primary">
<app-icon icon="pencil" size="md"></app-icon>
</router-link>

View File

@ -273,6 +273,35 @@ class Api {
return this.del("/notes/" + note.id);
}
getLogList(page, per) {
const params = {
page,
per
};
return this.get("/logs", params);
}
buildLogParams(log) {
return {
log: {
date: log.date,
rating: log.rating,
notes: log.notes,
source_recipe_id: log.source_recipe_id,
recipe_attributes: this.buildRecipeParams(log.recipe)
}
};
}
postLog(log) {
return this.post("/logs/", this.buildLogParams(log));
}
patchLog(log) {
return this.patch("/logs/" + log.id, this.buildLogParams(log));
}
postLogin(username, password) {
const params = {
username: username,

View File

@ -17,6 +17,7 @@ import AppNavbar from "../components/AppNavbar";
import AppPager from "../components/AppPager";
import AppRating from "../components/AppRating";
import AppTagEditor from "../components/AppTagEditor";
import AppTextField from "../components/AppTextField";
Vue.component("AppAutocomplete", AppAutocomplete);
Vue.component("AppDateTime", AppDateTime);
@ -26,6 +27,7 @@ Vue.component("AppNavbar", AppNavbar);
Vue.component("AppPager", AppPager);
Vue.component("AppRating", AppRating);
Vue.component("AppTagEditor", AppTagEditor);
Vue.component("AppTextField", AppTextField);
Vue.use(VueProgressBar, {

View File

@ -4,6 +4,12 @@ import Router from 'vue-router';
import The404Page from './components/The404Page';
import TheAboutPage from './components/TheAboutPage';
import TheCalculator from './components/TheCalculator';
import TheLog from './components/TheLog';
import TheLogList from './components/TheLogList';
import TheLogCreator from './components/TheLogCreator';
import TheLogEditor from './components/TheLogEditor';
import TheIngredientList from './components/TheIngredientList';
import TheIngredient from "./components/TheIngredient";
import TheIngredientEditor from "./components/TheIngredientEditor";
@ -76,6 +82,26 @@ router.addRoutes(
name: "ingredient",
component: TheIngredient
},
{
path: "/logs",
name: "logs",
component: TheLogList
},
{
path: "/recipes/:recipeId/logs/new",
name: "new_log",
component: TheLogCreator
},
{
path: "/logs/:id/edit",
name: "edit_log",
component: TheLogEditor
},
{
path: "/logs/:id",
name: "log",
component: TheLog
},
{
path: "/notes",
name: "notes",

View File

@ -0,0 +1,16 @@
json.cache_root! [Log.all, @logs] do
json.extract! @logs, :total_count, :total_pages, :current_page
json.page_size @logs.limit_value
json.logs @logs do |l|
json.extract! l, :id, :date, :rating, :notes
json.recipe do
json.id l.recipe.id
json.name l.recipe.name
end
end
end