Product Page Reviews Overview (JudgeMe)

<div class="review-section">
  <t4 class="review-heading">REAL TALK, REAL CUSTOMERS</t4>
  <div class="rating-summary">
    <t1 class="avg-rating"></t1> <!-- Injected dynamically -->
    <div class="star-avg-rating">
      <div class="star-group"></div> <!-- Injected dynamically -->
      <t6 class="review-count"></t6>
    </div>
  </div>
  <outlined-button class="write-review" onclick="writeAReview()">Write a review</outlined-button>
  <div class="review-scroll-wrapper">
    <div class="review-scroll" id="judgeReviewContainer"></div>
  </div>
  <filled-button class="view-all" onclick="viewAllReviews()">View all reviews</filled-button>
</div>
.review-section {
  padding: 16px 16px;
}
.review-heading {
  font-weight: 700;
  margin-bottom: 8px;
}
.review-scroll {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}
.review-product-name {
  font-size: 16px;
  line-height: 24px;
}
.product-title {
  font-weight: 600;
}
.product-content {
  margin-top: 10px;
}
.rating-summary {
  display: flex;
  gap: 15px;
  flex-direction: row;
  justify-content: start;
  align-items: end;
}
.star-avg-rating {
  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;
}
.write-review {
  width: 100%;
  margin-top: 12px;
  text-align: center;
  font-weight: 500;
  padding: 10px;
  margin-bottom: 24px;
}
.review-scroll-wrapper {
  overflow-x: auto;
  padding-bottom: 8px;
  scrollbar-width: none;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
  touch-action: pan-x;
  -ms-touch-action: pan-x;
  overscroll-behavior-x: contain;
  height: auto;
}
.review-scroll-wrapper::-webkit-scrollbar {
  display: none;
}
.review-scroll {
  display: flex;
  gap: 16px;
}
.review-card {
  display: flex;
  flex-direction: column;
  flex: 0 0 80%;
  border-radius: 14px;
  padding: 16px;
  border: 1px solid #EEEEEE;
  height: auto;
  background: #f3f3f3;
}
.review-card h4 {
  margin: 0%;
  font-size: 16px;
  font-weight: 700;
  margin-bottom: 6px;
}
.review-card p {
  font-size: 14px;
  color: #333;
  margin: 0 0 12px;
}
.review-meta {
  border-top: 1px solid #E3E3E3;
  display: flex;
  flex-direction: column;
  justify-content: start;
  align-items: start;
  gap: 8px;
  padding-top: 12px;
}
.reviewer-info {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
}
.initials {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: #E3E3E3;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #111;
  font-size: 14px;
  flex-shrink: 0;
  /* ✅ Prevent shrinking inside flex container */
}
.meta-name-time {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.meta-name {
  font-weight: 600;
  font-size: 14px;
  white-space: nowrap;
}
.meta-time {
  font-weight: 400;
  font-size: 14px;
  white-space: nowrap;
}
.meta-stars {
  display: flex;
  gap: 2px;
}
.meta-stars img {
  width: 20px;
  height: 20px;
}
.view-all {
  display: none;
  text-align: center;
  font-size: 14px;
  margin-top: 16px;
  cursor: pointer;
}
// 📦 Judge.me widget API configuration
// 🛠️ Configuration
const apiToken = {API_TOKEN};
const shopDomain = {SHOPIFY_DOMAIN} ;
const productId = VajroSDK.variables.product.id;
console.log(productId)
const reviewsApiUrl = `https://judge.me/api/v1/widgets/product_review?shop_domain=${shopDomain}&api_token=${apiToken}&external_id=${productId}&per_page=6`;
// 🌟 Star rendering helper
function generateStars(rating) {
  console.log("rating");
  const filledStar = "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/golden-star_jwk9ox.png";
  const blankStar = "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/blank-star_mhyhcu.png";
  let stars = "";
  for (let i = 1; i <= 5; i++) {
    const src = i <= rating ? filledStar : blankStar;
    stars += `<img src="${src}" class="star-icon" alt="${i <= rating ? 'Filled' : 'Empty'} star"/>`;
  }
  return stars;
}
function formatDate(dateString) {
  if (!dateString) return "";
  // Convert "2025-06-09 08:20:17 UTC" → "2025-06-09T08:20:17Z"
  const isoString = dateString.replace(" ", "T").replace(" UTC", "Z");
  const date = new Date(isoString);
  return isNaN(date) ? "" : date.toLocaleDateString(undefined, {
    year: 'numeric',
    month: 'short',
  });
}
// 🔢 Format large review counts (e.g. 1.5k)
function formatCount(num) {
  return num >= 1000 ? `${(num / 1000).toFixed(1)}k` : num.toString();
}
// 📊 Render summary: avg rating + stars + total count
function renderSummary(avgRating, totalReviews) {
  document.querySelector('.avg-rating').textContent = avgRating.toFixed(1);
  document.querySelector('.review-count').textContent = `${formatCount(totalReviews)} ratings`;
  const starGroup = document.querySelector('.star-group');
  if (starGroup) {
    starGroup.innerHTML = "";
    const roundedRating = avgRating % 1 >= 0.5 ? Math.ceil(avgRating) : Math.floor(avgRating);
    for (let i = 1; i <= 5; i++) {
      const img = document.createElement('img');
      img.src = i <= roundedRating
        ? "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/golden-star_jwk9ox.png"
        : "https://res.cloudinary.com/dixyq8hvr/image/upload/v1747398424/blank-star_mhyhcu.png";
      img.className = 'star-icon';
      img.alt = i <= roundedRating ? 'Filled star' : 'Empty star';
      starGroup.appendChild(img);
    }
  }
}
// 💬 Render up to 5 reviews; show "View All" button if more
function renderReviews() {
  fetch(reviewsApiUrl)
    .then(res => res.json())
    .then(data => {
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = data.widget;
      const summary = tempDiv.querySelector('.jdgm-rev-widg');
      const avgRating = parseFloat(summary?.getAttribute('data-average-rating') || "0");
      const totalReviews = parseInt(summary?.getAttribute('data-number-of-reviews') || "0");
      renderSummary(avgRating, totalReviews);
      const rawReviews = tempDiv.querySelectorAll('.jdgm-rev');
      const reviews = Array.from(rawReviews).map(el => {
        const name = el.querySelector('.jdgm-rev__author')?.textContent.trim() || "Anonymous";
        return {
          name,
          initials: name.split(" ").map(n => n[0]).join("").slice(0, 2).toUpperCase(),
          rating: parseInt(el.querySelector('.jdgm-rev__rating')?.getAttribute('data-score') || "0"),
          content: el.querySelector('.jdgm-rev__body')?.textContent.trim() || "",
          timestamp: formatDate(el.querySelector('.jdgm-rev__timestamp')?.getAttribute('data-content')),
          productTitle: el.getAttribute('data-product-title') || ""
        };
      });
      const container = document.getElementById('judgeReviewContainer');
      const reviewScrollWrapper = document.querySelector('.review-scroll-wrapper');
      const viewAllButton = document.querySelector('.view-all');
      if (reviews.length > 0 && container && reviewScrollWrapper) {
        reviewScrollWrapper.style.display = "block";
        const visibleReviews = reviews.slice(0, 5);
        container.innerHTML = visibleReviews.map(r => `
          <div class="review-card">
            <t4 class="product-title">${r.productTitle}</t4>
             <t4 class="product-content">${r.content}</t4>
            <div class="review-meta">
              <div class="reviewer-info">
                <div class="initials">${r.initials}</div>
                <div class="meta-name-time">
                  <t4 class="meta-name">${r.name}</t4>
                  <t6 class="meta-time">${r.timestamp}</t6>
                </div>
              </div>
              <div class="meta-stars">${generateStars(r.rating)}</div>
            </div> 
          </div>
        `).join('');
        // Show "View All" button if more than 5 reviews
        viewAllButton.style.display = reviews.length > 5 ? "block" : "none";
      }
      else {
        document.querySelector(".review-count").style.display = "none"
        document.querySelector(".avg-rating").style.display = "none"
        document.querySelector(".write-review").style.marginBottom = "8px"
      }
    })
    .catch(err => {
      console.error("❌ Failed to load Judge.me reviews:", err);
    });
}
// 🔗 Open full reviews screen via SDK
function viewAllReviews() {
  VajroSDK.actions.openCustomBlock({
    id: "cbk_0m0ymn6rsja38", // 👈 update with actual block ID
    type: "full_screen",
    params: { productId }
  }).catch(err => {
    VajroSDK.actions.showAlert({ title: "Error", message: "Could not open reviews." });
  });
}
// ✍️ Trigger review form via SDK
function writeAReview() {
  VajroSDK.actions.openCustomBlock({
    id: "cbk_0m0zavn3sjj56", // 👈 update with actual review form block ID
    type: "popup_bottom",
    params: { productId }
  }).catch(err => {
    VajroSDK.actions.showAlert({ title: "Error", message: JSON.stringify(err) });
  });
}
// 🚀 Init reviews on load
renderReviews();
window.writeAReview = writeAReview;
window.viewAllReviews = viewAllReviews;