feat: implement customer reviews liquid block with singleton pattern and verified buyer check
This commit is contained in:
parent
4f0efb6454
commit
88b44085e6
@ -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 %}
|
||||||
<input type="hidden" name="productId" value="{{ product.id }}" />
|
<form id="review-form-{{ block.id }}" class="review-form-singleton">
|
||||||
|
<input type="hidden" name="productId" value="{{ product.id }}" />
|
||||||
|
<div class="review-form-fields">
|
||||||
|
<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>
|
||||||
|
<button type="submit" class="submit-review-btn">Submit Review</button>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
|
||||||
<input type="text" name="name" placeholder="Your name" required />
|
<div id="reviews-list-{{ block.id }}" class="reviews-list-container">
|
||||||
<textarea name="review" placeholder="Write your review" required></textarea>
|
<p class="loading-reviews">Checking your verified reviewer status...</p>
|
||||||
|
</div>
|
||||||
<button type="submit">Submit Review</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="reviews-list"></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>
|
||||||
const productId = "{{ product.id }}";
|
(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;
|
||||||
|
|
||||||
async function loadReviews() {
|
const blockId = "{{ block.id }}";
|
||||||
const response = await fetch(`/apps/reviews?productId=${productId}`);
|
const productId = "{{ product.id }}";
|
||||||
const reviews = await response.json();
|
const shop = "{{ shop.permanent_domain }}";
|
||||||
|
const container = document.getElementById(`reviews-list-${blockId}`);
|
||||||
|
const form = document.getElementById(`review-form-${blockId}`);
|
||||||
|
|
||||||
const container = document.getElementById("reviews-list");
|
async function loadReviews() {
|
||||||
container.innerHTML = "";
|
try {
|
||||||
|
const response = await fetch(`/apps/reviews?productId=${productId}&shop=${shop}`);
|
||||||
|
const text = await response.json().catch(() => null);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
reviews.forEach(r => {
|
const reviews = text;
|
||||||
container.innerHTML += `
|
container.innerHTML = "";
|
||||||
<div style="border:1px solid #ddd; padding:10px; margin:10px 0;">
|
|
||||||
<strong>${r.name}</strong>
|
|
||||||
<p>${r.review}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("review-form").addEventListener("submit", async function(e) {
|
if (reviews.error) {
|
||||||
e.preventDefault();
|
container.innerHTML = `<p>${reviews.error}</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData(this);
|
if (!Array.isArray(reviews) || reviews.length === 0) {
|
||||||
|
container.innerHTML = "<p>No reviews yet. Be the first!</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await fetch("/apps/reviews", {
|
reviews.forEach(r => {
|
||||||
method: "POST",
|
const item = document.createElement("div");
|
||||||
body: formData
|
item.className = "review-item";
|
||||||
});
|
item.innerHTML = `<strong>${escape(r.name)}</strong><p>${escape(r.review)}</p>`;
|
||||||
|
container.appendChild(item);
|
||||||
|
});
|
||||||
|
} 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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.reset();
|
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})();
|
||||||
|
|
||||||
loadReviews();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% schema %}
|
{% schema %}
|
||||||
{
|
{ "name": "Reviews", "target": "section", "settings": [] }
|
||||||
"name": "Reviews",
|
|
||||||
"target": "section",
|
|
||||||
"settings": []
|
|
||||||
}
|
|
||||||
{% endschema %}
|
{% endschema %}
|
||||||
Loading…
x
Reference in New Issue
Block a user