{"version":3,"file":"yotpo-product-reviews-section.min.js","mappings":"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","sources":["webpack://lange/./sections/yotpo-product-reviews/simple-rating-stars/index.ts","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/./lib/decorate-error.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-pagination/index.ts","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/./sections/yotpo-product-reviews/yotpo-review-create/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-review-create/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/yotpo-reviews/decode-html-entities.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":["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","/**\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 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 onCloseButtonClickedBound = this.onCloseButtonClicked.bind(this);\n  private onModalDismissRequestedBound = this.onModalDismissRequested.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 closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.addEventListener('click', this.onCloseButtonClickedBound);\n\n    const readAll = this.shadowRoot.querySelector<HTMLElement>('.read-all');\n    readAll?.addEventListener('click', this.onCloseButtonClickedBound);\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 closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseButtonClickedBound);\n\n    const readAll = this.shadowRoot.querySelector<HTMLElement>('.read-all');\n    readAll?.removeEventListener('click', this.onCloseButtonClickedBound);\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    const drawer = this.shadowRoot.querySelector<HTMLElement>('.drawer');\n\n    drawer.classList.add('active');\n    drawer.inert = false;\n\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\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('.content');\n    if (content instanceof HTMLElement) {\n      content.inert = true;\n    }\n\n    const modalShowEvent = new Event('modal-open-requested', { bubbles: true, composed: true });\n    drawer.dispatchEvent(modalShowEvent);\n\n    const closeButton = drawer.querySelector('close-button');\n    closeButton.focus({ focusVisible: true });\n  }\n\n  /**\n   * Handles modal's backdrop click and Esc button press\n   */\n  private onModalDismissRequested(_event: Event) {\n    this.closeDrawer();\n  }\n\n  /**\n   * Handles the drawer close button click and the \"Read all\" button click.\n   */\n  private onCloseButtonClicked(_event: MouseEvent) {\n    this.closeDrawer();\n  }\n\n  private closeDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('.drawer');\n    drawer.classList.remove('active');\n    drawer.inert = true;\n\n    const activeTopicLink = this.shadowRoot.querySelector<HTMLElement>(\n      '.topic-link[aria-expanded=\"true\"]');\n    activeTopicLink?.setAttribute('aria-expanded', 'false');\n\n    // This has to happen before the close request goes out, since the modal manager puts focus back\n    // on the topic button when it hears it, and a focus call into an inert subtree does nothing.\n    const content = this.shadowRoot.querySelector('.content');\n    if (content instanceof HTMLElement) {\n      content.inert = false;\n    }\n\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    const event = new Event('modal-close-requested', { bubbles: true, composed: true });\n    drawer.dispatchEvent(event);\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 description\n    const total = parseInt(this.dataset.reviewsTotal, 10);\n    const totalElement = this.shadowRoot.querySelector('.total');\n    const formatter = new Intl.NumberFormat('en-US');\n    totalElement.textContent = formatter.format(total);\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","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","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","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { v4 as uuidv4 } from 'uuid';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { createReview, type Input } from './yotpo-create-review.js';\n\n/**\n * Renders Yotpo reviews \"Write a review\" button and review form in a drawer.\n *\n * This has to observe changes to data-yotpo-app-key because of the way this element is initially\n * included in the parent element template. It is already connected and then the attribute is set\n * later.\n */\nclass YotpoReviewCreate extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Shopify product id to which the newly created review will pertain\n     */\n    productId: string;\n\n    /**\n     * Shopify product price to which the newly created review will pertain\n     */\n    productPrice: string;\n\n    /**\n     * Shopify product title to which the newly created review will pertain\n     */\n    productTitle: string;\n\n    /**\n     * Shopify product type to which the newly created review will pertain\n     */\n    productType: string;\n\n    /**\n     * Shopify product url to which the newly created review will pertain\n     */\n    productUrl: string;\n\n    /**\n     * Yotpo API app key\n     */\n    yotpoAppKey: string;\n  };\n\n  private onStarClickedBound = this.onStarClicked.bind(this);\n  private onWriteReviewButtonClickedBound = this.onWriteReviewButtonClicked.bind(this);\n  private onSubmitButtonClickedBound = this.onSubmitButtonClicked.bind(this);\n  private onCloseButtonClickedBound = this.onCloseButtonClicked.bind(this);\n  private onModalDismissRequestedBound = this.onModalDismissRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const writeReviewButton = this.shadowRoot.querySelector<HTMLButtonElement>('.write-review');\n    writeReviewButton.addEventListener('click', this.onWriteReviewButtonClickedBound);\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('button[type=\"submit\"]');\n    submitButton.addEventListener('click', this.onSubmitButtonClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseButtonClickedBound);\n\n    const ratingStarsElement = this.shadowRoot.querySelector<HTMLElement>('.rating-stars');\n    ratingStarsElement.addEventListener('click', this.onStarClickedBound);\n  }\n\n  public disconnectedCallback() {\n    const writeReviewButton = this.shadowRoot.querySelector<HTMLButtonElement>('.write-review');\n    writeReviewButton?.removeEventListener('click', this.onWriteReviewButtonClickedBound);\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('button[type=\"submit\"]');\n    submitButton?.removeEventListener('click', this.onSubmitButtonClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseButtonClickedBound);\n\n    const ratingStarsElement = this.shadowRoot.querySelector<HTMLElement>('.rating-stars');\n    ratingStarsElement?.removeEventListener('click', this.onStarClickedBound);\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 onSubmitButtonClicked(event: MouseEvent) {\n    event.preventDefault();\n    const submitButton = event.currentTarget;\n    if (!(submitButton instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    const form = this.shadowRoot.querySelector('form');\n\n    const isValid = form.checkValidity();\n    if (!isValid) {\n      // Show fields' error message\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\n      // we would get no focus move at all and the user would be left wondering where to go. That\n      // happens on an empty form, which is the most common way to fail this. So when the score is\n      // the first thing missing we send them to the first star instead.\n      if (firstInvalid instanceof HTMLInputElement && firstInvalid.name === 'score') {\n        firstInvalid = this.shadowRoot.querySelector<HTMLButtonElement>('.star');\n      }\n\n      firstInvalid?.focus({ focusVisible: true });\n\n      // Show a general error message\n      const errorSummary = this.shadowRoot.querySelector<HTMLElement>('.error-summary');\n      errorSummary.hidden = false;\n\n      return;\n    }\n\n    const errorSummary = this.shadowRoot.querySelector<HTMLElement>('.error-summary');\n    errorSummary.hidden = true;\n\n    const formData = new FormData(form);\n\n    // Product ids are sent in the sku property to Yotpo.\n    const displayName = `${formData.get('first_name')} ${formData.get('last_name')}`;\n    const createReviewInput: Partial<Input> = {\n      appkey: this.dataset.yotpoAppKey,\n      display_name: displayName,\n      email: <string>formData.get('email'),\n      product_title: this.dataset.productTitle,\n      product_url: this.dataset.productUrl,\n      review_content: <string>formData.get('review'),\n      review_score: parseInt(<string>formData.get('score'), 10),\n      review_title: <string>formData.get('title'),\n      sku: this.dataset.productId\n    };\n\n    const loader = this.shadowRoot.querySelector('loading-indicator');\n    submitButton.disabled = true;\n    loader.hidden = false;\n\n    createReview(createReviewInput).then(() => {\n      submitButton.disabled = false;\n      loader.hidden = true;\n      form.reset();\n      this.closeDrawer();\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: createReviewInput.review_content,\n          category: this.dataset.productType,\n          email: createReviewInput.email,\n          error_message: '',\n          event_id: uuidv4(),\n          first_name: <string>formData.get('first_name'),\n          last_name: <string>formData.get('last_name'),\n          person_name: createReviewInput.display_name,\n          product_id: parseInt(this.dataset.productId, 10),\n          product_price: parseInt(this.dataset.productPrice, 10),\n          product_title: this.dataset.productTitle,\n          rating: createReviewInput.review_score,\n          review_title: createReviewInput.review_title,\n          sku: createReviewInput.sku\n        }\n      });\n      this.dispatchEvent(submittedEvent);\n\n      const identityEvent = new CustomEvent('identity-captured', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          email: createReviewInput.email,\n          first_name: <string>formData.get('first_name'),\n          last_name: <string>formData.get('last_name')\n        }\n      });\n      this.dispatchEvent(identityEvent);\n    }).catch(error => {\n      submitButton.disabled = false;\n      loader.hidden = true;\n      errorSummary.textContent = 'Sorry we are having temporary problems, please try again later.';\n      errorSummary.parentElement.classList.add('error');\n      errorSummary.hidden = false;\n\n      console.error(error, createReviewInput);\n    });\n  }\n\n  /**\n   * Opens the write a review form drawer when the button is clicked\n   */\n  private onWriteReviewButtonClicked(_event: MouseEvent) {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('#drawer');\n    drawer.classList.add('active');\n    drawer.inert = false;\n\n    // handle write a review button accessibility\n    const writeReview = this.shadowRoot.querySelector('.write-review');\n    writeReview.setAttribute('aria-expanded', 'true');\n\n    // listen for clicks outside the drawer and for Escape key presses\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\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    writeReview.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.\n    if (writeReview instanceof HTMLElement) {\n      writeReview.inert = true;\n    }\n\n    // notify modal-manager that the drawer is open\n    const event = new Event('modal-open-requested', { bubbles: true, composed: true });\n    drawer.dispatchEvent(event);\n\n    const closeButton = drawer.querySelector('close-button');\n    closeButton.focus({ focusVisible: true });\n  }\n\n  /**\n   * Handles the `click` event on the close button of the drawer.\n   */\n  private onCloseButtonClicked(_event: MouseEvent) {\n    this.closeDrawer();\n  }\n\n  /**\n   * Handles the event sent when clicking on the modal backdrop or pressing the Escape key. The\n   * Event is sent by modal-manager.\n   */\n  private onModalDismissRequested(_event: Event) {\n    this.closeDrawer();\n  }\n\n  /**\n   * Close the form drawer\n   */\n  private closeDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('#drawer');\n    drawer.classList.remove('active');\n    drawer.inert = true;\n\n    // handle write a review button accessibility\n    const writeReview = this.shadowRoot.querySelector('.write-review');\n    writeReview.setAttribute('aria-expanded', 'false');\n\n    // This has to happen before the close request goes out, since the modal manager puts focus back\n    // on this button when it hears it, and a focus call into an inert subtree does nothing.\n    if (writeReview instanceof HTMLElement) {\n      writeReview.inert = false;\n    }\n\n    // we need to remove listeners for clicks outside the drawer and for Escape key presses to not\n    // interfere with other modals\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    // notify modal-manager that the drawer is closed\n    const event = new Event('modal-close-requested', { bubbles: true, composed: true });\n    drawer.dispatchEvent(event);\n  }\n\n  /**\n   * Handles the `click` event on the rating stars. Since we're listening on the parent element, we\n   * need to check whether the click was on a star, not on the parent or another child element.\n   */\n  private onStarClicked(event: MouseEvent) {\n    if (event.target instanceof HTMLButtonElement && event.target.classList.contains('star')) {\n      this.selectStar(event.target);\n    }\n  }\n\n  /**\n   * Visually selects a star and all stars before it, and sets the value of the hidden input field\n   * to the selected star's value.\n   */\n  private selectStar(target: HTMLButtonElement) {\n    const score = parseInt(target.dataset.idx, 10);\n    const scoreInput = this.shadowRoot.querySelector<HTMLInputElement>('input[name=score]');\n    scoreInput.value = score.toString();\n\n    const stars = this.shadowRoot.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        // idx > score\n        star.textContent = '☆';\n        star.setAttribute('aria-checked', 'false');\n      }\n    }\n  }\n}\n\ninterface ReviewSubmittedDetail {\n  /**\n   * The body content of the review.\n   */\n  body: string;\n\n  /**\n   * Product type.\n   *\n   * @example \"hair-dryer\"\n   */\n  category: string;\n\n  /**\n   * An email address provided by the reviewer.\n   */\n  email: string;\n\n  /**\n   * Error message if review submission failed\n   */\n  error_message: string;\n\n  event_id?: string;\n\n  first_name: string;\n\n  last_name: string;\n\n  /**\n   * The name provided by the visitor submitting the review.\n   *\n   * @example \"Jane Doe\"\n   */\n  person_name: string;\n\n  product_id: number;\n\n  product_price: number;\n\n  /**\n   * The name of the product that was reviewed.\n   *\n   * @example \"Le Volume Blush 75mm\"\n   */\n  product_title: string;\n\n  /**\n   * The number of stars specified by the person submitting the review. May be 0.\n   *\n   * @example 1\n   */\n  rating: number;\n\n  review_title: string;\n\n  /**\n   * The SKU of the product that was reviewed\n   */\n  sku: string;\n}\n\n/**\n * This event is emitted when a user submits a product review.\n *\n * Technically this event is emitted after a successful request to the Yotpo API to create the\n * product review. There is no event emitted on error.\n */\ntype ReviewSubmittedEvent = CustomEvent<Partial<ReviewSubmittedDetail>>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-review-submitted': ReviewSubmittedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-review-create': YotpoReviewCreate;\n  }\n}\n\nif (!customElements.get('yotpo-review-create')) {\n  customElements.define('yotpo-review-create', YotpoReviewCreate);\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","/**\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 { 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 onModalOpenRequestedBound = this.onModalOpenRequested.bind(this);\n  private onModalCloseRequestedBound = this.onModalCloseRequested.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-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.addEventListener('modal-close-requested', this.onModalCloseRequestedBound);\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-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.removeEventListener('modal-close-requested', this.onModalCloseRequestedBound);\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    this.dispatchEvent(new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    }));\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   * Only the modal manager is meant to act on this request. We listen for it here only because it\n   * comes from one of our own child elements, and we run before the manager's listener on the\n   * window.\n   */\n  private onModalOpenRequested(event: HTMLElementEventMap['modal-open-requested']) {\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. We run before the modal manager's listener on the window\n   * puts focus back on the element that opened the modal, and a focus call into an inert subtree\n   * does nothing.\n   */\n  private onModalCloseRequested(_event: HTMLElementEventMap['modal-close-requested']) {\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    this.dispatchEvent(new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    }));\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 onModalOpenRequestedBound = this.onModalOpenRequested.bind(this);\n  private onModalCloseRequestedBound = this.onModalCloseRequested.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-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.addEventListener('modal-close-requested', this.onModalCloseRequestedBound);\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-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.removeEventListener('modal-close-requested', this.onModalCloseRequestedBound);\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   * Only the modal manager is meant to act on this request. We listen for it here only because it\n   * comes from our own slotted child, and we run before the manager's listener on the window.\n   */\n  private onModalOpenRequested(_event: HTMLElementEventMap['modal-open-requested']) {\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. We run before the modal manager's listener on\n   * the window puts focus back on the element that opened the drawer, and a focus call into an\n   * inert subtree does nothing.\n   */\n  private onModalCloseRequested(_event: HTMLElementEventMap['modal-close-requested']) {\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":["SimpleRatingStars","HTMLElement","observedAttributes","connectedCallback","this","initializeShadowRoot","render","attributeChangedCallback","_name","oldValue","newValue","isConnected","shadowRoot","attachShadow","mode","innerHTML","rating","parseInt","dataset","querySelector","textContent","assert","expression","message","AssertionError","customElements","get","define","Error","constructor","super","name","decorateError","error","key","value","assignmentError","reason","console","log","tag","Object","prototype","toString","call","YotpoAiSummary","fetchStarted","paragraphSummary","summaryTopics","summaryHighlights","onTopicLinksClickedBound","onTopicLinksClicked","bind","onCloseButtonClickedBound","onCloseButtonClicked","onModalDismissRequestedBound","onModalDismissRequested","topicLinks","addEventListener","closeButton","readAll","startFetching","catch","warn","disconnectedCallback","removeEventListener","yotpoAppKey","productId","response","async","input","app_key","Number","isInteger","product_id","path","fetch","ok","status","code","url","responseText","body","text","JSON","parse","fetchReviewsSummary","event","target","HTMLButtonElement","setAttribute","drawer","classList","add","inert","focusEvent","Event","bubbles","composed","dispatchEvent","content","modalShowEvent","focus","focusVisible","_event","closeDrawer","remove","activeTopicLink","copy","hidden","topicObject","topic","document","createElement","displayTopic","coverage","appendChild","total","reviewsTotal","totalElement","formatter","Intl","NumberFormat","format","topics","title","summary","mentioned","li","append","highlightsList","highlight","blockquote","length","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","querySelectorAll","pill","clearAll","currentTarget","HTMLInputElement","clearTimeout","searchDebounceTimer","dispatchFilter","free_text_search","undefined","stopPropagation","query","timeout","setTimeout","HTMLSelectElement","hair_type","scores","score","detail","stars","pictured","ascending","includes","direction","split","sort_by","filters","sortings","merged","_extends","selectedFilters","CustomEvent","page","selectedSortings","renderAppliedFilters","container","existingPills","clearButton","push","dimension","label","button","className","span","select","clearAllFilters","inputs","disabled","YotpoPagination","currentPage","lastPage","maxButtons","onPreviousButtonClickedBound","onPreviousButtonClicked","onNextButtonClickedBound","onNextButtonClicked","onPageClickedBound","onPageClicked","onMediaQueryChangedBound","onMediaQueryChanged","prevButton","nextButton","pages","mediaQuery","matchMedia","matches","contains","changeEvent","idx","mediaQueryList","newMaxButtons","listWrapper","buttonPrev","buttonNext","perPage","Math","ceil","middleItemIndex","floor","startingIndex","numberOfItemsToDisplay","showFirstEllipsis","showSecondEllipsis","i","currentIndex","ellipsis","type","rnds8","Uint8Array","byteToHex","slice","unsafeStringify","arr","offset","toLowerCase","dist_v4","options","buf","crypto","randomUUID","_ref","_options$random","rnds","random","rng","getRandomValues","RangeError","_v4","SwellPopupType","SwellReferralMedium","YotpoReviewCreate","onStarClickedBound","onStarClicked","onWriteReviewButtonClickedBound","onWriteReviewButtonClicked","onSubmitButtonClickedBound","onSubmitButtonClicked","writeReviewButton","submitButton","ratingStarsElement","preventDefault","form","checkValidity","_firstInvalid","firstInvalid","element","elements","HTMLTextAreaElement","parentElement","errorSummary","formData","FormData","displayName","createReviewInput","appkey","display_name","email","product_title","productTitle","product_url","productUrl","review_content","review_score","review_title","sku","loader","responseBody","stringify","headers","method","createReview","then","reset","submittedEvent","category","productType","error_message","event_id","first_name","last_name","person_name","product_price","productPrice","identityEvent","writeReview","selectStar","star","decode_html_entities_element","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","onModalOpenRequestedBound","onModalOpenRequested","onModalCloseRequestedBound","onModalCloseRequested","reviewsPerPage","useCacheForReviews","loadingTimer","showLoading","onInitialReviewsFetched","onInitialReviewsFetchFailed","finally","hideLoading","paginationReviews","correctReviewsCount","renderReviews","renderPagination","searchEvent","isRoutineYotpoError","delay","showAlert","alert","alertPosition","alertType","fadeDelay","closable","composedPath","containers","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","review","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","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","updateStarRatingBar","filterClickedEvent","breakdown","percent","style","width","toFixed","replace"],"sourceRoot":""}