{"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-featured-review/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-featured-reviews/index.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-fetch-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-search-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/decode-html-entities.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, store fetched data, and render 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    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    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 a single featured review card. All content coming from the light DOM via slots, this\n * component encapsulates styles.\n */\nclass YotpoFeaturedReview extends HTMLElement implements CustomElement {\n  public connectedCallback() {\n    this.initializeShadowRoot();\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\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-featured-review': YotpoFeaturedReview;\n  }\n}\n\nif (!customElements.get('yotpo-featured-review')) {\n  customElements.define('yotpo-featured-review', YotpoFeaturedReview);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders Yotpo featured reviews carousel. Admin creates these reviews as Shopify metaobjects, so\n * this component is not really connected to Yotpo reviews. The carousel itself is a\n * `carousel-element` the section's liquid renders into the default slot.\n */\nclass YotpoFeaturedReviews extends HTMLElement implements CustomElement {\n  public connectedCallback() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-featured-reviews': YotpoFeaturedReviews;\n  }\n}\n\nif (!customElements.get('yotpo-featured-reviews')) {\n  customElements.define('yotpo-featured-reviews', YotpoFeaturedReviews);\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 depend on screen size to be sure the device can\n   * fit the pagination. We can't use less than 5 because we need to show the first and last page\n   * buttons, and two ellipsis buttons. If we use 5, we will show the first page, the current page,\n   * the last page, and two ellipses (\"< 1 … 5 … 10 >\"). Use only odd numbers to ensure the current\n   * 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 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 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    // 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    // 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","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","/**\n * Initialized on first call to encodeEntities, then re-used\n */\nlet element: HTMLTextAreaElement;\n\n/**\n * Encodes HTML entities in a mostly safe manner but at the expense of touching the dom.\n */\nexport function decode(input: string) {\n  if (!element) {\n    element = document.createElement('textarea');\n  }\n\n  element.innerHTML = input;\n\n  const output = element.value;\n\n  // avoid leaving around stuff in dom indefinitely, even when detached\n  element.innerHTML = '';\n\n  return output;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { LegacyReview, Review } from '@integrabeauty/yotpo-browser-types';\nimport { type Pagination, searchProductReviews } from '../yotpo-fetch-product-reviews.js';\nimport { decode } from './decode-html-entities.js';\nimport html from './index.html';\nimport styles from './index.scss';\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 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 form 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 declare loadingTimer: ReturnType<typeof setTimeout>;\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 at that\n   * 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.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\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    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 => {\n      this.hideLoading(loader);\n\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\n      const reviewsTotal = parseInt(this.dataset.reviewsTotal, 10);\n      if (reviewsTotal <= 0) {\n        return;\n      }\n\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    }).catch(error => {\n      this.hideLoading(loader);\n\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  /**\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 display 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  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    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 returns 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 returns different results)\n   *\n   * therefore this 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 * 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\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.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\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\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","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","content","length","YotpoFeaturedReview","YotpoFeaturedReviews","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","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","decode_html_entities_element","decode","output","YotpoReviews","onPageChangedBound","onPageChanged","onYotpoReviewSubmittedBound","onYotpoReviewSubmitted","onClearButtonClickedBound","onClearButtonClicked","reviewsPerPage","useCacheForReviews","loadingTimer","showLoading","hideLoading","paginationReviews","correctReviewsCount","renderReviews","renderPagination","searchEvent","isRoutineYotpoError","delay","showAlert","alert","alertPosition","alertType","fadeDelay","closable","overlay","answers","questionId","rect","getBoundingClientRect","headerOffset","parseFloat","getComputedStyle","getPropertyValue","documentElement","scrollTop","scrollY","top","scrollToElement","searchedEvent","finally","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","percent","style","width","toFixed","replace"],"sourceRoot":""}