spree_product_reviews 1.0.0 → 1.2.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 +4 -4
- data/app/controllers/spree/admin/product_reviews_controller.rb +15 -7
- data/app/controllers/spree/admin/review_settings_controller.rb +23 -0
- data/app/controllers/spree/admin/reviews_controller.rb +18 -0
- data/app/controllers/spree/product_reviews_controller.rb +40 -9
- data/app/models/spree_product_reviews/spree/store_decorator.rb +19 -0
- data/app/views/spree/admin/product_reviews/_product_review.html.erb +60 -52
- data/app/views/spree/admin/product_reviews/_table.html.erb +8 -2
- data/app/views/spree/admin/product_reviews/index.html.erb +83 -10
- data/app/views/spree/admin/review_settings/edit.html.erb +259 -0
- data/app/views/spree/admin/reviews/index.html.erb +16 -0
- data/app/views/spree/page_sections/_product_reviews.html.erb +9 -6
- data/app/views/spree/product_reviews/_form.html.erb +1 -3
- data/app/views/spree/product_reviews/new.html.erb +40 -0
- data/app/views/{spree → themes/default/spree}/products/_json_ld.html.erb +1 -1
- data/config/initializers/spree_product_reviews.rb +15 -7
- data/config/locales/en.yml +38 -6
- data/config/routes.rb +11 -3
- data/db/migrate/20250501173852_add_reviews.rb +1 -1
- data/db/migrate/20250501174531_add_review_info_to_products.rb +1 -1
- data/db/migrate/20260105145731_add_spam_to_spree_product_reviews.rb +5 -0
- data/lib/generators/spree_product_reviews/install/install_generator.rb +2 -3
- data/lib/spree_product_reviews/configuration.rb +5 -10
- data/lib/spree_product_reviews/engine.rb +1 -6
- data/lib/spree_product_reviews/version.rb +1 -1
- metadata +11 -8
- data/app/views/themes/default/spree/page_sections/_product_details.html.erb +0 -89
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<% content_for :page_title do %>
|
|
2
|
+
<%= Spree.t(:review_settings) %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-12 col-lg-6">
|
|
7
|
+
<div class="card mb-4">
|
|
8
|
+
<div class="card-header">
|
|
9
|
+
<h5 class="card-title mb-0"><%= Spree.t(:general_settings) %></h5>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="card-body">
|
|
12
|
+
<%= form_tag admin_review_settings_path, method: :put do %>
|
|
13
|
+
|
|
14
|
+
<div class="form-group mb-4">
|
|
15
|
+
<label class="font-weight-bold"><%= Spree.t(:auto_approve_reviews) %></label>
|
|
16
|
+
<%= select_tag :review_status_default,
|
|
17
|
+
options_for_select([
|
|
18
|
+
[Spree.t(:default_unapprove), 'pending'],
|
|
19
|
+
[Spree.t(:approve_automatically), 'approved']
|
|
20
|
+
], current_store.preferred_review_status_default),
|
|
21
|
+
class: 'custom-select' %>
|
|
22
|
+
<small class="form-text text-muted mt-2"><%= Spree.t(:auto_approve_help_text) %></small>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<hr>
|
|
26
|
+
|
|
27
|
+
<h6 class="mb-3 font-weight-bold">Content Moderation</h6>
|
|
28
|
+
|
|
29
|
+
<div class="form-group mb-2">
|
|
30
|
+
<div class="custom-control custom-switch">
|
|
31
|
+
<%= check_box_tag :block_spam_reviews, '1', current_store.preferred_block_spam_reviews, class: 'custom-control-input', id: 'block_spam_reviews', data: { action: "click->toggle-visibility#toggle" } %>
|
|
32
|
+
<label class="custom-control-label" for="block_spam_reviews">
|
|
33
|
+
Block Spam Reviews
|
|
34
|
+
</label>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="mt-3 pl-4 border-left">
|
|
38
|
+
<label class="small text-muted font-weight-bold">Spam Keywords (Comma separated)</label>
|
|
39
|
+
<%= text_area_tag :spam_words, current_store.preferred_spam_words, rows: 3, class: 'form-control', placeholder: "casino, viagra, free money..." %>
|
|
40
|
+
<small class="form-text text-muted mt-2">Reviews containing these words will be automatically marked as Spam.</small>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="form-group mb-2">
|
|
45
|
+
<div class="custom-control custom-switch">
|
|
46
|
+
<%= check_box_tag :disable_review_links, '1', current_store.preferred_disable_review_links, class: 'custom-control-input', id: 'disable_review_links' %>
|
|
47
|
+
<label class="custom-control-label" for="disable_review_links">
|
|
48
|
+
Auto-Disapprove Reviews containing Links (URLs)
|
|
49
|
+
</label>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="form-group mb-4">
|
|
54
|
+
<div class="custom-control custom-switch">
|
|
55
|
+
<%= check_box_tag :disable_review_emails, '1', current_store.preferred_disable_review_emails, class: 'custom-control-input', id: 'disable_review_emails' %>
|
|
56
|
+
<label class="custom-control-label" for="disable_review_emails">
|
|
57
|
+
Auto-Disapprove Reviews containing Email Addresses
|
|
58
|
+
</label>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="form-actions">
|
|
63
|
+
<%= button_tag Spree.t('actions.update'), class: 'btn btn-primary btn-block' %>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="col-12 col-lg-6">
|
|
71
|
+
<div class="card seo-form">
|
|
72
|
+
<div class="card-header">
|
|
73
|
+
<h5 class="card-title mb-0">SEO Review Snippet Preview</h5>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="card-body">
|
|
76
|
+
<p class="text-muted small mb-3">
|
|
77
|
+
See how your product reviews will appear in Google Search results. Select a product to generate a live preview.
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<div class="form-group">
|
|
81
|
+
<label>Select Product for Preview</label>
|
|
82
|
+
<%= select_tag "seo_preview_product_id",
|
|
83
|
+
options_from_collection_for_select(@products, "slug", "name"),
|
|
84
|
+
prompt: "Select a product...",
|
|
85
|
+
class: "custom-select select2",
|
|
86
|
+
id: "seo-product-select" %>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<%# --- GOOGLE PREVIEW BOX --- %>
|
|
90
|
+
<div id="seo-preview-container" class="mt-4 p-3 bg-white border rounded hidden" style="font-family: Arial, sans-serif; max-width: 600px;">
|
|
91
|
+
|
|
92
|
+
<%# Header: Favicon + Breadcrumb %>
|
|
93
|
+
<div class="d-flex align-items-center mb-3">
|
|
94
|
+
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center mr-2" style="width: 28px; height: 28px; overflow:hidden; border: 1px solid #f1f3f4;">
|
|
95
|
+
<% if current_store.favicon_image.attached? %>
|
|
96
|
+
<%= spree_image_tag(current_store.favicon_image, width: 16, height: 16, class: "object-cover") %>
|
|
97
|
+
<% else %>
|
|
98
|
+
<span style="font-size: 14px;">🛍️</span>
|
|
99
|
+
<% end %>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="d-flex flex-column" style="line-height: 1.3;">
|
|
102
|
+
<span style="font-size: 14px; color: #202124;"><%= current_store.name %></span>
|
|
103
|
+
<span style="font-size: 12px; color: #5f6368;" id="seo-url">
|
|
104
|
+
<%= current_store.url.sub(%r{^https?://}, '') %> › products › ...
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<%# Content Row (Text Left, Image Right) %>
|
|
110
|
+
<div class="d-flex">
|
|
111
|
+
<%# Left Side: Text %>
|
|
112
|
+
<div class="flex-grow-1 pr-3" style="min-width: 0;">
|
|
113
|
+
<div class="mb-1">
|
|
114
|
+
<a href="#" onclick="return false;" style="font-size: 20px; color: #1a0dab; text-decoration: none; display: block; line-height: 1.3;" id="seo-title">Product Name</a>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="d-flex align-items-center mb-1 flex-wrap" style="font-size: 14px; color: #4d5156;">
|
|
118
|
+
<span style="color: #e37400;">★★★★★</span>
|
|
119
|
+
<span class="ml-1" id="seo-rating">4.5</span>
|
|
120
|
+
<span class="mx-1">·</span>
|
|
121
|
+
<span id="seo-reviews">120 reviews</span>
|
|
122
|
+
<span class="mx-1">·</span>
|
|
123
|
+
<span style="font-weight: bold;" id="seo-price">$0.00</span>
|
|
124
|
+
<span class="mx-1">·</span>
|
|
125
|
+
<span style="color: #188038;" id="seo-stock">In stock</span>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div style="font-size: 14px; color: #4d5156; line-height: 1.58; word-wrap: break-word;" id="seo-desc">
|
|
129
|
+
Product description...
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<%# Right Side: Image %>
|
|
134
|
+
<div id="seo-image-wrapper" class="hidden ml-2" style="flex-shrink: 0;">
|
|
135
|
+
<div style="width: 104px; height: 104px; border-radius: 8px; border: 1px solid #dadce0; overflow: hidden;">
|
|
136
|
+
<img id="seo-image" src="" style="width: 100%; height: 100%; object-fit: cover;">
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div id="seo-loading" class="text-center py-4 hidden">
|
|
143
|
+
<div class="spinner-border text-primary" role="status"></div>
|
|
144
|
+
<p class="mt-2 text-muted">Fetching product data...</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<script>
|
|
153
|
+
document.addEventListener("turbo:load", function() {
|
|
154
|
+
const select = document.getElementById("seo-product-select");
|
|
155
|
+
const container = document.getElementById("seo-preview-container");
|
|
156
|
+
const loading = document.getElementById("seo-loading");
|
|
157
|
+
|
|
158
|
+
// UI Elements
|
|
159
|
+
const elTitle = document.getElementById("seo-title");
|
|
160
|
+
const elDesc = document.getElementById("seo-desc");
|
|
161
|
+
const elPrice = document.getElementById("seo-price");
|
|
162
|
+
const elStock = document.getElementById("seo-stock");
|
|
163
|
+
const elRating = document.getElementById("seo-rating");
|
|
164
|
+
const elReviews = document.getElementById("seo-reviews");
|
|
165
|
+
const elUrl = document.getElementById("seo-url");
|
|
166
|
+
const elImageWrapper = document.getElementById("seo-image-wrapper");
|
|
167
|
+
const elImage = document.getElementById("seo-image");
|
|
168
|
+
|
|
169
|
+
const storeUrl = "<%= current_store.url.sub(%r{^https?://}, '') %>";
|
|
170
|
+
|
|
171
|
+
if (!select) return;
|
|
172
|
+
|
|
173
|
+
select.addEventListener("change", function(e) {
|
|
174
|
+
const slug = e.target.value;
|
|
175
|
+
|
|
176
|
+
if (!slug || slug === "") {
|
|
177
|
+
container.classList.add("hidden");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
container.classList.add("hidden");
|
|
182
|
+
loading.classList.remove("hidden");
|
|
183
|
+
|
|
184
|
+
// Fetch product with images included
|
|
185
|
+
fetch(`/api/v2/storefront/products/${slug}?include=images,default_variant`)
|
|
186
|
+
.then(response => {
|
|
187
|
+
if (!response.ok) throw new Error("API Not Found");
|
|
188
|
+
return response.json();
|
|
189
|
+
})
|
|
190
|
+
.then(data => {
|
|
191
|
+
const p = data.data.attributes;
|
|
192
|
+
|
|
193
|
+
// 1. Text Data
|
|
194
|
+
elTitle.textContent = p.name;
|
|
195
|
+
|
|
196
|
+
// FIX: Decode HTML Entities (e.g. “ -> “) using DOMParser
|
|
197
|
+
let rawDesc = p.description || "No description available.";
|
|
198
|
+
// Step A: Strip Tags
|
|
199
|
+
let noTags = rawDesc.replace(/<[^>]*>?/gm, '');
|
|
200
|
+
// Step B: Decode Entities
|
|
201
|
+
const parser = new DOMParser();
|
|
202
|
+
const decodedDesc = parser.parseFromString(`<!doctype html><body>${noTags}`, 'text/html').body.textContent;
|
|
203
|
+
|
|
204
|
+
// Step C: Truncate
|
|
205
|
+
elDesc.textContent = decodedDesc.substring(0, 155) + (decodedDesc.length > 155 ? "..." : "");
|
|
206
|
+
|
|
207
|
+
elPrice.textContent = p.display_price;
|
|
208
|
+
elUrl.textContent = `${storeUrl} › products › ${p.slug}`;
|
|
209
|
+
|
|
210
|
+
// 2. Mock Ratings (Randomized for preview)
|
|
211
|
+
const rating = (Math.random() * (5.0 - 4.0) + 4.0).toFixed(1);
|
|
212
|
+
const count = Math.floor(Math.random() * 50) + 1;
|
|
213
|
+
elRating.textContent = rating;
|
|
214
|
+
elReviews.textContent = `${count} reviews`;
|
|
215
|
+
elStock.textContent = p.purchasable ? "In stock" : "Out of stock";
|
|
216
|
+
elStock.style.color = p.purchasable ? "#188038" : "#d93025";
|
|
217
|
+
|
|
218
|
+
// 3. Image Logic
|
|
219
|
+
elImageWrapper.classList.add("hidden");
|
|
220
|
+
let foundImage = null;
|
|
221
|
+
|
|
222
|
+
if (data.included) {
|
|
223
|
+
// Find first Image object
|
|
224
|
+
const imgData = data.included.find(x => x.type === 'image');
|
|
225
|
+
|
|
226
|
+
if (imgData && imgData.attributes) {
|
|
227
|
+
// Priority 1: Check for styles (Standard Spree API response)
|
|
228
|
+
if (imgData.attributes.styles) {
|
|
229
|
+
const small = imgData.attributes.styles.find(s => s.width == 240 || s.width == 100);
|
|
230
|
+
foundImage = small ? small.url : imgData.attributes.original_url;
|
|
231
|
+
} else {
|
|
232
|
+
// Priority 2: Fallback to original
|
|
233
|
+
foundImage = imgData.attributes.original_url;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (foundImage) {
|
|
239
|
+
elImage.src = foundImage;
|
|
240
|
+
elImageWrapper.classList.remove("hidden");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
loading.classList.add("hidden");
|
|
244
|
+
container.classList.remove("hidden");
|
|
245
|
+
})
|
|
246
|
+
.catch(err => {
|
|
247
|
+
console.error(err);
|
|
248
|
+
loading.classList.add("hidden");
|
|
249
|
+
alert("Could not load product details.");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<style>
|
|
256
|
+
.hidden { display: none !important; }
|
|
257
|
+
/* Ensure the select2 looks correct */
|
|
258
|
+
.select2-container { width: 100% !important; }
|
|
259
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% content_for :page_title do %>
|
|
2
|
+
<%= Spree.t(:product_reviews) %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<div class="card p-0">
|
|
6
|
+
<div class="card-body p-0">
|
|
7
|
+
<div class="table-responsive">
|
|
8
|
+
<%# We pass global_view: true implicitly via the partial logic because @product is nil in this controller %>
|
|
9
|
+
<%= render "spree/admin/product_reviews/table" %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="p-3">
|
|
12
|
+
<%# Removed 'theme' argument to let Spree use its default pagination style %>
|
|
13
|
+
<%= paginate @product_reviews %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
@@ -84,12 +84,15 @@
|
|
|
84
84
|
</div>
|
|
85
85
|
<% end %>
|
|
86
86
|
<% else %>
|
|
87
|
-
<div class="p-6 rounded-md text-center">
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
87
|
+
<div class="p-6 rounded-md text-center flex flex-col items-center">
|
|
88
|
+
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M38.813 9.20323L13.2319 34.7843C14.1962 33.82 15.7597 33.82 16.724 34.7843C17.6883 35.7486 17.6883 37.3121 16.724 38.2764L25.3177 29.6826C26.282 28.7183 27.8455 28.7183 28.8098 29.6826C29.7741 30.6469 29.7741 32.2104 28.8098 33.1747L45.8882 16.0963L38.813 9.20323Z" fill="#FFF5D7"></path><path d="M34.9385 36.4658C35.7811 35.6232 37.1473 35.6232 37.9895 36.4658C38.8124 37.2882 38.8254 38.6063 38.0415 39.4526L35.912 41.586C36.7542 40.7434 38.1204 40.7434 38.963 41.586C39.7905 42.413 39.8006 43.7424 39.0029 44.5879L51.7261 31.8647L45.9878 25.3988L34.9385 36.4658Z" fill="#FFF5D7"></path><path d="M8.16602 18.0595C9.00861 17.217 10.3745 17.217 11.217 18.0595C12.0399 18.882 12.0529 20.2 11.2691 21.0464L9.13911 23.1793C9.9817 22.3371 11.348 22.3371 12.1901 23.1793C13.0176 24.0068 13.0277 25.3361 12.2304 26.1816L24.9536 13.4584L19.2149 6.99257L8.16602 18.0595Z" fill="#FFF5D7"></path><path d="M40.237 1.69043L41.0145 6.3679C41.0775 6.74597 40.8865 7.12069 40.5437 7.29189L36.3026 9.4118C35.0379 10.0442 35.3367 11.9278 36.7344 12.138L41.4232 12.8434C41.8021 12.9005 42.0996 13.198 42.1563 13.5769L42.4941 15.8189L42.915 18.1365C43.1252 19.5346 44.682 18.9178 45.3139 17.6531L47.4447 14.1262C47.6159 13.7834 48.2839 13.365 48.662 13.4279L53.1737 14.529C54.5681 14.7611 54.6642 13.856 53.6571 12.864L50.6493 9.26535C50.3762 8.99638 50.1235 7.64479 50.3002 7.3049L52.8783 3.75998C53.5296 2.50532 52.6555 2.19523 51.4013 2.84689L49.4715 2.71933L47.0473 3.97818C46.707 4.15484 46.2916 4.08896 46.0231 3.81621L42.6959 0.437454C41.7044 -0.570046 40.0049 0.295624 40.237 1.69043Z" fill="#FFE07D"></path><path d="M39.7229 16.1499C39.7229 16.3731 39.6377 16.5968 39.4674 16.7671L1.49135 54.7432C1.1502 55.0843 0.597565 55.0843 0.256837 54.7432C-0.0843103 54.402 -0.0843103 53.8498 0.256837 53.5087L38.2329 15.5326C38.574 15.1915 39.1262 15.1915 39.4674 15.5326C39.6377 15.703 39.7229 15.9266 39.7229 16.1499Z" fill="#FFEAA8"></path><path d="M34.0476 14.8407C34.0476 15.0639 33.9624 15.2876 33.7916 15.4579L10.6569 38.5931C10.3157 38.9338 9.76351 38.9338 9.42237 38.5931C9.08122 38.2519 9.08122 37.6993 9.42237 37.3586L32.5575 14.2234C32.8982 13.8823 33.4509 13.8823 33.7916 14.2234C33.9624 14.3938 34.0476 14.6174 34.0476 14.8407Z" fill="#FFEAA8"></path><path d="M17.4601 15.7144C17.4601 15.9376 17.375 16.1609 17.2046 16.3317L1.49037 32.0459C1.14922 32.387 0.59659 32.387 0.255861 32.0459C-0.0852871 31.7047 -0.0852871 31.1525 0.255861 30.8114L15.9701 15.0972C16.3112 14.756 16.8634 14.756 17.2046 15.0972C17.375 15.2675 17.4601 15.4908 17.4601 15.7144Z" fill="#FFEAA8"></path><path d="M12.6584 14.4043C12.6584 14.6275 12.5733 14.8512 12.4029 15.0216L7.60121 19.8232C7.26007 20.1644 6.70785 20.1644 6.3667 19.8232C6.02555 19.4821 6.02555 18.9299 6.3667 18.5887L11.1684 13.7871C11.5095 13.4459 12.0617 13.4459 12.4029 13.7871C12.5733 13.9574 12.6584 14.1811 12.6584 14.4043Z" fill="#FFEAA8"></path><path d="M19.2063 20.0791C19.2063 20.3024 19.1211 20.526 18.9507 20.6964L11.0934 28.5537C10.7523 28.8949 10.2 28.8949 9.85889 28.5537C9.51774 28.2126 9.51774 27.6603 9.85889 27.3192L17.7162 19.4619C18.0574 19.1207 18.6096 19.1207 18.9507 19.4619C19.1211 19.6327 19.2063 19.8559 19.2063 20.0791Z" fill="#FFEAA8"></path><path d="M45.117 24.603L45.6562 27.8458L42.7159 29.3157C41.9443 29.7013 42.1264 30.8511 42.9795 30.9795L46.2298 31.4683L46.4019 32.6101L46.8714 34.5722C46.9998 35.4253 47.8285 35.3083 48.2142 34.5366L49.8435 31.7256L52.9768 32.227C53.8278 32.3684 54.0389 31.8116 53.4246 31.2065L51.3475 28.8432L52.7263 25.7993C53.1237 25.0339 52.7041 24.5782 51.9383 24.976L50.5749 25.3235L48.9246 26.1808L46.618 23.8385C46.0124 23.2237 44.9756 23.752 45.117 24.603Z" fill="#FFE07D"></path><path d="M41.0317 21.8255C41.0317 22.0492 40.9465 22.2724 40.7761 22.4428L25.9347 37.2842C25.5936 37.6253 25.0413 37.6253 24.7002 37.2842C24.359 36.943 24.359 36.3908 24.7002 36.0497L39.5416 21.2083C39.8824 20.8675 40.435 20.8675 40.7761 21.2083C40.9465 21.379 41.0317 21.6023 41.0317 21.8255Z" fill="#FFEAA8"></path><path d="M49.4719 2.71848C49.6557 3.53001 49.7618 4.37051 49.7618 5.23785C49.7618 10.0651 46.7427 14.1778 42.4937 15.8181L42.8621 18.2653C43.0723 19.663 44.9564 19.9613 45.5883 18.697L47.7082 14.4556C47.8794 14.1132 48.2546 13.9218 48.6322 13.9848L53.3101 14.7627C54.7045 14.9944 55.5702 13.2949 54.5631 12.3034L51.1843 8.97663C50.9112 8.70766 50.8457 8.29224 51.0219 7.95193L53.2073 3.7436C53.859 2.48937 52.5103 1.14072 51.2557 1.79196L49.4719 2.71848Z" fill="#FFD064"></path><path d="M51.8418 24.6651L50.5754 25.3226C50.6106 25.6076 50.6349 25.8954 50.6349 26.19C50.6349 29.0698 48.8906 31.5414 46.4019 32.6093L46.7195 34.7183C46.8475 35.5714 47.9972 35.7535 48.3829 34.9818L49.8528 32.0416L53.0956 32.5808C53.9466 32.7222 54.4749 31.6853 53.8601 31.0798L51.5178 28.7736L53.0326 25.856C53.4304 25.0906 52.6071 24.2673 51.8418 24.6651Z" fill="#FFD064"></path><path d="M18.35 6.08718L18.8892 9.32997L15.949 10.7999C15.1773 11.1855 15.359 12.3349 16.2125 12.4633L19.4629 12.9525L19.6345 14.0943L20.0612 16.0862C20.1896 16.9393 21.1258 16.8848 21.5114 16.1131L22.9885 13.2664L26.2648 13.8304C27.1158 13.9718 27.363 13.3075 26.7482 12.7024L24.385 10.4734L25.9963 7.43876C26.3941 6.67296 25.849 5.9936 25.0832 6.3914L23.8088 6.80724L22.1572 7.66493L19.8506 5.32263C19.2455 4.70789 18.2082 5.23619 18.35 6.08718Z" fill="#FFE07D"></path><path d="M25.0743 6.15047L23.8083 6.80801C23.8435 7.09251 23.8674 7.38078 23.8674 7.67535C23.8674 10.5552 22.1235 13.0267 19.6348 14.0946L19.952 16.2036C20.0804 17.0567 21.2301 17.2388 21.6158 16.4672L23.0857 13.5269L26.3285 14.0661C27.1795 14.2075 27.7078 13.1702 27.093 12.5651L24.7507 10.2585L26.2655 7.34134C26.6629 6.57596 25.8401 5.75267 25.0743 6.15047Z" fill="#FFD064"></path><path d="M44.1579 34.232C44.1579 34.4556 44.0727 34.6789 43.9023 34.8492L28.1881 50.5635C27.847 50.9046 27.2948 50.9046 26.9536 50.5635C26.6125 50.2227 26.6125 49.6701 26.9536 49.3294L42.6678 33.6147C43.009 33.274 43.5612 33.274 43.9023 33.6147C44.0727 33.7855 44.1579 34.0088 44.1579 34.232Z" fill="#FFEAA8"></path><path d="M39.3568 32.9233C39.3568 33.1465 39.2712 33.3698 39.1008 33.5405L34.2991 38.3418C33.9584 38.6829 33.4058 38.6829 33.0646 38.3418C32.7239 38.0011 32.7239 37.4484 33.0646 37.1073L37.8663 32.306C38.2075 31.9649 38.7597 31.9649 39.1008 32.306C39.2716 32.4764 39.3568 32.6996 39.3568 32.9233Z" fill="#FFEAA8"></path><path d="M45.9041 38.5971C45.9041 38.8204 45.8185 39.044 45.6482 39.2144L37.7908 47.0713C37.4501 47.4124 36.8975 47.4124 36.5563 47.0713C36.2156 46.7306 36.2156 46.1779 36.5563 45.8368L44.4137 37.9799C44.7548 37.6387 45.307 37.6387 45.6482 37.9799C45.8185 38.1502 45.9041 38.3739 45.9041 38.5971Z" fill="#FFEAA8"></path></g><defs><clipPath id="clip0"><rect width="55" height="55" fill="white" transform="translate(0 55) rotate(-90)"></rect></clipPath></defs></svg>
|
|
89
|
+
|
|
90
|
+
<div class="text-center mt-6 max-w-md">
|
|
91
|
+
<h3 class="text-xl font-bold text-gray-900 mb-2">Looking for stars!</h3>
|
|
92
|
+
<p class="text-gray-500 mb-8">We want to know how you're styling yourselves</p>
|
|
93
|
+
|
|
94
|
+
<%= link_to "Be the first to write a review!", "/user/sign_in", class: "inline-block bg-black text-white font-bold py-3 px-8 rounded hover:bg-gray-800 transition-colors uppercase text-sm tracking-wide" %>
|
|
92
95
|
</div>
|
|
93
96
|
</div>
|
|
94
97
|
<% end %>
|
|
95
|
-
</div>
|
|
98
|
+
</div>
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<div class="review-form-container mb-8 p-4 border border-gray-200 rounded-md shadow-sm bg-white">
|
|
10
|
-
|
|
11
|
-
url: spree.product_product_reviews_path(@product),
|
|
12
|
-
html: { multipart: true, data: { turbo: false } } do |f| %>
|
|
10
|
+
<%= form_for [:product, product_review], url: spree.product_product_reviews_path(product) do |f| %>
|
|
13
11
|
<% if product_review.errors.any? %>
|
|
14
12
|
<div class="error-messages bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
15
13
|
<h2 class="font-bold mb-2"><%= pluralize(product_review.errors.count, "error") %> prohibited this review from being saved:</h2>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<div class="container py-5">
|
|
2
|
+
<div class="row justify-content-center">
|
|
3
|
+
<div class="col-12 col-lg-8">
|
|
4
|
+
|
|
5
|
+
<%# --- Header --- %>
|
|
6
|
+
<div class="text-center mb-5">
|
|
7
|
+
<h1 class="h2 font-weight-bold mb-3"><%= Spree.t(:new_product_review) %></h1>
|
|
8
|
+
<p class="text-muted">
|
|
9
|
+
<%= Spree.t(:for) %>
|
|
10
|
+
<strong><%= link_to @product.name, spree.product_path(@product), class: "text-dark" %></strong>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<%# --- Errors Display --- %>
|
|
15
|
+
<% if @product_review.errors.any? %>
|
|
16
|
+
<div class="alert alert-danger mb-4">
|
|
17
|
+
<h4 class="alert-heading h6"><%= Spree.t(:error_saving_review) %></h4>
|
|
18
|
+
<ul class="mb-0 pl-3">
|
|
19
|
+
<% @product_review.errors.full_messages.each do |msg| %>
|
|
20
|
+
<li><%= msg %></li>
|
|
21
|
+
<% end %>
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<%# --- The Form --- %>
|
|
27
|
+
<%# We reuse the existing form partial. We pass a nil section so defaults are used. %>
|
|
28
|
+
<%= render 'form',
|
|
29
|
+
product: @product,
|
|
30
|
+
product_review: @product_review,
|
|
31
|
+
block: Spree::PageBlocks::ProductReviewForm.new,
|
|
32
|
+
section: nil %>
|
|
33
|
+
|
|
34
|
+
<div class="text-center mt-4">
|
|
35
|
+
<%= link_to Spree.t(:back_to_product), spree.product_path(@product), class: "text-muted" %>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
Rails.application.config.after_initialize do
|
|
2
|
-
Rails.application.config.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Rails.application.config.spree.page_sections << Spree::PageSections::AddAReview
|
|
7
|
-
Rails.application.config.spree.page_blocks << Spree::PageBlocks::ProductReviewForm
|
|
8
|
-
end
|
|
2
|
+
if Rails.application.config.respond_to?(:spree)
|
|
3
|
+
Rails.application.config.spree.page_sections << Spree::PageSections::AddAReview
|
|
4
|
+
Rails.application.config.spree.page_blocks << Spree::PageBlocks::ProductReviewForm
|
|
5
|
+
end
|
|
9
6
|
|
|
7
|
+
if Spree.respond_to?(:admin) && Spree.admin.respond_to?(:navigation)
|
|
8
|
+
sidebar = Spree.admin.navigation.sidebar
|
|
9
|
+
|
|
10
|
+
sidebar.add :reviews, label: :reviews, icon: 'star', url: :admin_reviews_path, position: 45, if: -> { can?(:manage, Spree::ProductReview) }
|
|
11
|
+
sidebar.add :all_reviews, parent: :reviews, label: :product_reviews, url: :admin_reviews_path, active: -> { controller_name == 'reviews' }
|
|
12
|
+
sidebar.add :review_settings, parent: :reviews, label: :review_settings, url: :edit_admin_review_settings_path, active: -> { controller_name == 'review_settings' }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Spree::Ability.register_ability(Spree::ProductReviewsAbility)
|
|
16
|
+
Rails.application.config.spree_admin.product_dropdown_partials << "spree_product_reviews/admin/product_reviews_dropdown"
|
|
17
|
+
end
|
data/config/locales/en.yml
CHANGED
|
@@ -3,39 +3,71 @@
|
|
|
3
3
|
|
|
4
4
|
en:
|
|
5
5
|
spree:
|
|
6
|
-
|
|
7
|
-
review_disapproved: "Review disapproved."
|
|
8
|
-
approve: "Approve"
|
|
9
|
-
approve: "Approve"
|
|
6
|
+
# General
|
|
10
7
|
product_reviews: "Product Reviews"
|
|
11
|
-
|
|
8
|
+
reviews: "Reviews"
|
|
9
|
+
customer_reviews: "Customer Reviews"
|
|
10
|
+
review_settings: "Review Settings"
|
|
11
|
+
|
|
12
|
+
# Action Buttons
|
|
13
|
+
approve: "Approve"
|
|
14
|
+
disapprove: "Disapprove"
|
|
15
|
+
|
|
16
|
+
# Model Attributes & Columns
|
|
12
17
|
review_title: "Review Title"
|
|
13
18
|
review_images: "Review Images"
|
|
14
19
|
product_review_rating: "Rating"
|
|
20
|
+
date: "Date"
|
|
21
|
+
status: "Status"
|
|
22
|
+
product: "Product"
|
|
23
|
+
email: "Email"
|
|
24
|
+
|
|
25
|
+
# Form / Frontend
|
|
26
|
+
new_product_review: "Leave a new product review"
|
|
15
27
|
add_a_review_heading: "Leave a Review"
|
|
16
28
|
thanks_for_review_heading: "Thank you for your review! Your Review is Approved!"
|
|
17
29
|
thanks_for_review_heading_pending: "Thank you for your review! Your Review is waiting for Approval!"
|
|
18
30
|
add_a_review_text: "We value your feedback! Please leave a review for this product."
|
|
31
|
+
|
|
32
|
+
# Settings Page
|
|
33
|
+
auto_approve_reviews: "Auto Approve Reviews"
|
|
34
|
+
default_unapprove: "Default (Unapproved/Pending)"
|
|
35
|
+
approve_automatically: "Approve Automatically"
|
|
36
|
+
auto_approve_help_text: "Determine if new reviews should be published immediately or require admin approval."
|
|
37
|
+
|
|
38
|
+
# Flash Messages
|
|
39
|
+
review_approved: "Review approved successfully."
|
|
40
|
+
review_disapproved: "Review disapproved."
|
|
41
|
+
review_deleted: "Review deleted."
|
|
42
|
+
please_log_in: "Please log in to write a review."
|
|
43
|
+
successfully_updated: "%{resource} updated successfully"
|
|
44
|
+
|
|
19
45
|
product_review:
|
|
20
46
|
flash_messages:
|
|
21
47
|
create:
|
|
22
48
|
success: "Your review has been submitted and is pending approval."
|
|
23
49
|
error: "There was an error submitting your review. Please try again."
|
|
50
|
+
approved: "Your review has been submitted! Thank you!"
|
|
24
51
|
failure: "There was an error submitting your review. Please try again."
|
|
25
|
-
approve: "Review approved successfully."
|
|
26
52
|
update:
|
|
27
53
|
success: "Your review has been updated."
|
|
28
54
|
error: "There was an error updating your review. Please try again."
|
|
29
55
|
failure: "There was an error updating your review. Please try again."
|
|
56
|
+
|
|
57
|
+
# Page Builder Blocks
|
|
30
58
|
page_blocks:
|
|
31
59
|
reviews_summary: "Reviews"
|
|
32
60
|
product_review_form:
|
|
33
61
|
button_text_default: "Add your review"
|
|
34
62
|
placeholder_default: "Write your review here..."
|
|
63
|
+
|
|
64
|
+
# Page Builder Sections
|
|
35
65
|
page_sections:
|
|
36
66
|
product_reviews:
|
|
37
67
|
reviewed_by: "Reviewed by:"
|
|
38
68
|
pending_product_review_instructions: "Thanks for your review! It is currently pending approval."
|
|
69
|
+
|
|
70
|
+
# Admin UI Descriptions
|
|
39
71
|
admin:
|
|
40
72
|
page_sections:
|
|
41
73
|
product_reviews:
|
data/config/routes.rb
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
Spree::Core::Engine.add_routes do
|
|
2
2
|
namespace :admin do
|
|
3
|
+
# Existing nested resources
|
|
3
4
|
resources :products do
|
|
4
5
|
resources :product_reviews, only: %i[index destroy edit update] do
|
|
5
6
|
member do
|
|
6
7
|
get :approve
|
|
7
8
|
get :disapprove
|
|
8
|
-
post :attach_image
|
|
9
|
-
delete :purge_images
|
|
9
|
+
post :attach_image
|
|
10
|
+
delete :purge_images
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
end
|
|
14
|
+
|
|
15
|
+
# NEW: Global Reviews Controller (not nested)
|
|
16
|
+
resources :reviews, only: [:index]
|
|
17
|
+
|
|
18
|
+
# NEW: Review Settings
|
|
19
|
+
resource :review_settings, only: [:edit, :update]
|
|
13
20
|
end
|
|
14
21
|
|
|
22
|
+
# Frontend routes remain the same...
|
|
15
23
|
resources :products, only: [] do
|
|
16
24
|
resources :product_reviews, only: %i[index new create]
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
get '/forbidden', to: 'spree/home#forbidden', as: :product_reviews_forbidden
|
|
20
|
-
end
|
|
28
|
+
end
|
|
@@ -24,4 +24,4 @@ class AddReviews < ActiveRecord::Migration[7.2]
|
|
|
24
24
|
add_index :spree_product_reviews, :product_id, name: "index_reviews_on_product_id" unless index_exists?(:spree_product_reviews, :product_id, name: "index_reviews_on_product_id")
|
|
25
25
|
add_index :spree_product_reviews, :variant_id, name: "index_reviews_on_variant_id" unless index_exists?(:spree_product_reviews, :variant_id, name: "index_reviews_on_variant_id")
|
|
26
26
|
end
|
|
27
|
-
end
|
|
27
|
+
end
|
|
@@ -22,10 +22,9 @@ module SpreeProductReviews
|
|
|
22
22
|
def add_reviews_page_block
|
|
23
23
|
say_status :spree_product_reviews, "Adding Reviews block to Product Details sections", :green
|
|
24
24
|
|
|
25
|
-
# Load app environment explicitly (important for generators)
|
|
26
25
|
require Rails.root.join("config/environment")
|
|
27
26
|
|
|
28
|
-
Spree::PageSection
|
|
27
|
+
::Spree::PageSection
|
|
29
28
|
.where(type: "Spree::PageSections::ProductDetails")
|
|
30
29
|
.find_each do |section|
|
|
31
30
|
|
|
@@ -41,4 +40,4 @@ module SpreeProductReviews
|
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
end
|
|
44
|
-
end
|
|
43
|
+
end
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
module SpreeProductReviews
|
|
2
2
|
class Configuration < Spree::Preferences::Configuration
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# preference :enabled, :boolean, default: true
|
|
8
|
-
# preference :dark_chocolate, :boolean, default: true
|
|
9
|
-
# preference :color, :string, default: 'Red'
|
|
10
|
-
# preference :favorite_number, :integer
|
|
11
|
-
# preference :supported_locales, :array, default: [:en]
|
|
3
|
+
preference :review_status_default, :string, default: 'pending'
|
|
4
|
+
preference :block_spam_reviews, :boolean, default: false
|
|
5
|
+
preference :disable_review_links, :boolean, default: false
|
|
6
|
+
preference :disable_review_emails, :boolean, default: false
|
|
12
7
|
end
|
|
13
|
-
end
|
|
8
|
+
end
|
|
@@ -4,15 +4,10 @@ module SpreeProductReviews
|
|
|
4
4
|
isolate_namespace Spree
|
|
5
5
|
engine_name 'spree_product_reviews'
|
|
6
6
|
|
|
7
|
-
# use rspec for tests
|
|
8
7
|
config.generators do |g|
|
|
9
8
|
g.test_framework :rspec
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
initializer 'spree_product_reviews.environment', before: :load_config_initializers do |_app|
|
|
13
|
-
SpreeProductReviews::Config = SpreeProductReviews::Configuration.new
|
|
14
|
-
end
|
|
15
|
-
|
|
16
11
|
def self.activate
|
|
17
12
|
Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
|
|
18
13
|
Rails.configuration.cache_classes ? require(c) : load(c)
|
|
@@ -21,4 +16,4 @@ module SpreeProductReviews
|
|
|
21
16
|
|
|
22
17
|
config.to_prepare(&method(:activate).to_proc)
|
|
23
18
|
end
|
|
24
|
-
end
|
|
19
|
+
end
|