Skip to content

Commit

Permalink
Permit reviews creation/update inside book details page
Browse files Browse the repository at this point in the history
  • Loading branch information
mattia-malnis committed Aug 29, 2024
1 parent 1414407 commit 4039de6
Show file tree
Hide file tree
Showing 28 changed files with 301 additions and 30 deletions.
4 changes: 4 additions & 0 deletions app/assets/images/pencil-square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern

rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

before_action :configure_permitted_parameters, if: :devise_controller?

unless Rails.env.production?
Expand All @@ -20,4 +22,8 @@ def n_plus_one_detection
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [ :nickname ])
end

def record_not_found
render file: Rails.root.join("public", "404.html"), status: :not_found, layout: false
end
end
55 changes: 55 additions & 0 deletions app/controllers/books/reviews_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class Books::ReviewsController < ApplicationController
include FindBook

before_action :authenticate_user!
before_action :find_review, only: [ :edit, :update ]
before_action :authorize_user, only: [ :edit, :update ]

def new
@review = @book.reviews.new
end

def create
@review = @book.reviews.new(review_params)
@review.user = current_user

if @review.save
respond_to do |format|
format.html { redirect_to book_path(@book) }
format.turbo_stream
end
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
if @review.update(review_params)
respond_to do |format|
format.html { redirect_to(book_path(@book)) }
format.turbo_stream
end
else
render :edit, status: :unprocessable_entity
end
end

private

def review_params
params.require(:review).permit(:title, :description, :rating)
end

def find_review
@review = @book.reviews.find(params[:id])
end

def authorize_user
unless @review.user_id == current_user.id
redirect_to book_path(@book)
end
end
end
10 changes: 2 additions & 8 deletions app/controllers/books_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class BooksController < ApplicationController
include Pagy::Backend
include FindBook

before_action :find_book, only: [ :show ]

Expand All @@ -10,19 +11,12 @@ def index
@books = Book.all
end

@books = @books.with_attached_image
@pagy, @books = pagy(@books)
end

def show
@reviews = @book.reviews.eager_load(:user)
@pagy, @reviews = pagy(@reviews)
end

private

def find_book
@book = Book.find(params[:id])
rescue ActiveRecord::RecordNotFound
render file: Rails.root.join("public", "404.html"), status: :not_found, layout: false
end
end
13 changes: 13 additions & 0 deletions app/controllers/concerns/find_book.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module FindBook
extend ActiveSupport::Concern

included do
before_action :find_book
end

private

def find_book
@book = Book.find(params[:book_id] || params[:id])
end
end
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def modal_form_for(record, options = {}, &block)
options[:html] ||= {}
options[:html][:class] = [ "min-w-[350px] space-y-6", options[:html][:class] ]
options[:data] ||= {}
options[:data][:action] = "turbo:submit-end->modal#reloadOnSuccess"
options[:data][:action] = "turbo:submit-end->modal#closeOnSuccess" unless options[:data].key? :action
form_for(record, options, &block)
end
end
16 changes: 16 additions & 0 deletions app/helpers/books/reviews_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Books::ReviewsHelper
def rating_field(f)
content_tag :div, class: "flex flex-row-reverse justify-end items-center" do
(1..5).reverse_each.map do |rating|
f.radio_button(:rating, rating, class: "peer -ms-5 size-5 bg-transparent border-0 text-transparent cursor-pointer appearance-none checked:bg-none focus:bg-none focus:ring-0 focus:ring-offset-0") +
f.label("rating_#{rating}", class: "peer-checked:text-yellow-400 text-gray-300 pointer-events-none") do
inline_svg_tag("star.svg", class: "size-6 shrink-0")
end
end.join.html_safe
end
end

def user_is_creator?(review)
review.user_id == current_user&.id
end
end
2 changes: 1 addition & 1 deletion app/helpers/books_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def render_stars(rating, max_stars = 5)
def reviews_average(reviews)
return if reviews.blank?

average = reviews.average(:rating)
average = reviews.average(:rating).round(2)
content_tag :div, class: "flex items-center text-sm text-gray-500 mb-2" do
concat(inline_svg_tag("star.svg", class: "size-5 text-yellow-500 mr-1"))
concat("#{average} / 5")
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/styled_form_builder.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class StyledFormBuilder < ActionView::Helpers::FormBuilder
INPUT_TYPES = [ :text_field, :email_field, :password_field, :number_field, :date_field ]
INPUT_TYPES = [ :text_field, :email_field, :password_field, :number_field, :date_field, :text_area ]

INPUT_TYPES.each do |input_type|
define_method(input_type) do |method, options = {}|
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/controllers/modal_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export default class extends Controller {
this.element.close();
}

closeOnSuccess(e) {
if (e.detail.success) {
this.close();
}
}

