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();