This commit is contained in:
Dan Elbert 2016-08-15 17:43:02 -05:00
parent 72fffcfbca
commit e6a9e00f82
13 changed files with 190 additions and 80 deletions

View File

@ -0,0 +1,10 @@
(function($) {
$(document).on("turbolinks:load", function() {
$(".log-form input.datepicker").datepicker({autoclose: true, todayBtn: "linked", format: "yyyy-mm-dd"});
$(".log-form input.rating").starRating();
$(".log-table input.rating").starRating({readOnly: true});
});
})(jQuery);

View File

@ -3,6 +3,8 @@
$(document).on("turbolinks:load", function() { $(document).on("turbolinks:load", function() {
$(".recipe-view ul.ingredients").checkable(); $(".recipe-view ul.ingredients").checkable();
$(".recipe-view ol.steps").checkable(); $(".recipe-view ol.steps").checkable();
$(".recipe-table input.rating").starRating({readOnly: true, interval: 0.25, size: '20px'});
}); });
})(jQuery); })(jQuery);

View File

@ -1,76 +1,115 @@
(function ($) { (function ($) {
var pluginName = "starRating"; var pluginName = "starRating";
var defaultOptions = { var defaultOptions = {
starCount: 5 starCount: 5,
}; readOnly: false,
interval: 1,
size: '30px'
};
var methods = { var methods = {
initialize: function(opts) { initialize: function(opts) {
return this.each(function() { return this.each(function() {
var options = _.extend({}, defaultOptions, opts); var $input = $(this);
var $input = $(this); if ($input.data(pluginName.toLowerCase()) === true) {
options.$input = $input; if (console && console.log) {
console.log("star rating has already been initialized; skipping...");
}
return;
}
var $widget = $("<span />").addClass("star-rating"); $input.attr("data-" + pluginName.toLowerCase(), "true");
for (var x = 1; x <= options.starCount; x++) { var options = _.extend({}, defaultOptions, opts);
$widget.append(
$("<span />").addClass("star").data("rating", x) var $widget = $("<span />").addClass("star-rating").css({'font-size': options.size});
); var $emptySet = $("<span />").addClass("empty-set").appendTo($widget);
var $filledSet = $("<span />").addClass("filled-set").appendTo($widget);
options.$input = $input;
options.$emptySet = $emptySet;
for (var x = 1; x <= options.starCount; x++) {
$emptySet.append(
$("<span />").addClass("star empty")
);
$filledSet.append(
$("<span />").addClass("star full")
);
}
$widget.data(pluginName + ".options", options);
$input.data(pluginName, true).hide().after($widget);
privateMethods.updateStars($widget);
if (!options.readOnly) {
$widget
.on("click." + pluginName, function(e) {
var value = privateMethods.calculateRating($widget, e.pageX);
privateMethods.setValue($widget, value);
})
.on("mousemove." + pluginName, function(e) {
var value = privateMethods.calculateRating($widget, e.pageX);
privateMethods.updateStars($widget, value);
})
.on("mouseleave." + pluginName, function (e) {
privateMethods.updateStars($widget);
});
}
$input
.on("change." + pluginName, function() {
privateMethods.updateStars($widget);
});
});
} }
};
$widget.data(pluginName + ".options", options); var privateMethods = {
updateStars: function($widget, value) {
var options = $widget.data(pluginName + ".options");
value = (value == null ? (parseFloat(options.$input.val() || 0)) : value);
$widget.find(".filled-set").css({width: privateMethods.calculateWidth($widget, value)});
},
$input.hide().after($widget); setValue: function($widget, value) {
var options = $widget.data(pluginName + ".options");
options.$input.val(value);
privateMethods.updateStars($widget);
},
privateMethods.updateStars($widget); calculateWidth: function($widget, value) {
var options = $widget.data(pluginName + ".options");
var width = options.$emptySet.width();
return ((value / options.starCount) * 100).toString() + "%";
},
$widget.on("click." + pluginName, "span.star", function(e) { // Given a screen X coordinate, calculates the nearest valid value for this rating widget
var value = $(this).data("rating"); calculateRating: function($widget, screenX) {
privateMethods.setValue($widget, value); var options = $widget.data(pluginName + ".options");
}); var offset = options.$emptySet.offset();
var width = options.$emptySet.width();
var ratio = (screenX - offset.left) / width;
ratio = Math.max(0, Math.min(1, ratio));
$input.on("change." + pluginName, function() { return Math.ceil(options.starCount * (1 / options.interval) * ratio) / (1 / options.interval);
privateMethods.updateStars($widget); }
}); };
});
}
};
var privateMethods = { $.fn[pluginName] = function(method) {
updateStars: function($widget) { if (methods[method]) {
var options = $widget.data(pluginName + ".options"); return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
var value = parseInt(options.$input.val()); } else if (typeof method === 'object' || ! method) {
return methods.initialize.apply(this, arguments);
$widget.find(".star").each(function(idx, elem) {
var $star = $(elem);
$star.removeClass("empty full");
if ($star.data("rating") <= value) {
$star.addClass("full");
} else { } else {
$star.addClass("empty"); $.error('Method ' + method + ' does not exist on jQuery.' + pluginName);
} }
}); };
},
setValue: function($widget, value) {
var options = $widget.data(pluginName + ".options");
options.$input.val(value);
privateMethods.updateStars($widget);
}
};
$.fn[pluginName] = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || ! method) {
return methods.initialize.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.' + pluginName);
}
};
})(jQuery); })(jQuery);

