[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Video/Vimeo.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Vimeo extends Component {
  static getThumbUrl = (url, thumbWidth) =>
    new Promise((resolve, reject) => {
      const getUrl = `https://vimeo.com/api/oembed.json?url=${url}`
      fetch(getUrl)
        .then(response => {
          return response.json()
        })
        .then(response => {
          resolve(Vimeo.thumbUrlFromResp(response, thumbWidth))
        })
        .catch(() => reject())
    })

  static thumbUrlFromResp(response, thumbWidth) {
    const { height, width } = response
    const thumb = response.thumbnail_url_with_play_button

    thumbWidth = thumbWidth || response.thumbnail_width
    const thumbHeight = Math.ceil((thumbWidth * height) / width)

    return thumb.replace(
      /_[0123456789]*x[0123456789]*./,
      `_${thumbWidth}x${thumbHeight}.`
    )
  }

  constructor(props) {
    super(props)

    this.state = { iframe: {} }

    const { loop, autoplay, width, height, showTitle, url } = this.props
    const params = `autoplay=${autoplay}&loop=${loop}&title=${showTitle}&width=${width}&height=${height}`
    const getUrl = `https://vimeo.com/api/oembed.json?url=${url}&${params}`

    this.iframeRef = React.createRef()

    fetch(getUrl)
      .then(response => {
        return response.json()
      })
      .then(response => {
        const { height, width, html, title } = response

        const thumbUrl = Vimeo.thumbUrlFromResp(response, props.thumbWidth)
        props.setThumb && props.setThumb(thumbUrl, title)

        const src = html.match(/src= *" *([^"]*) *"/)[1] // Get url from response

        this.setState({
          iframe: {
            divStyle: { padding: `${(100 * height) / width}% 0 0 0` },
            src: src,
          },
        })
      })
  }

  componentDidMount() {
    this.iframeRef.current.onload = () => (this.frameReady = true)
  }

  executeCommand = command => () => {
    if (!this.frameReady) return

    const vimeoCommand = JSON.stringify({ method: command })
    this.iframeRef.current.contentWindow.postMessage(
      vimeoCommand,
      'https://player.vimeo.com'
    )
  }

  render() {
    const { iframe } = this.state
    const { className, id } = this.props

    this.props.playing
      ? this.executeCommand('play')()
      : this.executeCommand('pause')()

    return (
      <div style={iframe.divStyle} className={`relative ${className}`}>
        <iframe
          ref={this.iframeRef}
          title={id}
          className="absolute top-0 left-0 w-100 h-100"
          src={iframe.src}
          frameBorder="0"
          allowFullScreen
          allow="autoplay"
        />
      </div>
    )
  }
}

Vimeo.propTypes = {
  url: PropTypes.string.isRequired,
  id: PropTypes.number.isRequired, // Unique ID for iframe title
  setThumb: PropTypes.func,
  thumbWidth: PropTypes.number,
  className: PropTypes.string,
  loop: PropTypes.bool,
  autoplay: PropTypes.bool,
  showTitle: PropTypes.bool,
  width: PropTypes.number,
  height: PropTypes.number,
  playing: PropTypes.bool,
}

Vimeo.defaultProps = {
  loop: true,
  autoplay: false,
  width: null,
  height: null,
  showTitle: false,
  className: '',
}

export default Vimeo

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Video/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Vimeo from './Vimeo'
import Youtube from './Youtube'

class Video extends Component {
  static getThumbUrl(url, thumbWidth, apiKey) {
    if (url.search('vimeo') !== -1) {
      return Vimeo.getThumbUrl(url, thumbWidth)
    }
    if (url.search('youtube') !== -1) {
      return Youtube.getThumbUrl(url, apiKey)
    }
    return Promise.reject(`Unsupported video url format, ${url}`)
  }

  render() {
    const { url } = this.props
    if (url.search('vimeo') !== -1) {
      return <Vimeo {...this.props} />
    }
    if (url.search('youtube') !== -1) {
      return <Youtube {...this.props} />
    }
    return null
  }
}

Video.propTypes = {
  /** Video url */
  url: PropTypes.string.isRequired,
  /** Unique id for iFrame */
  id: PropTypes.number.isRequired,
  /** Function that receives the video thumbnail URL as prop */
  setThumb: PropTypes.func,
  /** Desired width for the thumbnail retrieved by the function above */
  thumbWidth: PropTypes.number,
  className: PropTypes.string,
  /** Dynamic prop that pauses and plays the video */
  play: PropTypes.bool,
  apiKey: PropTypes.string,
}

