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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/app/controllers/spree/admin/product_reviews_controller.rb +71 -0
  3. data/app/controllers/spree/home_controller.rb +7 -0
  4. data/app/controllers/spree/product_reviews_controller.rb +72 -0
  5. data/app/models/spree/page_blocks/product_review_form.rb +13 -0
  6. data/app/models/spree/page_blocks/products/reviews.rb +37 -0
  7. data/app/models/spree/page_sections/add_a_review.rb +80 -0
  8. data/app/models/spree/page_sections/product_details_decorator.rb +10 -0
  9. data/app/models/spree/product_review.rb +73 -0
  10. data/app/models/spree/product_reviews_ability.rb +25 -0
  11. data/app/models/spree_product_reviews/spree/product_decorator.rb +13 -0
  12. data/app/models/spree_product_reviews/spree/product_review_decorator.rb +12 -0
  13. data/app/models/spree_product_reviews/spree/user_decorator.rb +22 -0
  14. data/app/views/spree/admin/page_blocks/forms/_product_review_form.html.erb +22 -0
  15. data/app/views/spree/admin/page_blocks/forms/_reviews.html.erb +52 -0
  16. data/app/views/spree/admin/page_sections/forms/_add_a_review.html.erb +112 -0
  17. data/app/views/spree/admin/product_reviews/_product_review.html.erb +89 -0
  18. data/app/views/spree/admin/product_reviews/_table.html.erb +31 -0
  19. data/app/views/spree/admin/product_reviews/edit.html.erb +103 -0
  20. data/app/views/spree/admin/product_reviews/index.html.erb +15 -0
  21. data/app/views/spree/admin/product_reviews/update.turbo_stream.erb +1 -0
  22. data/app/views/spree/page_blocks/products/reviews/_reviews.html.erb +42 -0
  23. data/app/views/spree/page_sections/_add_a_review.html.erb +75 -0
  24. data/app/views/spree/page_sections/_product_reviews.html.erb +95 -0
  25. data/app/views/spree/product_reviews/_form.html.erb +275 -0
  26. data/app/views/spree/product_reviews/_images.html.erb +80 -0
  27. data/app/views/spree/product_reviews/_pending.html.erb +15 -0
  28. data/app/views/spree/product_reviews/_review.html.erb +38 -0
  29. data/app/views/spree/products/_json_ld.html.erb +97 -0
  30. data/app/views/spree_product_reviews/admin/_product_reviews_dropdown.html.erb +6 -0
  31. data/app/views/themes/default/spree/page_sections/_product_details.html.erb +89 -0
  32. data/config/initializers/spree_product_reviews.rb +9 -0
  33. data/config/locales/en.yml +59 -0
  34. data/config/routes.rb +20 -0
  35. data/db/migrate/20250501173852_add_reviews.rb +27 -0
  36. data/db/migrate/20250501174531_add_review_info_to_products.rb +13 -0
  37. data/lib/generators/spree_product_reviews/install/install_generator.rb +44 -0
  38. data/lib/spree_product_reviews/configuration.rb +13 -0
  39. data/lib/spree_product_reviews/engine.rb +24 -0
  40. data/lib/spree_product_reviews/factories.rb +6 -0
  41. data/lib/spree_product_reviews/version.rb +8 -0
  42. data/lib/spree_product_reviews.rb +5 -0
  43. 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,7 @@
1
+ module Spree
2
+ class HomeController < Spree::StoreController
3
+ def forbidden
4
+ render plain: "You are not authorized to access this page.", status: :forbidden
5
+ end
6
+ end
7
+ 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 %>