{"version":3,"file":"yotpo-product-reviews-section.min.js","mappings":"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","sources":["webpack://lange/./sections/yotpo-product-reviews/review-drawer.ts","webpack://lange/webpack/bootstrap","webpack://lange/../node_modules/uuid/dist/rng.js","webpack://lange/../node_modules/uuid/dist/stringify.js","webpack://lange/../node_modules/uuid/dist/v4.js","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/swell/swell-popup-type.js","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/swell/swell-referral-medium.js","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/./lib/decorate-error.ts","webpack://lange/./sections/yotpo-product-reviews/review-form.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-create-review.ts","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/reviews/create-review-api-response-body.js","webpack://lange/./sections/yotpo-product-reviews/simple-rating-stars/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-ai-summary/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-fetch-reviews-summary.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-filters/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/decode-html-entities.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-pagination/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/yotpo-fetch-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/yotpo-search-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-stars-breakdown/index.ts"],"sourcesContent":["/**\n * Opens the write a review drawer.\n */\nfunction onWriteReviewClicked(event: MouseEvent) {\n  const button = event.currentTarget;\n  if (!(button instanceof HTMLButtonElement)) {\n    return;\n  }\n\n  button.setAttribute('aria-expanded', 'true');\n\n  // Claim focus return before opening. Without this the modal manager has nothing of ours on its\n  // focus stack, so closing the drawer reaches down to whichever opener an earlier modal left\n  // there, and focusing that one scrolls the page away to wherever it sits.\n  const focusEvent = new Event('focus-stack-push-requested', {\n    bubbles: true,\n    composed: true\n  });\n  button.dispatchEvent(focusEvent);\n\n  // The modal manager leaves the reviews section active while the drawer is open, so the button\n  // would stay reachable by Tab behind it. The reviews element inerts the rest of the section. We\n  // inert the button here rather than when the drawer reports it has opened, so it is out of reach\n  // right away.\n  button.inert = true;\n\n  const container = button.closest('.review-create');\n  const modal = container.querySelector('modal-element');\n  modal.dataset.open = 'true';\n}\n\n/**\n * Puts the write a review button back the way it was once the drawer closes. The modal manager puts\n * focus back on the button right after this runs, and a focus call into an inert element does\n * nothing.\n */\nfunction onModalClosed(event: GlobalEventHandlersEventMap['modal-closed']) {\n  const target = event.currentTarget;\n  if (target instanceof HTMLElement) {\n    const writeReview = target.querySelector<HTMLElement>('.write-review');\n    writeReview.setAttribute('aria-expanded', 'false');\n    writeReview.inert = false;\n  }\n}\n\n// Each reviews section on the page carries its own copy of this script, and every copy runs, so we\n// skip the containers an earlier copy already set up.\nconst selector = '.review-create:not([data-drawer-initialized])';\nconst containers = document.querySelectorAll<HTMLElement>(selector);\nfor (const container of containers) {\n  const writeReview = container.querySelector<HTMLElement>('.write-review');\n  writeReview.addEventListener('click', onWriteReviewClicked);\n\n  container.addEventListener('modal-closed', onModalClosed);\n  container.dataset.drawerInitialized = 'true';\n}\n","// The module cache\nlet __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tlet cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tlet module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","const rnds8 = new Uint8Array(16);\nexport default function rng() {\n    return crypto.getRandomValues(rnds8);\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n    byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n    return (byteToHex[arr[offset + 0]] +\n        byteToHex[arr[offset + 1]] +\n        byteToHex[arr[offset + 2]] +\n        byteToHex[arr[offset + 3]] +\n        '-' +\n        byteToHex[arr[offset + 4]] +\n        byteToHex[arr[offset + 5]] +\n        '-' +\n        byteToHex[arr[offset + 6]] +\n        byteToHex[arr[offset + 7]] +\n        '-' +\n        byteToHex[arr[offset + 8]] +\n        byteToHex[arr[offset + 9]] +\n        '-' +\n        byteToHex[arr[offset + 10]] +\n        byteToHex[arr[offset + 11]] +\n        byteToHex[arr[offset + 12]] +\n        byteToHex[arr[offset + 13]] +\n        byteToHex[arr[offset + 14]] +\n        byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n    const uuid = unsafeStringify(arr, offset);\n    if (!validate(uuid)) {\n        throw TypeError('Stringified UUID is invalid');\n    }\n    return uuid;\n}\nexport default stringify;\n","import rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n    if (!buf && !options && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return _v4(options, buf, offset);\n}\nfunction _v4(options, buf, offset) {\n    options = options || {};\n    const rnds = options.random ?? options.rng?.() ?? rng();\n    if (rnds.length < 16) {\n        throw new Error('Random bytes length must be >= 16');\n    }\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n    if (buf) {\n        offset = offset || 0;\n        if (offset < 0 || offset + 16 > buf.length) {\n            throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n        }\n        for (let i = 0; i < 16; ++i) {\n            buf[offset + i] = rnds[i];\n        }\n        return buf;\n    }\n    return unsafeStringify(rnds);\n}\nexport default v4;\n","export var SwellPopupType;\n(function (SwellPopupType) {\n    /**\n     * The standard rewards popup that customers use to earn points, redeem points, manage referral\n     * program, view history, and read faqs. You can configure it in the Rewards Popup section of\n     * the Swell Admin.\n     */\n    SwellPopupType[\"REWARDS_POPUP\"] = \"RewardsPopup\";\n    /**\n     * This popup is used to collect email addresses for your newsletter. You can configure and\n     * design it in the Newsletter Signup section of the Swell Admin.\n     */\n    SwellPopupType[\"EMAIL_CAPTURE_POPUP\"] = \"EmailCapturePopup\";\n    /**\n     * This popup is used by customers to view and share their referral link via social media and\n     * email. You can configure and design it in the Referral Program section of the Swell Admin.\n     */\n    SwellPopupType[\"REFERRAL_LINK_POPUP\"] = \"ReferralLinkPopup\";\n    /**\n     * This popup is used to view all point earning and redemption activity. It will also display\n     * any coupon codes earned by the customer.\n     */\n    SwellPopupType[\"HISTORY_POPUP\"] = \"HistoryPopup\";\n})(SwellPopupType || (SwellPopupType = {}));\n","export var SwellReferralMedium;\n(function (SwellReferralMedium) {\n    SwellReferralMedium[\"UNDEFINED\"] = \"undefined\";\n    SwellReferralMedium[\"FACEBOOK\"] = \"facebook\";\n    SwellReferralMedium[\"TWITTER\"] = \"twitter\";\n    SwellReferralMedium[\"SMS\"] = \"sms\";\n    SwellReferralMedium[\"EMAIL\"] = \"email\";\n    SwellReferralMedium[\"COPY_LINK\"] = \"copy_link\";\n})(SwellReferralMedium || (SwellReferralMedium = {}));\n","/**\n * Throws an assertion error if the given condition is falsy.\n *\n * @param expression any expression, although one that evaluates to true or false is preferred by\n * convention\n * @param message optional error text\n */\nexport function assert(expression: unknown, message?: string) {\n  if (!expression) {\n    throw new AssertionError(message);\n  }\n}\n\nexport class AssertionError extends Error {\n  constructor(message = 'Assertion error') {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n","/**\n * Copies a single piece of supplementary context onto an error before it is thrown or reported.\n *\n * Use this when decorating a value the calling code did not construct, such as a caught value or an\n * error handed in by another module. An error the caller just built with new is always writable, so\n * plain assignment is fine there. A value from elsewhere may be a foreign object this code does not\n * own. Such a value can carry a non-writable property, such as a DOMException whose code is a read-\n * only accessor, or be frozen outright, and assigning to it then throws. Attaching context must\n * never itself fail, so a rejected assignment is swallowed and the remaining context is still\n * attempted. Guarding each property in isolation means a single read-only slot, the common case,\n * costs only that one property rather than all of them.\n *\n * In practice the value that triggers this is almost always a DOMException that reached the caller\n * from a fetch the browser cut short, such as an abort when an in-flight request is interrupted.\n * The platform defines a DOMException's code, name, and message as read-only attributes, so\n * assigning a code onto one throws under the strict mode that all bundled module code runs in. It\n * surfaces from Safari far more than elsewhere for two reasons. The reported wording, attempted to\n * assign to readonly property, is JavaScriptCore's phrasing for this failure, so the same bug in\n * another engine is worded differently and groups as a separate report, which makes this one look\n * Safari specific even though the read-only rule is not. And iOS Safari abandons in-flight fetches\n * aggressively when a tab is backgrounded or a connection drops, which is when such a DOMException\n * tends to be produced and handed to code that assumed an ordinary writable error.\n *\n * A swallowed failure is logged at a level low enough that it is recorded as a breadcrumb for\n * monitoring rather than escalated into its own captured event. That keeps the lost context visible\n * alongside the eventual report without adding fresh noise of its own. The breadcrumb names the\n * property that could not be attached, the kind of value that rejected it, and why, which is enough\n * to recognize a read-only or frozen caught value after the fact.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Read-only\n * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException\n * @see https://webidl.spec.whatwg.org/#dfn-read-only\n */\nexport function decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (assignmentError) {\n    let reason: string;\n    if (assignmentError instanceof Error) {\n      reason = assignmentError.message;\n    } else {\n      reason = `${assignmentError}`;\n    }\n\n    console.log('Failed to assign error property', {\n      key,\n      name: error?.name,\n      tag: Object.prototype.toString.call(error),\n      reason\n    });\n  }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { createReview, type Input } from './yotpo-create-review.js';\n\n/**\n * Takes away the errors from an earlier attempt, so someone who closed the drawer partway and comes\n * back later doesn't reopen it to a form full of red. What they typed stays, and the errors come\n * back on the next post if it's still not right.\n */\nfunction onModalOpened(event: GlobalEventHandlersEventMap['modal-opened']) {\n  if (!(event.currentTarget instanceof HTMLElement)) {\n    return;\n  }\n\n  const form = event.currentTarget.querySelector('form');\n  form.classList.remove('error');\n\n  const errorElements = form.querySelectorAll('.error');\n  for (const element of errorElements) {\n    element.classList.remove('error');\n  }\n\n  const summaries = form.querySelectorAll<HTMLElement>('.error-summary, .post-error');\n  for (const summary of summaries) {\n    summary.hidden = true;\n  }\n}\n\n/**\n * Handles a click in the rating stars. We listen on the stars' container, so we check whether the\n * click landed on a star rather than between them.\n */\nfunction onStarsClicked(event: MouseEvent) {\n  if (event.target instanceof HTMLButtonElement && event.target.classList.contains('star')) {\n    selectStar(event.target);\n  }\n}\n\n/**\n * Fills in the clicked star and every star before it, and puts its number in the hidden score\n * input.\n */\nfunction selectStar(target: HTMLButtonElement) {\n  const score = parseInt(target.dataset.idx, 10);\n  const form = target.form;\n  const scoreInput = form.querySelector<HTMLInputElement>('input[name=score]');\n  scoreInput.value = score.toString();\n\n  const stars = form.querySelectorAll<HTMLElement>('.star');\n  for (const star of stars) {\n    const idx = parseInt(star.dataset.idx, 10);\n    if (idx === score) {\n      star.textContent = '★';\n      star.setAttribute('aria-checked', 'true');\n    } else if (idx < score) {\n      star.textContent = '★';\n      star.setAttribute('aria-checked', 'false');\n    } else {\n      star.textContent = '☆';\n      star.setAttribute('aria-checked', 'false');\n    }\n  }\n}\n\n/**\n * Checks the write a review form, and posts the review when every field is filled in right.\n */\nfunction onSubmitClicked(event: MouseEvent) {\n  event.preventDefault();\n\n  const submitButton = event.currentTarget;\n  if (!(submitButton instanceof HTMLButtonElement)) {\n    return;\n  }\n\n  const form = submitButton.form;\n  const errorSummary = form.querySelector<HTMLElement>('.error-summary');\n\n  if (!form.checkValidity()) {\n    let firstInvalid: HTMLElement = null;\n    for (const element of form.elements) {\n      if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {\n        continue;\n      }\n\n      if (element.checkValidity()) {\n        element.parentElement.classList.remove('error');\n        continue;\n      }\n\n      element.parentElement.classList.add('error');\n      firstInvalid ??= element;\n    }\n\n    // We want to drop the user into the first thing they still have to fill in. Usually that is\n    // just the field itself, but the score is a special case. The score input holds the number of\n    // stars the user picked and we keep it hidden, because the five star buttons above it are the\n    // real thing people click. Nothing can move focus into a hidden input, so if we asked for it we\n    // would get no focus move at all and the user would be left wondering where to go. That happens\n    // on an empty form, which is the most common way to fail this. So when the score is the first\n    // thing missing we send them to the first star instead.\n    if (firstInvalid instanceof HTMLInputElement && firstInvalid.name === 'score') {\n      firstInvalid = form.querySelector<HTMLButtonElement>('.star');\n    }\n\n    firstInvalid?.focus({ focusVisible: true });\n\n    errorSummary.hidden = false;\n    return;\n  }\n\n  errorSummary.hidden = true;\n\n  postReview(form, submitButton).catch(console.error);\n}\n\n/**\n * Sends the review to Yotpo. Once it is accepted, we close the drawer and let the rest of the page\n * know a review was written and who wrote it.\n */\nasync function postReview(form: HTMLFormElement, submitButton: HTMLButtonElement) {\n  const formData = new FormData(form);\n  const firstName = formData.get('first_name');\n  const lastName = formData.get('last_name');\n  const email = formData.get('email');\n  const review = formData.get('review');\n  const score = formData.get('score');\n  const title = formData.get('title');\n\n  // Product ids are sent in the sku property to Yotpo.\n  const input: Partial<Input> = {\n    appkey: form.dataset.yotpoAppKey,\n    display_name: `${firstName} ${lastName}`,\n    email: isString(email) ? email : '',\n    product_title: form.dataset.productTitle,\n    product_url: form.dataset.productUrl,\n    review_content: isString(review) ? review : '',\n    review_score: isString(score) ? parseInt(score, 10) : 0,\n    review_title: isString(title) ? title : '',\n    sku: form.dataset.productId\n  };\n\n  const loader = form.querySelector('loading-indicator');\n  submitButton.disabled = true;\n  loader.hidden = false;\n\n  try {\n    await createReview(input);\n  } catch (error) {\n    submitButton.disabled = false;\n    loader.hidden = true;\n    form.classList.add('error');\n\n    const postError = form.querySelector<HTMLElement>('.post-error');\n    postError.hidden = false;\n\n    console.error(error, input);\n    return;\n  }\n\n  submitButton.disabled = false;\n  loader.hidden = true;\n  form.reset();\n\n  const modal = form.closest('modal-element');\n  delete modal.dataset.open;\n\n  type Detail = WindowEventMap['yotpo-review-submitted']['detail'];\n  const submittedEvent = new CustomEvent<Detail>('yotpo-review-submitted', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      body: input.review_content,\n      category: form.dataset.productType,\n      email: input.email,\n      error_message: '',\n      event_id: uuidv4(),\n      first_name: isString(firstName) ? firstName : '',\n      last_name: isString(lastName) ? lastName : '',\n      person_name: input.display_name,\n      product_id: parseInt(form.dataset.productId, 10),\n      product_price: parseInt(form.dataset.productPrice, 10),\n      product_title: form.dataset.productTitle,\n      rating: input.review_score,\n      review_title: input.review_title,\n      sku: input.sku\n    }\n  });\n  form.dispatchEvent(submittedEvent);\n\n  const identityEvent = new CustomEvent('identity-captured', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      email: input.email,\n      first_name: isString(firstName) ? firstName : '',\n      last_name: isString(lastName) ? lastName : ''\n    }\n  });\n  form.dispatchEvent(identityEvent);\n}\n\nfunction isString(value: any): value is string {\n  return typeof value === 'string';\n}\n\n// Each reviews section on the page carries its own copy of this script, and every copy runs, so we\n// skip the containers an earlier copy already set up.\nconst containers = document.querySelectorAll<HTMLElement>(\n  '.review-create:not([data-form-initialized])');\nfor (const container of containers) {\n  const modal = container.querySelector('modal-element');\n  modal.addEventListener('modal-opened', onModalOpened);\n\n  const stars = container.querySelector<HTMLElement>('.rating-stars');\n  stars.addEventListener('click', onStarsClicked);\n\n  const submitButton = container.querySelector<HTMLElement>('form button[type=\"submit\"]');\n  submitButton.addEventListener('click', onSubmitClicked);\n\n  container.dataset.formInitialized = 'true';\n}\n","import { assert } from '@integrabeauty/assert';\nimport { isCreateReviewAPIResponseBody } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../lib/decorate-error.js';\n\nexport interface Input {\n  /**\n   * The store ID\n   */\n  appkey: string;\n\n  customer_metadata?: {\n    /**\n     * The customer's address\n     */\n    address?: string;\n\n    /**\n     * Two-letter uppercase country code indicating the customer country. To comply with Google\n     * Seller Ratings conventions, the country code value must be from this list of compatible\n     * codes. Default value: US.\n     */\n    country?: string;\n\n    custom_properties?: {\n      name?: string;\n      value?: string;\n    };\n\n    /**\n     * The customer's phone number\n     */\n    phone_number?: string;\n\n    /**\n     * The state the customer is located in\n     */\n    state?: string;\n  };\n\n  /**\n   * The type of delivery designated for this purchase e.g. express, expedited, standard.\n   */\n  delivery_type?: string;\n\n  /**\n   * The reviewer's name\n   */\n  display_name: string;\n\n  /**\n   * The account domain\n   */\n  domain?: string;\n\n  /**\n   * The email of the reviewer\n   */\n  email: string;\n\n  /**\n   * The type of incentive provided for the review\n   */\n  incentive_type?:\n    'coupon' |\n    'employee_review' |\n    'free_product' |\n    'loyalty_points' |\n    'other' |\n    'paid_promotion';\n\n  /**\n   * Indicates if the review comes from an incentivized source (for example, a product sampling\n   * company).\n   */\n  is_incentivized?: boolean;\n\n  order_metadata?: {\n    /**\n     * Part of order_metadata, indicates whether a coupon was used at purchase\n     */\n    coupon_used?: boolean;\n\n    /**\n     * The name of an order-related custom property e.g. order history\n     */\n    name?: string;\n\n    /**\n     * The order-related custom_properties value\n     */\n    value?: string;\n  };\n\n  /**\n   * The url of the product image\n   *\n   * @example \"http://www.shop.com/phone.jpg\"\n   */\n  product_image_url?: string;\n\n  product_metadata?: {\n    color?: string;\n    coupon_used?: boolean;\n    custom_properties?: {\n      name?: string;\n      value?: string;\n    };\n    material?: string;\n    model?: string;\n    size?: string;\n    vendor?: string;\n  };\n\n  /**\n   * The title of the product\n   */\n  product_title: string;\n\n  /**\n   * The url of the product\n   */\n  product_url: string;\n\n  /**\n   * The content of the review\n   */\n  review_content: string;\n\n  /**\n   * The review score. Yotpo documents this as an int32, so it is a whole number.\n   */\n  review_score: number;\n\n  /**\n   * The title of the review\n   */\n  review_title: string;\n\n  /**\n   * Required for customers using the Trusted Vendors feature. Possible values: verified_buyer or\n   * verified_reviewer.\n   */\n  reviewer_type?: string;\n\n  /**\n   * Relevant only for customers using the Trusted Vendors feature. The signature is the hexadecimal\n   * representation of a computed Hash-based Message Authentication Code, using SHA256 as the\n   * cryptographic function (HMAC-SHA256). The secret for the function is the account secret. Note:\n   * The time_stamp and reviewer_type parameters are mandatory when using the signature parameter.\n   */\n  signature?: string;\n\n  /**\n   * The unique ID of the product on your site (doesn't have to be the SKU). To create a site\n   * review, use the value: yotpo_site_reviews. This parameter supports alphanumeric (a...z, A...Z,\n   * 0...9), \"_\" and \"-\" characters.\n   */\n  sku: string;\n\n  /**\n   * Relevant only for customers using the Trusted Vendors feature. The date the review was created\n   * (if not today). Can be up to 7 days in the past. If you want to add a creation date that is\n   * more than 7 days in the past, please contact support.\n   */\n  submission_time_stamp?: string;\n\n  /**\n   * Required for customers using the Trusted Vendors feature. Unix timestamp.\n   */\n  time_stamp?: string;\n}\n\n/**\n * Send a request to Yotpo to create a product review.\n *\n * If successful, the output is the response body from Yotpo.\n *\n * If there is an error, the output is undefined.\n *\n * This is the asynchronous method and is recommended for general use.\n *\n * This call will create reviews only as anonymous and will send out review verification emails to\n * all the customers you've created reviews for.\n *\n * The following parameters are relevant only for customers using the Trusted Vendors feature:\n * signature, time_stamp, submission_time_stamp, and reviewer_type.\n *\n * Note that custom fields should carry values which are in line with the type of question being\n * asked. For example, a rating question type would carry numerical values e.g. 1-5 while a textual\n * question type would carry values such as \"good\" \"okay\" \"poor\".\n *\n * @see https://apidocs.yotpo.com/reference/create-review\n */\nexport async function createReview(input: Partial<Input>) {\n  assert(input.appkey, 'Invalid app key');\n  const path = '/apps/yotpo-ugc/v1/widget/reviews';\n\n  let response;\n  try {\n    response = await fetch(path, {\n      body: JSON.stringify(input),\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'POST',\n      mode: 'cors'\n    });\n  } catch (error) {\n    decorateError(error, 'url', path);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} creating review`);\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  if (!isCreateReviewAPIResponseBody(responseBody)) {\n    const error: any = new Error('Malformed response body in create review');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  if (responseBody.code > 399) {\n    const error: any = new Error('Response body status code is not ok');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  return responseBody;\n}\n","/**\n * Returns whether the given value looks sufficiently like the expected parsed response body for\n * creating a review.\n */\nexport function isCreateReviewAPIResponseBody(value) {\n    return typeof value === 'object' && value !== null && typeof value.code === 'number';\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a simple star rating display (0–5 stars). It doesn't render empty stars, reviews count,\n * or partial stars. It renders a visually hidden label for accessibility. Size is configured via\n * font-size on the element.\n */\nclass SimpleRatingStars extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-rating'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * The numeric rating to display, between 0 and 5. Supports only integer values.\n     */\n    rating: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && newValue !== oldValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    const rating = parseInt(this.dataset.rating, 10);\n\n    const label = this.shadowRoot.querySelector('.visually-hidden');\n    label.textContent = `${rating} out of 5 stars`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'simple-rating-stars': SimpleRatingStars;\n  }\n}\n\nif (!customElements.get('simple-rating-stars')) {\n  customElements.define('simple-rating-stars', SimpleRatingStars);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { fetchReviewsSummary, type SummaryHighlight, type SummaryTopic } from '../yotpo-fetch-reviews-summary.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders Yotpo AI summary for a product. Note that not every product will have an AI summary, as\n * it depends on the number of reviews and other factors. So we should gracefully handle the case\n * when there is no summary to display.\n *\n * @see https://support.yotpo.com/docs/adding-the-reviews-summary\n */\nclass YotpoAiSummary extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The product id, which could be a product sku, with which this element is associated.\n     */\n    productId: string;\n\n    /**\n     * Number of Yotpo customer reviews for this product\n     */\n    reviewsTotal: string;\n\n    /**\n     * The api key used to send requests to the Yotpo reviews API.\n     */\n    yotpoAppKey: string;\n  };\n\n  /**\n   * Whether this element started to fetch results.\n   */\n  private fetchStarted = false;\n\n  /**\n   * One paragraph of summary highlights returned from the Yotpo reviews summary API.\n   */\n  private paragraphSummary = '';\n\n  /**\n   * Topic-level summaries returned from the Yotpo reviews summary API.\n   */\n  private summaryTopics: SummaryTopic[] = [];\n\n  /**\n   * Individual review highlights returned from the Yotpo reviews summary API.\n   */\n  private summaryHighlights: SummaryHighlight[] = [];\n\n  private onTopicLinksClickedBound = this.onTopicLinksClicked.bind(this);\n  private onModalClosedBound = this.onModalClosed.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    topicLinks?.addEventListener('click', this.onTopicLinksClickedBound);\n\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal.addEventListener('modal-closed', this.onModalClosedBound);\n\n    this.startFetching().catch(error => {\n      // The Load failed error is a routinely occurring networking error that we are not interested\n      // in tracing. It has moderate frequency and is outside of our control.\n\n      if (error instanceof Error && error.message === 'Load failed') {\n        console.log(error);\n      } else {\n        console.warn(error);\n      }\n    });\n  }\n\n  public disconnectedCallback() {\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    topicLinks?.removeEventListener('click', this.onTopicLinksClickedBound);\n\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal?.removeEventListener('modal-closed', this.onModalClosedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * Fetches the reviews summary data from Yotpo, stores fetched data, and renders the component\n   * accordingly.\n   */\n  private async startFetching() {\n    // Allow this method to be called multiple times but only run once.\n    if (this.fetchStarted) {\n      return;\n    }\n\n    this.fetchStarted = true;\n\n    if (!this.dataset.yotpoAppKey || !this.dataset.productId) {\n      return;\n    }\n\n    const productId = parseInt(this.dataset.productId, 10);\n    if (!productId) {\n      return;\n    }\n\n    const response = await fetchReviewsSummary({\n      app_key: this.dataset.yotpoAppKey,\n      product_id: productId\n    });\n\n    this.paragraphSummary = response?.paragraphSummary || '';\n    this.summaryTopics = response?.summaryTopics || [];\n    this.summaryHighlights = response?.summaryHighlights || [];\n\n    this.render();\n  }\n\n  /**\n   * Handles the click on a topic link, which opens the drawer with the summary for that topic. Any\n   * topic button should just open the drawer no matter which topic is clicked.\n   */\n  private onTopicLinksClicked(event: MouseEvent) {\n    // We listen for clicks on the parent container, but we only want to handle clicks on buttons\n    // inside.\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    event.target.setAttribute('aria-expanded', 'true');\n\n    // Claim focus return before opening. Without this the modal manager has nothing of ours on its\n    // focus stack, so closing the drawer reaches down to whichever opener an earlier modal left\n    // there, and focusing that one scrolls the page away to wherever it sits.\n    const focusEvent = new Event('focus-stack-push-requested', {\n      bubbles: true,\n      composed: true\n    });\n    event.target.dispatchEvent(focusEvent);\n\n    // The modal manager leaves the reviews section active while the drawer is open, so the topic\n    // buttons would stay reachable by Tab behind it. The elements around us inert their own parts.\n    const content = this.shadowRoot.querySelector<HTMLElement>('.content');\n    content.inert = true;\n\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal.dataset.open = 'true';\n  }\n\n  /**\n   * Puts the topic buttons back the way they were once the drawer closes. The modal manager puts\n   * focus back on the topic button right after this runs, and a focus call into an inert subtree\n   * does nothing.\n   */\n  private onModalClosed(_event: HTMLElementEventMap['modal-closed']) {\n    const activeTopicLink = this.shadowRoot.querySelector<HTMLElement>(\n      '.topic-link[aria-expanded=\"true\"]');\n    activeTopicLink?.setAttribute('aria-expanded', 'false');\n\n    const content = this.shadowRoot.querySelector<HTMLElement>('.content');\n    content.inert = false;\n  }\n\n  private render() {\n    // Render the paragraph summary if the product has content for it\n    const copy = this.shadowRoot.querySelector<HTMLElement>('.copy');\n    if (this.paragraphSummary) {\n      copy.textContent = this.paragraphSummary;\n      copy.hidden = false;\n    } else {\n      copy.textContent = '';\n      copy.hidden = true;\n    }\n\n    // render topic links\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    for (const topicObject of this.summaryTopics) {\n      const topic = document.createElement('button');\n      topic.classList.add('topic-link');\n      topic.textContent = `${topicObject.displayTopic} (${topicObject.coverage})`;\n      topic.setAttribute('aria-expanded', 'false');\n      topic.setAttribute('aria-haspopup', 'dialog');\n      topic.setAttribute('aria-controls', 'drawer');\n\n      topicLinks.appendChild(topic);\n    }\n\n    // render drawer content\n\n    // put total reviews in the drawer subtitle\n    const total = parseInt(this.dataset.reviewsTotal, 10);\n    const formatter = new Intl.NumberFormat('en-US');\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal.dataset.subtitle = 'These are the topics customers are talking about based on ' +\n      `${formatter.format(total)} customer reviews.`;\n\n    // render summary by topics\n    const topics = this.shadowRoot.querySelector<HTMLElement>('.topics');\n    for (const topic of this.summaryTopics) {\n      const title = document.createElement('h4');\n      title.textContent = topic.displayTopic;\n\n      const copy = document.createElement('p');\n      copy.textContent = topic.summary;\n\n      const mentioned = document.createElement('p');\n      mentioned.classList.add('mentioned');\n      mentioned.textContent = `Mentioned in ${topic.coverage} of reviews`;\n\n      const li = document.createElement('li');\n      li.appendChild(title);\n      li.appendChild(copy);\n      li.appendChild(mentioned);\n\n      topics.append(li);\n    }\n\n    // render highlights\n    const highlightsList = this.shadowRoot.querySelector<HTMLElement>('.highlights ul');\n    for (const highlight of this.summaryHighlights) {\n      const blockquote = document.createElement('blockquote');\n      blockquote.textContent = highlight.highlight;\n\n      const li = document.createElement('li');\n      li.appendChild(blockquote);\n\n      highlightsList.appendChild(li);\n    }\n\n    // Show the whole content wrapper with a title if there is any content to display\n    const content = this.shadowRoot.querySelector<HTMLElement>('.content');\n    if (this.summaryHighlights.length > 0 || this.summaryTopics.length > 0 ||\n      this.paragraphSummary) {\n      content.hidden = false;\n    } else {\n      content.hidden = true;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-ai-summary': YotpoAiSummary;\n  }\n}\n\nif (!customElements.get('yotpo-ai-summary')) {\n  customElements.define('yotpo-ai-summary', YotpoAiSummary);\n}\n","import { assert } from '@integrabeauty/assert';\nimport { decorateError } from '../../lib/decorate-error.js';\n\nexport interface Input {\n  /**\n   * The store ID\n   */\n  app_key: string;\n\n  /**\n   * Shopify product ID\n   */\n  product_id: number;\n}\n\nexport interface SummaryTopic {\n  avgSentiment: number;\n\n  /**\n   * Number of reviews covering this topic\n   */\n  coverage: string;\n\n  displayTopic: string;\n\n  summary: string;\n\n  topic: string;\n}\n\nexport interface SummaryHighlight {\n  highlight: string;\n\n  reviewId: number;\n\n  reviewerName: string;\n}\n\ninterface ResponseData {\n  bundledProductsData: unknown;\n  countries: unknown;\n  customQuestions: unknown;\n  externalId: string;\n  metadata: unknown;\n\n  /**\n   * A paragraph summarizing all reviews for the product\n   */\n  paragraphSummary: string;\n\n  productId: string;\n  productVariants: unknown;\n  storeId: string;\n  summaryHighlights: SummaryHighlight[];\n  summaryTopics: SummaryTopic[];\n  topics: unknown[];\n}\n\nexport interface ResponseBody {\n  response: ResponseData;\n\n  status: {\n    /**\n     * @example 200\n     */\n    code: number;\n\n    /**\n     * @example \"OK\"\n     */\n    message: string;\n  };\n}\n\n/**\n * Fetch AI-generated review summary data for a product from Yotpo.\n */\nexport async function fetchReviewsSummary(input: Input) {\n  assert(input.app_key, 'Invalid app key');\n  assert(Number.isInteger(input.product_id) && input.product_id > 0, 'Invalid product id');\n\n  const path = `/apps/yotpo-ugc-api-cdn/v3/storefront/stores/${input.app_key}/products/` +\n    `${input.product_id}/productData?include=reviewsSummary`;\n\n  const response = await fetch(path);\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching reviews summary`);\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'code', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let body;\n  try {\n    body = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'code', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isResponseBody(body)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = body;\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  if (body.status.code > 399) {\n    const error: any = new Error('Nested response body status code is not 200');\n    error.body = body;\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  return body.response;\n}\n\nfunction isResponseBody(value: any): value is ResponseBody {\n  return typeof value === 'object' && value !== null && typeof value.status === 'object' &&\n    value.status !== null && typeof value.response === 'object' && value.response !== null;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders Yotpo product reviews filters.\n */\nclass YotpoFilters extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The product id, which could be a product sku, with which this element is associated.\n     */\n    productId: string;\n  };\n\n  private declare selectedFilters: YotpoReviewFilters | null;\n  private declare selectedSortings: YotpoReviewSortings;\n\n  private declare searchDebounceTimer: ReturnType<typeof setTimeout>;\n\n  private onHairTypeSelectedBound = this.onHairTypeSelected.bind(this);\n  private onMediaSelectedBound = this.onMediaSelected.bind(this);\n  private onRatingSelectedBound = this.onRatingSelected.bind(this);\n  private onSearchClearedBound = this.onSearchCleared.bind(this);\n  private onSearchKeyUpBound = this.onSearchKeyUp.bind(this);\n  private onSortSelectedBound = this.onSortSelected.bind(this);\n  private onStarsFilterClickedBound = this.onStarsFilterClicked.bind(this);\n  private onReviewsFilterChangeBound = this.onReviewsFilterChange.bind(this);\n  private onPillClickedBound = this.onPillClicked.bind(this);\n  private onClearAllClickedBound = this.onClearAllClicked.bind(this);\n  private onClearRequestedBound = this.onClearRequested.bind(this);\n  private onSearchStartedBound = this.onSearchStarted.bind(this);\n  private onSearchCompletedBound = this.onSearchCompleted.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    searchInput?.addEventListener('keyup', this.onSearchKeyUpBound);\n    searchInput?.addEventListener('search', this.onSearchClearedBound);\n\n    const hairTypeSelect = this.shadowRoot.querySelector('.filter-hair-type select');\n    hairTypeSelect?.addEventListener('change', this.onHairTypeSelectedBound);\n\n    const mediaSelect = this.shadowRoot.querySelector('.filter-media select');\n    mediaSelect?.addEventListener('change', this.onMediaSelectedBound);\n\n    const ratingSelect = this.shadowRoot.querySelector('.filter-rating select');\n    ratingSelect?.addEventListener('change', this.onRatingSelectedBound);\n\n    const sortSelect = this.shadowRoot.querySelector('.filter-sort select');\n    sortSelect?.addEventListener('change', this.onSortSelectedBound);\n\n    addEventListener('star-filter-clicked', this.onStarsFilterClickedBound);\n    addEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    addEventListener('yotpo-filter-clear-requested', this.onClearRequestedBound);\n    addEventListener('yotpo-reviews-search-started', this.onSearchStartedBound);\n    addEventListener('yotpo-reviews-searched', this.onSearchCompletedBound);\n  }\n\n  public disconnectedCallback() {\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    searchInput?.removeEventListener('keyup', this.onSearchKeyUpBound);\n    searchInput?.removeEventListener('search', this.onSearchClearedBound);\n\n    const hairTypeSelect = this.shadowRoot.querySelector('.filter-hair-type select');\n    hairTypeSelect?.removeEventListener('change', this.onHairTypeSelectedBound);\n\n    const mediaSelect = this.shadowRoot.querySelector('.filter-media select');\n    mediaSelect?.removeEventListener('change', this.onMediaSelectedBound);\n\n    const ratingSelect = this.shadowRoot.querySelector('.filter-rating select');\n    ratingSelect?.removeEventListener('change', this.onRatingSelectedBound);\n\n    const sort = this.shadowRoot.querySelector('.filter-sort select');\n    sort?.removeEventListener('change', this.onSortSelectedBound);\n\n    const pills = this.shadowRoot.querySelectorAll<HTMLElement>('.pill');\n    for (const pill of pills) {\n      pill.removeEventListener('click', this.onPillClickedBound);\n    }\n\n    const clearAll = this.shadowRoot.querySelector<HTMLElement>('.clear');\n    clearAll?.removeEventListener('click', this.onClearAllClickedBound);\n\n    removeEventListener('star-filter-clicked', this.onStarsFilterClickedBound);\n    removeEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    removeEventListener('yotpo-filter-clear-requested', this.onClearRequestedBound);\n    removeEventListener('yotpo-reviews-search-started', this.onSearchStartedBound);\n    removeEventListener('yotpo-reviews-searched', this.onSearchCompletedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onSearchCleared(event: Event) {\n    if (event.currentTarget instanceof HTMLInputElement && event.currentTarget.value === '') {\n      clearTimeout(this.searchDebounceTimer);\n      this.dispatchFilter({ free_text_search: undefined });\n    }\n  }\n\n  private onSearchKeyUp(event: KeyboardEvent) {\n    event.stopPropagation();\n\n    if (!(event.currentTarget instanceof HTMLInputElement)) {\n      return;\n    }\n\n    const query = event.currentTarget.value;\n\n    clearTimeout(this.searchDebounceTimer);\n\n    if (!query) {\n      this.dispatchFilter({ free_text_search: undefined });\n      return;\n    }\n\n    let timeout = 500;\n    if (query.length < 4) {\n      timeout += 500;\n    }\n\n    this.searchDebounceTimer = setTimeout(this.dispatchFilter.bind(this), timeout,\n      { free_text_search: query });\n  }\n\n  private onHairTypeSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '') {\n      this.dispatchFilter({ hair_type: undefined });\n      return;\n    }\n\n    this.dispatchFilter({ hair_type: event.target.value });\n  }\n\n  private onRatingSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '0') {\n      this.dispatchFilter({ scores: [] });\n      return;\n    }\n\n    const score = parseInt(event.target.value, 10);\n    this.dispatchFilter({ scores: [score] });\n  }\n\n  /**\n   * Handles stars link click on the rating breakdown component\n   */\n  private onStarsFilterClicked(event: WindowEventMap['star-filter-clicked']) {\n    const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n    select.value = event.detail.stars.toString();\n    this.dispatchFilter({ scores: [event.detail.stars] });\n  }\n\n  private onMediaSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '') {\n      this.dispatchFilter({ pictured: null });\n      return;\n    }\n\n    this.dispatchFilter({ pictured: event.target.value === 'true' });\n  }\n\n  private onSortSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '0') {\n      return;\n    }\n\n    const sort = event.target.value;\n\n    let ascending = false;\n    let name = sort;\n\n    if (sort.includes('score')) {\n      name = 'score';\n      const [,direction] = sort.split('_');\n      if (direction === 'up') {\n        ascending = true;\n      }\n    }\n\n    this.dispatchFilter({}, [{ sort_by: name, ascending }]);\n  }\n\n  private dispatchFilter(filters: YotpoReviewFilters, sortings?: YotpoReviewSortings) {\n    if (!this.dataset.productId) {\n      return;\n    }\n\n    const selected = this.selectedFilters || {};\n    const merged = { ...selected, ...filters };\n\n    type Detail = HTMLElementEventMap['yotpo-reviews-filtered']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-reviews-filtered', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: merged,\n\n        // Go back to the first page whenever filters change. Otherwise, if the user is on page 5,\n        // for example, but the new filters only return 2 pages of results, no reviews would be\n        // shown.\n        page: 0,\n\n        productId: parseInt(this.dataset.productId, 10),\n\n        // People don't think of the sort as a filter. It is a preference about how they want to\n        // read the list, so it stays put unless this is the sort itself changing.\n        sortings: sortings || this.selectedSortings\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onReviewsFilterChange(event: HTMLElementEventMap['yotpo-reviews-filtered']) {\n    this.selectedFilters = event.detail.filters;\n    this.selectedSortings = event.detail.sortings;\n    this.renderAppliedFilters();\n  }\n\n  private renderAppliedFilters() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.pills');\n    if (!container) {\n      return;\n    }\n\n    // Remove all existing pills and their event listeners before rendering new ones.\n    const existingPills = container.querySelectorAll<HTMLElement>('.pill');\n    for (const pill of existingPills) {\n      pill.removeEventListener('click', this.onPillClickedBound);\n    }\n\n    const clearButton = container.querySelector<HTMLElement>('.clear');\n    if (clearButton) {\n      clearButton.removeEventListener('click', this.onClearAllClickedBound);\n    }\n\n    container.innerHTML = '';\n\n    const filters = this.selectedFilters;\n    if (!filters) {\n      container.hidden = true;\n      return;\n    }\n\n    // building the pills array to render the applied filters\n    const pills: { dimension: string; label: string; }[] = [];\n\n    if (filters.scores && filters.scores.length > 0) {\n      for (const score of filters.scores) {\n        pills.push({ dimension: 'scores', label: 'Rating: ' + score + ' Stars' });\n      }\n    }\n\n    if (filters.pictured === true) {\n      pills.push({ dimension: 'pictured', label: 'With Images & Videos' });\n    } else if (filters.pictured === false) {\n      pills.push({ dimension: 'pictured', label: 'Without Images & Videos' });\n    }\n\n    if (filters.hair_type) {\n      pills.push({ dimension: 'hair_type', label: 'Hair Type: ' + filters.hair_type });\n    }\n\n    if (filters.free_text_search) {\n      pills.push({ dimension: 'free_text_search', label: 'Search: ' + filters.free_text_search });\n    }\n\n    // if no filters applied then hide the pills container\n    if (pills.length === 0) {\n      container.hidden = true;\n      return;\n    }\n\n    for (const pill of pills) {\n      const button = document.createElement('button');\n      button.className = 'pill';\n      button.dataset.dimension = pill.dimension;\n      button.addEventListener('click', this.onPillClickedBound);\n\n      const span = document.createElement('span');\n      span.textContent = pill.label;\n      button.appendChild(span);\n      container.appendChild(button);\n    }\n\n    const clearAll = document.createElement('button');\n    clearAll.className = 'clear';\n    clearAll.textContent = 'CLEAR ALL';\n    clearAll.addEventListener('click', this.onClearAllClickedBound);\n    container.appendChild(clearAll);\n\n    container.hidden = false;\n  }\n\n  private onPillClicked(event: MouseEvent) {\n    if (!(event.currentTarget instanceof HTMLElement)) {\n      return;\n    }\n\n    const dimension = event.currentTarget.dataset.dimension;\n    if (!dimension) {\n      return;\n    }\n\n    if (dimension === 'scores') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n      if (select) {\n        select.value = '0';\n      }\n\n      this.dispatchFilter({ scores: [] });\n    } else if (dimension === 'pictured') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-media select');\n      if (select) {\n        select.value = '';\n      }\n\n      this.dispatchFilter({ pictured: null });\n    } else if (dimension === 'hair_type') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-hair-type select');\n      if (select) {\n        select.value = '';\n      }\n\n      this.dispatchFilter({ hair_type: undefined });\n    } else if (dimension === 'free_text_search') {\n      const input = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n      if (input) {\n        input.value = '';\n      }\n\n      this.dispatchFilter({ free_text_search: undefined });\n    }\n  }\n\n  private onClearAllClicked(_event: MouseEvent) {\n    this.clearAllFilters();\n  }\n\n  private onClearRequested(_event: WindowEventMap['yotpo-filter-clear-requested']) {\n    this.clearAllFilters();\n  }\n\n  private onSearchStarted() {\n    const inputs = this.shadowRoot.querySelectorAll<HTMLInputElement | HTMLSelectElement>(\n      'input, select');\n    for (const input of inputs) {\n      input.disabled = true;\n    }\n  }\n\n  private onSearchCompleted() {\n    const inputs = this.shadowRoot.querySelectorAll<HTMLInputElement | HTMLSelectElement>(\n      'input, select');\n    for (const input of inputs) {\n      input.disabled = false;\n    }\n  }\n\n  private clearAllFilters() {\n    const ratingSelect = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n    if (ratingSelect) {\n      ratingSelect.value = '0';\n    }\n\n    const mediaSelect = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-media select');\n    if (mediaSelect) {\n      mediaSelect.value = '';\n    }\n\n    const hairTypeSelect = this.shadowRoot.querySelector<HTMLSelectElement>(\n      '.filter-hair-type select');\n    if (hairTypeSelect) {\n      hairTypeSelect.value = '';\n    }\n\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    if (searchInput) {\n      searchInput.value = '';\n    }\n\n    this.dispatchFilter({\n      free_text_search: undefined,\n      hair_type: undefined,\n      pictured: null,\n      scores: []\n    });\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-filters': YotpoFilters;\n  }\n}\n\nif (!customElements.get('yotpo-filters')) {\n  customElements.define('yotpo-filters', YotpoFilters);\n}\n","/**\n * Initialized on first call to encodeEntities, then re-used\n */\nlet element: HTMLTextAreaElement;\n\n/**\n * Encodes HTML entities in a mostly safe manner but at the expense of touching the dom.\n */\nexport function decode(input: string) {\n  if (!element) {\n    element = document.createElement('textarea');\n  }\n\n  element.innerHTML = input;\n\n  const output = element.value;\n\n  // avoid leaving around stuff in dom indefinitely, even when detached\n  element.innerHTML = '';\n\n  return output;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders pagination links for Yotpo reviews and questions.\n */\nclass YotpoPagination extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-page', 'data-update'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Currently rendered page number.\n     */\n    page: string;\n\n    /**\n     * Number of items to be rendered per page.\n     */\n    perPage: string;\n\n    /**\n     * Number of all items to be rendered.\n     */\n    total: string;\n\n    /**\n     * Setting this attribute triggers rendering.\n     */\n    update: string;\n  };\n\n  /**\n   * Currently selected page.\n   */\n  private currentPage = 0;\n\n  /**\n   * Total number of pages.\n   */\n  private lastPage = 0;\n\n  /**\n   * Max number of page buttons to display. Arrow buttons are not included. We use it to determine\n   * if we need to show the arrow buttons and change it depending on screen size to be sure the\n   * device can fit the pagination. We can't use less than 5 because we need to show the first and\n   * last page buttons, and two ellipsis buttons. If we use 5, we will show the first page, the\n   * current page, the last page, and two ellipses (\"< 1 … 5 … 10 >\"). Use only odd numbers to\n   * ensure the current page is always in the middle of the pagination buttons.\n   */\n  private maxButtons = 5;\n\n  /**\n   * Media query to determine if the screen is larger than 768px. If so, we will show more buttons.\n   */\n  private declare mediaQuery: MediaQueryList;\n\n  private onPreviousButtonClickedBound = this.onPreviousButtonClicked.bind(this);\n  private onNextButtonClickedBound = this.onNextButtonClicked.bind(this);\n  private onPageClickedBound = this.onPageClicked.bind(this);\n  private onMediaQueryChangedBound = this.onMediaQueryChanged.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const prevButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-prev');\n    prevButton?.addEventListener('click', this.onPreviousButtonClickedBound);\n\n    const nextButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-next');\n    nextButton?.addEventListener('click', this.onNextButtonClickedBound);\n\n    const pages = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n    pages?.addEventListener('click', this.onPageClickedBound);\n\n    this.mediaQuery = matchMedia('(min-width: 768px)');\n    this.mediaQuery.addEventListener('change', this.onMediaQueryChangedBound);\n\n    // Check if the media query matches on initial load and update the maxButtons value accordingly\n    // before the first render.\n    if (this.mediaQuery.matches) {\n      this.maxButtons = 7;\n    }\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const prevButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-prev');\n    prevButton?.removeEventListener('click', this.onPreviousButtonClickedBound);\n\n    const nextButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-next');\n    nextButton?.removeEventListener('click', this.onNextButtonClickedBound);\n\n    const pages = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n    pages?.removeEventListener('click', this.onPageClickedBound);\n\n    if (this.mediaQuery) {\n      this.mediaQuery.removeEventListener('change', this.onMediaQueryChangedBound);\n    }\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (oldValue !== newValue) {\n      switch (name) {\n        case 'data-page':\n          this.currentPage = parseInt(newValue, 10);\n          break;\n        case 'data-update':\n          this.render();\n          break;\n      }\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onPreviousButtonClicked(_event: MouseEvent) {\n    if (this.currentPage === 1) {\n      return;\n    }\n\n    // We leave the current page alone here. It only changes once the reviews element has fetched\n    // the new page and hands the page number back, so the highlight never gets ahead of the reviews\n    // on screen, and stays put if the fetch fails.\n\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: this.currentPage - 1\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onNextButtonClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: this.currentPage + 1 > this.lastPage ? this.lastPage : this.currentPage + 1\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Handles clicks on page buttons. We are using event delegation and listening for clicks on the\n   * wrapper.\n   */\n  private onPageClicked(event: MouseEvent) {\n    // The ellipsis is a span, not a button, so we need to check that the click target is a button.\n    // It has the same class as the button because the styles are almost the same, so the\n    // classList.contains check below is not enough.\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    // We are listening for clicks on the wrapper element, so we need to check if the click target\n    // is a page button. If not, we can ignore the click.\n    if (!event.target.classList.contains('page-button')) {\n      return;\n    }\n\n    // Prevent clicking on the active page button. We don't want to trigger a page change event if\n    // the user clicks on the active page button. We can ignore the click in that case.\n    if (event.target.classList.contains('active')) {\n      return;\n    }\n\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const changeEvent = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: parseInt(event.target.dataset.idx, 10)\n      }\n    });\n    this.dispatchEvent(changeEvent);\n  }\n\n  /**\n   * Handles the media query changes. We use this to update the maxButtons property and rerender the\n   * pagination to reflect the new maxButtons value. This is important because we want to show more\n   * buttons on larger screens and less on smaller screens.\n   */\n  private onMediaQueryChanged(mediaQueryList: MediaQueryListEvent) {\n    let newMaxButtons: number;\n    if (mediaQueryList.matches) {\n      newMaxButtons = 7;\n    } else {\n      newMaxButtons = 5;\n    }\n\n    // rerender only if the maxButtons value has changed\n    if (newMaxButtons !== this.maxButtons) {\n      this.maxButtons = newMaxButtons;\n      this.render();\n    }\n  }\n\n  private render() {\n    const listWrapper = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n\n    // Reset on every render. One reason this is important is because on tab switch there might be\n    // zero results. Total gets updated to 0 and this re-renders. We do not want to keep showing old\n    // pagination.\n    listWrapper.innerHTML = '';\n\n    const buttonPrev = this.shadowRoot.querySelector<HTMLButtonElement>('.pagination-prev');\n    if (buttonPrev) {\n      buttonPrev.hidden = true;\n    }\n\n    const buttonNext = this.shadowRoot.querySelector<HTMLButtonElement>('.pagination-next');\n    if (buttonNext) {\n      buttonNext.hidden = true;\n    }\n\n    const perPage = parseInt(this.dataset.perPage, 10);\n    const total = parseInt(this.dataset.total, 10) || 0;\n\n    if (this.currentPage === 0 || perPage === 0 || total === 0) {\n      return;\n    }\n\n    // Disable left arrow on first page.\n    if (buttonPrev) {\n      if (this.currentPage === 1) {\n        buttonPrev.disabled = true;\n      } else {\n        buttonPrev.disabled = false;\n      }\n    }\n\n    this.lastPage = Math.ceil(total / perPage);\n\n    // The middle item index is used to determine the correct starting index. We subtract 1 because\n    // the second-to-last item is reserved for the ellipsis\n    const middleItemIndex = Math.floor(this.maxButtons / 2);\n\n    // Disable right arrow on last page.\n    if (buttonNext) {\n      if (this.currentPage >= this.lastPage) {\n        buttonNext.disabled = true;\n      } else {\n        buttonNext.disabled = false;\n      }\n    }\n\n    // if all pages can't be displayed, then show the prev and next buttons\n    if (this.lastPage > this.maxButtons) {\n      if (buttonPrev) {\n        buttonPrev.hidden = false;\n      }\n\n      if (buttonNext) {\n        buttonNext.hidden = false;\n      }\n    }\n\n    // Set the starting index. It's an index of the first button to display. We want to display the\n    // current page in the middle of the pagination buttons, so we subtract the middle item index\n    // from the current page to get the starting index. This will ensure that the current page is\n    // always in the middle of the pagination buttons, unless we are at the beginning or end of the\n    // pagination.\n    let startingIndex = this.currentPage - middleItemIndex;\n\n    // If the current page is less than or equal to the middle item index, we want to start\n    // displaying the buttons from the first page.\n    if (startingIndex <= 0) {\n      startingIndex = 1;\n    }\n\n    // If the starting index plus the max buttons is greater than the last page, that means we are\n    // at the end of pagination. In that case, we want to start displaying the buttons from the last\n    // page minus the max buttons.\n    if (startingIndex + this.maxButtons > this.lastPage) {\n      startingIndex = (this.lastPage - this.maxButtons) + 1;\n    }\n\n    // Set the number of items to display. Usually, we show the maximum number of buttons, set by\n    // this.maxButtons. But if there are fewer pages than maxButtons, we show all pages, so the\n    // starting index should be 1 and the number of items to display should be the last page.\n    let numberOfItemsToDisplay = this.maxButtons;\n    if (this.lastPage < this.maxButtons) {\n      numberOfItemsToDisplay = this.lastPage;\n      startingIndex = 1;\n    }\n\n    // The first ellipsis (second item after the first page) should be rendered if the starting\n    // index is greater than 1, which means there are more pages before the starting index that are\n    // not being displayed.\n    const showFirstEllipsis = startingIndex > 1;\n\n    // Check if we need to show the ellipsis at the end of the pagination before the last page. We\n    // need to subtract 1 because of the ellipsis at the beginning of the pagination right after the\n    // first page.\n    const showSecondEllipsis = startingIndex + numberOfItemsToDisplay - 1 < this.lastPage;\n\n    // Rendering pagination buttons\n    for (let i = 0; i < numberOfItemsToDisplay; i++) {\n      // The current index is the starting index plus the loop index. This is used to determine\n      // which page number to display on the button\n      let currentIndex = startingIndex + i;\n\n      // If it's the last button to render we need to always display the last page number\n      if (i + 1 === numberOfItemsToDisplay) {\n        currentIndex = this.lastPage;\n      }\n\n      // If it's the first button to render and we need to show the first ellipsis, we need to\n      // always display the first page number\n      if (showFirstEllipsis && i === 0) {\n        currentIndex = 1;\n      }\n\n      // Check if we need to render an ellipsis instead of page button. It happens in two cases:\n      // 1) when the first ellipsis should be rendered and we are rendering the second item\n      // 2) when the second ellipsis should be rendered and we are rendering the second-to-last item\n      const shouldRenderEllipsis = showFirstEllipsis && i === 1 ||\n        showSecondEllipsis && i === numberOfItemsToDisplay - 2;\n      if (shouldRenderEllipsis) {\n        const ellipsis = document.createElement('span');\n        ellipsis.textContent = '...';\n        ellipsis.classList.add('page-button');\n\n        const li = document.createElement('li');\n        li.appendChild(ellipsis);\n        listWrapper.appendChild(li);\n      } else {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.classList.add('page-button');\n        button.dataset.idx = `${currentIndex}`;\n        button.textContent = `${currentIndex}`;\n        button.setAttribute('aria-label', `Go to page ${currentIndex}`);\n\n        // set the active pagination button\n        if (this.currentPage === currentIndex) {\n          button.classList.add('active');\n          button.setAttribute('aria-current', 'page');\n        }\n\n        const li = document.createElement('li');\n        li.appendChild(button);\n        listWrapper.appendChild(li);\n      }\n    }\n  }\n}\n\n/**\n * Fired when a user interacts with the pagination element.\n */\ntype PageChangedEvent = CustomEvent<{\n  page: number;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-page-changed': PageChangedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-pagination': YotpoPagination;\n  }\n}\n\nif (!customElements.get('yotpo-pagination')) {\n  customElements.define('yotpo-pagination', YotpoPagination);\n}\n","import type { LegacyReview } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../../lib/decorate-error.js';\nimport { type CustomReviewField, type SearchResponseBody, searchProductReviews as liveSearchProductReviews } from './yotpo-search-product-reviews.js';\n\n/**\n * Where in the result set a response sits. The cached shape and the live shape disagree on the\n * spelling of the page size, so this module settles on the snake_case one the components use and\n * translates the live response into it.\n */\nexport interface Pagination {\n  page: number;\n  per_page: number;\n  total: number;\n}\n\n/**\n * One ordering our components ask for, under the snake_case name they use for it.\n */\nexport interface FilterSorting {\n  ascending?: boolean;\n  sort_by?: string;\n}\n\n/**\n * The filters our own components speak, in the snake_case names they have always used. The endpoint\n * reads camelCase, so this module translates on the way down.\n */\nexport interface Filters {\n  crfs: CustomReviewField[];\n  free_text_search: string;\n  pictured: boolean;\n  scores: number[];\n  sortings: FilterSorting[];\n  topic_names: string[];\n}\n\nexport interface Input {\n  /**\n   * The Yotpo store, which Yotpo's admin calls the app key.\n   */\n  app_key: string;\n\n  /**\n   * Read the product's first page of reviews out of our own cache instead of asking Yotpo. Only the\n   * unfiltered first page is cached, so this cannot be combined with a filter or a page number.\n   */\n  cache?: boolean;\n\n  /**\n   * Which reviews to narrow the results down to. Every filter is optional, so leaving this out asks\n   * for all of the product's reviews.\n   */\n  filters?: Partial<Filters>;\n\n  /**\n   * Which page of results to return, counting from 1. Leave it out for the first page.\n   */\n  page?: number;\n\n  /**\n   * The number of results to return per page\n   */\n  per_page: number;\n\n  /**\n   * The Shopify id of the product the reviews are about.\n   */\n  product_id: number;\n}\n\n/**\n * What sits under the cached body's `response` key, carrying the older snake_case field names. This\n * is the payload rather than a body of its own, and it is what the cached branch hands back. The\n * reviews component decides per review at runtime which of the two shapes it got.\n */\ninterface LegacyPayload {\n  /**\n   * The product's review count and average score. The live search does not return this, so it is\n   * one of the ways the two shapes differ.\n   */\n  bottomline: Record<string, unknown>;\n\n  found_filtered_reviews: boolean;\n  grouping_data: Record<string, unknown>;\n  pagination: Pagination;\n  product_tags: string[];\n  products: Record<string, unknown>[];\n  reviews: LegacyReview[];\n  syndication_data: Record<string, unknown>;\n}\n\n/**\n * The body Yotpo's live search gives back, with its pagination rewritten into the snake_case shape\n * the cached response already uses. Everything else comes through untouched.\n */\ninterface LiveResponseBody extends Omit<SearchResponseBody, 'pagination'> {\n  pagination: Pagination;\n}\n\n/**\n * The whole body the cache file holds, where the payload sits under a status wrapper. Yotpo's live\n * search has no such wrapper, which is the other way the two shapes differ.\n */\nexport interface CachedResponseBody {\n  response: LegacyPayload;\n\n  status: {\n    /**\n     * @example 200\n     */\n    code: number;\n\n    /**\n     * @example \"OK\"\n     */\n    message: string;\n  };\n}\n\n/**\n * Fetch a product's reviews, from our cache when the caller asks for it and from Yotpo otherwise.\n *\n * The cache is a JSON file we generate per product holding its unfiltered first page of reviews. It\n * exists because the first paint of the reviews list should not wait on Yotpo, so the initial load\n * reads the file and everything after it goes to Yotpo. A cached read answers with the older\n * snake_case shape, so callers get one of two response shapes and have to handle both.\n *\n * The cache also runs behind, so a cached review count comes in lower than the live one. Treat it\n * as approximate.\n *\n * This is the module to call. It holds what we know about how this store uses reviews, and\n * searchProductReviews underneath it stays a plain client for the Yotpo endpoint.\n */\nexport async function searchProductReviews(input: Input) {\n  if (!input.cache) {\n    const sortings = [];\n    for (const sorting of input.filters?.sortings || []) {\n      sortings.push({\n        ascending: sorting.ascending,\n        sortBy: sorting.sort_by\n      });\n    }\n\n    // Reaching api-cdn.yotpo.com from the browser would be a cross origin request, so we go through\n    // the Shopify app proxy that sits on our own domain.\n    const response = await liveSearchProductReviews({\n      baseUrl: 'https://langehair.com',\n      crfs: input.filters?.crfs,\n      freeTextSearch: input.filters?.free_text_search,\n      page: input.page,\n      pathPrefix: '/apps/yotpo-ugc-api-cdn',\n      perPage: input.per_page,\n      pictured: input.filters?.pictured,\n      productId: input.product_id,\n      scores: input.filters?.scores,\n      sortings,\n      storeId: input.app_key,\n      topicNames: input.filters?.topic_names\n    });\n\n    const output: LiveResponseBody = {\n      ...response,\n      pagination: {\n        page: response.pagination.page,\n        per_page: response.pagination.perPage,\n        total: response.pagination.total\n      }\n    };\n\n    return output;\n  }\n\n  const pathPrefix = '/apps/yotpo-reviews-cache';\n  const path = pathPrefix + `/lange-product-reviews-${input.product_id}-first-page.json`;\n  const url = new URL(path, 'https://langehair.com');\n\n  let response;\n  try {\n    response = await fetch(url);\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching product reviews`);\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isCachedResponseBody(responseBody)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  if (responseBody.status.code > 399) {\n    const error: any = new Error('Nested response body status code is not 200');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  // This unwraps the response body to return just the useful bits.\n  const payload = responseBody.response;\n\n  return payload;\n}\n\n/**\n * Returns whether the given value satisfies the type's schema. This does not fully validate the\n * values, this just provides runtime type safety.\n */\nfunction isCachedResponseBody(value: any): value is CachedResponseBody {\n  return typeof value === 'object' && value !== null && typeof value.status === 'object' &&\n    value.status !== null;\n}\n","import { assert } from '@integrabeauty/assert';\nimport type { Review } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../../lib/decorate-error.js';\n\n/**\n * One of the custom questions set up in the Yotpo admin, such as the hair type a customer picks\n * when writing a review. The question id is the Yotpo id of that question and the answers are the\n * values to match against what the customer answered.\n *\n * displayAnswers does not affect which reviews match, so it reads as a display hint.\n */\nexport interface CustomReviewField {\n  answers: string[];\n  displayAnswers?: string[];\n  questionId: number;\n}\n\n/**\n * One entry of the order metadata a review came in with, which Yotpo describes as covering\n * customer, product, and order properties. The question is the field's label and the answers are\n * the values to match against it.\n */\nexport interface OrderMetadataField {\n  answers: string[];\n  question: string;\n\n  /**\n   * A string here, unlike the number a custom review field uses for the same idea. That is what the\n   * example in Yotpo's OpenAPI definition shows.\n   */\n  questionId: string;\n\n  subType: string;\n}\n\n/**\n * One entry of the custom metadata collected alongside a review. The question is the field's label\n * and the answers are the values to match against it.\n */\nexport interface ReviewMetadataField {\n  answers: string[];\n  fieldType: string;\n  question: string;\n}\n\n/**\n * One ordering to apply to the results. Several can be sent at once.\n */\nexport interface Sorting {\n  /**\n   * True sorts ascending and false sorts descending.\n   */\n  ascending?: boolean;\n\n  /**\n   * What to sort on. `score`, `rating`, `date`, `votes_up`, `images`, `smart`, and `_score` all\n   * work, and `score` and `rating` behave identically. `image_count` and `reviewer_type` return\n   * nothing, so do not carry those two over from Yotpo's older docs.\n   */\n  sortBy?: string;\n}\n\n/**\n * How to search Yotpo for product reviews.\n *\n * @see https://apidocs.yotpo.com/v3.0/reference/storefront-product-reviews-search.md\n */\nexport interface Input {\n  /**\n   * What the path is resolved against, handed straight to the URL constructor as its base. Leaving\n   * it out means the path has to stand on its own as a full URL, which is what pathPrefix is for.\n   *\n   * @example 'https://langehair.com'\n   */\n  baseUrl?: string;\n\n  /**\n   * The country the reviewer ordered from, as a two letter code.\n   */\n  country?: string;\n\n  /**\n   * The custom review fields to filter on, which is what Yotpo abbreviates as crfs.\n   */\n  crfs?: CustomReviewField[];\n\n  /**\n   * Which way to sort. `asc` and `desc` both work. Yotpo documents the parameter without saying\n   * which values it takes, so those two are the ones we know of.\n   */\n  direction?: string;\n\n  /**\n   * Free text search terms. Supported languages are English, French, German, Italian, Norwegian,\n   * Portuguese, Spanish, Russian, Swedish, and Turkish.\n   */\n  freeTextSearch?: string;\n\n  /**\n   * Return only these reviews, by Yotpo review id.\n   */\n  ids?: number[];\n\n  /**\n   * A language code, which Yotpo documents as localizing the review content. It changed nothing in\n   * anything we tried, so we do not know what makes it do something. Unverified.\n   */\n  lang?: string;\n\n  /**\n   * Custom metadata collected with the review. Unverified, because we could not find a review\n   * carrying any metadata to match against.\n   */\n  metadata?: ReviewMetadataField[];\n\n  /**\n   * Order metadata the review came in with. Unverified for the same reason as metadata.\n   */\n  orders?: OrderMetadataField[];\n\n  /**\n   * Which page of results to return, counting from 1. Leave it out for the first page.\n   */\n  page?: number;\n\n  /**\n   * Goes in front of the endpoint's own path, with no trailing slash. Leave it out to request the\n   * path as Yotpo documents it. A caller that reaches the endpoint through a proxy passes the\n   * proxy's prefix here.\n   *\n   * @example '/apps/yotpo-ugc-api-cdn'\n   */\n  pathPrefix?: string;\n\n  /**\n   * How many reviews to return per page.\n   */\n  perPage: number;\n\n  /**\n   * True returns only reviews with media, meaning pictures and videos, and false returns only\n   * reviews without it. Leave it out to get reviews either way.\n   */\n  pictured?: boolean;\n\n  /**\n   * How many pictures to return per review. It capped nothing, so it looks ignored here.\n   * Unverified.\n   */\n  picturesPerReview?: number;\n\n  /**\n   * The product to return reviews for. Yotpo matches this against the product's Shopify id.\n   */\n  productId: number;\n\n  /**\n   * The variants the reviewer bought, as option name to option value, such as\n   * `{ Size: 'Grande', Color: 'Blush' }`.\n   */\n  productVariants?: Record<string, string>;\n\n  /**\n   * The star ratings to return, so 4 returns the four star reviews.\n   */\n  scores?: number[];\n\n  /**\n   * How to order the results. Each of these gives a different ordering, and a value outside the\n   * list is ignored rather than rejected, so a typo silently returns the default order.\n   *\n   * This overlaps with the sortings filter in the request body, which reaches the same orderings\n   * through its own names. We have not checked which one wins when both are set.\n   */\n  sort?: 'badge' | 'date' | 'images' | 'rating' | 'smart';\n\n  /**\n   * How to order the results, under the endpoint's own sorting names. See sort for how the two\n   * overlap.\n   */\n  sortings?: Sorting[];\n\n  /**\n   * Which Yotpo store to search. This is the value Yotpo's admin calls the app key.\n   */\n  storeId: string;\n\n  /**\n   * The topics mentioned in the review, matching the names the trending topics endpoint returns.\n   */\n  topicNames?: string[];\n}\n\n/**\n * Which of the parameters travel in the JSON body. Everything else is a path param, a query param,\n * or one of ours for saying where to send the request.\n */\ntype RequestBody = Pick<Input, 'country' | 'crfs' | 'freeTextSearch' | 'ids' | 'metadata' |\n  'orders' | 'pictured' | 'picturesPerReview' | 'productVariants' | 'scores' | 'sortings' |\n  'topicNames'>;\n\n/**\n * Part of the response body\n */\nexport interface Pagination {\n  /**\n   * @example 1\n   */\n  page: number;\n\n  /**\n   * @example 1\n   */\n  perPage: number;\n\n  /**\n   * @example 2\n   */\n  total: number;\n}\n\n/**\n * The whole body this endpoint returns, with no status wrapper around it. Every other Yotpo\n * endpoint we call wraps its payload, and this one is the outlier. These seven fields are the whole\n * top level.\n */\nexport interface SearchResponseBody {\n  foundFilteredReviews: boolean;\n  groupingData: Record<string, unknown>;\n  pagination: Pagination;\n  productTags: string[];\n  products: Record<string, unknown>[];\n  reviews: Review[];\n\n  /**\n   * Null for everything we have seen, so we do not know the shape when it is populated.\n   */\n  syndicationData: Record<string, unknown>;\n}\n\n/**\n * Search Yotpo for a product's public reviews.\n *\n * @see https://apidocs.yotpo.com/v3.0/reference/storefront-product-reviews-search\n */\nexport async function searchProductReviews(input: Input) {\n  assert(input.storeId, 'Invalid store id');\n  assert(Number.isInteger(input.perPage) && input.perPage > 0, 'Invalid per page input');\n\n  const pathPrefix = input.pathPrefix ?? '';\n  const path = pathPrefix +\n    `/v3/storefront/store/${input.storeId}/product/${input.productId}/reviews`;\n  const url = new URL(path, input.baseUrl);\n\n  if (input.page) {\n    url.searchParams.append('page', input.page.toString());\n  }\n\n  if (input.perPage) {\n    url.searchParams.append('perPage', input.perPage.toString());\n  }\n\n  if (input.sort) {\n    url.searchParams.append('sort', input.sort);\n  }\n\n  if (input.direction) {\n    url.searchParams.append('direction', input.direction);\n  }\n\n  if (input.lang) {\n    url.searchParams.append('lang', input.lang);\n  }\n\n  const requestBody: RequestBody = {};\n\n  if (input.country) {\n    requestBody.country = input.country;\n  }\n\n  if (input.crfs?.length) {\n    requestBody.crfs = input.crfs;\n  }\n\n  if (input.freeTextSearch) {\n    requestBody.freeTextSearch = input.freeTextSearch;\n  }\n\n  if (input.ids?.length) {\n    requestBody.ids = input.ids;\n  }\n\n  if (input.metadata?.length) {\n    requestBody.metadata = input.metadata;\n  }\n\n  if (input.orders?.length) {\n    requestBody.orders = input.orders;\n  }\n\n  if (input.pictured !== undefined) {\n    requestBody.pictured = input.pictured;\n  }\n\n  if (input.picturesPerReview !== undefined) {\n    requestBody.picturesPerReview = input.picturesPerReview;\n  }\n\n  if (input.productVariants) {\n    requestBody.productVariants = input.productVariants;\n  }\n\n  if (input.scores?.length) {\n    requestBody.scores = input.scores;\n  }\n\n  if (input.sortings?.length) {\n    const sortings = [];\n    for (const sorting of input.sortings) {\n      sortings.push({\n        ascending: sorting.ascending ?? false,\n        sortBy: sorting.sortBy\n      });\n    }\n\n    requestBody.sortings = sortings;\n  }\n\n  if (input.topicNames?.length) {\n    requestBody.topicNames = input.topicNames;\n  }\n\n  const init: RequestInit = {\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify(requestBody),\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} searching product reviews`);\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isSearchResponseBody(responseBody)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  return responseBody;\n}\n\n/**\n * Returns whether the given value satisfies the type's schema. This does not fully validate the\n * values, this just provides runtime type safety.\n */\nfunction isSearchResponseBody(value: any): value is SearchResponseBody {\n  return typeof value === 'object' && value !== null && value.pagination !== null &&\n    value.reviews !== null;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { LegacyReview, Review } from '@integrabeauty/yotpo-browser-types';\nimport { decode } from './decode-html-entities.js';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { type Pagination, searchProductReviews } from './yotpo-fetch-product-reviews.js';\n\ntype SearchProductReviewsResponse = Awaited<ReturnType<typeof searchProductReviews>>;\n\n/**\n * Renders product reviews element feed with reviews fetched from Yotpo API. You can access this\n * element from the PDP page.\n */\nclass YotpoReviews extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Shopify customer id of currently logged in user\n     */\n    customerId: string;\n\n    /**\n     * Shopify product id to which the reviews in this element pertain\n     */\n    productId: string;\n\n    /**\n     * Shopify product price to which the reviews in this element pertain\n     */\n    productPrice: string;\n\n    /**\n     * Shopify product title to which the reviews in this element pertain\n     */\n    productTitle: string;\n\n    /**\n     * Shopify product type to which the reviews in this element pertain\n     */\n    productType: string;\n\n    /**\n     * Shopify product url to which the reviews in this element pertain\n     */\n    productUrl: string;\n\n    /**\n     * Average review score from the automatically inserted metafield value via the Yotpo App\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of reviews fetched per page from Yotpo API\n     */\n    reviewsPerPage: string;\n\n    /**\n     * Total reviews value from the automatically inserted metafield value via the Yotpo App\n     */\n    reviewsTotal: string;\n\n    /**\n     * Number of reviews that gave 1 star.\n     */\n    star1Count: string;\n\n    /**\n     * Number of reviews that gave 2 stars.\n     */\n    star2Count: string;\n\n    /**\n     * Number of reviews that gave 3 stars.\n     */\n    star3Count: string;\n\n    /**\n     * Number of reviews that gave 4 stars.\n     */\n    star4Count: string;\n\n    /**\n     * Number of reviews that gave 5 stars.\n     */\n    star5Count: string;\n\n    /**\n     * Yotpo API app key.\n     */\n    yotpoAppKey: string;\n  };\n\n  private onPageChangedBound = this.onPageChanged.bind(this);\n  private onReviewsFilterChangeBound = this.onReviewsFilterChange.bind(this);\n  private onYotpoReviewSubmittedBound = this.onYotpoReviewSubmitted.bind(this);\n  private onClearButtonClickedBound = this.onClearButtonClicked.bind(this);\n  private onModalOpenedBound = this.onModalOpened.bind(this);\n  private onModalClosedBound = this.onModalClosed.bind(this);\n  private declare loadingTimer: ReturnType<typeof setTimeout>;\n\n  /**\n   * The container of our template holding the drawer or photo popup open in this section, if any.\n   */\n  private declare modalContainer?: Element;\n  private declare paginationReviews?: Pagination;\n  private productId = 0;\n  private reviews: LegacyReview[] | Review[] = [];\n  private reviewsPerPage = 5;\n  private selectedFilters: Record<string, any> = {};\n  private declare selectedSortings: YotpoReviewSortings;\n\n  /**\n   * This controls whether to fetch reviews from Integra's Yotpo cache service instead of fetching\n   * reviews from Yotpo. Not everyone reads reviews, but this fetch has to run on every page load.\n   * The reviews are dynamic and derived from request parameters, leading to several permutations,\n   * so it is infeasible to cache all the variations. Therefore, we only cache the first page based\n   * on the same criteria we would normally use to fetch the first page of reviews from the API and\n   * require an API call for all other situations.\n   *\n   * Initially this is true. If this is true, the cache is used. After the first fetch, this is set\n   * to false. That way all future calls use the API.\n   *\n   * This is not a perfect solution. It is possible someone loads the page, the cache is used\n   * initially, then the user changes the review filters, where this is false, and then resets the\n   * filters back to the original state, where this could become true again, but we are not\n   * bothering to ever transition back. We could use the cache again if we detected a revert to\n   * original state. The thing is, detecting that at the moment is a little complex, and we are at\n   * that point where the user is already \"interacting\" with the reviews, which happens far less\n   * frequently than the number of page loads, so skipping the cache is ok.\n   */\n  private useCacheForReviews = true;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    addEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    addEventListener('yotpo-page-changed', this.onPageChangedBound);\n    addEventListener('yotpo-review-submitted', this.onYotpoReviewSubmittedBound);\n\n    const clearButton = this.shadowRoot.querySelector<HTMLButtonElement>('.clear-filters');\n    clearButton?.addEventListener('click', this.onClearButtonClickedBound);\n\n    this.shadowRoot.addEventListener('modal-opened', this.onModalOpenedBound);\n    this.shadowRoot.addEventListener('modal-closed', this.onModalClosedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    clearTimeout(this.loadingTimer);\n\n    removeEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    removeEventListener('yotpo-page-changed', this.onPageChangedBound);\n    removeEventListener('yotpo-review-submitted', this.onYotpoReviewSubmittedBound);\n\n    const clearButton = this.shadowRoot.querySelector<HTMLButtonElement>('.clear-filters');\n    clearButton?.removeEventListener('click', this.onClearButtonClickedBound);\n\n    this.shadowRoot.removeEventListener('modal-opened', this.onModalOpenedBound);\n    this.shadowRoot.removeEventListener('modal-closed', this.onModalClosedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.yotpoAppKey) {\n      return;\n    }\n\n    this.productId = parseInt(this.dataset.productId, 10);\n    if (!this.productId) {\n      return;\n    }\n\n    const reviewsPerPage = parseInt(this.dataset.reviewsPerPage, 10);\n    if (Number.isInteger(reviewsPerPage) && reviewsPerPage > 0) {\n      this.reviewsPerPage = reviewsPerPage;\n    }\n\n    // A product with no reviews has nothing to fetch, and fetching anyway flashes the loading\n    // indicator under the section's no-reviews message.\n    const reviewsTotal = parseInt(this.dataset.reviewsTotal, 10);\n    if (reviewsTotal <= 0) {\n      return;\n    }\n\n    const loader = this.shadowRoot.querySelector<HTMLElement>('#first-load-indicator');\n    this.showLoading(loader, 300);\n    const searchStarted = new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    });\n    this.dispatchEvent(searchStarted);\n\n    searchProductReviews({\n      app_key: this.dataset.yotpoAppKey,\n      cache: this.useCacheForReviews,\n      per_page: this.reviewsPerPage,\n      product_id: this.productId\n    }).then(response => this.onInitialReviewsFetched(response))\n      .catch(error => this.onInitialReviewsFetchFailed(error))\n      .finally(() => this.hideLoading(loader));\n  }\n\n  /**\n   * Renders the first page of reviews once the initial fetch comes back.\n   */\n  private onInitialReviewsFetched(response: SearchProductReviewsResponse) {\n    // Prevent future calls from using the cache\n    this.useCacheForReviews = false;\n\n    this.reviews = response?.reviews || [];\n    this.paginationReviews = response?.pagination;\n\n    this.correctReviewsCount();\n    this.renderReviews();\n    this.renderPagination(true);\n\n    type Detail = YotpoReviewsSearchedEvent['detail'];\n    const searchEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: {},\n        reviews: response?.reviews,\n        pagination: response?.pagination\n      }\n    });\n    this.dispatchEvent(searchEvent);\n  }\n\n  /**\n   * Logs the failed initial fetch and tells listeners the search came back empty.\n   */\n  private onInitialReviewsFetchFailed(error: any) {\n    if (isRoutineYotpoError(error)) {\n      console.log(error);\n    } else {\n      console.warn(error);\n    }\n\n    // Prevent future calls from using the cache\n    this.useCacheForReviews = false;\n\n    type Detail = YotpoReviewsSearchedEvent['detail'];\n    const searchEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: {},\n        reviews: [],\n        pagination: { page: 0, per_page: 0, total: 0 }\n      }\n    });\n    this.dispatchEvent(searchEvent);\n  }\n\n  /**\n   * Delays showing the loading indicator so a fast API response never flashes it. If the fetch\n   * resolves before the delay in milliseconds runs out, the timer is cleared before the indicator\n   * ever appears.\n   */\n  private showLoading(loader: HTMLElement, delay: number) {\n    clearTimeout(this.loadingTimer);\n    this.loadingTimer = setTimeout(() => {\n      if (loader) {\n        loader.hidden = false;\n      }\n    }, delay);\n  }\n\n  /**\n   * Cancels a pending show-loading timer if the fetch beat it, or hides the indicator if it was\n   * already visible.\n   */\n  private hideLoading(loader: HTMLElement) {\n    clearTimeout(this.loadingTimer);\n    if (loader) {\n      loader.hidden = true;\n    }\n  }\n\n  /**\n   * Creates and displays a success alert message when the review is submitted successfully.\n   */\n  private showAlert(message: string) {\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertPosition = 'right';\n    alert.dataset.alertType = 'success';\n    alert.dataset.fadeDelay = '3000';\n    alert.dataset.closable = 'true';\n    alert.innerHTML = `<p>${message}</p>`;\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  /**\n   * On click of the clear filters button in no results element, dispatch a custom event to notify\n   * the yotpo-filters component to clear all the filters and reset the reviews list. We don't do it\n   * right here, because the yotpo-filters component handles all the logic for clearing the filters.\n   */\n  private onClearButtonClicked(_event: MouseEvent) {\n    const event = new Event('yotpo-filter-clear-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * The modal manager inerts every section but the one holding the open modal, so while a drawer or\n   * a review's photo popup in this section is open, the rest of the section stays reachable by Tab.\n   * The element that owns the modal inerts its own parts beside it, and we inert the parts of our\n   * shadow root beside that element.\n   */\n  private onModalOpened(event: HTMLElementEventMap['modal-opened']) {\n    // The event's path tells us which container of our template the modal sits in\n    const path = event.composedPath();\n    const containers = this.shadowRoot.querySelectorAll(\n      'slot[name=\"stars-breakdown\"], slot[name=\"create-form\"], .items-container');\n    let modalContainer: Element;\n    for (const container of containers) {\n      if (path.includes(container)) {\n        modalContainer = container;\n      }\n    }\n\n    if (!modalContainer) {\n      return;\n    }\n\n    // Anything on the path is skipped, so the review holding a photo popup stays out of inert\n    const parts = this.shadowRoot.querySelectorAll(getPartsBesideSelector(modalContainer));\n    for (const part of parts) {\n      if (part instanceof HTMLElement && !path.includes(part)) {\n        part.inert = true;\n      }\n    }\n\n    this.modalContainer = modalContainer;\n  }\n\n  /**\n   * Takes our parts back out of inert. The modal manager puts focus back on the element that opened\n   * the modal only after this runs, so that element is no longer inert by then.\n   */\n  private onModalClosed(_event: HTMLElementEventMap['modal-closed']) {\n    this.releaseParts();\n  }\n\n  private releaseParts() {\n    if (!this.modalContainer) {\n      return;\n    }\n\n    const parts = this.shadowRoot.querySelectorAll(getPartsBesideSelector(this.modalContainer));\n    for (const part of parts) {\n      if (part instanceof HTMLElement) {\n        part.inert = false;\n      }\n    }\n\n    this.modalContainer = undefined;\n  }\n\n  private onYotpoReviewSubmitted(_event: WindowEventMap['yotpo-review-submitted']) {\n    this.showAlert('Thank you! Your review has been submitted.');\n  }\n\n  private onReviewsFilterChange(event: HTMLElementEventMap['yotpo-reviews-filtered']) {\n    // Changes happen after the initial render so always make a live API call and never use the\n    // cache. This guarantees that if someone changes which reviews should be displayed, we attempt\n    // to fetch those reviews from Yotpo instead.\n\n    // The overlay is the only sign a click registered, since the page highlight waits for the\n    // fetch, so it gets a much shorter delay than the first load. It still skips the rare instant\n    // response.\n    const overlay = this.shadowRoot.querySelector<HTMLElement>('#loading-overlay');\n    this.showLoading(overlay, 50);\n\n    const searchStarted = new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    });\n    this.dispatchEvent(searchStarted);\n\n    // Hair Type is one of the custom questions customers answer when they write a review, and the\n    // fetch takes those as a list of question ids rather than knowing about hair type, so we look\n    // up the id here. Yotpo gives the question id 103765 and we do not expect that to change.\n    const crfs = event.detail.filters.hair_type ?\n      [{ answers: [event.detail.filters.hair_type], questionId: 103765 }] :\n      [];\n\n    searchProductReviews({\n      app_key: this.dataset.yotpoAppKey,\n      cache: false,\n      filters: {\n        crfs,\n        free_text_search: event.detail.filters.free_text_search,\n        pictured: event.detail.filters.pictured,\n        scores: event.detail.filters.scores,\n        sortings: event.detail.sortings,\n        topic_names: event.detail.filters.topic_names\n      },\n      page: event.detail.page,\n      per_page: this.reviewsPerPage,\n      product_id: event.detail.productId\n    }).then(response => {\n      this.reviews = response.reviews;\n      this.selectedFilters = event.detail.filters;\n      this.selectedSortings = event.detail.sortings;\n      this.paginationReviews = response.pagination;\n\n      this.renderReviews();\n      this.renderPagination(false);\n\n      scrollToElement(this.shadowRoot.querySelector<HTMLElement>('.items-container'));\n\n      type Detail = WindowEventMap['yotpo-reviews-searched']['detail'];\n      const searchedEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          filters: event.detail.filters,\n          reviews: response.reviews,\n          pagination: response.pagination\n        }\n      });\n      this.dispatchEvent(searchedEvent);\n    }).catch(error => {\n      if (isRoutineYotpoError(error)) {\n        console.log(error);\n      } else {\n        console.warn(error);\n      }\n\n      type Detail = WindowEventMap['yotpo-reviews-searched']['detail'];\n      const searchedEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          filters: event.detail.filters,\n          reviews: [],\n          pagination: { page: 0, per_page: 0, total: 0 }\n        }\n      });\n      this.dispatchEvent(searchedEvent);\n    }).finally(() => {\n      // We hide the overlay here rather than in the success and failure handlers above, so it comes\n      // down even when one of those handlers throws.\n      this.hideLoading(overlay);\n    });\n  }\n\n  /**\n   * When the user clicks on one of the page links in the pagination element we want to scroll to\n   * the top of the reviews element.\n   */\n  private onPageChanged(event: WindowEventMap['yotpo-page-changed']) {\n    type Detail = WindowEventMap['yotpo-reviews-filtered']['detail'];\n    const filteredEvent = new CustomEvent<Detail>('yotpo-reviews-filtered', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: { ...this.selectedFilters },\n        page: event.detail.page,\n        productId: this.productId,\n        sortings: this.selectedSortings\n      }\n    });\n    this.dispatchEvent(filteredEvent);\n  }\n\n  private renderReviews() {\n    const itemsContainer = this.shadowRoot.querySelector('.items-container');\n\n    // Clearing the list removes any review whose photo popup is open. The close request that review\n    // sends on its way out goes straight to the window rather than through us, and the modal\n    // manager puts focus back during the clearing, so we take our parts out of inert first.\n    if (this.modalContainer === itemsContainer) {\n      this.releaseParts();\n    }\n\n    itemsContainer.innerHTML = '';\n\n    const noResults = this.shadowRoot.querySelector<HTMLParagraphElement>('.no-results');\n    if (this.reviews.length === 0) {\n      noResults.hidden = false;\n\n      return;\n    }\n\n    noResults.hidden = true;\n\n    for (const review of this.reviews) {\n      itemsContainer.appendChild(createReviewDetail(review));\n    }\n  }\n\n  /**\n   * Renders the pagination element based on the current reviews and pagination data. There's a flag\n   * if data\n   */\n  private renderPagination(fromCache: boolean) {\n    const paginationElement = this.shadowRoot.querySelector('yotpo-pagination');\n    if (!paginationElement) {\n      return;\n    }\n\n    const pagination = this.paginationReviews;\n\n    if (pagination && pagination.total > pagination.per_page) {\n      paginationElement.dataset.page = pagination.page.toString();\n      paginationElement.dataset.perPage = pagination.per_page.toString();\n\n      // The total reviews number comes from different sources: metafields and yotpo api where 2\n      // cached and uncached calls return different results. The cached total reviews value looks\n      // inaccurate while the metafield value (this.dataset.reviewsTotal) and value from uncached\n      // response looks accurate. Therefore, if the reviews are fetched from cache, we use the\n      // metafield value instead of the cached value.\n      let total: number;\n      if (fromCache) {\n        total = parseInt(this.dataset.reviewsTotal, 10);\n      } else {\n        total = pagination.total;\n      }\n\n      // Yotpo only pages through the first 10000 reviews. Ask for a page past that and it answers\n      // with the last page it will serve rather than the page we asked for, so on a product with\n      // more reviews than that the later page buttons all land on the same reviews. We hand the\n      // pagination element a total that stops at the last whole page inside the window, so it only\n      // offers pages Yotpo will serve. Yotpo does not document the limit, we measured it.\n      const reachableTotal = Math.floor(10000 / pagination.per_page) * pagination.per_page;\n      if (total > reachableTotal) {\n        total = reachableTotal;\n      }\n\n      paginationElement.dataset.total = total.toString();\n\n      // it triggers the pagination element to rerender\n      paginationElement.dataset.update = Date.now().toString();\n\n      paginationElement.hidden = false;\n    } else {\n      paginationElement.hidden = true;\n    }\n  }\n\n  /**\n   * numbers come from different sources:\n   * * metafields\n   * * yotpo api (where 2 methods return different results)\n   *\n   * therefore this is a workaround for correcting values\n   */\n  private correctReviewsCount() {\n    type ProductRatingStarsElement = HTMLElementTagNameMap['product-rating-stars'];\n    const ratingStarsComponent = document.querySelector<ProductRatingStarsElement>(\n      'product-rating-stars[data-product-id=\"' + this.productId + '\"]');\n\n    if (!ratingStarsComponent) {\n      return;\n    }\n\n    const ratingStarsCount = parseInt(ratingStarsComponent.dataset.count, 10);\n    const reviewsTotal = parseInt(this.dataset.reviewsTotal, 10);\n    if (ratingStarsCount !== reviewsTotal) {\n      ratingStarsComponent.dataset.count = this.dataset.reviewsTotal;\n    }\n  }\n}\n\n/**\n * Returns a selector for the parts of the reviews template beside a container that can hold an open\n * modal. It leaves out the container's own ancestors, since inerting one of those would inert the\n * modal too.\n *\n * @param container The stars breakdown slot, the create form slot, or the review list\n */\nfunction getPartsBesideSelector(container: Element) {\n  if (container.matches('slot[name=\"stars-breakdown\"]')) {\n    return 'slot[name=\"featured\"], slot[name=\"create-form\"], #reviews-list';\n  }\n\n  if (container.matches('slot[name=\"create-form\"]')) {\n    return 'slot[name=\"stars-breakdown\"], slot[name=\"featured\"], #reviews-list';\n  }\n\n  return 'slot[name=\"stars-breakdown\"], slot[name=\"featured\"], slot[name=\"create-form\"], ' +\n    'slot[name=\"filters\"], .reviews-pagination, .items-container > yotpo-review-detail';\n}\n\n/**\n * Builds the element that displays one review, filled in and ready to append.\n *\n * Yotpo hands us reviews in two shapes, an older one with snake case properties and a newer one\n * with camel case, so we work out which shape this review is and read from it accordingly.\n */\nfunction createReviewDetail(review: LegacyReview | Review) {\n  const detail = document.createElement('yotpo-review-detail');\n  detail.dataset.reviewId = review.id.toString();\n\n  const useLegacy = isLegacyReview(review);\n\n  let imagesData: Pick<Review['imagesData'][number], 'id' | 'originalUrl' | 'thumbUrl'>[] = [];\n\n  if (useLegacy && review.images_data) {\n    imagesData = review.images_data.map(image => ({\n      id: image.image_id,\n      thumbUrl: image.thumb_url,\n      // @ts-expect-error: original_url is not in the type but exists in legacy data; it is not\n      // documented and used only in some API responses, so instead of adding it to the interface,\n      // we just ignore the type error here.\n      originalUrl: image.original_image_url || <string>image.original_url\n    }));\n  } else if (!useLegacy && review.imagesData) {\n    imagesData = review.imagesData;\n  }\n\n  detail.dataset.originalImages = imagesData.map(image => image.originalUrl).join(',');\n  detail.dataset.thumbnailImages = imagesData.map(image => image.thumbUrl).join(',');\n\n  const votesUp = useLegacy ? review.votes_up : review.votesUp;\n  detail.dataset.votesUp = (votesUp || 0).toString();\n\n  if (useLegacy) {\n    detail.dataset.incentivized = review.is_incentivized ? 'true' : 'false';\n  } else {\n    detail.dataset.incentivized = review.isIncentivized ? 'true' : 'false';\n  }\n\n  // create profile badge element for slot\n  const avatar = document.createElement('yotpo-avatar-v2');\n  avatar.setAttribute('slot', 'badge');\n  avatar.dataset.date = useLegacy ? review.created_at : review.createdAt;\n  avatar.dataset.name = useLegacy ? review.user.display_name : review.user.displayName;\n\n  const verified = useLegacy ? review.verified_buyer : review.verifiedBuyer;\n  avatar.dataset.verified = verified ? 'true' : 'false';\n  detail.appendChild(avatar);\n\n  // create rating stars for slot\n  const rating = document.createElement('simple-rating-stars');\n  rating.setAttribute('slot', 'rating-stars');\n  rating.dataset.rating = review.score.toString();\n  detail.appendChild(rating);\n\n  // create hair type for slot\n  const customFields = useLegacy ? review.custom_fields : review.customFields;\n  const hairType = getHairTypeValue(customFields);\n  if (hairType) {\n    const element = document.createElement('div');\n    element.setAttribute('slot', 'hair-type');\n\n    const label = document.createElement('strong');\n    label.textContent = 'Hair Type: ';\n    element.appendChild(label);\n    element.appendChild(document.createTextNode(hairType));\n\n    detail.appendChild(element);\n  }\n\n  // create content for slot\n  const content = document.createElement('div');\n  content.setAttribute('slot', 'content');\n  content.className = 'review';\n\n  // Review title and content can contain HTML tags and HTML entities. Review title and content are\n  // user-generated content and fundamentally unsafe. Therefore, we never want to set inner HTML.\n  // However, we do want reviews to render properly as many reviews use entities. So we manually\n  // decode the entities for beauty but only set text content for safety.\n\n  const title = document.createElement('strong');\n  title.classList.add('customer-review-title');\n  title.textContent = decode(review.title);\n  content.appendChild(title);\n\n  const text = document.createElement('p');\n  text.classList.add('customer-review-text');\n  text.textContent = decode(review.content);\n  content.appendChild(text);\n\n  detail.appendChild(content);\n\n  return detail;\n}\n\n/**\n * Validation and type guard to check for legacy reviews types when building out the <yotpo-review-\n * detail> custom elements.\n */\nfunction isLegacyReview(value: any): value is LegacyReview {\n  return typeof value === 'object' && value !== null && typeof value.votes_up === 'number' &&\n    typeof value.votes_down === 'number';\n}\n\n/**\n * Returns the Hair Type custom field value from a review's custom fields object. The field appears\n * under key \"103765\" in v3 API responses and \"--103765\" in legacy responses.\n */\nfunction getHairTypeValue(customFields: Record<string, any>): string | undefined {\n  if (typeof customFields !== 'object' || customFields === null) {\n    return;\n  }\n\n  const field = customFields['103765'] || customFields['--103765'];\n  if (typeof field === 'object' && field !== null &&\n    typeof field.value === 'string' && field.value) {\n    return field.value;\n  }\n}\n\nfunction isRoutineYotpoError(error: any): error is Error {\n  if (!(error instanceof Error)) {\n    return false;\n  }\n\n  // A 429 here is not really about the reviews. Shopify throttles the whole storefront domain at\n  // once, so when we get one the cart and the recommendations are usually failing alongside it, and\n  // there is nothing this element can do about that.\n  //\n  // The status gets attached to the error where that is possible, but attaching it can quietly fail\n  // on an error we did not create, so we look for the property rather than count on it.\n  const decorated: any = error;\n  if (decorated.status === 429) {\n    return true;\n  }\n\n  return [\n    'Load failed',\n    'Failed to get response body finding reviews by product'\n  ].includes(error.message);\n}\n\nfunction scrollToElement(element?: HTMLElement) {\n  if (!element) {\n    return;\n  }\n\n  const rect = element.getBoundingClientRect();\n\n  // The header script measures the sticky site header and publishes the viewport y of its bottom\n  // edge as --content-position, so reading it puts the element just below the site header at\n  // whatever height that header currently has. The fallback covers a page that renders no header\n  // section, where the variable is absent and parseFloat returns NaN.\n  const headerOffset = parseFloat(\n    getComputedStyle(document.body).getPropertyValue('--content-position')) || 80;\n\n  document.documentElement.scrollTop = scrollY + rect.top - headerOffset;\n}\n\n/**\n * Fired when a call to the Yotpo API returns search results.\n */\ntype YotpoReviewsSearchedEvent = CustomEvent<{\n  filters: YotpoReviewFilters | Record<string, unknown>;\n  pagination: Pagination;\n  productId?: number;\n  reviews: LegacyReview[] | Review[];\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-filter-clear-requested': Event;\n    'yotpo-reviews-search-started': Event;\n    'yotpo-reviews-searched': YotpoReviewsSearchedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-reviews': YotpoReviews;\n  }\n}\n\nif (!customElements.get('yotpo-reviews')) {\n  customElements.define('yotpo-reviews', YotpoReviews);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype StarFilterClickedEvent = CustomEvent<{\n  stars: number;\n}>;\n\n/**\n * The rating summary for a product's reviews, showing the average score, the total number of\n * reviews, and a bar for each star level with the share of reviews that gave that rating.\n *\n * Each star level bar is also a filter. Clicking one emits an event so the reviews list can narrow\n * to reviews with that rating.\n */\nclass YotpoStarsBreakdown extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Value of Yotpo product score on scale 1 - 5\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of Yotpo customer reviews for this product\n     */\n    reviewsTotal: string;\n\n    /**\n     * Number of reviews that gave 1 star.\n     */\n    star1Count: string;\n\n    /**\n     * Number of reviews that gave 2 stars.\n     */\n    star2Count: string;\n\n    /**\n     * Number of reviews that gave 3 stars.\n     */\n    star3Count: string;\n\n    /**\n     * Number of reviews that gave 4 stars.\n     */\n    star4Count: string;\n\n    /**\n     * Number of reviews that gave 5 stars.\n     */\n    star5Count: string;\n  };\n\n  private onStarFilterClickedBound = this.onStarFilterClicked.bind(this);\n  private onModalOpenedBound = this.onModalOpened.bind(this);\n  private onModalClosedBound = this.onModalClosed.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const titleButtons = this.shadowRoot.querySelectorAll<HTMLButtonElement>('.title button');\n    for (const button of titleButtons) {\n      button.addEventListener('click', this.onStarFilterClickedBound);\n    }\n\n    this.shadowRoot.addEventListener('modal-opened', this.onModalOpenedBound);\n    this.shadowRoot.addEventListener('modal-closed', this.onModalClosedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const titleButtons = this.shadowRoot.querySelectorAll<HTMLButtonElement>('.title button');\n    for (const button of titleButtons) {\n      button.removeEventListener('click', this.onStarFilterClickedBound);\n    }\n\n    this.shadowRoot.removeEventListener('modal-opened', this.onModalOpenedBound);\n    this.shadowRoot.removeEventListener('modal-closed', this.onModalClosedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    const star1Count = parseInt(this.dataset.star1Count, 10);\n    const star2Count = parseInt(this.dataset.star2Count, 10);\n    const star3Count = parseInt(this.dataset.star3Count, 10);\n    const star4Count = parseInt(this.dataset.star4Count, 10);\n    const star5Count = parseInt(this.dataset.star5Count, 10);\n    const total = parseInt(this.dataset.reviewsTotal, 10);\n    // the actual sum may differ from the total - Yotpo provides different values\n    const starCountSum = star1Count + star2Count + star3Count + star4Count + star5Count;\n\n    const wrapper = this.shadowRoot.querySelector<HTMLElement>('.stats');\n\n    // if any of the star counts is NaN - their sum will be NaN as well\n    if (isNaN(starCountSum) || starCountSum === 0 || isNaN(total) || total === 0) {\n      wrapper.hidden = true;\n      return;\n    }\n\n    const ratingStars = this.shadowRoot.querySelector('product-rating-stars');\n    ratingStars.dataset.average = this.dataset.reviewsAverage;\n    ratingStars.dataset.count = this.dataset.reviewsTotal;\n\n    const averageElement = this.shadowRoot.querySelector('.average');\n    averageElement.textContent = this.dataset.reviewsAverage;\n\n    const totalElement = this.shadowRoot.querySelector('.total');\n    const formatter = new Intl.NumberFormat('en-US');\n    totalElement.textContent = formatter.format(total);\n\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star5'), star5Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star4'), star4Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star3'), star3Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star2'), star2Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star1'), star1Count, total);\n\n    wrapper.hidden = false;\n  }\n\n  private onStarFilterClicked(event: MouseEvent) {\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    const stars = event.target.dataset.stars;\n    if (stars) {\n      type Detail = StarFilterClickedEvent['detail'];\n      const filterClickedEvent = new CustomEvent<Detail>('star-filter-clicked', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          stars: parseInt(stars, 10)\n        }\n      });\n      this.dispatchEvent(filterClickedEvent);\n    }\n  }\n\n  /**\n   * The modal manager leaves the reviews section active while the AI summary drawer slotted into us\n   * is open, so the star filter buttons would stay reachable by Tab behind it. We inert them, and\n   * the elements around us inert their own parts.\n   */\n  private onModalOpened(_event: HTMLElementEventMap['modal-opened']) {\n    const breakdown = this.shadowRoot.querySelector('.breakdown');\n    if (breakdown instanceof HTMLElement) {\n      breakdown.inert = true;\n    }\n  }\n\n  /**\n   * Takes the star filter buttons back out of inert. The modal manager puts focus back on the\n   * element that opened the drawer only after this runs, so that element is no longer inert by\n   * then.\n   */\n  private onModalClosed(_event: HTMLElementEventMap['modal-closed']) {\n    const breakdown = this.shadowRoot.querySelector('.breakdown');\n    if (breakdown instanceof HTMLElement) {\n      breakdown.inert = false;\n    }\n  }\n}\n\nfunction updateStarRatingBar(element: HTMLElement, count: number, total: number) {\n  const percent = (count / total) * 100;\n  const bar = element.querySelector<HTMLElement>('.fill');\n  bar.style.width = percent + '%';\n  const countElement = element.querySelector<HTMLElement>('.count');\n  countElement.textContent = percent.toFixed(1).replace('.0', '') + '%';\n  if (count === 0) {\n    const button = element.querySelector('button');\n    button.disabled = true;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-stars-breakdown': YotpoStarsBreakdown;\n  }\n\n  interface GlobalEventHandlersEventMap {\n    'star-filter-clicked': StarFilterClickedEvent;\n  }\n}\n\nif (!customElements.get('yotpo-stars-breakdown')) {\n  customElements.define('yotpo-stars-breakdown', YotpoStarsBreakdown);\n}\n"],"names":["onWriteReviewClicked","event","button","currentTarget","HTMLButtonElement","setAttribute","focusEvent","Event","bubbles","composed","dispatchEvent","inert","closest","querySelector","dataset","open","onModalClosed","target","HTMLElement","writeReview","containers","document","querySelectorAll","container","addEventListener","drawerInitialized","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","rnds8","Uint8Array","byteToHex","i","push","toString","slice","unsafeStringify","arr","offset","toLowerCase","SwellPopupType","SwellReferralMedium","dist_v4","options","buf","crypto","randomUUID","_ref","_options$random","rnds","random","rng","getRandomValues","length","Error","RangeError","_v4","assert","expression","message","AssertionError","constructor","super","this","name","decorateError","error","key","value","assignmentError","reason","console","log","tag","Object","prototype","call","onModalOpened","form","classList","remove","errorElements","element","summaries","summary","hidden","onStarsClicked","contains","score","parseInt","idx","stars","star","textContent","selectStar","onSubmitClicked","preventDefault","submitButton","errorSummary","checkValidity","_firstInvalid","firstInvalid","elements","HTMLInputElement","HTMLTextAreaElement","parentElement","add","focus","focusVisible","async","formData","FormData","firstName","get","lastName","email","review","title","input","appkey","yotpoAppKey","display_name","isString","product_title","productTitle","product_url","productUrl","review_content","review_score","review_title","sku","productId","loader","disabled","path","response","responseText","responseBody","fetch","body","JSON","stringify","headers","method","mode","ok","status","url","text","parse","code","createReview","reset","modal","submittedEvent","CustomEvent","detail","category","productType","error_message","event_id","first_name","last_name","person_name","product_id","product_price","productPrice","rating","identityEvent","postReview","catch","formInitialized","SimpleRatingStars","observedAttributes","connectedCallback","initializeShadowRoot","render","attributeChangedCallback","_name","oldValue","newValue","isConnected","shadowRoot","attachShadow","innerHTML","customElements","define","YotpoAiSummary","fetchStarted","paragraphSummary","summaryTopics","summaryHighlights","onTopicLinksClickedBound","onTopicLinksClicked","bind","onModalClosedBound","topicLinks","startFetching","warn","disconnectedCallback","removeEventListener","app_key","Number","isInteger","fetchReviewsSummary","_event","activeTopicLink","copy","topicObject","topic","createElement","displayTopic","coverage","appendChild","total","reviewsTotal","formatter","Intl","NumberFormat","subtitle","format","topics","mentioned","li","append","highlightsList","highlight","blockquote","content","YotpoFilters","onHairTypeSelectedBound","onHairTypeSelected","onMediaSelectedBound","onMediaSelected","onRatingSelectedBound","onRatingSelected","onSearchClearedBound","onSearchCleared","onSearchKeyUpBound","onSearchKeyUp","onSortSelectedBound","onSortSelected","onStarsFilterClickedBound","onStarsFilterClicked","onReviewsFilterChangeBound","onReviewsFilterChange","onPillClickedBound","onPillClicked","onClearAllClickedBound","onClearAllClicked","onClearRequestedBound","onClearRequested","onSearchStartedBound","onSearchStarted","onSearchCompletedBound","onSearchCompleted","searchInput","hairTypeSelect","mediaSelect","ratingSelect","sortSelect","sort","pills","pill","clearAll","clearTimeout","searchDebounceTimer","dispatchFilter","free_text_search","stopPropagation","query","timeout","setTimeout","HTMLSelectElement","hair_type","scores","pictured","ascending","includes","direction","split","sort_by","filters","sortings","merged","_extends","selectedFilters","page","selectedSortings","renderAppliedFilters","existingPills","clearButton","dimension","label","className","span","select","clearAllFilters","inputs","decode_html_entities_element","YotpoPagination","currentPage","lastPage","maxButtons","onPreviousButtonClickedBound","onPreviousButtonClicked","onNextButtonClickedBound","onNextButtonClicked","onPageClickedBound","onPageClicked","onMediaQueryChangedBound","onMediaQueryChanged","prevButton","nextButton","pages","mediaQuery","matchMedia","matches","changeEvent","mediaQueryList","newMaxButtons","listWrapper","buttonPrev","buttonNext","perPage","Math","ceil","middleItemIndex","floor","startingIndex","numberOfItemsToDisplay","showFirstEllipsis","showSecondEllipsis","currentIndex","ellipsis","type","decode","output","yotpo_fetch_product_reviews_searchProductReviews","cache","_input$filters2","_input$filters3","_input$filters4","_input$filters5","_input$filters6","sorting","_input$filters","sortBy","_input$pathPrefix","_input$crfs","_input$ids","_input$metadata","_input$orders","_input$scores","_input$sortings","_input$topicNames","storeId","pathPrefix","URL","baseUrl","searchParams","lang","requestBody","country","crfs","freeTextSearch","ids","metadata","orders","picturesPerReview","productVariants","_sorting$ascending","topicNames","init","href","pagination","reviews","searchProductReviews","per_page","topic_names","yotpo_fetch_product_reviews_extends","YotpoReviews","onPageChangedBound","onPageChanged","onYotpoReviewSubmittedBound","onYotpoReviewSubmitted","onClearButtonClickedBound","onClearButtonClicked","onModalOpenedBound","reviewsPerPage","useCacheForReviews","loadingTimer","showLoading","searchStarted","then","onInitialReviewsFetched","onInitialReviewsFetchFailed","finally","hideLoading","paginationReviews","correctReviewsCount","renderReviews","renderPagination","searchEvent","isRoutineYotpoError","delay","showAlert","alert","alertPosition","alertType","fadeDelay","closable","composedPath","modalContainer","parts","getPartsBesideSelector","part","releaseParts","overlay","answers","questionId","rect","getBoundingClientRect","headerOffset","parseFloat","getComputedStyle","getPropertyValue","documentElement","scrollTop","scrollY","top","scrollToElement","searchedEvent","filteredEvent","yotpo_reviews_extends","itemsContainer","noResults","createReviewDetail","fromCache","paginationElement","reachableTotal","update","Date","now","ratingStarsComponent","count","reviewId","id","useLegacy","votes_up","votes_down","imagesData","images_data","map","image","image_id","thumbUrl","thumb_url","originalUrl","original_image_url","original_url","originalImages","join","thumbnailImages","votesUp","incentivized","is_incentivized","isIncentivized","avatar","date","created_at","createdAt","user","displayName","verified","verified_buyer","verifiedBuyer","hairType","customFields","field","getHairTypeValue","custom_fields","createTextNode","YotpoStarsBreakdown","onStarFilterClickedBound","onStarFilterClicked","titleButtons","star1Count","star2Count","star3Count","star4Count","star5Count","starCountSum","wrapper","isNaN","ratingStars","average","reviewsAverage","totalElement","updateStarRatingBar","filterClickedEvent","breakdown","percent","style","width","toFixed","replace"],"sourceRoot":""}