View File

@ -2,10 +2,23 @@
span.star-rating { span.star-rating {
display: block; display: inline-block;
font-size: 30px;
color: gold; color: gold;
cursor: default; cursor: default;
position: relative;
white-space: nowrap;
.empty-set {
color: gray;
opacity: 0.5;
}
.filled-set {
overflow-x: hidden;
position: absolute;
top: 0;
left: 0;
}
span.star { span.star {

View File

@ -3,14 +3,11 @@ class LogsController < ApplicationController
before_action :ensure_valid_user before_action :ensure_valid_user
before_action :set_log, only: [:show, :edit, :update, :destroy] before_action :set_log, only: [:show, :edit, :update, :destroy]
before_action :set_recipe, only: [:index, :new, :create] before_action :set_recipe, only: [:new, :create]
before_action :require_recipe, only: [:new, :create] before_action :require_recipe, only: [:new, :create]
def index def index
@logs = Log.for_user(current_user) @logs = Log.for_user(current_user).order(:date)
if @recipe
@logs = @logs.for_recipe(@recipe)
end
end end
def show def show
@ -41,8 +38,9 @@ class LogsController < ApplicationController
def create def create
@log = Log.new @log = Log.new
@log.recipe = @recipe.log_copy(current_user)
@log.assign_attributes(log_params) @log.assign_attributes(log_params)
@log.recipe.is_log = true
@log.recipe.user = current_user
@log.user = current_user @log.user = current_user
@log.source_recipe = @recipe @log.source_recipe = @recipe
@ -79,7 +77,7 @@ class LogsController < ApplicationController
end end
def log_params def log_params
params.require(:log).permit(:date, :rating, recipe_attributes: [:name, :description, :source, :yields, :total_time, :active_time, recipe_ingredients_attributes: [:name, :ingredient_id, :quantity, :units, :preparation, :sort_order, :id, :_destroy], recipe_steps_attributes: [:step, :sort_order, :id, :_destroy]]) params.require(:log).permit(:date, :rating, :notes, recipe_attributes: [:name, :description, :source, :yields, :total_time, :active_time, recipe_ingredients_attributes: [:name, :ingredient_id, :quantity, :units, :preparation, :sort_order, :id, :_destroy], recipe_steps_attributes: [:step, :sort_order, :id, :_destroy]])
end end
end end

View File

@ -1,9 +1,11 @@
class LogDecorator < BaseDecorator class LogDecorator < BaseDecorator
def recipe
RecipeDecorator.decorate(wrapped.recipe, h)
end
def date def date
v = super v = super
puts v
puts '================'
if v && v.respond_to?(:strftime) if v && v.respond_to?(:strftime)
v.strftime("%Y-%m-%d") v.strftime("%Y-%m-%d")
else else

View File

@ -24,4 +24,8 @@ class RecipeDecorator < BaseDecorator
end end
end end
def average_rating
@average_rating ||= (Log.for_recipe(wrapped).for_user(self.user).where('rating IS NOT NULL').average(:rating) || 0)
end
end end

View File

@ -21,6 +21,7 @@ module ApplicationHelper
nav = [ nav = [
nav_item('Recipes', recipes_path, 'recipes'), nav_item('Recipes', recipes_path, 'recipes'),
nav_item('Ingredients', ingredients_path, 'ingredients'), nav_item('Ingredients', ingredients_path, 'ingredients'),
nav_item('Logs', logs_path, 'logs'),
nav_item('Calculator', calculator_path, 'calculator'), nav_item('Calculator', calculator_path, 'calculator'),
nav_item('About', about_path, 'home') nav_item('About', about_path, 'home')
] ]

View File

@ -1,6 +1,6 @@
<% @log = decorate(@log, LogDecorator) %> <% @log = decorate(@log, LogDecorator) %>
<%= form_for([@recipe, @log]) do |f| %> <%= form_for([@recipe, @log], html: {class: 'log-form'}) do |f| %>
<%= render partial: 'shared/error_list', locals: {model: @log} %> <%= render partial: 'shared/error_list', locals: {model: @log} %>
@ -44,10 +44,3 @@
<% end %> <% end %>
<script type="text/javascript">
$(document).on("turbolinks:load", function() {
$("input.datepicker").datepicker({autoclose: true, todayBtn: "linked", format: "yyyy-mm-dd"});
$("input.rating").starRating();
});
</script>

View File

@ -0,0 +1,40 @@
<div class="row">
<div class="col-xs-12">
<div class="page-header">
<h1>Log Entries</h1>
</div>
<% if @logs.empty? %>
<p>No Entries</p>
<% else %>
<div class="table-responsive">
<table class="log-table table table-striped table-hover">
<thead>
<tr>
<th>Recipe</th>
<th>Date</th>
<th>Rating</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<% decorate(@logs, LogDecorator).each do |log| %>
<tr>
<td><%= link_to log.recipe.short_name, log %></td>
<td><%= log.date %></td>
<td><input type="hidden" class="rating" value="<%= log.rating %>" /></td>
<td><%= %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</div>

View File

@ -10,10 +10,11 @@
<% else %> <% else %>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="recipe-table table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Rating</th>
<th>Yields</th> <th>Yields</th>
<th>Time</th> <th>Time</th>
<th>Created</th> <th>Created</th>
@ -27,6 +28,13 @@
<% decorate(@recipes, RecipeDecorator).each do |recipe| %> <% decorate(@recipes, RecipeDecorator).each do |recipe| %>
<tr> <tr>
<td><%= link_to recipe.short_name, recipe %></td> <td><%= link_to recipe.short_name, recipe %></td>
<td>
<% if recipe.average_rating > 0 %>
<input type="hidden" class="rating" value="<%= recipe.average_rating %>" />
<% else %>
--
<% end %>
</td>
<td><%= recipe.yields %></td> <td><%= recipe.yields %></td>
<td><%= recipe_time(recipe) %></td> <td><%= recipe_time(recipe) %></td>
<td><%= timestamp(recipe.created_at) %></td> <td><%= timestamp(recipe.created_at) %></td>

View File

@ -1,6 +1,6 @@
<% if model.errors.any? %> <% if model.errors.any? %>
<div id="error_explanation" class="alert alert-danger"> <div id="error_explanation" class="alert alert-danger">
<h4><%= pluralize(model.errors.count, 'error') %> prohibited this <%= model.class.model_name.human %> from being saved:</h4> <h4><%= pluralize(model.errors.count, 'error') %> prohibited this <%= model.model_name.human %> from being saved:</h4>
<ul> <ul>
<% model.errors.full_messages.each do |msg| %> <% model.errors.full_messages.each do |msg| %>

View File

@ -1,7 +1,7 @@
Rails.application.routes.draw do Rails.application.routes.draw do
resources :recipes do resources :recipes do
resources :logs, only: [:index, :new, :create] resources :logs, only: [:new, :create]
end end
resources :logs, except: [:new, :create] resources :logs, except: [:new, :create]