Full Screen Reviews List (JudgeMe)
<div class="review-section">
<div class="review-scroll-wrapper">
<div class="review-scroll" id="judgeReviewContainer">
<!-- Dynamic review cards will be injected here -->
</div>
</div>
</div>.write-review {
width: 100%;
text-align: center;
height: 40px;
font-size: 14px;
font-weight: 500;
border-radius: 10px;
border: 1.5px solid #111;
background: transparent;
margin: 16px 0;
cursor: pointer;
}
.review-scroll-wrapper {
overflow-x: auto;
padding-bottom: 8px;
scrollbar-width: none;
}
.review-scroll-wrapper::-webkit-scrollbar {
display: none;
}
.review-scroll {
display: flex;
flex-direction: column;
gap: 16px;{
display: flex;
flex-direction: column;
}
.avg-rating {
font-size: 40px;
font-weight: 400;
}
.star-group {
display: flex;
gap: 2px;
margin: 6px 0;
}
.star-group img {
width: 20px;
height: 20px;
}
.review-count {
font-size: 12px;
color: #555;
}
}
.review-card {
display: flex;
flex-direction: column;
flex: 0 0 80%;
border-bottom: 1px solid #EEEEEE;
}.review-section {
padding: 16px 16px;
font-family: "Poppins", sans-serif;
}
.review-heading {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.review-product-name {
font-size: 16px;
line-height: 24px;
}
.rating-summary {
display: flex;
gap: 15px;
flex-direction: row;
justify-content: start;
align-items: end;
}
.star-avg-rating
.review-card h4 {
margin: 0%;
font-size: 16px;
font-weight: 700;
}
.review-card p {
margin-top: 8px;
font-size: 14px;
color: #333;
}
.review-footer {
display: flex;
flex-direction: column;
}
/* ⭐ Stars in a single row */
.star-group {
display: flex;
gap: 4px;
flex-wrap: nowrap;
}
.star-group img {
width: 20px;
height: 20px;
}
/* 👤 Name | Date line */
.review-meta-line {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0 16px;
}
.review-meta-line t5 {
color: #646464;
white-space: nowrap;
}
.review-meta-line .separator {
color: #ACACAC;
}// 🛠️ Configuration
const apiToken = {API_TOKEN};
const shopDomain = {SHOPIFY_DOMAIN} ;
// ✅ Supports SDK or fallback product ID
const productId = typeof params !== "undefined" && params.productId ? params.productId : "8154928021745";
const container = document.getElementById("judgeReviewContainer");
let currentPage = 1;
let isLoading = false;
let allReviewsLoaded = false;
// 📡 Fetch a single page of reviews
async function loadReviews(page) {
const url = `https://judge.me/api/v1/widgets/product_review?shop_domain=${shopDomain}&api_token=${apiToken}&external_id=${productId}&page=${page}&per_page=50`;
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Request failed");
const data = await res.json();
return extractReviewsFromHtml(data.widget);
} catch (err) {
console.error("❌ Failed to load reviews for page", page, err);
return [];
}
}
// 🧩 Parse Judge.me widget HTML and extract review content
function extractReviewsFromHtml(htmlString) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlString;
const elements = tempDiv.querySelectorAll(".jdgm-rev");
const reviews = [];
elements.forEach((el) => {
const name = el.querySelector(".jdgm-rev__author")?.textContent.trim() || "Anonymous";
reviews.push({
title: el.querySelector(".jdgm-rev__title")?.textContent.trim() || el.getAttribute("data-product-title"),
rating: parseInt(el.querySelector(".jdgm-rev__rating")?.getAttribute("data-score")) || 0,
timestamp: formatMonthYear(el.querySelector(".jdgm-rev__timestamp")?.getAttribute("data-content")),
name,
content: el.querySelector(".jdgm-rev__body p")?.textContent.trim() || "",
});
});
return reviews;
}
// 📅 Format date as "Jun 2024"
function formatMonthYear(dateString) {
if (!dateString) return "";
const date = new Date(dateString);
return date.toLocaleDateString(undefined, { year: "numeric", month: "short" });
}
// ⭐ Render stars using rating
function generateStarImages(rating) {
const filled = "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/golden-star_jwk9ox.png";
const blank = "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/blank-star_mhyhcu.png";
const rounded = rating % 1 >= 0.5 ? Math.ceil(rating) : Math.floor(rating);
return Array.from({ length: 5 }).map((_, i) =>
`<img src="${i < rounded ? filled : blank}" class="star-icon" alt="${i < rounded ? 'Filled' : 'Blank'} star" />`
).join("");
}
// 🎨 Render a single review card using SDK-styled tags
function renderReviewCard(review) {
return `
<div class="review-card">
<t2>${review.title}</t2>
${review.content ? `<t4>${review.content}</t4>` : ""}
<div class="review-footer">
<div class="star-group">${generateStarImages(review.rating)}</div>
<div class="review-meta-line">
<t5>Jane Doe</t5>
<t5 class="separator">|</t5>
<t5>Jun 2024</t5>
</div>
</div>
</div>
`;
}
// 🔁 Load next page of reviews on scroll
async function loadNextPage() {
if (isLoading || allReviewsLoaded) return;
isLoading = true;
const reviews = await loadReviews(currentPage);
if (!reviews.length) {
allReviewsLoaded = true;
return;
}
const html = reviews.map(renderReviewCard).join("");
container.insertAdjacentHTML("beforeend", html);
currentPage++;
isLoading = false;
}
// 👀 Detect scroll near bottom and load next page
window.addEventListener("scroll", () => {
const scrollBuffer = 100;
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollBuffer;
if (nearBottom) loadNextPage();
});
// 🚀 Initial load
loadNextPage();Updated 4 months ago