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
@@ -0,0 +1,89 @@
1
+ <tr id="<%= dom_id(product_review) %>">
2
+ <td class="w-10 cursor-pointer" data-action="click->row-link#openLink">
3
+ <% if product_review.approved? %>
4
+ <span class="badge badge-shipped">
5
+ <i class="ti ti-check"></i> Approved
6
+ </span>
7
+ <% elsif product_review.approved == false %>
8
+ <span class="badge badge-canceled">
9
+ <i class="ti ti-x"></i> Disapproved
10
+ </span>
11
+ <% else %>
12
+ <span class="badge badge-pending">
13
+ <i class="ti ti-progress"></i> Pending
14
+ </span>
15
+ <% end %>
16
+ </td>
17
+
18
+ <td><%= product_review.user&.email || "Guest" %></td>
19
+ <td><%= product_review.title %></td>
20
+ <td><%= product_review.rating %></td>
21
+ <td><%= l(product_review.created_at, format: :short) %></td>
22
+ <td>
23
+ <% if product_review.images.attached? %>
24
+ <div data-controller="lightbox" class="flex gap-1">
25
+ <% product_review.images.each do |img| %>
26
+ <% next unless img.blob.present? %>
27
+ <% thumb = img.variant(resize_to_limit: [50, 50], saver: { quality: 70 }) %>
28
+
29
+ <%= link_to main_app.rails_blob_path(img, only_path: true),
30
+ data: {
31
+ pswp_width: img.blob.metadata[:width] || 2000,
32
+ pswp_height: img.blob.metadata[:height] || 2000
33
+ },
34
+ class: "inline-block rounded border border-gray-200 admin-review-thumb" do %>
35
+ <%= image_tag main_app.rails_representation_path(thumb, only_path: true),
36
+ width: 50,
37
+ height: 50,
38
+ class: "rounded",
39
+ loading: "lazy",
40
+ decoding: "async" %>
41
+ <% end %>
42
+ <% end %>
43
+ </div>
44
+ <% end %>
45
+ </td>
46
+
47
+ <td class="actions">
48
+ <% if product_review.approved? %>
49
+ <%= button_to "Disapprove",
50
+ disapprove_admin_product_product_review_path(
51
+ product_review.product,
52
+ product_review
53
+ ),
54
+ method: :get,
55
+ class: "btn btn-warning btn-sm mr-2" %>
56
+ <% else %>
57
+ <%= link_to Spree.t(:approve),
58
+ approve_admin_product_product_review_path(
59
+ product_review.product,
60
+ product_review
61
+ ),
62
+ method: :get,
63
+ class: "btn btn-success btn-sm mr-2" %>
64
+ <% end %>
65
+
66
+ <% if can? :edit, product_review %>
67
+ <%= link_to_edit(
68
+ product_review,
69
+ no_text: true,
70
+ url: edit_admin_product_product_review_path(
71
+ product_review.product,
72
+ product_review,
73
+ return_to: request.fullpath
74
+ )
75
+ ) %>
76
+ <% end %>
77
+
78
+ <% if can? :destroy, product_review %>
79
+ <%= link_to_delete(
80
+ product_review,
81
+ url: admin_product_product_review_path(
82
+ product_review.product,
83
+ product_review
84
+ ),
85
+ no_text: true
86
+ ) %>
87
+ <% end %>
88
+ </td>
89
+ </tr>
@@ -0,0 +1,31 @@
1
+ <% colspan = 6 %> <!-- Fix: define colspan -->
2
+
3
+ <table class="table">
4
+ <thead>
5
+ <tr>
6
+ <th><%= Spree.t(:status) %></th>
7
+ <th><%= Spree.t(:email) %></th>
8
+ <th><%= Spree.t(:review_title) %></th>
9
+ <th><%= Spree.t(:product_review_rating) %></th>
10
+ <th><%= Spree.t(:date) %></th>
11
+ <th><%= Spree.t(:review_images) %></th>
12
+ <th></th>
13
+ </tr>
14
+ </thead>
15
+
16
+ <tbody>
17
+ <%= render partial: "spree/admin/product_reviews/product_review",
18
+ collection: @product_reviews,
19
+ cached: spree_base_cache_scope %>
20
+
21
+ <% if @product_reviews.empty? %>
22
+ <tr>
23
+ <td colspan="<%= colspan %>">
24
+ <div class="text-muted p-5 d-flex align-items-center w-100 justify-content-center">
25
+ <%= Spree.t(:no_resource_found, resource: Spree.t(:product_reviews)) %>
26
+ </div>
27
+ </td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
@@ -0,0 +1,103 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:edit_product_review) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= link_to(Spree.t(:back), params[:return_to].presence || admin_product_product_reviews_path(@product), class: "btn btn-secondary") %>
7
+ <% end %>
8
+
9
+ <div class="card">
10
+ <div class="card-body">
11
+
12
+ <%= form_with( model: [:admin, @product, @product_review], id: dom_id(@product_review, :edit), data: { turbo: true } ) do |f| %>
13
+
14
+ <div class="form-group mb-3">
15
+ <%= f.label :title, Spree.t(:review_title) %>
16
+ <%= f.text_field :title, class: "form-control" %>
17
+ </div>
18
+
19
+ <div class="form-group mb-3">
20
+ <%= f.label :review, Spree.t(:review) %>
21
+ <%= f.text_area :review, class: "form-control", rows: 5 %>
22
+ </div>
23
+
24
+ <div class="form-group mb-4">
25
+ <%= f.label :rating, Spree.t(:product_review_rating) %>
26
+ <div class="btn-group d-flex gap-2" data-rating-group>
27
+ <% (1..5).each do |i| %>
28
+ <label
29
+ class="btn btn-outline-primary <%= 'active' if @product_review.rating == i %>" data-rating-button >
30
+ <%= f.radio_button :rating, i, checked: @product_review.rating == i, class: "d-none" %>
31
+ <%= i %>
32
+ </label>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+ <% if @product_review.images.attached? %>
37
+ <div class="card mb-4">
38
+ <div class="card-header d-flex justify-content-between align-items-center">
39
+ <strong>Images</strong>
40
+ <button type="button" class="btn btn-sm btn-danger" data-review-delete data-url="<%= purge_images_admin_product_product_review_path(@product, @product_review) %>">
41
+ <i class="ti ti-trash"></i> Delete Selected
42
+ </button>
43
+ </div>
44
+ <div class="card-body d-flex flex-wrap gap-3">
45
+ <% @product_review.images.each do |img| %>
46
+ <% thumb = img.variant(resize_to_limit: [120, 120], saver: { quality: 70 }) %>
47
+ <div class="position-relative review-image-thumb" id="review_image_<%= img.id %>">
48
+ <div class="form-check position-absolute top-0 start-0 m-1">
49
+ <input type="checkbox" class="form-check-input" value="<%= img.id %>">
50
+ </div>
51
+ <%= image_tag(
52
+ main_app.rails_representation_path(thumb, only_path: true),
53
+ class: "rounded border",
54
+ loading: "lazy"
55
+ ) %>
56
+ </div>
57
+ <% end %>
58
+ </div>
59
+ </div>
60
+ <% end %>
61
+ <% end %>
62
+ </div>
63
+ </div>
64
+
65
+ <script>
66
+ document.addEventListener("click", async function(event) {
67
+ const ratingBtn = event.target.closest("[data-rating-button]")
68
+ if (ratingBtn) {
69
+ const group = ratingBtn.closest("[data-rating-group]")
70
+ group.querySelectorAll("[data-rating-button]").forEach(btn => {
71
+ btn.classList.remove("active")
72
+ btn.querySelector("input[type='radio']").checked = false
73
+ })
74
+ ratingBtn.classList.add("active")
75
+ ratingBtn.querySelector("input[type='radio']").checked = true
76
+ return
77
+ }
78
+ const button = event.target.closest("[data-review-delete]")
79
+ if (!button) return
80
+ event.preventDefault()
81
+ if (button.dataset.processing === "true") return
82
+ button.dataset.processing = "true"
83
+ const form = button.closest("form") || document
84
+ const checked = form.querySelectorAll(".review-image-thumb input[type='checkbox']:checked")
85
+ const ids = Array.from(checked).map(cb => cb.value)
86
+ if (ids.length === 0) { button.dataset.processing = "false"; return }
87
+ if (!confirm("Delete selected images?")) { button.dataset.processing = "false"; return }
88
+ try {
89
+ const token = document.querySelector("meta[name='csrf-token']").content
90
+ const response = await fetch(button.dataset.url, {
91
+ method: "DELETE",
92
+ headers: { "X-CSRF-Token": token, "Content-Type": "application/json" },
93
+ body: JSON.stringify({ ids })
94
+ })
95
+ if (!response.ok) throw new Error("Delete failed")
96
+ ids.forEach(id => form.querySelector(`#review_image_${id}`)?.remove())
97
+ } catch(e) {
98
+ alert("Failed to delete images")
99
+ } finally {
100
+ button.dataset.processing = "false"
101
+ }
102
+ })
103
+ </script>
@@ -0,0 +1,15 @@
1
+ <% content_for :page_title do %>
2
+ <%= page_header_back_button spree.admin_product_path(@product) %>
3
+ <span><%= Spree.t(:product_reviews) %></span>
4
+ <% end %>
5
+
6
+ <% content_for :page_actions do %>
7
+ <% end %>
8
+
9
+ <div class="card p-0">
10
+ <div class="card-body p-0">
11
+ <div class="table-responsive" id="<%= dom_id(@product, :product_reviews) %>">
12
+ <%= render "spree/admin/product_reviews/table" %>
13
+ </div>
14
+ </div>
15
+ </div>
@@ -0,0 +1 @@
1
+ <%= turbo_stream.replace dom_id(@product_review), partial: "spree/admin/product_reviews/product_review", locals: { product_review: @product_review } %>
@@ -0,0 +1,42 @@
1
+ <%
2
+ star_color = block.preferred_star_color.presence || '#FFA500'
3
+ star_size = block.preferred_star_font_size || 16
4
+ show_numbers = block.preferred_display_numbers
5
+ %>
6
+
7
+ <%
8
+ product = local_assigns[:product] || @product
9
+ return unless product
10
+
11
+ if spree_current_user
12
+ reviews = product.product_reviews.where(
13
+ "approved = ? OR user_id = ?",
14
+ true,
15
+ spree_current_user.id
16
+ )
17
+ else
18
+ reviews = product.product_reviews.where(approved: true)
19
+ end
20
+ %>
21
+
22
+ <% return unless reviews.any? %>
23
+
24
+ <%
25
+ avg_rating = reviews.average(:rating).to_f
26
+ full_stars = avg_rating.floor
27
+ half_star = (avg_rating - full_stars) >= 0.5
28
+ %>
29
+
30
+ <div class="reviews-summary mb-0">
31
+ <div class="flex items-center mb-0 mt-2">
32
+ <div class="flex gap-0.5" style="color:<%= star_color %>; font-size:<%= star_size %>px;">
33
+ <% full_stars.times { %><span>★</span><% } %>
34
+ <% if half_star %><span>★</span><% end %>
35
+ <% (5 - full_stars - (half_star ? 1 : 0)).times do %>
36
+ <span class="text-gray-300">☆</span>
37
+ <% end %>
38
+ </div>
39
+
40
+ <% if show_numbers %><div class="ml-2 text-gray-600 text-sm">(<%= reviews.count %> <%= "review".pluralize(reviews.count) %>)</div><% end %>
41
+ </div>
42
+ </div>
@@ -0,0 +1,75 @@
1
+ <%
2
+ section ||= local_assigns[:section] || @page_section
3
+ product = local_assigns[:product] || (section.try(:product)) || @product
4
+ user_reviewed = spree_current_user&.product_review_for(product)
5
+ user_purchased = spree_current_user&.recent_purchase_date_for(product).present?
6
+ allow_unverified = section&.preferred_allow_unverified_purchase_reviews
7
+ show_form = spree_current_user && (user_purchased || allow_unverified)
8
+ %>
9
+
10
+ <div id="section-<%= section.id %>" class="section-add-a-review">
11
+ <div class="page-container" style="<%= section_styles(section) %>">
12
+ <div class="flex flex-col items-center">
13
+ <h2 class="<%= section.preferred_heading_size == 'small' ? 'text-lg' : 'text-2xl' %> font-medium">
14
+ <% if spree_current_user %>
15
+ <% if user_reviewed&.pending? %>
16
+ <%= section.preferred_heading_pending_review %>
17
+ <% elsif user_reviewed&.approved? %>
18
+ <%= section.preferred_heading_review_approved %>
19
+ <% else %>
20
+ <%= section.preferred_heading_no_review_yet %>
21
+ <% end %>
22
+ <% else %>
23
+ <%= section.preferred_heading_no_review_yet %>
24
+ <% end %>
25
+ </h2>
26
+
27
+ <%# --- CACHE STATIC BLOCKS --- %>
28
+ <% cache_unless page_builder_enabled?,
29
+ [spree_base_cache_scope.call(section), "static-blocks", section.updated_at] do %>
30
+ <% section.blocks.includes(:rich_text_text).where.not(type: "Spree::PageBlocks::ProductReviewForm").each do |block| %>
31
+ <% case block.type %>
32
+ <% when "Spree::PageBlocks::Heading" %>
33
+ <h3 class="<%= block.preferred_size == 'small' ? 'text-lg' : 'text-2xl' %> font-medium" <%= block_attributes(block) %>>
34
+ <%= block.text %>
35
+ </h3>
36
+ <% when "Spree::PageBlocks::Text" %>
37
+ <div class="max-w-prose text-md leading-relaxed font-normal" <%= block_attributes(block) %>>
38
+ <%= block.text %>
39
+ </div>
40
+ <% when "Spree::PageBlocks::Image" %>
41
+ <div class="flex justify-center" <%= block_attributes(block) %>>
42
+ <%= render "spree/page_blocks/image", block: block %>
43
+ </div>
44
+ <% end %>
45
+ <% end %>
46
+ <% end %>
47
+
48
+ <% section.blocks.where(type: "Spree::PageBlocks::ProductReviewForm").each do |block| %>
49
+ <% if show_form %>
50
+ <div class="flex-1 w-full">
51
+ <% if user_reviewed %>
52
+ <%= render "spree/product_reviews/pending", product_review: user_reviewed, section: section %>
53
+ <% else %>
54
+ <%= render "spree/product_reviews/form",
55
+ product: product,
56
+ product_review: Spree::ProductReview.new(product: product),
57
+ block: block,
58
+ section: section %>
59
+ <% end %>
60
+ </div>
61
+ <% else %>
62
+ <div class="text-sm text-gray-600 mt-4">
63
+ <% if spree_current_user %>
64
+ <p>You must purchase this product to leave a review.</p>
65
+ <% else %>
66
+ <p>Please <a href="<%= spree_login_path(redirect_to: request.fullpath) %>" class="text-blue-600 underline">log in</a> to leave a review.</p>
67
+ <% end %>
68
+ </div>
69
+ <% end %>
70
+ <% end %>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <%= render 'spree/page_sections/product_reviews', product: product, section: section %>
@@ -0,0 +1,95 @@
1
+ <%# Determine the correct product object from local assigns or fallback to @product %>
2
+ <% product = local_assigns[:product] || @product %>
3
+ <% section ||= local_assigns[:section] %>
4
+ <% badge_color = section&.preferred_verified_badge_color || '#00a63e' %>
5
+
6
+ <% if spree_current_user %>
7
+ <% product_reviews = product.product_reviews.where("approved = ? OR user_id = ?", true, spree_current_user.id).order(created_at: :desc) %>
8
+ <% else %>
9
+ <% product_reviews = product.product_reviews.where(approved: true).order(created_at: :desc) %>
10
+ <% end %>
11
+
12
+ <div class="page-container mx-auto py-8 pb-6">
13
+ <h1 class="text-2xl font-bold mb-6 text-center"><%= Spree.t('customer_reviews') %></h1>
14
+
15
+ <% if product_reviews.any? %>
16
+ <% cache ["product-reviews", product.id, product_reviews.maximum(:updated_at)] do %>
17
+ <div class="reviews-summary mb-6">
18
+ <div class="flex items-center mb-2">
19
+ <div class="text-xl font-bold mr-2">
20
+ <%= product_reviews.average(:rating).to_f.round(1) %>
21
+ </div>
22
+ <%
23
+ star_color = section&.preferred_rating_color || '#FFA500'
24
+ star_size = section&.preferred_review_font_size || 14
25
+ %>
26
+ <div class="flex gap-0.5" style="color:<%= star_color %>; font-size:<%= star_size %>px;">
27
+ <% full_stars = product_reviews.average(:rating).to_f.floor %>
28
+ <% half_star = (product_reviews.average(:rating).to_f - full_stars) >= 0.5 %>
29
+ <% full_stars.times do %> <span>★</span> <% end %>
30
+ <% if half_star %> <span>★</span> <% end %>
31
+ <% (5 - full_stars - (half_star ? 1 : 0)).times do %>
32
+ <span class="text-gray-300">☆</span>
33
+ <% end %>
34
+ </div>
35
+ <div class="ml-2 text-gray-600">
36
+ (<%= product_reviews.count %> <%= 'review'.pluralize(product_reviews.count) %>)
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="reviews-list grid gap-3">
42
+ <% product_reviews.each do |review| %>
43
+ <div class="review-item border p-4 border-gray-200 py-4 rounded-md">
44
+ <div class="flex justify-between items-start mb-2">
45
+ <div>
46
+ <h3 class="font-medium" style="font-size:<%= section&.preferred_title_font_size || 18 %>px;"><%= review.title %></h3>
47
+ <%
48
+ review_star_color = section&.preferred_review_star_color || '#FFA500'
49
+ %>
50
+ <div style="color:<%= review_star_color %>;">
51
+ <%= '★' * review.rating %>
52
+ <%= '☆' * (5 - review.rating) %>
53
+ </div>
54
+ </div>
55
+ <div class="text-sm text-gray-500">
56
+ <%= l(review.created_at.to_date, format: :long) %>
57
+ </div>
58
+ </div>
59
+
60
+ <div
61
+ class="review-content my-2"
62
+ style="font-size:<%= section&.preferred_review_font_size || 14 %>px;">
63
+ <p><%= review.review %></p>
64
+ </div>
65
+
66
+ <%= render 'spree/product_reviews/images', review: review, section: section %>
67
+
68
+ <% if review.show_identifier && review.user %>
69
+ <div class="review-author text-sm text-gray-600" style="display: flex;align-items: flex-end;height: 35px;column-gap: 5px;">
70
+ <%= Spree.t(:by) %> <span style="font-weight: 600;"><%= review.user.email.split('@').first %></span>
71
+ <% if review.purchase_date.present? %>
72
+ <div class="inline-flex items-center gap-1 mt-1" style="color:<%= badge_color %>;">
73
+ <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4 text-green-600 icon icon-tabler icons-tabler-filled icon-tabler-rosette-discount-check" >
74
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
75
+ <path d="M12.01 2.011a3.2 3.2 0 0 1 2.113 .797l.154 .145l.698 .698a1.2 1.2 0 0 0 .71 .341l.135 .008h1a3.2 3.2 0 0 1 3.195 3.018l.005 .182v1c0 .27 .092 .533 .258 .743l.09 .1l.697 .698a3.2 3.2 0 0 1 .147 4.382l-.145 .154l-.698 .698a1.2 1.2 0 0 0 -.341 .71l-.008 .135v1a3.2 3.2 0 0 1 -3.018 3.195l-.182 .005h-1a1.2 1.2 0 0 0 -.743 .258l-.1 .09l-.698 .697a3.2 3.2 0 0 1 -4.382 .147l-.154 -.145l-.698 -.698a1.2 1.2 0 0 0 -.71 -.341l-.135 -.008h-1a3.2 3.2 0 0 1 -3.195 -3.018l-.005 -.182v-1a1.2 1.2 0 0 0 -.258 -.743l-.09 -.1l-.697 -.698a3.2 3.2 0 0 1 -.147 -4.382l.145 -.154l.698 -.698a1.2 1.2 0 0 0 .341 -.71l.008 -.135v-1l.005 -.182a3.2 3.2 0 0 1 3.013 -3.013l.182 -.005h1a1.2 1.2 0 0 0 .743 -.258l.1 -.09l.698 -.697a3.2 3.2 0 0 1 2.269 -.944zm3.697 7.282a1 1 0 0 0 -1.414 0l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.32 1.497l2 2l.094 .083a1 1 0 0 0 1.32 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
76
+ </svg>
77
+ <span class="text-sm font-medium">Verified purchase</span>
78
+ </div>
79
+ <% end %>
80
+ </div>
81
+ <% end %>
82
+ </div>
83
+ <% end %>
84
+ </div>
85
+ <% end %>
86
+ <% else %>
87
+ <div class="p-6 rounded-md text-center">
88
+ <svg width="80pt" height="80pt" id="Flat" viewBox="0 0 62 62" xmlns="http://www.w3.org/2000/svg" class="mx-auto py-3"><path d="m16 26c0 12.15 9.85 22 22 22 2.23 0 4.38-.33 6.41-.95.73.59 1.6 1.15 2.61 1.6 3.7 1.66 7.04.95 8.04.7-1.1-1.13-2.67-3.04-3.79-5.8 5.3-4.01 8.73-10.38 8.73-17.55 0-12.15-9.85-22-22-22s-22 9.85-22 22z" fill="#b8c3d5"/><path d="m50 24h-24c-.55 0-1-.45-1-1s.45-1 1-1h24c.55 0 1 .45 1 1s-.45 1-1 1z" fill="#325068"/><path d="m50 18h-24c-.55 0-1-.45-1-1s.45-1 1-1h24c.55 0 1 .45 1 1s-.45 1-1 1z" fill="#325068"/><path d="m50 30h-24c-.55 0-1-.45-1-1s.45-1 1-1h24c.55 0 1 .45 1 1s-.45 1-1 1z" fill="#325068"/><path d="m50 36h-24c-.55 0-1-.45-1-1s.45-1 1-1h24c.55 0 1 .45 1 1s-.45 1-1 1z" fill="#325068"/><path d="m48 34c0 12.15-9.85 22-22 22-2.23 0-4.38-.33-6.41-.95-.73.59-1.6 1.15-2.61 1.6-3.7 1.66-7.04.95-8.04.7 1.1-1.13 2.67-3.04 3.79-5.8-5.3-4.01-8.73-10.38-8.73-17.55 0-12.15 9.85-22 22-22s22 9.85 22 22z" fill="#d3dce5"/><g fill="#536882"><path d="m19 32.78c-.26 0-.51-.1-.71-.29-.39-.39-.39-1.02 0-1.41l3.66-3.66c.39-.39 1.02-.39 1.41 0s.39 1.02 0 1.41l-3.66 3.66c-.2.2-.45.29-.71.29z"/><path d="m22.66 32.78c-.26 0-.51-.1-.71-.29l-3.66-3.66c-.39-.39-.39-1.02 0-1.41s1.02-.39 1.41 0l3.66 3.66c.39.39.39 1.02 0 1.41-.2.2-.45.29-.71.29z"/><path d="m29.34 32.78c-.26 0-.51-.1-.71-.29-.39-.39-.39-1.02 0-1.41l3.66-3.66c.39-.39 1.02-.39 1.41 0s.39 1.02 0 1.41l-3.66 3.66c-.2.2-.45.29-.71.29z"/><path d="m33 32.78c-.26 0-.51-.1-.71-.29l-3.66-3.66c-.39-.39-.39-1.02 0-1.41s1.02-.39 1.41 0l3.66 3.66c.39.39.39 1.02 0 1.41-.2.2-.45.29-.71.29z"/><path d="m32.47 42.59c-.36 0-.7-.19-.88-.53-1.1-2.07-3.24-3.35-5.58-3.35s-4.48 1.28-5.58 3.35c-.26.49-.87.67-1.35.41-.49-.26-.67-.87-.41-1.35 1.45-2.72 4.27-4.41 7.35-4.41s5.9 1.69 7.35 4.41c.26.49.08 1.09-.41 1.35-.15.08-.31.12-.47.12z"/></g></svg>
89
+ <div class="text-center text-gray-500 mt-6">
90
+ <p class="text-lg font-semibold"><%= Spree.t('no_reviews_yet') %></p>
91
+ <p class="mt-1 text-blue-600 text-sm"><%= Spree.t('be_the_first_to_write_a_review') %></p>
92
+ </div>
93
+ </div>
94
+ <% end %>
95
+ </div>