Color Swatches

484

This example is going to explain how to build color swatches that include a tooltip with the color name as well as an option to show more colors if there are more than e.g. five available and clicking on a color changes the product image and product URL to the variant that belongs to the specific color.

Required: Merchant Dashboard Settings

In order for this to work, you will have to update a few settings in your Dashboard.

This functionality uses Color Mapping. You can find the settings within your dashboard under Setup > Color Mapping. (It's the same as for the filters.)

The filter doesn't have to be active as in the image below to be included in the response that you are going to need. Simply click on the pencil/edit icon to see further options ...

884

... and then you can return the colors in the response per each variant.

585

The same process is required for:

  • id
  • product_url
  • image_url

Now run the product sync and you're ready to customize the code.

931

Component Customizations

📘

Components:

Let's update product card view. Check out the comments within the code example for further information:

/**
 * @module components/Cards/Product
 */

import React from 'react'
import classNames from 'classnames'
import ImageComponent from 'components/common/Image'
import Truncate from 'components/common/Truncate'
import Text from 'components/Text' 
import Rating from 'components/Cards/Product/Rating'; 
import Price from 'components/Cards/Product/Price';
import template from 'helpers/template'; 
import { DiscountSticker, OutOfStockSticker  } from 'components/Cards/Product/Stickers';
import { List } from 'immutable'
import { IProduct, MJSConfiguration, ThemedSFCProps } from 'types/index';
import BundleAction from 'components/Cards/Product/BundleAction';
import { compose, withPropsOnChange, branch, withStateHandlers, withProps, lifecycle } from 'recompose';
import { Item } from 'react-connect/lib/immutable/item';
import cx from 'classnames';

const getImageUrlForColor = (() => {
  const sampleUrl = window.SAMPLE_ASSET_URL_FOR_FINDIFY;
  if (!sampleUrl) {
    return color => color;
  }
  return color => {
    const colorAsImage = `${color.toLowerCase().replace(' ', '-')}.png`;
    return sampleUrl.replace('test.txt', colorAsImage);
  }
})();  

const swatchEnhancer = compose(
  withStateHandlers(
    { background: null },
    { setBackground: () => background => ({ background }) }
  ),
  lifecycle({
    componentDidMount() {
      const colorImageUrl = getImageUrlForColor(this.props.color);
      const image = new Image();
      image.addEventListener('load', () => this.props.setBackground(colorImageUrl));
      image.src = colorImageUrl;
    } 
  })
);

const shouldDrawBorderForColor = color => color.toLowerCase() === 'white';
 
const Swatch = swatchEnhancer(({ color, availability, onClick, selected, background }) =>
<div onClick={onClick} className={cx('findify-components-swatch', !availability && 'findify-components-swatch-unavailable', selected && 'findify-components-swatch-selected')}>
  <span className="findify-components-swatch-color-ball-wrapper">
    <span
      className={cx('findify-components-swatch-color-ball', shouldDrawBorderForColor(color) && 'findify-components-swatch-color-ball-with-border')}
      style={
        background
          ? { background: `url('${background}')` }
          : {}
      }
    />
  </span>
  <span className="findify-components-swatch-tooltip">
    {color}
  </span>
</div>
);

const Swatches = ({ swatches, selectedVariantID, onClick }) =>
<div className="findify-components-swatches-container collection-swatches">
  {swatches.map(s => <Swatch
    {...s} 
    key={s.color}
    selected={selectedVariantID === s.variantID}
    onClick={e => {
      e.preventDefault();
      e.stopPropagation();
      onClick(s);
    }}
  />)}
</div>

const Title: any = ({ text, theme, ...rest }) => (
  <Text display-if={!!text} className={theme.title} {...rest}>{text}</Text>
);

export interface IProductCardProps extends ThemedSFCProps {
  item: IProduct;
  config: MJSConfiguration;
}

const withType = withPropsOnChange(['config'], ({ config }) => ({
  isAutocomplete: config.get('cssSelector').indexOf('findify-autocomplete') >= 0,
  isRecommend: config.get('cssSelector').indexOf('findify-recommendation') >= 0,
  isMobile: window.innerWidth < config.get('mobileBreakpoint')
}));

const withColorSwatches = withPropsOnChange(['item'], ({ item, isAutocomplete, isMobile }) => {
  if (isAutocomplete) {
    return { swatches: [] };
  }

  const colorsAvailable = item.get('variants').reduce((colorsAvailable, variant) => {
    const color = variant.getIn(['custom_fields', 'old_colors']) || variant.getIn(['color', 0]);
    const lowercasedColor = color && color.toLowerCase();
    const availability = variant.get('availability');
    if(availability){
      return{
        ...colorsAvailable,
        [lowercasedColor]: true
      }
    }
    return { ...colorsAvailable };
  }, {});

  const { swatches } = item.get('variants').reduce(({ swatches, colors }, variant) => {
    const color = variant.getIn(['custom_fields', 'old_colors']) || variant.getIn(['color', 0]);
    const lowercasedColor = color && color.toLowerCase();
    const imageUrl = variant.getIn(['custom_fields', 'variant_image_url']);
    if (!color || !imageUrl || colors[lowercasedColor]) {
      return { swatches, colors };
    }
    return {
      swatches: [
        ...swatches,
        {
          availability: colorsAvailable[lowercasedColor] || variant.get('availability'),
          color,
          variantID: variant.get('id'),
          imageUrl: imageUrl.replace('.jpg', '_large.jpg')
        }
      ],
      colors: {
        ...colors,
        [lowercasedColor]: true
      }
    };
  }, { swatches: [], colors: {} });

  return { swatches };
});

const applyVariantID = (item, variantID) => item.get('product_url')
  .replace(/\?variant=\d+/g, variantID ? `?variant=${variantID}` : '');

const enhancer = compose(
  withType,
  withColorSwatches,
  branch(
    ({ swatches }) => swatches.length > 1,
    compose(
      withStateHandlers(
        ({ swatches }) => ({ ...swatches[0], prevImage: swatches[0].imageUrl }),
        {
          setVariantID: ({ imageUrl }, { swatches }) => variantID => {
            const selectedVariant = swatches.find(s => s.variantID === variantID);
            return selectedVariant
              ? { ...selectedVariant, prevImage: imageUrl }
              : { ...swatches[0], prevImage: imageUrl };
          }
        }
      ),
      withProps(({ item, variantID }) => ({
        item: new Item(item.set('product_url', applyVariantID(item, variantID)), item.meta, item.analytics)
      }))
    )
  ),
  withProps(({ item, variantID }) => {
    const currentVariantID = variantID || item.get('selected_variant_id');
    const selectedVariant = item.get('variants').find(v => v.get('id') === currentVariantID);
    return {
      currentPrice: selectedVariant.get('price'),
      currentSalePrice: parseFloat(selectedVariant.getIn(['custom_fields', 'sale_price']))
    };
  }),
   withStateHandlers(
    ({ item }) => ({ secImage: false }),
    {
      displaySecImage: ({secImage}) => (secImage) => ({secImage: secImage}),
    }
  )
);

const ProductCardView: React.SFC<IProductCardProps> = ({
  item,
  config,
  theme,
  isAutocomplete,
  isRecommend,
  isMobile,
  swatches,
  imageUrl,
  variantID,
  setVariantID,
  prevImage,
  currentPrice,
  currentSalePrice,
  dispalySecImage, 
  onMouseOver,
  onMouseOut,
  displaySecImage,
  secImage
}: any) => (
  <a
    onClick={item.onClick}
    href={item.get('product_url')}
    className={classNames(
      theme.root,
      config.get('simple') && theme.simple,
      theme.productCard,
      !isAutocomplete && !isRecommend && 'findify-components-search-results-card',
      isAutocomplete && 'findify-components-autocomplete-card',
      isRecommend && 'findify-components-recommend-card',
      isMobile && 'findify-components-mobile-card',
      !isMobile && 'findify-components-desktop-card'
    )}
  >
    <div className={classNames(theme.imageWrap, {'one-image': (!item.get('image_2_url') && item.get('image_2_url') === item.get('image_url'))})} >
      <ImageComponent 
        lazy={!isAutocomplete}
        className={classNames(theme.image, "primary-product-image")}
        aspectRatio={0}
        thumbnail={prevImage || item.get('image_url') || item.get('thumbnail_url')}
        src={(imageUrl || item.get('image_url') || item.get('thumbnail_url')).replace('small.jpg', 'medium.jpg')}
        alt={item.get('title')}
      />
      <ImageComponent 
        className={classNames(theme.image, "secondary-product-image")}
        aspectRatio={0}
        src={item.get('image_2_url') || (imageUrl || item.get('image_url') || item.get('thumbnail_url')).replace('small.jpg', 'medium.jpg')}
        alt={item.get('title')}
      />
    </div>
    <div
      display-if={
        false && config.getIn(['product', 'reviews', 'display']) &&
        (!!item.getIn(['reviews', 'count']) || !!item.getIn(['reviews', 'total_reviews']))
      }
      className={theme.rating}>
      <Rating
        value={item.getIn(['reviews', 'average_score'])}
        count={item.getIn(['reviews', 'count']) || item.getIn(['reviews', 'total_reviews'])} />
    </div>
    <div className={theme.content}>
      <div className="findify-components-card-title-container">
        <div display-if={item.get('brand') && item.get('brand') !== "United By Blue"} className="findify-product-brand-name">{item.get('brand')}</div>
        <Title
          theme={theme}
          display-if={config.getIn(['product', 'title', 'display'])}
          text={item.get('title')}
          config={config.getIn(['product', 'title'])} />
      </div>
      <Price
        className={theme.priceWrapper}
        display-if={config.getIn(['product', 'price', 'display'])}
        price={currentPrice}
        oldPrice={currentSalePrice}
        discount={item.get('discount')}
        currency={config.get('currency_config').toJS()} />
    </div>
    <Swatches
      display-if={swatches.length > 1}
      swatches={swatches}
      selectedVariantID={variantID}
      onClick={s => setVariantID(s.variantID)}
    />
  </a>
)

export default enhancer(ProductCardView);
/* COLOR SWATCHES */
.findify-variant-container {
  justify-content: center;
  display: flex;
  flex-wrap: wrap;
  min-height: 74px;
  justify-content: center;
  align-items: center;
}

.findify-variant-container-small {
  justify-content: center;
  display: flex;
  flex-wrap: wrap;
  min-height: 74px;
  justify-content: center;
  align-items: center;
}

.findify-empty-variant-container {
  min-height: 74px;
  justify-content: center;
  align-items: center;
}

.findify-components--cards-product__color-ball {
  width: 30px;
  height: 30px;
  display: block;
  border-radius: 2px;
  background-size: inherit !important;
  border: 1px solid #cad9df;
}

.findify-components--cards-product__color-item {
    display: inline-block;
    position: relative;
    margin-right: 7px;
    margin-bottom: 7px;
}

.findify-components--color-swatch-tooltip {
    position: absolute;
    left: 50%;
    bottom: 50px;
    transform: translate(-50%,15px);
    color: #fff;
    padding: 10px;
    white-space: nowrap;
    margin-bottom: 15px;
    background-color: gray;
    text-transform: capitalize;
    transition: transform, opacity .3s;
    visibility: hidden;
    min-width: 100px;
    text-align: center;
    font-size: 100%;
    height: 42px;
    line-height: 1.8;
}

.findify-components--color-swatch-tooltip:after {
  content: " ";
  position: absolute;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-top: 10px solid gray;
  bottom: -10px;
  left: 50%;
  margin-left: -13px;
}

.findify-components--cards-product__color-item:hover .findify-components--color-swatch-tooltip {
  visibility: visible;
}

button.findify-swatch--button {
    -webkit-border-radius: 2px;
    -moz-border-radius: 2px;
    border-radius: 2px;
    border: #ccc 1px solid;
    background-color: #fff;
    text-align: center;
    white-space: nowrap;
    text-transform: uppercase;
    cursor: pointer;
    float: none;
    display: inline-block;
    -webkit-transform: translateZ(0);
    -webkit-font-smoothing: antialiased;
    margin: 0px 7px 7px 0 !important;
    position: relative;
    padding: 3px 8px;
    color: #000 !important;
}