From 980df5f6fc42b5bf942cc3a0317fbd788e84a92f Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Wed, 17 Dec 2025 12:10:26 -0700 Subject: [PATCH] chore: use API for reviews overview --- blocks/pdp/pdp.css | 71 ++++++++++++++++++++++++ blocks/pdp/pdp.js | 132 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 201 insertions(+), 2 deletions(-) diff --git a/blocks/pdp/pdp.css b/blocks/pdp/pdp.css index b8ff99ad..0be647bb 100644 --- a/blocks/pdp/pdp.css +++ b/blocks/pdp/pdp.css @@ -489,6 +489,77 @@ a.button.pdp-find-locally-button { min-height: 22.5px; } +/* Custom BV Rating Summary Styles */ +.pdp .pdp-reviews-summary-placeholder .bv-rating-summary-container { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.pdp .pdp-reviews-summary-placeholder .bv-rating-summary-container a.bv-rating-summary, +.pdp .pdp-reviews-summary-placeholder .bv-rating-summary-container a.bv-rating-summary:link, +.pdp .pdp-reviews-summary-placeholder .bv-rating-summary-container a.bv-rating-summary:visited { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: #2b5085; +} + +.pdp .pdp-reviews-summary-placeholder .bv-rating-summary-container a.bv-rating-summary:hover { + text-decoration: underline; +} + +.pdp .pdp-reviews-summary-placeholder .bv-stars { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.pdp .pdp-reviews-summary-placeholder .bv-star { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.pdp .pdp-reviews-summary-placeholder .bv-star.bv-star-full { + color: #2b5085; +} + +.pdp .pdp-reviews-summary-placeholder .bv-star.bv-star-empty { + color: #a8b5c1; +} + +.pdp .pdp-reviews-summary-placeholder .bv-star.bv-star-half { + color: #2b5085; +} + +.pdp .pdp-reviews-summary-placeholder .bv-rating-value { + font-size: 1.125rem; + font-weight: 400; + color: #2b5085; +} + +.pdp .pdp-reviews-summary-placeholder .bv-review-count { + font-size: 1.125rem; + font-weight: 400; + color: #2b5085; +} + +.pdp .pdp-reviews-summary-placeholder a.bv-write-review, +.pdp .pdp-reviews-summary-placeholder a.bv-write-review:link, +.pdp .pdp-reviews-summary-placeholder a.bv-write-review:visited { + font-size: 1.125rem; + font-weight: 400; + color: #2b5085; + text-decoration: none; +} + +.pdp .pdp-reviews-summary-placeholder a.bv-write-review:hover { + text-decoration: underline; +} + .pdp-reviews-container { grid-area: reviews; } diff --git a/blocks/pdp/pdp.js b/blocks/pdp/pdp.js index 359910b8..f4ae3e07 100644 --- a/blocks/pdp/pdp.js +++ b/blocks/pdp/pdp.js @@ -9,6 +9,130 @@ import { loadFragment } from '../fragment/fragment.js'; import { checkVariantOutOfStock, isProductOutOfStock, isNextPipeline } from '../../scripts/scripts.js'; import { openModal } from '../modal/modal.js'; +// Bazaarvoice API configuration +const BV_API_CONFIG = { + // TODO: Switch to production endpoint and passkey when ready + endpoint: 'https://stg.api.bazaarvoice.com/data', + passkey: 'caB45h2jBqXFw1OE043qoMBD1gJC8EwFNCjktzgwncXY4', + apiVersion: '5.4', +}; + +// Check if running on localhost (BV API has CORS restrictions on localhost) +const isLocalDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; + +/** + * Fetches review statistics from Bazaarvoice API + * @param {string} productId - The product's review ID + * @returns {Promise} Review statistics or null on error + */ +async function fetchReviewStats(productId) { + try { + const params = new URLSearchParams({ + apiversion: BV_API_CONFIG.apiVersion, + passkey: BV_API_CONFIG.passkey, + Filter: `ProductId:${productId}`, + Stats: 'Reviews', + }); + + const response = await fetch(`${BV_API_CONFIG.endpoint}/products.json?${params}`); + if (!response.ok) return null; + + const data = await response.json(); + if (data.HasErrors || !data.Results?.length) return null; + + const product = data.Results[0]; + return product.ReviewStatistics || null; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching review stats:', error); + return null; + } +} + +/** + * SVG star paths + */ +const STAR_SVG = { + full: ` + + `, + half: ` + + + + + + + + `, + empty: ` + + `, +}; + +/** + * Renders star rating HTML + * @param {number} rating - Average rating (0-5) + * @returns {string} HTML string for stars + */ +function renderStars(rating) { + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.25; + const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); + + return ` + + ${STAR_SVG.full.repeat(fullStars)}${hasHalfStar ? STAR_SVG.half : ''}${STAR_SVG.empty.repeat(emptyStars)} + + `; +} + +/** + * Renders the rating summary HTML + * @param {number} avgRating - Average rating + * @param {number} totalReviews - Total review count + * @returns {string} HTML string + */ +function renderRatingSummaryHTML(avgRating, totalReviews) { + return ` + + `; +} + +/** + * Loads and renders review summary asynchronously (does not block LCP) + * @param {Element} placeholder - The placeholder element to fill + * @param {string} reviewsId - The product's review ID + */ +async function loadRatingSummary(placeholder, reviewsId) { + // On localhost, BV API has CORS restrictions - show mock data for development + if (isLocalDev) { + // eslint-disable-next-line no-console + console.info('[BV Reviews] Using mock data on localhost (CORS restriction)'); + placeholder.innerHTML = renderRatingSummaryHTML(4.5, 127); + return; + } + + const stats = await fetchReviewStats(reviewsId); + + if (!stats || stats.TotalReviewCount === 0) { + placeholder.innerHTML = ''; + return; + } + + const avgRating = stats.AverageOverallRating || 0; + const totalReviews = stats.TotalReviewCount || 0; + + placeholder.innerHTML = renderRatingSummaryHTML(avgRating, totalReviews); +} + /** * Renders the title section of the PDP block. * @param {Element} block - The PDP block element @@ -20,7 +144,11 @@ function renderTitle(block, custom, reviewsId) { const reviewsPlaceholder = document.createElement('div'); reviewsPlaceholder.classList.add('pdp-reviews-summary-placeholder'); - reviewsPlaceholder.innerHTML = `
`; + + // Load rating summary asynchronously to avoid blocking LCP + setTimeout(() => { + loadRatingSummary(reviewsPlaceholder, reviewsId); + }, 100); const { collection } = custom; const collectionContainer = document.createElement('p'); @@ -57,8 +185,8 @@ function renderDetails(features) { */ // eslint-disable-next-line no-unused-vars async function renderReviews(block, reviewsId) { - // TODO: Add Bazaarvoice reviews const bazaarvoiceContainer = document.createElement('div'); + bazaarvoiceContainer.id = 'pdp-reviews-container'; bazaarvoiceContainer.classList.add('pdp-reviews-container'); bazaarvoiceContainer.innerHTML = `
`;