reloadOnSuccess(e) {
if (e.detail.success) {
this.close();
Expand Down
2 changes: 1 addition & 1 deletion app/models/book.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class Book < ApplicationRecord
include PgSearch::Model
include ImageUploadable

has_many :reviews
has_many :reviews, dependent: :destroy

validates :title, presence: true

Expand Down
2 changes: 2 additions & 0 deletions app/models/review.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Review < ApplicationRecord

before_validation :normalize_fields

default_scope { order(created_at: :desc) }

private

def normalize_fields
Expand Down
8 changes: 8 additions & 0 deletions app/views/books/_book_description.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%# locals: (book:) -%>

<section class="mt-4 lg:mt-0 lg:ml-6" id="book-description">
<%= reviews_average(book.reviews) %>
<h1 class="text-2xl font-bold text-gray-800 mb-1"><%= book.title %></h1>
<p class="text-gray-600 text-lg italic"><%= book.subtitle %></p>
<%= link_to "Leave a review", new_book_review_path(book), class: "px-4 py-2 mt-4 inline-block font-semibold text-sm bg-blue-500 text-white rounded-full shadow-sm", data: { turbo_frame: "modal" } if user_signed_in? %>
</section>
6 changes: 0 additions & 6 deletions app/views/books/_review.html.erb

This file was deleted.

26 changes: 26 additions & 0 deletions app/views/books/reviews/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<%# locals: (url:, submit_label:) -%>
<%= modal({title: "Review: #{@book.title}"}) do %>
<%= modal_form_for(@review, url: url) do |f| %>
<%= alert_error(@review.errors.full_messages) %>

<div class="field">
<%= f.label :rating %>
<%= rating_field(f) %>
</div>

<div class="field">
<%= f.label :title %>
<%= f.text_field :title, autofocus: true %>
</div>

<div class="field">
<%= f.label :description %>
<%= f.text_area :description %>
</div>

<div class="actions">
<%= f.submit submit_label %>
</div>
<% end %>
<% end %>
15 changes: 15 additions & 0 deletions app/views/books/reviews/_review.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<%# locals: (review:) -%>
<%= content_tag :div, class:"flex justify-between my-6 border-b pb-4", id: dom_id(review) do %>
<div>
<%= render_stars(review.rating) %>
<h3 class="text-lg font-semibold text-gray-700"><%= review.title %></h3>
<p class="text-sm text-gray-500">By <%= review.user.nickname %></p>
<p class="text-gray-600 mt-2"><%= review.description %></p>
</div>
<% if user_is_creator?(review) %>
<div>
<%= link_to inline_svg_tag("pencil-square.svg", class: "size-6"), edit_book_review_path(@book, review), data: { turbo_frame: "modal" } %>
</div>
<% end %>
<% end %>
8 changes: 8 additions & 0 deletions app/views/books/reviews/create.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%= turbo_stream.prepend("reviews-list", partial: "review", locals: { review: @review }) %>
<%= turbo_stream.replace("book-description", partial: "books/book_description", locals: { book: @book }) %>
<%= turbo_stream.replace("empty-reviews-message") do %>
<% # In the case this is the first review we need to replace the "empty-reviews-message" element %>
<div id="reviews-list">
<%= render partial: "review", locals: { review: @review } %>
</div>
<% end %>
1 change: 1 addition & 0 deletions app/views/books/reviews/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render partial: "form", locals: { url: book_review_path(@book, @review), submit_label: "Update review" } %>
1 change: 1 addition & 0 deletions app/views/books/reviews/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render partial: "form", locals: { url: book_reviews_path, submit_label: "Leave review" } %>
2 changes: 2 additions & 0 deletions app/views/books/reviews/update.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<%= turbo_stream.replace(dom_id(@review), partial: "review", locals: { review: @review }) %>
<%= turbo_stream.replace("book-description", partial: "books/book_description", locals: { book: @book }) %>
14 changes: 6 additions & 8 deletions app/views/books/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@
<section class="book-image lg:flex-shrink-0">
<%= book_image(@book, "max-h-64 rounded-lg shadow-lg") %>
</section>
<section class="book-description mt-4 lg:mt-0 lg:ml-6">
<%= reviews_average(@reviews) %>
<h1 class="text-2xl font-bold text-gray-800 mb-1"><%= @book.title %></h1>
<p class="text-gray-600 text-lg italic"><%= @book.subtitle %></p>
</section>
<%= render partial: "book_description", locals: { book: @book } %>
</div>

<section class="bg-gray-50 p-6 shadow-lg rounded-lg mt-8 overflow-hidden">
<h1 class="text-2xl font-semibold text-gray-800 mb-6 border-b-2 pb-2">Reviews</h1>
<% if @reviews.any? %>
<%= render partial: "review", collection: @reviews %>
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
<div id="reviews-list">
<%= render partial: "books/reviews/review", collection: @reviews %>
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
</div>
<% else %>
<p class="text-gray-600">No reviews yet. Be the first to leave a review!</p>
<p class="text-gray-600" id="empty-reviews-message">No reviews yet. Be the first to leave a review!</p>
<% end %>
</section>
</article>
Expand Down
2 changes: 1 addition & 1 deletion app/views/devise/passwords/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= modal({title: "Forgot your password?"}) do %>
<%= modal_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :post }) do |f| %>
<%= modal_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :post }, data: { action: "turbo:submit-end->modal#reloadOnSuccess" }) do |f| %>
<%= alert_error(resource.errors.full_messages, I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase)) %>

<div class="field">
Expand Down
2 changes: 1 addition & 1 deletion app/views/devise/registrations/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= modal({title: "Sign up"}) do %>
<%= modal_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= modal_form_for(resource, as: resource_name, url: registration_path(resource_name), data: { action: "turbo:submit-end->modal#reloadOnSuccess" }) do |f| %>
<%= alert_error(resource.errors.full_messages, I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase)) %>

<div class="field">
Expand Down
2 changes: 1 addition & 1 deletion app/views/devise/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= modal({title: "Log in"}) do %>
<%= modal_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<%= modal_form_for(resource, as: resource_name, url: session_path(resource_name), data: { action: "turbo:submit-end->modal#reloadOnSuccess" }) do |f| %>
<%= alert_error(flash[:alert]) %>

<div class="field">
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@
# Defines the root path route ("/")
root "books#index"

resources :books, only: [ :show ]
resources :books, only: [ :show ] do
resources :reviews, only: [ :new, :create, :edit, :update ], module: :books
end
end
1 change: 1 addition & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'support/devise'

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
Expand Down
Loading

0 comments on commit 4039de6

Please sign in to comment.