feat: implement customer reviews liquid block with singleton pattern and verified buyer check

This commit is contained in:
Divya Pahuja 2026-03-10 03:01:38 +05:30
parent 4f0efb6454
commit 88b44085e6

View File

@ -1,59 +1,133 @@
<div class="review-box"> <div id="review-box-{{ block.id }}" class="review-box" data-block-id="{{ block.id }}">
<h3>Customer Reviews</h3> <h3>Customer Reviews</h3>
<form id="review-form"> {% if customer %}
<form id="review-form-{{ block.id }}" class="review-form-singleton">
<input type="hidden" name="productId" value="{{ product.id }}" /> <input type="hidden" name="productId" value="{{ product.id }}" />
<div class="review-form-fields">
<input type="text" name="name" placeholder="Your name" required /> <input type="text" name="name" placeholder="Your name" value="{{ customer.first_name }} {{ customer.last_name }}" required />
<textarea name="review" placeholder="Write your review" required></textarea> <textarea name="review" placeholder="Write your review" required></textarea>
<button type="submit" class="submit-review-btn">Submit Review</button>
<button type="submit">Submit Review</button> </div>
</form> </form>
{% else %}
<div class="login-prompt">
<p>Please <a href="{{ routes.account_login_url }}">login</a> to write a review for this product.</p>
</div>
{% endif %}
<div id="reviews-list"></div> <div id="reviews-list-{{ block.id }}" class="reviews-list-container">
<p class="loading-reviews">Checking your verified reviewer status...</p>
</div>
</div> </div>
<style>
.review-box { margin: 20px 0; font-family: sans-serif; max-width: 600px; }
.review-form-singleton { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
.review-form-fields { display: flex; flex-direction: column; gap: 10px; }
.review-form-fields input, .review-form-fields textarea { padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%; }
.review-form-fields button { padding: 10px; background: #000; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.review-form-fields button:disabled { background: #666; cursor: not-allowed; }
.login-prompt { padding: 15px; border: 1px dashed #ccc; color: #555; margin-bottom: 20px; text-align: center; }
.review-item { border-bottom: 1px solid #eee; padding: 15px 0; margin-bottom: 10px; }
.review-item strong { display: block; margin-bottom: 5px; color: #333; }
.review-item p { margin: 0; color: #666; line-height: 1.4; }
.optimistic-item { background: #f9f9f9; border-left: 4px solid #000; padding-left: 15px !important; }
</style>
<script> <script>
(function() {
// SINGLETON CHECK: If we already have initialized a review block on this page, hide the second one.
if (window.shopifyProductReviewInitialized) {
document.querySelector('[data-block-id="{{ block.id }}"]').style.display = 'none';
return;
}
window.shopifyProductReviewInitialized = true;
const blockId = "{{ block.id }}";
const productId = "{{ product.id }}"; const productId = "{{ product.id }}";
const shop = "{{ shop.permanent_domain }}";
const container = document.getElementById(`reviews-list-${blockId}`);
const form = document.getElementById(`review-form-${blockId}`);
async function loadReviews() { async function loadReviews() {
const response = await fetch(`/apps/reviews?productId=${productId}`); try {
const reviews = await response.json(); const response = await fetch(`/apps/reviews?productId=${productId}&shop=${shop}`);
const text = await response.json().catch(() => null);
const container = document.getElementById("reviews-list"); if (!text || (typeof text === 'string' && text.trim().startsWith("<!doctype"))) {
container.innerHTML = "<p style='color:red'><b>⚠️ Permissions Error:</b> Please open the app in your Shopify Admin dashboard once to activate it, then refresh this page.</p>";
return;
}
const reviews = text;
container.innerHTML = ""; container.innerHTML = "";
if (reviews.error) {
container.innerHTML = `<p>${reviews.error}</p>`;
return;
}
if (!Array.isArray(reviews) || reviews.length === 0) {
container.innerHTML = "<p>No reviews yet. Be the first!</p>";
return;
}
reviews.forEach(r => { reviews.forEach(r => {
container.innerHTML += ` const item = document.createElement("div");
<div style="border:1px solid #ddd; padding:10px; margin:10px 0;"> item.className = "review-item";
<strong>${r.name}</strong> item.innerHTML = `<strong>${escape(r.name)}</strong><p>${escape(r.review)}</p>`;
<p>${r.review}</p> container.appendChild(item);
</div> });
`; } catch (err) {
container.innerHTML = "<p>Ready to write a review.</p>";
}
}
function escape(t) {
const d = document.createElement('div');
d.textContent = t;
return d.innerHTML;
}
if (form) {
form.addEventListener("submit", async function(e) {
e.preventDefault();
const btn = this.querySelector(".submit-review-btn");
btn.disabled = true;
btn.textContent = "Verifying purchase...";
try {
const formData = new FormData(this);
const response = await fetch(`/apps/reviews?shop=${shop}&logged_in_customer_id={{ customer.id }}`, { method: "POST", body: formData });
const textData = await response.json().catch(() => ({error: "App connection issue. Open App Dashboard once."}));
console.log("Submit Response:", textData);
if (textData.error) {
const details = textData.details ? `\n\nDetails: ${JSON.stringify(textData.details)}` : "";
alert(`${textData.error}${details}`);
} else if (textData.success) {
const newItem = document.createElement("div");
newItem.className = "review-item optimistic-item";
newItem.innerHTML = `<strong>${escape(formData.get("name"))} (Just now)</strong><p>${escape(formData.get("review"))}</p>`;
container.insertBefore(newItem, container.firstChild);
this.reset();
alert("Review submitted successfully!");
setTimeout(loadReviews, 3000);
}
} catch (err) {
alert("Connection error: Try refreshing the page.");
} finally {
btn.disabled = false;
btn.textContent = "Submit Review";
}
}); });
} }
document.getElementById("review-form").addEventListener("submit", async function(e) {
e.preventDefault();
const formData = new FormData(this);
await fetch("/apps/reviews", {
method: "POST",
body: formData
});
this.reset();
loadReviews();
});
loadReviews(); loadReviews();
})();
</script> </script>
{% schema %} {% schema %}
{ { "name": "Reviews", "target": "section", "settings": [] }
"name": "Reviews",
"target": "section",
"settings": []
}
{% endschema %} {% endschema %}