export default Video

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Video/Youtube.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Youtube extends Component {
  static getThumbUrl = (url, apiKey) =>
    new Promise((resolve, reject) => {
      const videoId = Youtube.extractVideoID(url)
      const getUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${apiKey}`
      fetch(getUrl)
        .then(responseonse => {
          return responseonse.json()
        })
        .then(response => {
          if (response.items.length === 0) {
            reject(`No video found with URL: ${url} and apiKey: ${apiKey}`)
            return
          }
          response = response.items[0].snippet

          resolve(response.thumbnails.default.url)
        })
    })

  constructor(props) {
    super(props)

    this.state = { iframe: {} }

    const { loop, autoplay, url, apiKey } = this.props
    const params = `autoplay=${autoplay}&loop=${loop}&enablejsapi=1&iv_load_policy=3&modestbranding=1`
    const videoId = Youtube.extractVideoID(url)
    const getUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${apiKey}`

    this.iframeRef = React.createRef()

    fetch(getUrl)
      .then(responseonse => {
        return responseonse.json()
      })
      .then(response => {
        if (response.items.length === 0) return
        response = response.items[0].snippet

        const { title } = response
        const { width, height } = response.thumbnails.default
        const thumbUrl = response.thumbnails.default.url
        props.setThumb && props.setThumb(thumbUrl, title)

        const src = `https://www.youtube.com/embed/${videoId}?${params}`

        this.setState({
          iframe: {
            divStyle: { padding: `${(100 * height) / width}% 0 0 0` },
            src: src,
          },
        })
      })
  }

  static extractVideoID = url => {
    const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
    const match = url.match(regExp)
    if (match && match[7].length === 11) {
      return match[7]
    }
    console.error('Could not extract youtube video ID')
    return ''
  }

  componentDidMount() {
    this.iframeRef.current.onload = () => (this.frameReady = true)
  }

  executeCommand = command => () => {
    if (!this.frameReady) return

    const youtubeCommand = JSON.stringify({ event: 'command', func: command })
    this.iframeRef.current.contentWindow.postMessage(
      youtubeCommand,
      'https://www.youtube.com'
    )
  }

  render() {
    const { iframe } = this.state
    const { className, id } = this.props

    this.props.playing
      ? this.executeCommand('playVideo')()
      : this.executeCommand('pauseVideo')()

    return (
      <div style={iframe.divStyle} className={`relative ${className}`}>
        <iframe
          ref={this.iframeRef}
          title={id}
          className="absolute top-0 left-0 w-100 h-100"
          src={iframe.src}
          frameBorder="0"
          allowFullScreen
          allow="autoplay"
        />
      </div>
    )
  }
}

Youtube.propTypes = {
  url: PropTypes.string.isRequired,
  id: PropTypes.number.isRequired, // Unique ID for iframe title
  setThumb: PropTypes.func,
  className: PropTypes.string,
  loop: PropTypes.bool,
  autoplay: PropTypes.bool,
  width: PropTypes.number,
  height: PropTypes.number,
  playing: PropTypes.bool,
  apiKey: PropTypes.string.isRequired, // API key necessary for youtube requests
}

Youtube.defaultProps = {
  loop: true,
  autoplay: false,
  width: null,
  height: null,
  className: '',
}

export default Youtube

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ImageResizer/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import PropTypes from 'prop-types'

class ImageResizer extends React.Component {
  constructor(props) {
    super(props)
    this.canvas = React.createRef()
  }

  componentDidMount() {
    this.drawImage()
  }

  drawImage = () => {
    const { src } = this.props

    if (!src) return

    const image = new Image()

    image.onload = () => {
      const { minRatio } = this.props

      if (!this.canvas || !this.canvas.current) {
        return
      }

      const canvas = this.canvas.current

      const width =
        image.width >= image.height * minRatio
          ? image.width
          : image.height * minRatio

      const height = image.height

      const canvasContext = canvas.getContext('2d')
      canvas.width = width
      canvas.height = height

      canvasContext.drawImage(image, (width - image.width) / 2, 0)
    }
    image.src = src
  }

  componentDidUpdate(prevProps) {
    if (prevProps.src !== this.props.src) {
      this.drawImage()
    }
  }

  render() {
    const { alt, className } = this.props
    return <canvas ref={this.canvas} alt={alt} className={className} />
  }
}

ImageResizer.propTypes = {
  src: PropTypes.string,
  alt: PropTypes.string,
  minRatio: PropTypes.number,
  className: PropTypes.string,
}

export default ImageResizer

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductDescription/global.css from the butik VTEX app.

[Assistant]
Here's the implementation:

```css
.vtex-product-specifications-loader {
  height: 26em;
}

.border-collapse {
  border-collapse: collapse;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductDescription/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl, intlShape } from 'react-intl'
import HtmlParser from 'react-html-parser'
import GradientCollapse from '../GradientCollapse'

import './global.css'

/**
 * Product Description Component.
 * Render the description and technical specifications of a product
 */
class ProductDescription extends Component {
  render() {
    const { specifications, description } = this.props

    if (!description && !specifications) {
      return null
    }

    const specificationItems = specifications.map(specification => {
      return {
        property: specification.name,
        specifications: specification.values[0],
      }
    })

    return (
      <div className="vtex-product-description flex-l">
        <div className="w-100 w-60-l">
          {description && (
            <React.Fragment>
              <div className="lh-solid b c-on-base mb5">
                <FormattedMessage id="product-description.title" />
              </div>

              <div className="c-muted-1">
                <GradientCollapse collapseHeight={220}>
                  {HtmlParser(description)}
                </GradientCollapse>
              </div>
            </React.Fragment>
          )}
        </div>
        {specifications && specifications.length > 0 && (
          <div className="vtex-product-specifications mt6 mt0-l w-100 w-40-l pl8-l">
            <div className="vtex-product-specifications__title lh-solid b c-on-base mb5">
              <FormattedMessage id="technicalspecifications.title" />
            </div>
            <GradientCollapse collapseHeight={220}>
              <table className="vtex-product-specifications__table w-100 bg-base border-collapse">
                <thead>
                  <tr>
                    <th className="w-50 b--muted-4 bb bt c-muted-2 t-body tl pa5">
                      <FormattedMessage id="product-description.property" />
                    </th>
                    <th className="w-50 b--muted-4 bb bt c-muted-2 t-body tl pa5">
                      <FormattedMessage id="product-description.specification" />
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {specificationItems.map((specification, i) => (
                    <tr key={i}>
                      <td className="w-50 b--muted-4 bb pa5">
                        {specification.property}
                      </td>
                      <td className="w-50 b--muted-4 bb pa5">
                        {specification.specifications}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </GradientCollapse>
          </div>
        )}
      </div>
    )
  }
}

ProductDescription.defaultProps = {
  specifications: [],
}

ProductDescription.propTypes = {
  /** Product description string */
  description: PropTypes.string,
  /** Intl object to provides internationalization */
  intl: intlShape.isRequired,
  /** Specifications that will be displayed on the table */
  specifications: PropTypes.arrayOf(
    PropTypes.shape({
      /** Specification name */
      name: PropTypes.string.isRequired,
      /** Specifications value */
      values: PropTypes.arrayOf(PropTypes.string).isRequired,
    })
  ),
}

export default injectIntl(ProductDescription)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductDescription/README.md from the butik VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Product Description
Product Description is a canonical component that any VTEX app can import.

To import it into your code:
```js
import ProductDescription from components2
```

## Usage
You can use it in your code like a React component with the jsx tag: `<ProductDescription />`.
```jsx
<ProductDescription specifications={product.specs}>
   <span>{product.description}</span>
</ProductDescription>
```

| Prop name                | Type       | Description                                                                 |
| ------------------------ | ---------- | --------------------------------------------------------------------------- |
| `children`               | `Node!`    | Children component which contains the product description                   |
| `specifications`         | `Array`    | Specifications that will be displayed on the table                          |
| `specifications[n].name` | `String`   | Specification name                                                          |
| `specifications[n].value`| `String`   | Specifications value                                                        |

See an example at [Product Details](https://github.com/vtex-apps/product-details) app

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/FixedButton/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Fragment } from 'react'
import BottomExtraSpace from './BottomExtraSpace'
import NoSsr from '@material-ui/core/NoSsr'

export default props => (
  <Fragment>
    <div className="fixed bottom-0 left-0 w-100 z-999">{props.children}</div>
    <NoSsr>
      <BottomExtraSpace>{props.children}</BottomExtraSpace>
    </NoSsr>
  </Fragment>
)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/FixedButton/BottomExtraSpace.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'

class BottomExtraSpace extends Component {
  constructor(props) {
    super(props)

    this.ref = React.createRef()
    this.state = { mounted: false }
    this.el = document.createElement('div')
  }

  componentDidMount() {
    this.fixedRoot = this.ref.current

    while (this.fixedRoot &&
    window.getComputedStyle(this.fixedRoot)
      .getPropertyValue('overflow') !== 'auto') { this.fixedRoot = this.fixedRoot.parentElement }

    if (!this.fixedRoot)this.fixedRoot = document.body

    this.fixedRoot && this.fixedRoot.appendChild(this.el)
    this.setState({ mounted: true })
  }

  componentWillUnmount() {
    this.fixedRoot && this.fixedRoot.removeChild(this.el)
  }

  render() {
    const { mounted } = this.state

    if (!mounted) return <div ref={this.ref} />

    const children = this.props.children

    return ReactDOM.createPortal(
      <div className="o-0" tabIndex="-1">
        {children}
      </div>,
      this.el,
    )
  }
}

BottomExtraSpace.propTypes = {
  children: PropTypes.node,
}

export default BottomExtraSpace

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Carousel/global.css from the butik VTEX app.

[Assistant]
Here's the implementation:

```css
/**
 * Swiper 4.4.1
 * Most modern mobile touch slider and framework with hardware accelerated transitions
 * http://www.idangero.us/swiper/
 *
 * Copyright 2014-2018 Vladimir Kharlampidi
 *
 * Released under the MIT License
 *
 * Released on: September 14, 2018
 */
.swiper-container {
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  list-style: none;
  padding: 0;
  /* Fix of Webkit flickering */
  z-index: 1;
}
.swiper-container-no-flexbox .swiper-slide {
  float: left;
}
.swiper-container-vertical > .swiper-wrapper {
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -webkit-flex-direction: column;
  -ms-flex-direction: column;
  flex-direction: column;
}
.swiper-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  z-index: 1;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-transition-property: -webkit-transform;
  transition-property: -webkit-transform;
  -o-transition-property: transform;
  transition-property: transform;
  transition-property: transform, -webkit-transform;
  -webkit-box-sizing: content-box;
  box-sizing: content-box;
}
.swiper-container-android .swiper-slide,
.swiper-wrapper {
  -webkit-transform: translate3d(0px, 0, 0);
  transform: translate3d(0px, 0, 0);
}
.swiper-container-multirow > .swiper-wrapper {
  -webkit-flex-wrap: wrap;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
}
.swiper-container-free-mode > .swiper-wrapper {
  -webkit-transition-timing-function: ease-out;
  -o-transition-timing-function: ease-out;
  transition-timing-function: ease-out;
  margin: 0 auto;
}
.swiper-slide {
  -webkit-flex-shrink: 0;
  -ms-flex-negative: 0;
  flex-shrink: 0;
  width: 100%;
  height: 100%;
  position: relative;
  -webkit-transition-property: -webkit-transform;
  transition-property: -webkit-transform;
  -o-transition-property: transform;
  transition-property: transform;
  transition-property: transform, -webkit-transform;
}
.swiper-slide-invisible-blank {
  visibility: hidden;
}
/* Auto Height */
.swiper-container-autoheight,
.swiper-container-autoheight .swiper-slide {
  height: auto;
}
.swiper-container-autoheight .swiper-wrapper {
  -webkit-box-align: start;
  -webkit-align-items: flex-start;
  -ms-flex-align: start;
  align-items: flex-start;
  -webkit-transition-property: height, -webkit-transform;
  transition-property: height, -webkit-transform;
  -o-transition-property: transform, height;
  transition-property: transform, height;
  transition-property: transform, height, -webkit-transform;
}
/* 3D Effects */
.swiper-container-3d {
  -webkit-perspective: 1200px;
  perspective: 1200px;
}
.swiper-container-3d .swiper-wrapper,
.swiper-container-3d .swiper-slide,
.swiper-container-3d .swiper-slide-shadow-left,
.swiper-container-3d .swiper-slide-shadow-right,
.swiper-container-3d .swiper-slide-shadow-top,
.swiper-container-3d .swiper-slide-shadow-bottom,
.swiper-container-3d .swiper-cube-shadow {
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
}
.swiper-container-3d .swiper-slide-shadow-left,
.swiper-container-3d .swiper-slide-shadow-right,
.swiper-container-3d .swiper-slide-shadow-top,
.swiper-container-3d .swiper-slide-shadow-bottom {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 10;
}
.swiper-container-3d .swiper-slide-shadow-left {
  background-image: -webkit-gradient(linear, right top, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
  background-image: -webkit-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: -o-linear-gradient(right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
.swiper-container-3d .swiper-slide-shadow-right {
  background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
.swiper-container-3d .swiper-slide-shadow-top {
  background-image: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
  background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
.swiper-container-3d .swiper-slide-shadow-bottom {
  background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0)));
  background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
/* IE10 Windows Phone 8 Fixes */
.swiper-container-wp8-horizontal,
.swiper-container-wp8-horizontal > .swiper-wrapper {
  -ms-touch-action: pan-y;
  touch-action: pan-y;
}
.swiper-container-wp8-vertical,
.swiper-container-wp8-vertical > .swiper-wrapper {
  -ms-touch-action: pan-x;
  touch-action: pan-x;
}
.swiper-button-prev,
.swiper-button-next {
  position: absolute;
  top: 50%;
  width: 27px;
  height: 44px;
  margin-top: -22px;
  z-index: 10;
  cursor: pointer;
  background-size: 27px 44px;
  background-position: center;
  background-repeat: no-repeat;
}
.swiper-button-prev.swiper-button-disabled,
.swiper-button-next.swiper-button-disabled {
  opacity: 0.35;
  cursor: auto;
  pointer-events: none;
}
.swiper-button-prev,
.swiper-container-rtl .swiper-button-next {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E");
  left: 10px;
  right: auto;
}
.swiper-button-next,
.swiper-container-rtl .swiper-button-prev {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E");
  right: 10px;
  left: auto;
}
.swiper-button-prev.swiper-button-white,
.swiper-container-rtl .swiper-button-next.swiper-button-white {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-next.swiper-button-white,
.swiper-container-rtl .swiper-button-prev.swiper-button-white {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-prev.swiper-button-black,
.swiper-container-rtl .swiper-button-next.swiper-button-black {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-next.swiper-button-black,
.swiper-container-rtl .swiper-button-prev.swiper-button-black {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E");
}
.swiper-button-lock {
  display: none;
}
.swiper-pagination {
  position: absolute;
  text-align: center;
  -webkit-transition: 300ms opacity;
  -o-transition: 300ms opacity;
  transition: 300ms opacity;
  -webkit-transform: translate3d(0, 0, 0);
  transform: translate3d(0, 0, 0);
  z-index: 10;
}
.swiper-pagination.swiper-pagination-hidden {
  opacity: 0;
}
/* Common Styles */
.swiper-pagination-fraction,
.swiper-pagination-custom,
.swiper-container-horizontal > .swiper-pagination-bullets {
  bottom: 10px;
  left: 0;
  width: 100%;
}
/* Bullets */
.swiper-pagination-bullets-dynamic {
  overflow: hidden;
  font-size: 0;
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet {
  -webkit-transform: scale(0.33);
  -ms-transform: scale(0.33);
  transform: scale(0.33);
  position: relative;
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active {
  -webkit-transform: scale(1);
  -ms-transform: scale(1);
  transform: scale(1);
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main {
  -webkit-transform: scale(1);
  -ms-transform: scale(1);
  transform: scale(1);
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev {
  -webkit-transform: scale(0.66);
  -ms-transform: scale(0.66);
  transform: scale(0.66);
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev {
  -webkit-transform: scale(0.33);
  -ms-transform: scale(0.33);
  transform: scale(0.33);
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next {
  -webkit-transform: scale(0.66);
  -ms-transform: scale(0.66);
  transform: scale(0.66);
}
.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next {
  -webkit-transform: scale(0.33);
  -ms-transform: scale(0.33);
  transform: scale(0.33);
}
.swiper-pagination-bullet {
  width: 8px;
  height: 8px;
  display: inline-block;
  border-radius: 100%;
  background: #000;
  opacity: 0.2;
}
button.swiper-pagination-bullet {
  border: none;
  margin: 0;
  padding: 0;
  -webkit-box-shadow: none;
  box-shadow: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}
.swiper-pagination-clickable .swiper-pagination-bullet {
  cursor: pointer;
}
.swiper-pagination-bullet-active {
  opacity: 1;
  background: currentColor;
}
.swiper-container-vertical > .swiper-pagination-bullets {
  right: 10px;
  top: 50%;
  -webkit-transform: translate3d(0px, -50%, 0);
  transform: translate3d(0px, -50%, 0);
}
.swiper-container-vertical > .swiper-pagination-bullets .swiper-pagination-bullet {
  margin: 6px 0;
  display: block;
}
.swiper-container-vertical > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic {
  top: 50%;
  -webkit-transform: translateY(-50%);
  -ms-transform: translateY(-50%);
  transform: translateY(-50%);
  width: 8px;
}
.swiper-container-vertical > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet {
  display: inline-block;
  -webkit-transition: 200ms top, 200ms -webkit-transform;
  transition: 200ms top, 200ms -webkit-transform;
  -o-transition: 200ms transform, 200ms top;
  transition: 200ms transform, 200ms top;
  transition: 200ms transform, 200ms top, 200ms -webkit-transform;
}
.swiper-container-horizontal > .swiper-pagination-bullets .swiper-pagination-bullet {
  margin: 0 4px;
}
.swiper-container-horizontal > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic {
  left: 50%;
  -webkit-transform: translateX(-50%);
  -ms-transform: translateX(-50%);
  transform: translateX(-50%);
  white-space: nowrap;
}
.swiper-container-horizontal > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet {
  -webkit-transition: 200ms left, 200ms -webkit-transform;
  transition: 200ms left, 200ms -webkit-transform;
  -o-transition: 200ms transform, 200ms left;
  transition: 200ms transform, 200ms left;
  transition: 200ms transform, 200ms left, 200ms -webkit-transform;
}
.swiper-container-horizontal.swiper-container-rtl > .swiper-pagination-bullets-dynamic .swiper-pagination-bullet {
  -webkit-transition: 200ms right, 200ms -webkit-transform;
  transition: 200ms right, 200ms -webkit-transform;
  -o-transition: 200ms transform, 200ms right;
  transition: 200ms transform, 200ms right;
  transition: 200ms transform, 200ms right, 200ms -webkit-transform;
}
/* Progress */
.swiper-pagination-progressbar {
  background: rgba(0, 0, 0, 0.25);
  position: absolute;
}
.swiper-pagination-progressbar .swiper-pagination-progressbar-fill {
  background: currentColor;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  -webkit-transform: scale(0);
  -ms-transform: scale(0);
  transform: scale(0);
  -webkit-transform-origin: left top;
  -ms-transform-origin: left top;
  transform-origin: left top;
}
.swiper-container-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill {
  -webkit-transform-origin: right top;
  -ms-transform-origin: right top;
  transform-origin: right top;
}
.swiper-container-horizontal > .swiper-pagination-progressbar,
.swiper-container-vertical > .swiper-pagination-progressbar.swiper-pagination-progressbar-opposite {
  width: 100%;
  height: 4px;
  left: 0;
  top: 0;
}
.swiper-container-vertical > .swiper-pagination-progressbar,
.swiper-container-horizontal > .swiper-pagination-progressbar.swiper-pagination-progressbar-opposite {
  width: 4px;
  height: 100%;
  left: 0;
  top: 0;
}
.swiper-pagination-white .swiper-pagination-bullet-active {
  background: #ffffff;
}
.swiper-pagination-progressbar.swiper-pagination-white {
  background: rgba(255, 255, 255, 0.25);
}
.swiper-pagination-progressbar.swiper-pagination-white .swiper-pagination-progressbar-fill {
  background: #ffffff;
}
.swiper-pagination-black .swiper-pagination-bullet-active {
  background: #000000;
}
.swiper-pagination-progressbar.swiper-pagination-black {
  background: rgba(0, 0, 0, 0.25);
}
.swiper-pagination-progressbar.swiper-pagination-black .swiper-pagination-progressbar-fill {
  background: #000000;
}
.swiper-pagination-lock {
  display: none;
}
/* Scrollbar */
.swiper-scrollbar {
  border-radius: 10px;
  position: relative;
  -ms-touch-action: none;
  background: rgba(0, 0, 0, 0.1);
}
.swiper-container-horizontal > .swiper-scrollbar {
  position: absolute;
  left: 1%;
  bottom: 3px;
  z-index: 50;
  height: 5px;
  width: 98%;
}
.swiper-container-vertical > .swiper-scrollbar {
  position: absolute;
  right: 3px;
  top: 1%;
  z-index: 50;
  width: 5px;
  height: 98%;
}
.swiper-scrollbar-drag {
  height: 100%;
  width: 100%;
  position: relative;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 10px;
  left: 0;
  top: 0;
}
.swiper-scrollbar-cursor-drag {
  cursor: move;
}
.swiper-scrollbar-lock {
  display: none;
}
.swiper-zoom-container {
  width: 100%;
  height: 100%;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
  text-align: center;
}
.swiper-zoom-container > img,
.swiper-zoom-container > svg,
.swiper-zoom-container > canvas {
  max-width: 100%;
  max-height: 100%;
  -o-object-fit: contain;
  object-fit: contain;
}
.swiper-slide-zoomed {
  cursor: move;
}
/* Preloader */
.swiper-lazy-preloader {
  width: 42px;
  height: 42px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -21px;
  margin-top: -21px;
  z-index: 10;
  -webkit-transform-origin: 50%;
  -ms-transform-origin: 50%;
  transform-origin: 50%;
  -webkit-animation: swiper-preloader-spin 1s steps(12, end) infinite;
  animation: swiper-preloader-spin 1s steps(12, end) infinite;
}
.swiper-lazy-preloader:after {
  display: block;
  content: '';
  width: 100%;
  height: 100%;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%236c6c6c'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  background-position: 50%;
  background-size: 100%;
  background-repeat: no-repeat;
}
.swiper-lazy-preloader-white:after {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%23fff'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
@-webkit-keyframes swiper-preloader-spin {
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes swiper-preloader-spin {
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
/* a11y */
.swiper-container .swiper-notification {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  opacity: 0;
  z-index: -1000;
}
.swiper-container-fade.swiper-container-free-mode .swiper-slide {
  -webkit-transition-timing-function: ease-out;
  -o-transition-timing-function: ease-out;
  transition-timing-function: ease-out;
}
.swiper-container-fade .swiper-slide {
  pointer-events: none;
  -webkit-transition-property: opacity;
  -o-transition-property: opacity;
  transition-property: opacity;
}
.swiper-container-fade .swiper-slide .swiper-slide {
  pointer-events: none;
}
.swiper-container-fade .swiper-slide-active,
.swiper-container-fade .swiper-slide-active .swiper-slide-active {
  pointer-events: auto;
}
.swiper-container-cube {
  overflow: visible;
}
.swiper-container-cube .swiper-slide {
  pointer-events: none;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  z-index: 1;
  visibility: hidden;
  -webkit-transform-origin: 0 0;
  -ms-transform-origin: 0 0;
  transform-origin: 0 0;
  width: 100%;
  height: 100%;
}
.swiper-container-cube .swiper-slide .swiper-slide {
  pointer-events: none;
}
.swiper-container-cube.swiper-container-rtl .swiper-slide {
  -webkit-transform-origin: 100% 0;
  -ms-transform-origin: 100% 0;
  transform-origin: 100% 0;
}
.swiper-container-cube .swiper-slide-active,
.swiper-container-cube .swiper-slide-active .swiper-slide-active {
  pointer-events: auto;
}
.swiper-container-cube .swiper-slide-active,
.swiper-container-cube .swiper-slide-next,
.swiper-container-cube .swiper-slide-prev,
.swiper-container-cube .swiper-slide-next + .swiper-slide {
  pointer-events: auto;
  visibility: visible;
}
.swiper-container-cube .swiper-slide-shadow-top,
.swiper-container-cube .swiper-slide-shadow-bottom,
.swiper-container-cube .swiper-slide-shadow-left,
.swiper-container-cube .swiper-slide-shadow-right {
  z-index: 0;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.swiper-container-cube .swiper-cube-shadow {
  position: absolute;
  left: 0;
  bottom: 0px;
  width: 100%;
  height: 100%;
  background: #000;
  opacity: 0.6;
  -webkit-filter: blur(50px);
  filter: blur(50px);
  z-index: 0;
}
.swiper-container-flip {
  overflow: visible;
}
.swiper-container-flip .swiper-slide {
  pointer-events: none;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  z-index: 1;
}
.swiper-container-flip .swiper-slide .swiper-slide {
  pointer-events: none;
}
.swiper-container-flip .swiper-slide-active,
.swiper-container-flip .swiper-slide-active .swiper-slide-active {
  pointer-events: auto;
}
.swiper-container-flip .swiper-slide-shadow-top,
.swiper-container-flip .swiper-slide-shadow-bottom,
.swiper-container-flip .swiper-slide-shadow-left,
.swiper-container-flip .swiper-slide-shadow-right {
  z-index: 0;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.swiper-container-coverflow .swiper-wrapper {
  /* Windows 8 IE 10 fix */
  -ms-perspective: 1200px;
}

/*------------------ vtex -----------------------*/
.gallery-cursor {
  cursor: url('cursor.svg') 8 8, default;
}

.gallery-thumbs .swiper-slide {
  height: auto;
}

.gallery-thumbs .swiper-slide:not(.swiper-slide-thumb-active) .thumb-border {
  border-width: 0px;
}

@media screen and (min-width: 40em) {
  .ml-20-ns {
    margin-left: 20%;
  }
}

.center-all{
  margin: auto
}

.border-box{
  box-sizing: border-box;
}

.top-50{
  top: 50%;
}

.translate--50y{
  transform: translate(0, -50%);
}

.vtex__icon-caret-right,
.vtex__icon-caret-left {
  stroke: white;
  stroke-width: 0.5px;
  -webkit-filter: drop-shadow( 0px 1px 1px #CACBCC );
  filter: drop-shadow( 0px 1px 1px #CACBCC );
}

.cursor-default:hover{
  cursor: default;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Carousel/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import debounce from 'debounce'
import Loader from './Loader.js'
import { path } from 'ramda'
import BlurredLoader from '../BlurredLoader'
import Video from '../Video'
import CaretLeft from '@vtex/styleguide/lib/icon/CaretLeft'
import CaretRight from '@vtex/styleguide/lib/icon/CaretRight'
import placeholder from './noimage.svg'

import './global.css'

const Swiper = window.navigator ? require('react-id-swiper').default : null

const initialState = {
  Swiper: null,
  loaded: [],
  thumbSwiper: null,
  gallerySwiper: null,
  thumbUrl: [],
  alt: [],
  thumbsLoaded: false,
  activeIndex: 0,
  failedImageIndexes: [],
}

const placeholderSlide = {
  alt: 'Failed image',
  bestUrlIndex: 0,
  thumbUrl: placeholder,
  type: 'image',
  urls: [placeholder],
}

class Carousel extends Component {
  constructor(props) {
    super(props)

    this.CaretPrev = props.CaretPrev || CaretLeft
    this.CaretNext = props.CaretNext || CaretRight
    this.state = initialState
    this.setInitialVariablesState()
  }

  setInitialVariablesState() {
    const slides = this.props.slides

    this.isVideo = []
    this.rebuildGalleryOnUpdate = false
    this.thumbLoadCount = 0

    if (slides) {
      const thumbPromises = slides.map((slide, i) => {
        if (slide.type === 'video') {
          try {
            this.isVideo[i] = true
            const thumbUrl = Video.getThumbUrl(
              slide.src,
              slide.thumbWidth,
              slide.apiKey
            )
            return thumbUrl
              .then(url => ({ thumbUrl: url, index: i }))
              .catch(() => Promise.reject(i))
          } catch (error) {
            return Promise.reject(i)
          }
        }
        return Promise.resolve({
          thumbUrl: slide.thumbUrl,
          index: i,
        })
      })
      thumbPromises.forEach(promise => {
        promise.then(this.getThumb).catch(index => {
          this.incrementThumbsLoaded()
          this.setState({
            failedImageIndexes: this.state.failedImageIndexes.concat(index),
          })
        })
      })
    }
  }

  debouncedRebuildOnUpdate = debounce(() => {
    this.state.thumbSwiper && this.state.thumbSwiper.update()
  }, 500)

  getThumb = ({ thumbUrl, index }) => {
    if (!window.navigator) return // Image object doesn't exist when it's being rendered in the server side

    const image = new Image()
    image.onload = () => {
      this.incrementThumbsLoaded()
    }
    image.onerror = () => {
      this.incrementThumbsLoaded()
      this.setState({
        failedImageIndexes: this.state.failedImageIndexes.concat(index),
      })
    }
    image.src = thumbUrl
  }

  incrementThumbsLoaded() {
    this.thumbLoadCount++
    if (this.thumbLoadCount === this.props.slides.length) {
      this.setState({ thumbsLoaded: true })
    }
  }

  componentDidMount() {
    window.addEventListener('resize', this.debouncedRebuildOnUpdate)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.debouncedRebuildOnUpdate)
  }

  linkThumb = () => {
    const _this = this
    return function() {
      this.visibleSlidesIndexes || (this.visibleSlidesIndexes = []) // Swiper bug fix
      _this.rebuildGalleryOnUpdate = true
      _this.setState({ thumbSwiper: this })
    }
  }

  linkGallery = () => {
    const _this = this
    return function() {
      _this.setState({ gallerySwiper: this })
    }
  }

  onSlideChange = () => {
    const _this = this
    return function() {
      const { activeIndex } = this
      _this.setState({ activeIndex })
    }
  }

  setVideoThumb = i => (url, title) => {
    const thumbUrl = { ...this.state.thumbUrl }
    const alt = { ...this.state.alt }

    thumbUrl[i] = url
    alt[i] = title

    this.setState({ thumbUrl, alt })
  }

  onImageLoad = i => () => {
    const loaded = { ...this.state.loaded }
    loaded[i] = true
    this.setState({ loaded })
  }

  renderSlide = (slide, i) => {
    const { loaded } = this.state

    if (this.state.failedImageIndexes.includes(i)) {
      slide = placeholderSlide
    }

    switch (slide.type) {
      case 'image':
        return (
          <div
            className={`w-100 ${
              loaded[i] ? 'swiper-zoom-container' : 'overflow-hidden'
            }`}>
            <BlurredLoader
              loaderType="SPINNER"
              loaderUrl={slide.thumbUrl}
              realUrls={slide.urls}
              bestUrlIndex={slide.bestUrlIndex}
              alt={slide.alt}
              onload={this.onImageLoad(i)}
            />
          </div>
        )
      case 'video':
        try {
          return (
            <Video
              url={slide.src}
              setThumb={this.setVideoThumb(i)}
              playing={i === this.state.activeIndex}
              id={i}
              apiKey={slide.apiKey}
            />
          )
        } catch (err) {
          console.error(`Error while loading ${slide.src}`)
          return null
        }
      default:
        return null
    }
  }

  componentDidUpdate(prevProps) {
    const { gallerySwiper, loaded, activeIndex } = this.state
    const { isVideo } = this

    if (prevProps.slides !== this.props.slides) {
      this.setState(initialState, () => this.setInitialVariablesState())
      return
    }

    const paginationElement = path(['pagination', 'el'], gallerySwiper)
    if (paginationElement) paginationElement.hidden = isVideo[activeIndex]

    const gallerySwiperZoom = path(['zoom'], gallerySwiper)

    if (gallerySwiperZoom) {
      loaded[activeIndex]
        ? gallerySwiperZoom.enable()
        : gallerySwiperZoom.disable()
    }
  }

  render() {
    const { thumbSwiper, thumbsLoaded } = this.state
    const { rebuildGalleryOnUpdate } = this
    const slides = this.props.slides
      ? this.props.slides.map(slide => ({ ...slide }))
      : []

    if (!thumbsLoaded || Swiper == null) {
      return <Loader slidesAmount={slides ? slides.length : 0} />
    }

    const caretClassName =
      'pv7 absolute top-50 translate--50y z-2 pointer c-action-primary'
    const galleryParams = {
      containerClass: 'swiper-container',
      pagination:
        slides.length > 1
          ? {
              el: '.swiper-pagination',
              clickable: true,
              bulletActiveClass:
                'c-action-primary swiper-pagination-bullet-active',
            }
          : {},
      navigation:
        slides.length > 1
          ? {
              prevEl: '.swiper-caret-prev',
              nextEl: '.swiper-caret-next',
              disabledClass: 'c-disabled cursor-default',
            }
          : {},
      thumbs: {
        swiper: thumbSwiper,
      },
      zoom: {
        maxRatio: 2,
      },
      rebuildOnUpdate: rebuildGalleryOnUpdate,
      resistanceRatio: slides.length > 1 ? 0.85 : 0,
      renderNextButton: () => (
        <span className={`swiper-caret-next pl7 right-1 ${caretClassName}`}>
          <this.CaretNext />
        </span>
      ),
      renderPrevButton: () => (
        <span className={`swiper-caret-prev pr7 left-1 ${caretClassName}`}>
          <this.CaretPrev />
        </span>
      ),
      on: {
        init: this.linkGallery(),
        slideChange: this.onSlideChange(),
      },
    }
    this.rebuildGalleryOnUpdate = false

    const thumbnailParams = {
      observer: true,
      containerClass: 'swiper-container h-100',
      watchSlidesVisibility: true,
      watchSlidesProgress: true,
      freeMode: true,
      direction: 'vertical',
      slidesPerView: 'auto',
      touchRatio: 0.4,
      mousewheel: true,
      preloadImages: true,
      on: {
        init: this.linkThumb(),
      },
      shouldSwiperUpdate: true,
    }

    return (
      <div className={'w-100 relative overflow-hidden'}>
        <div
          className={`w-20 gallery-thumbs bottom-0 top-0 left-0 absolute pr5 dn
          ${slides.length > 1 ? 'db-ns' : ''}`}>
          <Swiper {...thumbnailParams}>
            {slides.map((slide, i) => {
              if (this.state.failedImageIndexes.includes(i)) {
                slide.thumbUrl = placeholderSlide.thumbUrl
              }
              return (
                <div key={i} className="swiper-slide w-100 h-auto mb5">
                  <img
                    className="w-100 h-auto db"
                    alt={slide.alt ? this.state.alt[i] : ''}
                    src={slide.thumbUrl || this.state.thumbUrl[i]}
                  />
                  <div className="absolute absolute--fill b--solid b--muted-2 bw1 thumb-border" />
                </div>
              )
            })}
          </Swiper>
        </div>
        <div
          className={`w-100 ${
            slides.length > 1 ? 'w-80-ns ml-20-ns' : ''
          } border-box gallery-cursor`}>
          <Swiper {...galleryParams}>
            {slides.map((slide, i) => (
              <div key={i} className="swiper-slide center-all">
                {this.renderSlide(slide, i)}
              </div>
            ))}
          </Swiper>
        </div>
      </div>
    )
  }
}

Carousel.propTypes = {
  CaretPrev: PropTypes.node,
  CaretNext: PropTypes.node,
  slides: PropTypes.arrayOf(
    PropTypes.oneOfType([
      PropTypes.shape({
        type: PropTypes.string,
        /** Props for type == 'image' */
        urls: PropTypes.arrayOf(PropTypes.string),
        alt: PropTypes.string,
        thumbUrl: PropTypes.string,
        bestUrlIndex: PropTypes.number,
      }),
      PropTypes.shape({
        type: PropTypes.string,
        /** Props for type 'video' */
        src: PropTypes.string,
        thumbWidth: PropTypes.number,
      }),
    ])
  ),
}

export default Carousel

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/Carousel/Loader.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import ContentLoader from 'react-content-loader'
import React from 'react'

export default props => {
  if (props.slidesAmount === 1) {
    return (
      <div className="w-100 aspect-ratio aspect-ratio--1x1">
        <ContentLoader
          style={{
            position: 'absolute',
            width: '100%',
            height: '100%',
          }}
          height="100%"
          width="100%">
          <rect width="100%" height="100%" />
        </ContentLoader>
      </div>
    )
  }

  return (
    <div className="w-100 aspect-ratio aspect-ratio--1x1">
      <ContentLoader
        style={{
          position: 'absolute',
          width: '100%',
          height: '100%',
        }}
        height="100%"
        width="100%">
        <rect className="dn db-ns" width="18%" height="18%" />
        <rect className="dn db-ns" width="18%" height="18%" y="20%" />
        <rect className="dn db-ns" width="18%" height="18%" y="40%" />
        <rect className="dn db-ns" width="18%" height="18%" y="60%" />
        <rect className="dn db-ns" width="89%" height="89%" x="20%" />
        <rect className="db dn-ns" width="100%" height="100%" />
      </ContentLoader>
    </div>
  )
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/GradientCollapse/global.css from the butik VTEX app.

[Assistant]
Here's the implementation:

```css
.fade-bottom {
  background-image: linear-gradient(transparent, white);
}

.pointer-events-none {
  pointer-events: none;
}

.pointer-events-auto {
  pointer-events: auto;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/GradientCollapse/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Transition } from 'react-transition-group'
import debounce from 'debounce'
import { FormattedMessage } from 'react-intl'
import './global.css'

const transitionStyle = transitionTime => ({
  transition: `${transitionTime}ms ease-in-out`,
})

class GradientCollapse extends Component {
  constructor(props) {
    super(props)
    this.state = { isCollapseVisible: true, collapsed: true, maxHeight: 'auto' }

    this.wrapper = React.createRef()
  }

  calcMaxHeight = () => {
    const { collapseHeight } = this.props
    const wrapper = this.wrapper.current

    if (wrapper.scrollHeight > collapseHeight) {
      const maxHeight = wrapper.scrollHeight + 60
      this.setState({ isCollapseVisible: true, maxHeight })
    } else this.setState({ isCollapseVisible: false, maxHeight: 'auto' })
  }

  debouncedCalcMaxHeight = debounce(this.calcMaxHeight, 500)

  componentDidMount() {
    window.addEventListener('resize', this.debouncedCalcMaxHeight)
    this.calcMaxHeight()
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.debouncedCalcMaxHeight)
  }

  render() {
    const { children, collapseHeight } = this.props
    const { collapsed, isCollapseVisible, maxHeight } = this.state
    const height = isCollapseVisible && collapsed ? collapseHeight : maxHeight
    const transitionTime = 600
    const fadeOutTime = 400

    return (
      <Transition timeout={transitionTime} in={!collapsed}>
        {state => (
          <div
            style={{
              ...transitionStyle(transitionTime),
              height,
              overflow: 'hidden',
            }}
            className="relative">
            <div ref={this.wrapper} className="h-auto">
              {children}
            </div>
            <div
              className={`${
                isCollapseVisible ? 'flex' : 'dn'
              } absolute bottom-0 pointer-events-none w-100 h-100 flex-column justify-end`}>
              <div
                style={transitionStyle(fadeOutTime)}
                className={`${
                  state === 'entered' ? 'o-0' : ''
                } fade-bottom w-100 h-50`}
              />
              <div
                className={`${
                  state === 'entered' ? 'bg-transparent' : 'bg-base'
                } tc w-100 pointer-events-auto`}>
                <div
                  className="c-action-primary t-action pointer ma5"
                  onClick={() => this.setState({ collapsed: !collapsed })}>
                  {state === 'entered' || (collapsed && state !== 'exited') ? (
                    <FormattedMessage id="collapse.showLess" />
                  ) : (
                    <FormattedMessage id="collapse.showMore" />
                  )}
                </div>
              </div>
            </div>
          </div>
        )}
      </Transition>
    )
  }
}

GradientCollapse.propTypes = {
  /** Maximum height collapsed */
  collapseHeight: PropTypes.number.isRequired,
  children: PropTypes.node,
}

export default GradientCollapse

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/BlurredLoader/global.css from the butik VTEX app.

[Assistant]
Here's the implementation:

```css
.blur-30 {
  filter: blur(30px);
}

.transition-opacity-1 {
  transition: 1s;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/BlurredLoader/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import PropTypes from 'prop-types'
import Spinner from '@vtex/styleguide/lib/Spinner'
import LinearProgress from '@material-ui/core/LinearProgress'
import { withStyles } from '@material-ui/core/styles'
import './global.css'
import ImageResizer from '../ImageResizer'

const styles = { barColorPrimary: { backgroundColor: 'currentColor' } }
const LinearProgressWithStyle = withStyles(styles)(LinearProgress)

const LOAD_STATES = {
  LOADING: 'LOADING',
  TRANSITION: 'TRANSITION',
  LOADED: 'LOADED',
}

const imageMinRatio = 2 / 3

export const LOADER_TYPES = {
  SPINNER: 'SPINNER',
  LINEAR: 'LINEAR',
}

class BlurredLoader extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      loadState: LOAD_STATES.LOADING,
      realUrlIndex: null,
    }
    this.timers = {}
    this.loadCounter = 0
  }

  generateImage = urlIndex => {
    const { realUrls } = this.props
    const { realUrlIndex } = this.state
    const bestUrlIndex = urlIndex || this.props.bestUrlIndex

    if (realUrlIndex && bestUrlIndex <= realUrlIndex) {
      return
    }

    const hdImageLoader = new Image()
    hdImageLoader.src = realUrls[bestUrlIndex]

    hdImageLoader.onerror = () => {
      let { realUrlIndex } = this.state
      if (!realUrlIndex) realUrlIndex = 0

      if (this.loadCounter > 10) return
      this.loadCounter++
      this.generateImage(Math.max(bestUrlIndex - 1, realUrlIndex))
    }

    hdImageLoader.onload = () => {
      const { realUrlIndex } = this.state
      if (realUrlIndex && bestUrlIndex <= realUrlIndex) return

      this.setState({
        loadState: LOAD_STATES.TRANSITION,
        realUrlIndex: bestUrlIndex,
      })

      const timer = setTimeout(() => {
        this.setState({ loadState: LOAD_STATES.LOADED })
        this.props.onload && this.props.onload()
        delete this.timers[timer]
      }, 1000)
      this.timers[timer] = true

      if (this.props.bestUrlIndex > bestUrlIndex) {
        this.generateImage()
      }
    }
  }

  Loader = () => {
    const { loaderType } = this.props
    const { loadState } = this.state
    switch (loaderType) {
      case LOADER_TYPES.LINEAR:
        return (
          <div
            className={`w-100 top-0 z-2 absolute transition-opacity-1 ${
              loadState === LOAD_STATES.LOADING ? 'o-100' : 'o-0'
            }`}>
            <LinearProgressWithStyle className="c-action-primary" />
          </div>
        )
      case LOADER_TYPES.SPINNER:
        return (
          <div
            className={`absolute z-2 center-all left-0 right-0 top-0 bottom-0 transition-opacity-1 ${
              loadState === LOAD_STATES.LOADING ? 'o-100' : 'o-0'
            }`}
            style={{ height: 40, width: 40 }}>
            <Spinner />
          </div>
        )
      default:
        return null
    }
  }

  componentDidMount() {
    this.generateImage()
  }

  componentWillUnmount() {
    const keys = Object.keys(this.timers)
    keys.forEach(key => this.timers[key] && clearTimeout(key))
  }

  render() {
    const { Loader } = this
    const { className, alt, loaderUrl, realUrls } = this.props
    const { loadState, realUrlIndex } = this.state
    const loaded = loadState === LOAD_STATES.LOADED

    return (
      <React.Fragment>
        <ImageResizer
          className={`w-100 ${loaded ? 'db' : 'dn'}`}
          alt={alt}
          src={realUrls[realUrlIndex]}
          minRatio={imageMinRatio}
        />
        {!loaded && (
          <div className="relative w-100 db">
            <Loader />
            <ImageResizer
              alt={alt}
              src={loaderUrl}
              minRatio={imageMinRatio}
              className={`w-100 blur-30 transition-opacity-1 db z-2 o-100 ${className}`}
            />
            <ImageResizer
              alt=""
              src={realUrls[realUrlIndex]}
              minRatio={imageMinRatio}
              className={`absolute z-1 w-100 center left-0 right-0 bottom-0 top-0 transition-opacity-1 db ${
                loadState === LOAD_STATES.LOADING ? 'o-0' : 'o-100'
              }`}
            />
          </div>
        )}
      </React.Fragment>
    )
  }
}

BlurredLoader.propTypes = {
  className: PropTypes.string,
  alt: PropTypes.string,
  loaderUrl: PropTypes.string.isRequired,
  realUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
  thresholds: PropTypes.arrayOf(PropTypes.number),
  onload: PropTypes.func,
  bestUrlIndex: PropTypes.number,
  loaderType: PropTypes.oneOf([LOADER_TYPES.SPINNER, LOADER_TYPES.LINEAR]),
}

BlurredLoader.defaultProps = {
  thresholds: [],
}

export default BlurredLoader

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductPrice/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component } from 'react'
import classNames from 'classnames'
import { isNil } from 'ramda'
import ContentLoader from 'react-content-loader'
import { FormattedMessage, injectIntl } from 'react-intl'

import PricePropTypes from './propTypes'
import Installments from './Installments'

/**
 * The Price component. Shows the prices information of the Product Summary.
 */
class Price extends Component {
  static propTypes = PricePropTypes

  static defaultProps = {
    showListPrice: true,
    showLabels: true,
    showInstallments: false,
    showSavings: false,
    labelSellingPrice: null,
  }

  constructor(props) {
    super(props)

    this.currencyOptions = {
      style: 'currency',
      currency: props.currency,
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    }
  }

  render() {
    const {
      sellingPrice,
      listPrice,
      unavailable,
      showListPrice,
      showInstallments,
      showLabels,
      showSavings,
      showStartingAt,
      labelSellingPrice,
      className,
      loaderClass,
      listPriceContainerClass,
      listPriceLabelClass,
      listPriceClass,
      sellingPriceContainerClass,
      sellingPriceLabelClass,
      sellingPriceClass,
      savingsContainerClass,
      savingsClass,
      installments,
      installmentClass,
      interestRateClass,
      installmentContainerClass,
      startingAtClass,
      unavailableClass,
      styles,
      intl: { formatNumber },
    } = this.props

    if ((showListPrice && isNil(listPrice)) || isNil(sellingPrice)) {
      return <Price.Loader loaderClass={loaderClass} {...styles} />
    }

    if (unavailable)
      return (
        <div className={unavailableClass}>
          <FormattedMessage id="pricing.unavailable" />
        </div>
      )

    const differentPrices = showListPrice && sellingPrice !== listPrice

    return (
      <div className={classNames('vtex-price', className)}>
        {differentPrices && (
          <div
            className={classNames(
              'vtex-price-list__container',
              listPriceContainerClass
            )}>
            {showLabels && (
              <div
                className={classNames(
                  'vtex-price-list__label',
                  listPriceLabelClass
                )}>
                <FormattedMessage id="pricing.from" />
              </div>
            )}
            <span className={classNames('vtex-price-list', listPriceClass)}>
              {formatNumber(listPrice, this.currencyOptions)}
            </span>
          </div>
        )}
        <div
          className={classNames(
            'vtex-price-selling__container',
            sellingPriceContainerClass
          )}>
          {showLabels && (
            <div
              className={classNames(
                'vtex-price-selling__label',
                sellingPriceLabelClass
              )}>
              {labelSellingPrice || <FormattedMessage id="pricing.to" />}
            </div>
          )}
          {showStartingAt && (
            <span
              className={classNames(
                'vtex-price-selling__from-tag',
                startingAtClass
              )}>
              <FormattedMessage id="pricing.startingAtLabel" />{' '}
            </span>
          )}
          <div className={classNames('vtex-price-selling', sellingPriceClass)}>
            {formatNumber(sellingPrice, this.currencyOptions)}
          </div>
        </div>
        {showInstallments && (
          <Installments
            installments={installments}
            showLabels={showLabels}
            formatNumber={formatNumber}
            currencyOptions={this.currencyOptions}
            className={installmentContainerClass}
            interestRateClass={interestRateClass}
            installmentClass={installmentClass}
          />
        )}
        {differentPrices && showSavings && (
          <div
            className={classNames(
              'vtex-price-savings__container',
              savingsContainerClass
            )}>
            <div className={classNames('vtex-price-savings', savingsClass)}>
              <FormattedMessage
                id="pricing.savings"
                values={{
                  savings: formatNumber(
                    listPrice - sellingPrice,
                    this.currencyOptions
                  ),
                }}
              />
            </div>
          </div>
        )}
      </div>
    )
  }
}

Price.Loader = (loaderProps = {}) => {
  const { loaderClass, ...filteredLoaderProps } = loaderProps

  return (
    <div className={classNames('vtex-price vtex-price-loader', loaderClass)}>
      <ContentLoader
        style={{
          width: '100%',
          height: '100%',
        }}
        width={300}
        height={70}
        preserveAspectRatio="xMinYMin meet"
        {...filteredLoaderProps}>
        <rect
          height="0.75em"
          width="50%"
          x="25%"
          {...filteredLoaderProps['vtex-price-list__container--loader']}
        />
        <rect {...filteredLoaderProps['vtex-price-selling__label--loader']} />
        <rect
          height="1em"
          width="70%"
          x="15%"
          y="1.25em"
          {...filteredLoaderProps['vtex-price-selling--loader']}
        />
        <rect
          height="0.75em"
          width="80%"
          x="10%"
          y="2.75em"
          {...filteredLoaderProps['vtex-price-installments--loader']}
        />
        <rect {...filteredLoaderProps['vtex-price-savings--loader']} />
      </ContentLoader>
    </div>
  )
}

Price.Loader.displayName = 'Price.Loader'

const priceWithIntel = injectIntl(Price)

priceWithIntel.schema = {
  title: 'editor.productPrice.title',
  description: 'editor.productPrice.description',
  type: 'object',
  properties: {
    labelSellingPrice: {
      type: 'string',
      title: 'editor.productPrice.labelSellingPrice',
      default: Price.defaultProps.labelSellingPrice,
      isLayout: true,
    },
    showListPrice: {
      type: 'boolean',
      title: 'editor.productPrice.showListPrice',
      default: Price.defaultProps.showListPrice,
      isLayout: true,
    },
    showLabels: {
      type: 'boolean',
      title: 'editor.productPrice.showLabels',
      default: Price.defaultProps.showLabels,
      isLayout: true,
    },
    showInstallments: {
      type: 'boolean',
      title: 'editor.productPrice.showInstallments',
      default: Price.defaultProps.showInstallments,
      isLayout: true,
    },
    showSavings: {
      type: 'boolean',
      title: 'editor.productPrice.showSavings',
      default: Price.defaultProps.showSavings,
      isLayout: true,
    },
  },
}

export default priceWithIntel

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductPrice/Installments.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { Component, Fragment } from 'react'
import classNames from 'classnames'
import { isEmpty } from 'ramda'
import { FormattedMessage } from 'react-intl'
import PropTypes from 'prop-types'

import PricePropTypes from './propTypes'

/** Installments component */
export default class Installments extends Component {
  static propTypes = {
    /** Classes to be applied to the root element */
    className: PropTypes.string,
    /** Classes to be applied to installment value element */
    installmentClass: PropTypes.string,
    /** Classes to be applied to interest rate element */
    interestRateClass: PropTypes.string,
    /** Product installments to be displayed */
    installments: PricePropTypes.installments,
    /** Pages editor config to display labels */
    showLabels: PropTypes.bool.isRequired,
    /** react-intl function to format the prices*/
    formatNumber: PropTypes.func.isRequired,
    /** Options to be passe to the formatNumber function*/
    currencyOptions: PropTypes.shape({
      style: PropTypes.string.isRequired,
      currency: PropTypes.string.isRequired,
      minimumFractionDigits: PropTypes.number.isRequired,
      maximumFractionDigits: PropTypes.number.isRequired,
    }).isRequired,
  }
  render() {
    const {
      showLabels,
      formatNumber,
      installments,
      currencyOptions,
      className,
      installmentClass,
      interestRateClass,
    } = this.props

    if (!installments || isEmpty(installments)) {
      return null
    }

    const noInterestRateInstallments = installments.filter(
      installment => !installment.InterestRate
    )

    /*
     * - The selected installment will be the one with the highest `NumberOfInstallments`;
     * - If there is no 'interest-free' installments, the normal installments will be analyzed.
     */
    const installment = (isEmpty(noInterestRateInstallments)
      ? installments
      : noInterestRateInstallments
    ).reduce((previous, current) =>
      previous.NumberOfInstallments > current.NumberOfInstallments
        ? previous
        : current
    )

    const formattedInstallmentPrice = formatNumber(
      installment.Value,
      currencyOptions
    )

    const [installmentsElement, installmentPriceElement, timesElement] = [
      installment.NumberOfInstallments,
      formattedInstallmentPrice,
      <Fragment>x</Fragment>,
    ].map((element, index) => (
      <span className={installmentClass} key={index}>
        {element}
      </span>
    ))

    return (
      <div
        className={classNames('vtex-price-installments__container', className)}>
        {showLabels ? (
          <FormattedMessage
            id="pricing.installment-display"
            values={{
              installments: installmentsElement,
              installmentPrice: installmentPriceElement,
              times: timesElement,
            }}
          />
        ) : (
          <Fragment>
            {installmentsElement}
            {timesElement} {installmentPriceElement}
          </Fragment>
        )}
        {!installment.InterestRate && (
          <div
            className={classNames(
              'vtex-price-installments__interest-rate',
              interestRateClass
            )}>
            <FormattedMessage id="pricing.interest-free" />
          </div>
        )}
      </div>
    )
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductPrice/README.md from the butik VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Product Price

Product Price is a canonical component that any VTEX app can import.

To import it into your code:

```js
import ProductPrice from components2
```

## Usage

You can use it in your code like a React component with the jsx tag: `<ProductPrice />`.

```jsx
<ProductPrice
  showLabels={showLabels}
  showListPrice={showListPrice}
  showInstallments={showInstallments}
  sellingPrice={commertialOffer.Price}
  listPrice={commertialOffer.ListPrice}
  installments={commertialOffer.Installments}
/>
```

### Passing classes to the elements of the component
Every prop that ends with `Class` will be passed to an element of the component, see the following code
```jsx
<ProductPrice
  className="pv4 overflow-hidden"
  listPriceLabelClass="c-muted-4"
  savingsClass="h4 ph5 dib"
  showLabels={showLabels}
  showListPrice={showListPrice}
  showInstallments={showInstallments}
  sellingPrice={commertialOffer.Price}
  listPrice={commertialOffer.ListPrice}
  installments={commertialOffer.Installments}
/>
```

| Prop name | Type | Description |
| --- | --- | --- |
| `sellingPrice` | `Number` | Product selling price |
| `listPrice` | `Number` | Product list price |
| `showListPrice` | `Boolean!` | Set visibility of list price |
| `showLabels` | `Boolean!` | Set visibility of labels |
| `showInstallments` | `Boolean!` | Set visibility of installments |
| `showSavings` | `Boolean` | Set visibility of savings |
| `installments` | `Array` | Available installments |
| `installments.Value` | `Number!` | Installment price |
| `installments.InterestRate` | `Number!` | Installment interest rate (zero if interest-free) |
| `installments.TotalValuePlusInterestRate` | `Number` | Calculated total value |
| `installments.NumberOfInstallments` | `Number!` | Number of installments |
| `installments.Name` | `String` | Installment offer name |
| `className` | `String` | Classes to be applied to root element |
| `loaderClass` | `String` | Classes to be applied to loader root element |
| `listPriceContainerClass` | `String` | Classes to be applied to container of list price |
| `listPriceLabelClass` | `String` | Classes to be applied to label of price |
| `listPriceClass` | `String` | Classes to be applied to price value |
| `sellingPriceContainerClass` | `String` | Classes to be applied to selling price container |
| `sellingPriceLabelClass` | `String` | Classes to be applied to selling price label |
| `sellingPriceClass` | `String` | Classes to be applied to selling price value |
| `savingsContainerClass` | `String` | Classes to be applied to savings container |
| `savingsClass` | `String` | Classes to be applied to savings |
| `installmentClass` | `String` | Classes to be applied to installment element |
| `installmentContainerClass` | `String` | Classes to be applied to installment container |
| `interestRateClass` | `String` | Classes to be applied to interest rate element |

See an example at [Product Summary](https://github.com/vtex-apps/product-summary) app

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductPrice/propTypes.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import PropTypes from 'prop-types'
import { intlShape } from 'react-intl'

export default {
  /** Currency, e.g.: "BRL" */
  currency: PropTypes.string,
  /** Product selling price */
  sellingPrice: PropTypes.number,
  /** Product list price */
  listPrice: PropTypes.number,
  /** True if the product is unavailable */
  unavailable: PropTypes.bool,
  /** Set visibility of list price */
  showListPrice: PropTypes.bool.isRequired,
  /** Set visibility of labels */
  showLabels: PropTypes.bool.isRequired,
  /** Set visibility of installments */
  showInstallments: PropTypes.bool.isRequired,
  /** Set visibility of savings */
  showSavings: PropTypes.bool,
  /** Shows 'From ' before the actual price */
  showStartingAt: PropTypes.bool,
  /** Text to the selling price's label */
  labelSellingPrice: PropTypes.string,
  /** Available installments */
  installments: PropTypes.arrayOf(
    PropTypes.shape({
      /** Installment value */
      Value: PropTypes.number.isRequired,
      /** Interest rate (zero if interest-free) */
      InterestRate: PropTypes.number.isRequired,
      /** Calculated total value */
      TotalValuePlusInterestRate: PropTypes.number,
      /** Number of installments */
      NumberOfInstallments: PropTypes.number.isRequired,
      /** Installment offer name */
      Name: PropTypes.string,
    })
  ),
  /** Classes to be applied to root element */
  className: PropTypes.string,
  /** Classes to be applied to loader root element */
  loaderClass: PropTypes.string,
  /** Classes to be applied to container of list price */
  listPriceContainerClass: PropTypes.string,
  /** Classes to be applied to label of price */
  listPriceLabelClass: PropTypes.string,
  /** Classes to be applied to price value */
  listPriceClass: PropTypes.string,
  /** Classes to be applied to selling price container */
  sellingPriceContainerClass: PropTypes.string,
  /** Classes to be applied to selling price label */
  sellingPriceLabelClass: PropTypes.string,
  /** Classes to be applied to selling price value */
  sellingPriceClass: PropTypes.string,
  /** Classes to be applied to savings container */
  savingsContainerClass: PropTypes.string,
  /** Classes to be applied to savings */
  savingsClass: PropTypes.string,
  /** Classes to be applied to installment element */
  installmentClass: PropTypes.string,
  /** Classes to be applied to installment container */
  installmentContainerClass: PropTypes.string,
  /** Classes to be applied to interest rate element */
  interestRateClass: PropTypes.string,
  /** Classes to be applied to the fromLabel */
  startingAtClass: PropTypes.string,
  /** Classes to be applied to the unavailable message */
  unavailableClass: PropTypes.string,
  /** Component and content loader styles */
  styles: PropTypes.object,
  /** intl property to format data */
  intl: intlShape.isRequired,
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductName/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import ContentLoader from 'react-content-loader'
import classNames from 'classnames'

/**
 * Name component. Show name and relevant SKU information of the Product Summary
 */
class ProductName extends Component {
  static propTypes = {
    /** Name of the product */
    name: PropTypes.string,
    /** Selected SKU name */
    skuName: PropTypes.string,
    /** Show sku */
    showSku: PropTypes.bool,
    /** Product reference */
    productReference: PropTypes.string,
    /** Show product reference */
    showProductReference: PropTypes.bool,
    /** Brand name */
    brandName: PropTypes.string,
    /** Show brand name */
    showBrandName: PropTypes.bool,
    /** Component and content loader styles */
    styles: PropTypes.object,
    /** Classes to be applied to root element */
    className: PropTypes.string,
    /** Classes to be applied to brandName element */
    brandNameClass: PropTypes.string,
    /** Classes to be applied to skuName element */
    skuNameClass: PropTypes.string,
    /** Classes to be applied to productReference element */
    productReferenceClass: PropTypes.string,
    /** Classes to be applied to loader root element */
    loaderClass: PropTypes.string,
  }

  static defaultProps = {
    showBrandName: false,
    showProductReference: false,
    showSku: false,
  }

  static Loader = (loaderProps = {}) => (
    <div
      className={classNames(
        'vtex-product-name vtex-product-name-loader',
        loaderProps.className
      )}>
      <ContentLoader
        style={{
          width: '100%',
          height: '100%',
        }}
        width={456}
        height={100}
        preserveAspectRatio="xMinYMin meet"
        {...loaderProps}>
        <rect
          height="1.125em"
          width="75%"
          x="15%"
          {...loaderProps['vtex-product-name__brand--loader']}
        />
        <rect
          height="1.125em"
          width="50%"
          x="25%"
          y="1.75em"
          {...loaderProps['vtex-product-name__sku--loader']}
        />
      </ContentLoader>
    </div>
  )

  render() {
    const {
      productReferenceClass,
      brandNameClass,
      skuNameClass,
      loaderClass,
      className,
      name,
      styles,
      skuName,
      showSku,
      brandName,
      showBrandName,
      productReference,
      showProductReference,
    } = this.props

    if (!name) {
      return <ProductName.Loader className={loaderClass} {...styles} />
    }

    return (
      <div className={classNames('vtex-product-name', className)}>
        <span
          className={classNames('vtex-product-name__brand', brandNameClass)}>
          {name} {showBrandName && brandName && `- ${brandName}`}
        </span>
        {showSku && skuName && (
          <span className={classNames('vtex-product-name__sku', skuNameClass)}>
            {skuName}
          </span>
        )}
        {showProductReference && productReference && (
          <span
            className={classNames(
              'vtex-product-name__product-reference',
              productReferenceClass
            )}>
            {`REF: ${productReference}`}
          </span>
        )}
      </div>
    )
  }
}

ProductName.schema = {
  title: 'editor.productName.title',
  description: 'editor.productName.description',
  type: 'object',
  properties: {
    showBrandName: {
      type: 'boolean',
      title: 'editor.productName.showBrandName.title',
      default: false,
      isLayout: true,
    },
    showSku: {
      type: 'boolean',
      title: 'editor.productName.showSku.title',
      default: false,
      isLayout: true,
    },
    showProductReference: {
      type: 'boolean',
      title: 'editor.productName.showProductReference.title',
      default: false,
      isLayout: true,
    },
  },
}

export default ProductName

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/ProductName/README.md from the butik VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Product Name
Product Name is a canonical component that any VTEX app can import.

To import it into your code:
```js
import ProductName from components2
```

## Usage
You can use it in your code like a React component with the jsx tag: `<ProductName />`.
```jsx
<ProductName
  showSku
  showBrandName
  name={product.productName}
  skuName={selectedItem.name}
  brandName={product.brand}
/>
```

## Passing classes to the elements of the component
Every prop that ends with `Class` will be passed to an element of the component, see the code below
```jsx
<ProductName
  skuNameClass="some-other-css-class"
  className="pv4 overflow-hidden"
  brandNameClass="c-muted"
  showSku
  showBrandName
  brandName={product.brand}
  name={product.productName}
  skuName={selectedItem.name}
/>
})
```

| Prop name | Type | Description |
| --- | --- | --- |
| `name` | `String!` | Name of the product |
| `skuName` | `String` | Selected SKU name |
| `showSku` | `Boolean` | Show product SKU |
| `productReference` | `String` | Product reference |
| `showProductReference` | `Boolean` | Show product reference |
| `brandName` | `String` | Brand name |
| `showBrandName` | `Boolean` | Show brand name |
| `className` | `String` | Classes to be applied to root element |
| `brandNameClass` | `String` | Classes to be applied to brandName element |
| `skuNameClass` | `String` | Classes to be applied to skuName element |
| `productReferenceClass` | `String` | Classes to be applied to productReference element |
| `loaderClass` | `String` | Classes to be applied to loader root element |

See an example at [Product Details](https://github.com/vtex-apps/product-details) app

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/SKUSelector/global.css from the butik VTEX app.

[Assistant]
Here's the implementation:

```css
/* Hide the browser's default radio button */
.vtex-sku-selector__item {
  font-size: 90%;
  margin-bottom: 10px;
  margin-right: 10px;
  height: 36px;
}

.vtex-sku-selector__item-image {
  padding: 0;
  height: 40px;
  width: 40px;
}

.diagonal-cross {
  background-image: linear-gradient(
    to top right,
    transparent 44%,
    rgba(0, 0, 0, 0.3) 48%,
    currentColor 48%,
    currentColor 52%,
    transparent 52%
  );
}

.frame-around {
  bottom: -0.25rem;
  top: -0.25rem;
  left: -0.25rem;
  right: -0.25rem;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/SKUSelector/index.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import VariationSelector from './components/VariationSelector'
import { Machine } from 'xstate'
import { interpret } from 'xstate/lib/interpreter'
import hash from 'object-hash'
import './global.css'

/**
 * Display a list of SKU items of a product and its specifications.
 */
class SKUSelector extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {}
    this.visualVariations = []
    this.standardVariations = []
    this.stateMachine = {}
    this.variationsOptions = {}
  }

  componentDidMount() {
    const { skus, variations, selectedVariations } = this.props
    if (!variations || !variations.length) return

    variations.forEach(variation => {
      /** Separate all variations into visualVariations and standardVariations based on the thumbSrc value */
      if (variation.hasThumbs) this.visualVariations.push(variation.name)
      else this.standardVariations.push(variation.name)

      /** Fills variationsOptions with all possible variationOptions */
      this.variationsOptions[variation.name] = {}
      skus.forEach(sku => {
        this.variationsOptions[variation.name][sku[variation.name]] = true
      })
    })

    this.stateMachine.interpret = interpret(
      this.createStateMachine(
        skus,
        this.visualVariations,
        this.standardVariations,
        selectedVariations && hash(selectedVariations)
      )
    )

    this.setState({ stateMachineStateHash: this.stateMachine.initial })
    this.stateMachine.interpret.onTransition(this.handleStateTransition).start()
  }

  handleStateTransition = state => {
    const stateMachineStateHash = state.value
    const { onChange } = this.props

    this.setState({ stateMachineStateHash })

    const stateMachineState = this.stateMachine.states[stateMachineStateHash]

    const selectedVariations = stateMachineState.variations
    const { sku, price } = stateMachineState

    const allVariationsSelected = Object.keys(selectedVariations).reduce(
      (accumulator, variation) =>
        accumulator && selectedVariations[variation] !== null,
      true
    )

    onChange({
      sku,
      allVariationsSelected,
      selectedVariations,
      price,
    })
  }

  /** Returns the variation that has a different label if there is exactly one different variation
   * or undefined otherwise
   * */
  getDifferentVariation = (stateA, stateB) => {
    let counter = 0
    let differentVariation = null

    Object.keys(stateA).forEach(variation => {
      if (stateA[variation] !== stateB[variation]) {
        counter++
        differentVariation = variation
      }
    })

    if (counter === 1) return differentVariation
  }

  /** Creates a state machine whose state is based on the selected variations **/
  createStateMachine = (
    skus,
    visualVariations,
    standardVariations,
    initialState
  ) => {
    const { stateMachine } = this
    stateMachine.id = 'SKUSelector'
    stateMachine.states = {}
    stateMachine.transitions = {}

    const { states, transitions } = stateMachine

    skus.forEach(sku => {
      /** Creates all possible states without selecting some standardVariations **/
      const bitMaskLimit = 1 << standardVariations.length

      for (let bitMask = 0; bitMask < bitMaskLimit; bitMask++) {
        const state = { variations: {} }

        visualVariations.forEach(
          variation => (state.variations[variation] = sku[variation])
        )

        standardVariations.forEach((variation, variationIndex) => {
          if ((1 << variationIndex) & bitMask)
            state.variations[variation] = sku[variation]
          else state.variations[variation] = null
        })

        const stateKey = hash(state.variations)

        if (bitMask === 0 && !stateMachine.initial)
          stateMachine.initial = stateKey

        if (!states[stateKey] || !states[stateKey].available) {
          if (!states[stateKey]) states[stateKey] = {}

          states[stateKey].variations = state.variations
          states[stateKey].sku = sku.sku
          states[stateKey].available = sku.available
        }

        if (sku.available) {
          if (states[stateKey].price) {
            const { value, notUnique } = states[stateKey].price

            states[stateKey].price.notUnique =
              notUnique || sku.price.notUnique || sku.price.value !== value

            states[stateKey].price.value = Math.min(value, sku.price.value)
          } else states[stateKey].price = { ...sku.price }
        }
      }
    })

    /** Creates a transition from A to B if there is only one different variation **/
    Object.keys(states).forEach(keyA => {
      states[keyA].on = {}

      Object.keys(states).forEach(keyB => {
        const stateA = states[keyA].variations
        const stateB = states[keyB].variations

        const differentVariation = this.getDifferentVariation(stateA, stateB)
        if (!differentVariation) return

        const action = {
          variation: differentVariation,
          label: stateB[differentVariation],
        }

        const actionHash = hash(action)
        transitions[actionHash] = action
        states[keyA].on[actionHash] = keyB
      })
    })

    if (initialState && stateMachine.states[initialState])
      stateMachine.initial = initialState

    return Machine(stateMachine)
  }

  onSelectItem = actionHash => () => {
    this.stateMachine.interpret.send(actionHash)
  }

  render() {
    if (!this.props.variations || !this.props.variations.length) return null

    const { visualVariations, standardVariations } = this
    const { askToSelectVariations, designTokens } = this.props
    const variations = visualVariations.concat(standardVariations)

    const { stateMachineStateHash } = this.state
    if (!stateMachineStateHash) return <div />

    const stateMachineStates = this.stateMachine.states
    const stateMachineState = stateMachineStates[stateMachineStateHash]

    const transitions = stateMachineState.on
    const selectedVariations = stateMachineState.variations

    return (
      <React.Fragment>
        {variations.map((variation, variationIndex) => {
          const selectedItem = selectedVariations[variation]

          const items = Object.keys(this.variationsOptions[variation]).reduce(
            (accumulator, label) => {
              const selected = selectedItem === label

              const actionHash = hash({
                variation,
                label: selected ? null : label,
              })

              const nextStateHash = transitions[actionHash]

              if (nextStateHash || selected) {
                const available = selected
                  ? stateMachineState.available
                  : stateMachineStates[nextStateHash].available

                accumulator.push({
                  label,
                  available,
                  thumbSrc: null,
                  onSelectItem: this.onSelectItem(actionHash),
                })
              }
              return accumulator
            },
            []
          )

          return (
            <VariationSelector
              key={variationIndex}
              variation={{
                name: variation,
                items,
              }}
              designTokens={designTokens}
              selectedItem={selectedItem}
              askToSelectVariation={askToSelectVariations && !selectedItem}
            />
          )
        })}
      </React.Fragment>
    )
  }
}

SKUSelector.propTypes = {
  /** Variation name, the same used as key in 'skus' */
  variations: PropTypes.arrayOf(
    PropTypes.shape({
      name: PropTypes.string,
      hasThumbs: PropTypes.bool,
    })
  ),
  /** Can just pass vtex product.items */
  skus: PropTypes.arrayOf(
    PropTypes.shape({
      /** Value that will be returned when selected */
      sku: PropTypes.any,
      /** True if it's available, available quantity > 0 */
      available: PropTypes.bool,
      /** Necessary for the component return the minimum price at each state */
      price: PropTypes.shape({
        /** The minimum price of that Sku */
        value: PropTypes.number,
        /** True if this sku have different prices from multiple sellers */
        notUnique: PropTypes.bool,
      }),
      /** Also has the values for each variation as variationName: label */
    })
  ),
  /** selectedVariations in the initial state of the form {variationName: label}
   *  for all variations using null if it's not selected */
  selectedVariations: PropTypes.object,
  /** Function called when the state change is triggered. It receives the selected sku,
   *  the variations that are selected and a boolean telling if all variations are selected  */
  onChange: PropTypes.func,
  /** Boolean prop that displays a warning text beside not selected variations */
  askToSelectVariations: PropTypes.bool,
  /** Used to change internal designs. When set, will replace component className **/
  designTokens: PropTypes.shape({ skuSelectorContainer: PropTypes.string }),
}

export default SKUSelector

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of butik/react/components/SKUSelector/components/VariationSelector.js from the butik VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import { FormattedMessage } from 'react-intl'
import PropTypes from 'prop-types'
import ItemSelector from './ItemSelector'

class VariationSelector extends React.PureComponent {
  render() {
    const {
      variation,
      selectedItem,
      askToSelectVariation,
      designTokens,
    } = this.props

    return (
      <div
        className={
          (designTokens && designTokens.skuSelectorContainer) ||
          'vtex-sku-selector__container flex flex-column mb7'
        }>
        <div className="vtex-sku-selector__name-container ma1">
          <span className="vtex-sku-selector__name db t-small overflow-hidden mb3">
            <span className="c-muted-2">{variation.name}</span>
            {askToSelectVariation ? (
              <span className="c-danger ml3 t-mini">
                <FormattedMessage id="sku-selector.select-warning" />{' '}
                {variation.name}
              </span>
            ) : null}
          </span>
          <div className="inline-flex flex-wrap ml2">
            {variation.items.map((item, i) => (
              <ItemSelector
                selected={selectedItem === item.label}
                key={i}
                handleSelect={item.onSelectItem}
                label={item.label}
                thumbSrc={item.thumbSrc}
                isAvailable={item.available}
                variation={variation.name}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
}

VariationSelector.propTypes = {
  variation: PropTypes.shape({
    name: PropTypes.string.isRequired,
    items: PropTypes.arrayOf(
      PropTypes.shape({
        label: PropTypes.string.isRequired,
        available: PropTypes.bool.isRequired,
        thumbSrc: PropTypes.string,
        onSelectItem: PropTypes.func.isRequired,
      })
    ).isRequired,
  }).isRequired,
  /** It's either null when no item is selected or the selected item index */
  selectedItem: PropTypes.string,
  askToSelectVariation: PropTypes.bool,
  designTokens: PropTypes.shape({ skuSelectorContainer: PropTypes.string }),
}

export default VariationSelector

```