spree_product_reviews 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/app/controllers/spree/admin/product_reviews_controller.rb +71 -0
- data/app/controllers/spree/home_controller.rb +7 -0
- data/app/controllers/spree/product_reviews_controller.rb +72 -0
- data/app/models/spree/page_blocks/product_review_form.rb +13 -0
- data/app/models/spree/page_blocks/products/reviews.rb +37 -0
- data/app/models/spree/page_sections/add_a_review.rb +80 -0
- data/app/models/spree/page_sections/product_details_decorator.rb +10 -0
- data/app/models/spree/product_review.rb +73 -0
- data/app/models/spree/product_reviews_ability.rb +25 -0
- data/app/models/spree_product_reviews/spree/product_decorator.rb +13 -0
- data/app/models/spree_product_reviews/spree/product_review_decorator.rb +12 -0
- data/app/models/spree_product_reviews/spree/user_decorator.rb +22 -0
- data/app/views/spree/admin/page_blocks/forms/_product_review_form.html.erb +22 -0
- data/app/views/spree/admin/page_blocks/forms/_reviews.html.erb +52 -0
- data/app/views/spree/admin/page_sections/forms/_add_a_review.html.erb +112 -0
- data/app/views/spree/admin/product_reviews/_product_review.html.erb +89 -0
- data/app/views/spree/admin/product_reviews/_table.html.erb +31 -0
- data/app/views/spree/admin/product_reviews/edit.html.erb +103 -0
- data/app/views/spree/admin/product_reviews/index.html.erb +15 -0
- data/app/views/spree/admin/product_reviews/update.turbo_stream.erb +1 -0
- data/app/views/spree/page_blocks/products/reviews/_reviews.html.erb +42 -0
- data/app/views/spree/page_sections/_add_a_review.html.erb +75 -0
- data/app/views/spree/page_sections/_product_reviews.html.erb +95 -0
- data/app/views/spree/product_reviews/_form.html.erb +275 -0
- data/app/views/spree/product_reviews/_images.html.erb +80 -0
- data/app/views/spree/product_reviews/_pending.html.erb +15 -0
- data/app/views/spree/product_reviews/_review.html.erb +38 -0
- data/app/views/spree/products/_json_ld.html.erb +97 -0
- data/app/views/spree_product_reviews/admin/_product_reviews_dropdown.html.erb +6 -0
- data/app/views/themes/default/spree/page_sections/_product_details.html.erb +89 -0
- data/config/initializers/spree_product_reviews.rb +9 -0
- data/config/locales/en.yml +59 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20250501173852_add_reviews.rb +27 -0
- data/db/migrate/20250501174531_add_review_info_to_products.rb +13 -0
- data/lib/generators/spree_product_reviews/install/install_generator.rb +44 -0
- data/lib/spree_product_reviews/configuration.rb +13 -0
- data/lib/spree_product_reviews/engine.rb +24 -0
- data/lib/spree_product_reviews/factories.rb +6 -0
- data/lib/spree_product_reviews/version.rb +8 -0
- data/lib/spree_product_reviews.rb +5 -0
- metadata +156 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 743b927cdaf050b3014719af7d93fd72298516fe5906b9c7ee78aa11ac0aa9b1
|
|
4
|
+
data.tar.gz: f5343a9a1f75dc591b02edd6b0d88b208da346a1d5ea8932f67165d6e64432af
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9809e4fd93893bc2172f1a848b075832182bf4079cf16580d4ae0e2552a119f7c5a20d9085f6760414f940db38165c9a9533c1368fe8f4c19bf58ddaa447d106
|
|
7
|
+
data.tar.gz: e8fae77c66014f097f38d40d6bd7382215814bbc5cfba008e9df280a5b1e8e30ff50a7a6867cec47bdf86ffd37ca03d29a100deadd16e0b6c7f13bac8879d1e1
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class ProductReviewsController < ResourceController
|
|
4
|
+
belongs_to "spree/product", find_by: :slug
|
|
5
|
+
|
|
6
|
+
before_action :find_product_review, only: [:approve, :edit, :update, :destroy, :attach_image, :purge_images]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
# optionally add filtering/sorting
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def update
|
|
13
|
+
if @product_review.update(permitted_resource_params)
|
|
14
|
+
flash[:success] = Spree.t('product_review.flash_messages.update.success')
|
|
15
|
+
respond_to do |format|
|
|
16
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("dialog_modal_lg", "") }
|
|
17
|
+
format.html { redirect_to admin_product_product_reviews_path(@product) }
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
render :edit
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def approve
|
|
25
|
+
@product_review.update(approved: true)
|
|
26
|
+
flash[:success] = Spree.t(:review_approved)
|
|
27
|
+
redirect_to admin_product_product_reviews_path(@product_review.product)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def disapprove
|
|
31
|
+
@product_review.update(approved: false)
|
|
32
|
+
flash[:success] = Spree.t(:review_disapproved)
|
|
33
|
+
redirect_to admin_product_product_reviews_path(@product_review.product)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# --- Attach a single image ---
|
|
37
|
+
def attach_image
|
|
38
|
+
if params[:file].present?
|
|
39
|
+
@product_review.images.attach(params[:file])
|
|
40
|
+
end
|
|
41
|
+
head :ok
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# DELETE /admin/products/:product_id/product_reviews/:id/purge_images
|
|
45
|
+
def purge_images
|
|
46
|
+
ids = params[:ids] || []
|
|
47
|
+
@product_review.images.where(id: ids).each(&:purge_later)
|
|
48
|
+
|
|
49
|
+
respond_to do |format|
|
|
50
|
+
format.json { head :ok }
|
|
51
|
+
format.html { redirect_back fallback_location: edit_admin_product_product_review_path(@product, @product_review) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def permitted_resource_params
|
|
58
|
+
params.require(:product_review).permit(
|
|
59
|
+
:title,
|
|
60
|
+
:rating,
|
|
61
|
+
:review,
|
|
62
|
+
images: []
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def find_product_review
|
|
67
|
+
@product_review = Spree::ProductReview.find(params[:id])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class ProductReviewsController < Spree::StoreController
|
|
3
|
+
helper Spree::BaseHelper
|
|
4
|
+
before_action :load_product, only: %i[index new create destroy]
|
|
5
|
+
before_action :authenticate_user!, only: %i[new create]
|
|
6
|
+
|
|
7
|
+
def new
|
|
8
|
+
@product_review = Spree::ProductReview.new(product: @product)
|
|
9
|
+
authorize! :create, @product_review
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
@product_review = Spree::ProductReview.new(product_review_params)
|
|
14
|
+
@product_review.product = @product
|
|
15
|
+
@product_review.purchase_date = spree_current_user.recent_purchase_date_for @product
|
|
16
|
+
@product_review.user = spree_current_user
|
|
17
|
+
@product_review.ip_address = request.remote_ip
|
|
18
|
+
@product_review.locale = I18n.locale.to_s
|
|
19
|
+
@product_review.product_name = @product.name
|
|
20
|
+
|
|
21
|
+
authorize! :create, @product_review
|
|
22
|
+
|
|
23
|
+
if Spree::ProductReview.exists?(user_id: spree_current_user.id, product_id: @product.id)
|
|
24
|
+
flash[:error] = "You have already reviewed this product."
|
|
25
|
+
redirect_to spree.product_path(@product) and return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if @product_review.save
|
|
29
|
+
flash[:success] = Spree.t("product_review.flash_messages.create.success")
|
|
30
|
+
redirect_to spree.product_path(@product)
|
|
31
|
+
else
|
|
32
|
+
flash[:error] = Spree.t("product_review.flash_messages.create.failure")
|
|
33
|
+
render :new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def index
|
|
38
|
+
@product_reviews = @product.product_reviews.approved.order(created_at: :desc)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
@product_review = Spree::ProductReview.find(params[:id])
|
|
43
|
+
@product_review.destroy
|
|
44
|
+
# Force CanCanCan to reload abilities for the current user
|
|
45
|
+
session[:_csrf_token] = nil
|
|
46
|
+
redirect_to spree.product_path(@product), notice: Spree.t(:review_deleted)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def load_product
|
|
52
|
+
@product = Spree::Product.friendly.find(params[:product_id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def product_review_params
|
|
56
|
+
params.require(:product_review).permit(
|
|
57
|
+
:rating,
|
|
58
|
+
:review,
|
|
59
|
+
:show_identifier,
|
|
60
|
+
:title,
|
|
61
|
+
images: []
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def authenticate_user!
|
|
66
|
+
return if spree_current_user
|
|
67
|
+
|
|
68
|
+
session[:spree_user_return_to] = request.fullpath
|
|
69
|
+
redirect_to spree.login_path, alert: Spree.t(:please_log_in)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PageBlocks
|
|
3
|
+
class ProductReviewForm < Spree::PageBlock
|
|
4
|
+
preference :button_text, :string, default: Spree.t("page_blocks.product_review_form.button_text_default")
|
|
5
|
+
preference :placeholder, :string, default: Spree.t("page_blocks.product_review_form.placeholder_default")
|
|
6
|
+
preference :button_style, :string, default: "primary"
|
|
7
|
+
validates :preferred_button_style, inclusion: { in: %w[primary secondary] }
|
|
8
|
+
def icon_name
|
|
9
|
+
"forms"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PageBlocks
|
|
3
|
+
module Products
|
|
4
|
+
class Reviews < Spree::PageBlock
|
|
5
|
+
|
|
6
|
+
preference :star_color, :string, default: '#FFA500'
|
|
7
|
+
preference :display_numbers, :boolean, default: true
|
|
8
|
+
preference :star_font_size, :integer, default: 16
|
|
9
|
+
|
|
10
|
+
def self.block_name
|
|
11
|
+
"Reviews & Ratings"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.display_name
|
|
15
|
+
"Reviews"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def editor_name
|
|
19
|
+
"Reviews"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def icon_name
|
|
23
|
+
"input-spark"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(view_context, locals = {})
|
|
27
|
+
view_context.render(
|
|
28
|
+
partial: "spree/page_blocks/products/reviews/reviews",
|
|
29
|
+
locals: locals.merge(block: self)
|
|
30
|
+
)
|
|
31
|
+
rescue ActionView::MissingTemplate
|
|
32
|
+
""
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PageSections
|
|
3
|
+
class AddAReview < Spree::PageSection
|
|
4
|
+
TOP_PADDING_DEFAULT = 30
|
|
5
|
+
BOTTOM_PADDING_DEFAULT = 30
|
|
6
|
+
TOP_BORDER_WIDTH_DEFAULT = 0
|
|
7
|
+
BOTTOM_BORDER_WIDTH_DEFAULT = 0
|
|
8
|
+
|
|
9
|
+
preference :allow_unverified_purchase_reviews, :boolean, default: true
|
|
10
|
+
|
|
11
|
+
preference :heading_no_review_yet, :string, default: Spree.t("add_a_review_heading")
|
|
12
|
+
preference :heading_pending_review, :string, default: Spree.t("thanks_for_review_heading_pending")
|
|
13
|
+
preference :heading_review_approved, :string, default: Spree.t("thanks_for_review_heading")
|
|
14
|
+
|
|
15
|
+
preference :heading_size, :string, default: "large"
|
|
16
|
+
preference :heading_alignment, :string, default: "center"
|
|
17
|
+
preference :display, :string, default: "stacked"
|
|
18
|
+
|
|
19
|
+
validates :preferred_heading_size, inclusion: { in: %w[small medium large] }
|
|
20
|
+
validates :preferred_heading_alignment, inclusion: { in: %w[left center right] }
|
|
21
|
+
validates :preferred_display, inclusion: { in: %w[stacked inline] }
|
|
22
|
+
#New Sections
|
|
23
|
+
preference :rating_color, :string, default: '#FFA500'
|
|
24
|
+
preference :title_font_size, :integer, default: 18
|
|
25
|
+
|
|
26
|
+
preference :review_star_color, :string, default: '#FFA500'
|
|
27
|
+
preference :review_font_size, :integer, default: 14
|
|
28
|
+
|
|
29
|
+
preference :verified_badge_color, :string, default: '#00a63e'
|
|
30
|
+
|
|
31
|
+
preference :max_images_upload, :integer, default: 5
|
|
32
|
+
preference :max_image_size_mb, :integer, default: 5
|
|
33
|
+
|
|
34
|
+
preference :thumbnail_size, :integer, default: 72
|
|
35
|
+
|
|
36
|
+
def self.role
|
|
37
|
+
"content"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def display_name
|
|
41
|
+
"Add a Review"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blocks_available?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default_blocks
|
|
49
|
+
[
|
|
50
|
+
Spree::PageBlocks::Text.new(
|
|
51
|
+
text: Spree.t(:add_a_review_text),
|
|
52
|
+
preferred_text_alignment: "center",
|
|
53
|
+
preferred_container_alignment: "center",
|
|
54
|
+
preferred_bottom_padding: 30,
|
|
55
|
+
preferred_width_desktop: "75"
|
|
56
|
+
),
|
|
57
|
+
Spree::PageBlocks::ProductReviewForm.new,
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def available_blocks_to_add
|
|
62
|
+
[
|
|
63
|
+
Spree::PageBlocks::Heading,
|
|
64
|
+
Spree::PageBlocks::Text,
|
|
65
|
+
Spree::PageBlocks::Image,
|
|
66
|
+
Spree::PageBlocks::ProductReviewForm,
|
|
67
|
+
]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def icon_name
|
|
71
|
+
"device-ipad-horizontal-star"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def links_available?
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PageSections
|
|
3
|
+
module ProductDetailsDecorator
|
|
4
|
+
def available_blocks_to_add
|
|
5
|
+
super + [Spree::PageBlocks::Products::Reviews]
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
Spree::PageSections::ProductDetails.prepend(Spree::PageSections::ProductDetailsDecorator)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class ProductReview < Spree::Base
|
|
3
|
+
belongs_to :product, class_name: "Spree::Product"
|
|
4
|
+
belongs_to :user, class_name: Spree.user_class.to_s
|
|
5
|
+
belongs_to :variant, class_name: "Spree::Variant", optional: true
|
|
6
|
+
|
|
7
|
+
has_many_attached :images, dependent: :purge_later
|
|
8
|
+
|
|
9
|
+
validates :product, presence: true
|
|
10
|
+
validates :user, presence: true
|
|
11
|
+
validates :rating, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
|
|
12
|
+
validates :title, presence: true
|
|
13
|
+
validates :review, presence: true
|
|
14
|
+
validate :validate_max_images
|
|
15
|
+
validate :validate_image_size
|
|
16
|
+
attribute :show_identifier, :boolean, default: true
|
|
17
|
+
|
|
18
|
+
scope :approved, -> { where(approved: true) }
|
|
19
|
+
scope :pending, -> { where(approved: false) }
|
|
20
|
+
|
|
21
|
+
def pending?
|
|
22
|
+
!approved?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def approve!
|
|
26
|
+
update(approved: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reject!
|
|
30
|
+
update(approved: false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_max_images
|
|
34
|
+
return unless images.attached?
|
|
35
|
+
|
|
36
|
+
section = Spree::PageSections::AddAReview.first
|
|
37
|
+
max = section&.preferred_max_images_upload || 5
|
|
38
|
+
|
|
39
|
+
if images.count > max
|
|
40
|
+
errors.add(:images, "Maximum #{max} images allowed")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_image_size
|
|
45
|
+
return unless images.attached?
|
|
46
|
+
|
|
47
|
+
section = Spree::PageSections::AddAReview.first
|
|
48
|
+
max_mb = section&.preferred_max_image_size_mb || 5
|
|
49
|
+
max_bytes = max_mb.megabytes
|
|
50
|
+
|
|
51
|
+
images.each do |image|
|
|
52
|
+
if image.blob.byte_size > max_bytes
|
|
53
|
+
errors.add(
|
|
54
|
+
:images,
|
|
55
|
+
"Each image must be smaller than #{max_mb}MB"
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def reviewer_name
|
|
62
|
+
if user&.name && show_identifier
|
|
63
|
+
user.name
|
|
64
|
+
else
|
|
65
|
+
"Anonymous"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def review_date
|
|
70
|
+
created_at.strftime("%B %d, %Y at %I:%M%p")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class ProductReviewsAbility
|
|
3
|
+
include CanCan::Ability
|
|
4
|
+
|
|
5
|
+
def initialize(user)
|
|
6
|
+
user ||= Spree.user_class.new
|
|
7
|
+
Rails.logger.info "[ProductReviewsAbility] user=#{user.inspect}, roles=#{user.spree_roles.map(&:name).inspect}"
|
|
8
|
+
if user.has_spree_role? "admin"
|
|
9
|
+
can :manage, Spree::ProductReview
|
|
10
|
+
elsif user.persisted?
|
|
11
|
+
can :create, Spree::ProductReview
|
|
12
|
+
can :update, Spree::ProductReview do |review|
|
|
13
|
+
review.user_id == user.id
|
|
14
|
+
end
|
|
15
|
+
can :destroy, Spree::ProductReview do |review|
|
|
16
|
+
review.user_id == user.id
|
|
17
|
+
end
|
|
18
|
+
else
|
|
19
|
+
can :read, Spree::ProductReview, approved: true
|
|
20
|
+
cannot :create, Spree::ProductReview
|
|
21
|
+
cannot %i[update destroy], Spree::ProductReview
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeProductReviews
|
|
2
|
+
module Spree
|
|
3
|
+
module ProductDecorator
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.has_many :product_reviews, class_name: 'Spree::ProductReview', dependent: :destroy
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
::Spree::Product.prepend self
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Spree::Product.prepend SpreeProductReviews::Spree::ProductDecorator
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module SpreeProductReviews
|
|
2
|
+
module Spree
|
|
3
|
+
module ProductReviewDecorator
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
#base.has_one_attached :attachment
|
|
6
|
+
#base.has_many_attached :images
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Spree::ProductReview.prepend SpreeProductReviews::Spree::ProductReviewDecorator if defined?(Spree::ProductReview)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module SpreeProductReviews
|
|
2
|
+
module Spree
|
|
3
|
+
module UserDecorator
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.has_many :product_reviews, class_name: "Spree::ProductReview", dependent: :destroy, foreign_key: :user_id
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def product_review_for(product)
|
|
9
|
+
product_reviews.find_by(product_id: product.id, user_id: id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def recent_purchase_date_for(product)
|
|
13
|
+
orders.joins(:line_items, :variants).where(
|
|
14
|
+
spree_variants: { product_id: product.id }
|
|
15
|
+
).order("spree_orders.completed_at DESC").first&.completed_at
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Spree.user_class.prepend SpreeProductReviews::Spree::UserDecorator
|
|
22
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="form-group">
|
|
2
|
+
<%= f.label :preferred_button_text %>
|
|
3
|
+
<%= f.text_field :preferred_button_text, data: { action: 'auto-submit#submit' }, class: 'form-control' %>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="form-group">
|
|
6
|
+
<%= f.label :preferred_placeholder %>
|
|
7
|
+
<%= f.text_field :preferred_placeholder, data: { action: 'auto-submit#submit' }, class: 'form-control' %>
|
|
8
|
+
</div>
|
|
9
|
+
<% content_for(:design_tab) do %>
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<%= f.label :preferred_button_style %>
|
|
12
|
+
<%= f.select :preferred_button_style, options_for_select(['primary', 'secondary'], @page_block.preferred_button_style), {}, { class: 'custom-select', data: { action: 'auto-submit#submit' } } %>
|
|
13
|
+
</div>
|
|
14
|
+
<style>
|
|
15
|
+
.form-group:has(#page_block_preferred_top_padding) {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
.form-group:has(#page_block_preferred_bottom_padding) {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<p class="text-muted">This block shows the product’s average rating and total reviews.</p>
|
|
2
|
+
<hr>
|
|
3
|
+
<h5 class="mb-3">Appearance</h5><br>
|
|
4
|
+
<div class="mb-4 form-group"
|
|
5
|
+
data-controller="color-picker"
|
|
6
|
+
data-color-picker-clear-value="false"
|
|
7
|
+
data-color-picker-default-color-value="<%= f.object.preferred_star_color.presence || '#FFA500' %>">
|
|
8
|
+
<div class="d-flex align-items-center">
|
|
9
|
+
<%= f.hidden_field :preferred_star_color,
|
|
10
|
+
data: { color_picker_target: "input", action: "change->auto-submit#submit" },
|
|
11
|
+
autocomplete: "off" %>
|
|
12
|
+
<div>
|
|
13
|
+
<label class="mb-0">Ratings Color</label><br>
|
|
14
|
+
<span data-color-picker-target="value" class="text-muted">
|
|
15
|
+
<%= f.object.preferred_star_color.presence || '#FFA500' %>
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="ml-auto" style="width:40px">
|
|
19
|
+
<div data-color-picker-target="picker" class="border d-inline-block rounded-circle" style="height: 40px; width: 40px; background-color: <%= f.object.preferred_star_color.presence || '#FFA500' %>;" role="button" aria-label="toggle color picker dialog"></div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-group">
|
|
24
|
+
<label>Display Review Count</label>
|
|
25
|
+
<%= f.select :preferred_display_numbers,
|
|
26
|
+
options_for_select([["Yes", true], ["No", false]], f.object.preferred_display_numbers),
|
|
27
|
+
{},
|
|
28
|
+
class: "custom-select",
|
|
29
|
+
data: { action: "change->auto-submit#submit" } %>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-group">
|
|
32
|
+
<label>Ratings Size</label>
|
|
33
|
+
<div data-controller="better-slider"
|
|
34
|
+
data-better-slider-label-for-min-value=""
|
|
35
|
+
data-better-slider-unit-value="px">
|
|
36
|
+
<div class="flex flex-row w-full">
|
|
37
|
+
<%= f.number_field :preferred_star_font_size,
|
|
38
|
+
type: 'range',
|
|
39
|
+
min: 10,
|
|
40
|
+
max: 40,
|
|
41
|
+
step: 1,
|
|
42
|
+
class: 'custom-range',
|
|
43
|
+
data: {
|
|
44
|
+
better_slider_target: "rangeInput",
|
|
45
|
+
action: "change->auto-submit#submit"
|
|
46
|
+
} %>
|
|
47
|
+
</div>
|
|
48
|
+
<span data-better-slider-target="currentValueLabel">
|
|
49
|
+
<%= f.object.preferred_star_font_size %>px
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<div class="alert alert-warning"><p><%= Spree.t("admin.page_sections.product_reviews.display_description") %></p></div>
|
|
2
|
+
<h6 style="font-size: 1rem; padding-bottom: 0.8rem;"><%= Spree.t("admin.page_sections.product_reviews.display_style.heading_options") %></h6>
|
|
3
|
+
<div class="container">
|
|
4
|
+
<div class="form-group">
|
|
5
|
+
<%= f.label :preferred_heading_size, Spree.t("admin.page_builder.heading_size") %>
|
|
6
|
+
<%= f.select :preferred_heading_size, options_for_select([ ["Small", "small"], ["Medium", "medium"], ["Large", "large"] ], @page_section.preferred_heading_size ), {}, data: { action: "auto-submit#submit" }, class: "custom-select" %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="form-group">
|
|
9
|
+
<%= f.label :preferred_heading_alignment, Spree.t("admin.page_builder.heading_alignment") %>
|
|
10
|
+
<%= f.select :preferred_heading_alignment, options_for_select([ ["Left", "left"], ["Center", "center"], ["Right", "right"] ], @page_section.preferred_heading_alignment ), {}, data: { action: "auto-submit#submit" }, class: "custom-select" %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<hr class="mb-4">
|
|
14
|
+
<h6 style="font-size: 1rem; padding-bottom: 0.8rem;"><%= Spree.t("admin.page_sections.product_reviews.heading_labels.heading") %></h6>
|
|
15
|
+
<div class="container">
|
|
16
|
+
<div class="form-group">
|
|
17
|
+
<%= f.label :preferred_heading_no_review_yet, Spree.t("admin.page_sections.product_reviews.heading_labels.no_review_yet") %>
|
|
18
|
+
<%= f.text_field :preferred_heading_no_review_yet, data: { action: "auto-submit#submit" }, class: "form-control" %>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="form-group">
|
|
21
|
+
<%= f.label :preferred_heading_pending_review, Spree.t("admin.page_sections.product_reviews.heading_labels.review_pending") %>
|
|
22
|
+
<%= f.text_field :preferred_heading_pending_review, data: { action: "auto-submit#submit" }, class: "form-control" %>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="form-group">
|
|
25
|
+
<%= f.label :preferred_heading_review_approved, Spree.t("admin.page_sections.product_reviews.heading_labels.review_approved") %>
|
|
26
|
+
<%= f.text_field :preferred_heading_review_approved, data: { action: "auto-submit#submit" }, class: "form-control" %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<hr class="mb-4">
|
|
30
|
+
<h6 style="font-size: 1rem; padding-bottom: 0.8rem;"><%= Spree.t("admin.page_sections.product_reviews.control_preferences.label") %></h6>
|
|
31
|
+
<div class="container">
|
|
32
|
+
<div class="form-group">
|
|
33
|
+
<%= f.check_box :preferred_allow_unverified_purchase_reviews, class: "checkbox-input", data: { action: "auto-submit#submit" }, checked: @page_section.preferred_allow_unverified_purchase_reviews %>
|
|
34
|
+
<%= f.label :preferred_allow_unverified_purchase_reviews, Spree.t("admin.page_sections.product_reviews.control_preferences.allow_unverified_reviews") %>
|
|
35
|
+
<div class="alert alert-info" style="background-position: 20px 10px;">
|
|
36
|
+
<p><%= Spree.t("admin.page_sections.product_reviews.control_preferences.allow_unverified_reviews_enabled_description") %></p>
|
|
37
|
+
<p><%= Spree.t("admin.page_sections.product_reviews.control_preferences.allow_unverified_reviews_disabled_description") %></p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<hr class="mb-4">
|
|
42
|
+
<h6 style="font-size: 1rem; padding-bottom: 0.8rem;"><%= Spree.t("admin.page_sections.product_reviews.review_settings") %></h6>
|
|
43
|
+
<div class="mb-4 form-group" data-controller="color-picker" data-color-picker-clear-value="false" data-color-picker-default-color-value="<%= f.object.preferred_rating_color || '#FFA500' %>">
|
|
44
|
+
<label>Rating Color</label>
|
|
45
|
+
<%= f.hidden_field :preferred_rating_color, data: { color_picker_target: "input", action: "change->auto-submit#submit" } %>
|
|
46
|
+
<div class="d-flex align-items-center">
|
|
47
|
+
<span data-color-picker-target="value" class="text-muted"><%= f.object.preferred_rating_color || '#FFA500' %></span>
|
|
48
|
+
<div class="ml-auto" style="width:40px">
|
|
49
|
+
<div data-color-picker-target="picker" class="border rounded-circle" style="height:40px;width:40px;background:<%= f.object.preferred_rating_color || '#FFA500' %>;" role="button"></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="mb-4 form-group" data-controller="color-picker" data-color-picker-clear-value="false" data-color-picker-default-color-value="<%= f.object.preferred_review_star_color || '#FFA500' %>">
|
|
54
|
+
<label>Review Star Color</label>
|
|
55
|
+
<%= f.hidden_field :preferred_review_star_color, data: { color_picker_target: "input", action: "change->auto-submit#submit" } %>
|
|
56
|
+
<div class="d-flex align-items-center">
|
|
57
|
+
<span data-color-picker-target="value" class="text-muted"><%= f.object.preferred_review_star_color || '#FFA500' %></span>
|
|
58
|
+
<div class="ml-auto" style="width:40px"><div data-color-picker-target="picker" class="border rounded-circle" style="height:40px;width:40px;background:<%= f.object.preferred_review_star_color || '#FFA500' %>;" role="button"></div></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="mb-4 form-group" data-controller="color-picker" data-color-picker-clear-value="false" data-color-picker-default-color-value="<%= f.object.preferred_verified_badge_color %>">
|
|
62
|
+
<label>Verified Badge Color</label>
|
|
63
|
+
<%= f.hidden_field :preferred_verified_badge_color, data: { color_picker_target: "input", action: "change->auto-submit#submit" } %>
|
|
64
|
+
<div class="d-flex align-items-center">
|
|
65
|
+
<span data-color-picker-target="value" class="text-muted">
|
|
66
|
+
<%= f.object.preferred_verified_badge_color %>
|
|
67
|
+
</span>
|
|
68
|
+
<div class="ml-auto" style="width:40px">
|
|
69
|
+
<div data-color-picker-target="picker" class="border rounded-circle" style="height:40px;width:40px;background:<%= f.object.preferred_verified_badge_color %>;" role="button"></div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="form-group">
|
|
74
|
+
<label>Title Font Size</label>
|
|
75
|
+
<div data-controller="better-slider" data-better-slider-unit-value="px">
|
|
76
|
+
<%= f.number_field :preferred_title_font_size, type: 'range', min: 12, max: 48, step: 1, class: 'custom-range', data: { better_slider_target: "rangeInput", action: "change->auto-submit#submit" } %>
|
|
77
|
+
<span data-better-slider-target="currentValueLabel"><%= f.object.preferred_title_font_size %>px</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="form-group">
|
|
81
|
+
<label>Review Font Size</label>
|
|
82
|
+
<div data-controller="better-slider" data-better-slider-unit-value="px">
|
|
83
|
+
<%= f.number_field :preferred_review_font_size, type: 'range', min: 10, max: 30, step: 1, class: 'custom-range', data: { better_slider_target: "rangeInput", action: "change->auto-submit#submit" } %>
|
|
84
|
+
<span data-better-slider-target="currentValueLabel"> <%= f.object.preferred_review_font_size %>px </span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<hr class="mb-4">
|
|
88
|
+
<h6 style="font-size: 1rem; padding-bottom: 0.8rem;"><%= Spree.t("admin.page_sections.product_reviews.upload_preferences") %></h6>
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label>Max Images Upload</label>
|
|
91
|
+
<div data-controller="better-slider">
|
|
92
|
+
<%= f.number_field :preferred_max_images_upload, type: 'range', min: 1, max: 20, step: 1, class: 'custom-range', data: { better_slider_target: "rangeInput", action: "change->auto-submit#submit" } %>
|
|
93
|
+
<span data-better-slider-target="currentValueLabel"><%= f.object.preferred_max_images_upload %></span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-group">
|
|
97
|
+
<label>Max Image Size (MB)</label>
|
|
98
|
+
<div data-controller="better-slider">
|
|
99
|
+
<%= f.number_field :preferred_max_image_size_mb, type: 'range', min: 1, max: 20, step: 1, class: 'custom-range', data: { better_slider_target: "rangeInput", action: "change->auto-submit#submit" } %>
|
|
100
|
+
<span data-better-slider-target="currentValueLabel"><%= f.object.preferred_max_image_size_mb %> MB</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="form-group">
|
|
104
|
+
<label>Thumbnail Size</label>
|
|
105
|
+
<div data-controller="better-slider" data-better-slider-unit-value="px">
|
|
106
|
+
<%= f.number_field :preferred_thumbnail_size, type: 'range', min: 10, max: 200, step: 1, class: 'custom-range', data: { better_slider_target: "rangeInput", action: "change->auto-submit#submit" } %>
|
|
107
|
+
<span data-better-slider-target="currentValueLabel"><%= f.object.preferred_thumbnail_size %>px</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<% content_for(:design_tab) do %>
|
|
111
|
+
<div class="form-group"><%= f.label :preferred_display, Spree.t("admin.page_sections.product_reviews.display_style.label") %><%= f.select :preferred_display, options_for_select([ [Spree.t("admin.page_sections.product_reviews.display_style.stacked"), "stacked"], [Spree.t("admin.page_sections.product_reviews.display_style.inline"), "inline"]], @page_section.preferred_display ), {}, data: { action: "auto-submit#submit" }, class: "custom-select" %></div>
|
|
112
|
+
<% end %>
|