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
|
@@ -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>
|