Promo Cards: Activation (using Shopify meta fields)
#1. Background
Promo cards are non product assets displayed alongside products within the grid.

To read more about use cases and setup, please read here
This section cover how to activate Promo Cards in the front end.
This requires step 1 to be finalized:
Part 1
In order to follow the steps in this guide, you need to set up & populate the required meta fields in Shopify. Please find the full instruction here
To do this requires usage of Findify dev-tool extension.
If you have any challenges, Findify's team for professional services would be happy to assist.
Please get in touch through: [email protected]
Dev Tool Extension
To learn how to work with the Dev Tools, please read more here
#2. Time Estimates
Set up in Platform: n/a
Integration: 50 minutes
Styling: 10 minutes
#3. Integration Steps
Create new component getPromoCards
, where document.querySelector('.findify-promo-cards script').text
should take a json string.
import React from 'react';
import { fromJS } from 'immutable';
export const setPromoCard = (items, meta, isSmartCollection) => {
const collectionData = isSmartCollection && JSON.parse(document.querySelector('.findify-promo-cards script').text);
if(collectionData){
const sortedPromos = collectionData.sort( (a,b) => Number(a.position) - Number(b.position) )
const immutablePromos = fromJS(sortedPromos);
const offset = meta.get('offset','');
const newItems = immutablePromos.reduce( (acc, item, index) => {
if(item.get('position') <= offset) return acc;
return acc.insert(item.get('position') - index + (!index ? -1 : 0), item)
}, items)
return newItems;
}
return items;
}
If you use Lazy Loading, open helpers/withLazy.tsx
. Here you'll need to edit addItems
function.
If you use Pagination, open components/ItemsList/view.tsx
. Here you'll need to use the setPromoCard
function and put the withPromo
prop into the array prop.
import React from 'react';
import { Component, createFactory } from 'react';
import { is, List, Map } from 'immutable';
import { compose, withPropsOnChange, setDisplayName } from 'recompose';
import {setPromoCard} from 'getPromoCards'; //import this function
const isStateEqual = (prev, next) => ['filters', 'q', 'sort'].every(k =>
is(prev.get(k), next.get(k))
);
const hasRange = (ranges, offset) => !!ranges.find(r => r.get('from') === offset);
const createRange = meta => Map({
from: meta.get('offset'),
to: meta.get('offset') + meta.get('limit'),
});
const addItems = ({ ranges, items }, nextItems, meta, config) => {
const append = ranges.find(r => r.get('from') < meta.get('offset'));
const newRange = createRange(meta);
const _items = nextItems.filter(i => !items.includes(i));
const isSmartCollection = config.get('type') === 'smart-collection';
//create variable with product items and promo cards
const withPromo = setPromoCard(_items, meta, isSmartCollection);
//insert promo cards when click on load more/less
return {
ranges: append ? ranges.push(newRange) : ranges.insert(0, newRange),
items: append ? items.concat(withPromo) : withPromo.concat(items)
}
}
/**
* withLazy() returns a HOC for wrapping around component you want to include lazy loading to
* @returns HOC, accepting a view you want to add lazy loading to
*/
export default () => {
/**
* withLazy HOC allows you to add LazyLoading functionality to your Search views, for example.
* It controls items displaying correct range in the LazyView and automatically requests for more data if needed
* @param LazyView view you will be adding lazy loading to
* @returns LazyLoading HOC
*/
return BaseComponent => {
const factory = createFactory(BaseComponent);
return class Lazy extends Component<any, any>{
container: any;
autoLoadCount = 0;
constructor(props) {
super(props);
this.autoLoadCount = props.disableAutoLoad ? 0 : props.config.getIn(['loadMore', 'lazyLoadCount'], 2);
this.state = {
items: props.items,
ranges: List([createRange(props.meta)]),
columns: props.columns || '3',
pending: false
};
}
registerContainer = (ref) => {
if (!ref) return;
this.container = ref;
}
onLoadNext = () => {
const { update, meta } = this.props;
const { ranges } = this.state;
return update('offset', ranges.last().get('to'));
}
onLoadPrev = () => {
const { update, meta } = this.props;
const { ranges } = this.state;
return update('offset', ranges.first().get('from') - meta.get('limit'));
}
get lessAllowed() {
const { ranges } = this.state;
const firstRange = ranges.first();
return firstRange && firstRange.get('from') > 0
}
get moreAllowed() {
const { meta } = this.props;
const { ranges } = this.state;
const lastRange = ranges.last();
return lastRange && lastRange.get('to') < meta.get('total')
}
trackPosition = () =>
!this.state.pending &&
!!this.autoLoadCount &&
window.requestAnimationFrame(() => {
const offset = 300;
const { bottom } = this.container.getBoundingClientRect();
const height = window.innerHeight || document.documentElement.clientHeight;
const inView = bottom - height <= offset;
if (!inView || this.state.pending || !this.autoLoadCount || !this.moreAllowed) return;
this.autoLoadCount -= 1
this.setState({ pending: true });
this.onLoadNext();
})
componentDidMount() {
if (this.props.disableAutoLoad) return;
window.addEventListener('scroll', this.trackPosition);
}
componentWillUnmount() {
if (this.props.disableAutoLoad) return;
window.removeEventListener('scroll', this.trackPosition);
}
UNSAFE_componentWillReceiveProps({ items, meta, config }) {
// Do nothing if items are equal
if (items.equals(this.props.items)) return;
this.setState({ pending: false });
// Prepend or append new items
if (isStateEqual(meta, this.props.meta) && !hasRange(this.state.ranges, meta.get('offset'))) {
return this.setState({ ...addItems(this.state, items, meta, config) });
}
// Reset number of loads
if (!this.props.disableAutoLoad) this.autoLoadCount = config.getIn(['loadMore', 'lazyLoadCount'], 2);
// Reset items
const isSmartCollection = config.get('type') === 'smart-collection';
//insert promo cards by default
return this.setState({
items: setPromoCard(items, meta, isSmartCollection),
ranges: List([createRange(meta)]),
});
}
shouldComponentUpdate(props, state) {
return (
this.state.pending !== state.pending ||
!this.state.items.equals(state.items) ||
!!Object.keys(props).find(k => !is(this.props[k], props[k]))
)
}
render () {
const { ranges, items, columns, pending } = this.state;
const { meta } = this.props;
const content = factory({
...this.props,
items,
displayPrevButton: this.lessAllowed,
displayNextButton: !pending && this.moreAllowed,
onLoadNext: this.onLoadNext,
onLoadPrev: this.onLoadPrev,
});
return <div ref={this.registerContainer}>{content}</div>
}
};
}
}
/**
* @module components/ItemsList
*/
import React from 'react'
import ProductCard from 'components/Cards/Product'
import mapArray, { MapArrayProps } from 'components/common/MapArray';
import {setPromoCard} from 'getPromoCards';
// Default item factory is using ProductCard
const ItemFactory = React.createFactory(ProductCard);
/** Props that ItemList view accepts */
export interface IItemsListProps extends MapArrayProps {
/** Wrapper around mapArray */
wrapper: React.ComponentType;
/** Rest props that are passed to wrapper */
[x: string]: any;
}
export default ({ items, wrapper: Wrapper = React.Fragment, ...rest }: IItemsListProps) => {
const { limit, factory, keyAccessor, ...wrapperProps } = rest;
const { config, meta } = rest;
const isSmartCollection = config.get('type') === 'smart-collection';
const withPromo = setPromoCard(items, meta, isSmartCollection);
return (
<Wrapper {...wrapperProps}>
{ mapArray({ keyAccessor, limit, array: withPromo, factory: factory || ItemFactory, ...wrapperProps }) }
</Wrapper>
)
}
After all that, you can separate Product/view
.
/**
* @module components/Cards/Product
*/
import React from 'react'
import { IProduct, MJSConfiguration, ThemedSFCProps } from 'types/index';
import {withPropsOnChange} from 'recompose';
import ProductCardView from 'ProductCard';
import PromoCard from 'PromoCard';
export interface IProductCardProps extends ThemedSFCProps {
item: IProduct;
config: MJSConfiguration;
}
const ProductCard: React.SFC<IProductCardProps> = ({
item,
...props
}: any) => (
<div>
<ProductCardView display-if={!item.get('position')} item={item} {...props}/>
<PromoCard display-if={item.get('position')} item={item}/>
</div>
)
export default ProductCard;
/**
* @module components/Cards/Product
*/
import React from 'react'
import classNames from 'classnames'
import Image from 'components/common/Picture'
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';
const Title: any = ({ text, theme, ...rest }) => (
<Text display-if={!!text} className={theme.title} {...rest}>{text}</Text>
);
const Description: any = ({ text, theme, ...rest }) => (
<p
display-if={!!text}
className={theme.description}
{...rest}
>
<Truncate>{text}</Truncate>
</p>
);
export interface IProductCardProps extends ThemedSFCProps {
item: IProduct;
config: MJSConfiguration;
}
const ProductCardView: React.SFC<IProductCardProps> = ({
item,
config,
theme
}: any) => (
<a
onClick={item.onClick}
href={item.get('product_url')}
className={classNames(
theme.root,
config.get('simple') && theme.simple,
theme.productCard,
)}
>
<div className={classNames(theme.imageWrap)}>
<BundleAction display-if={config.get('bundle')} item={item} />
<Image
className={classNames(theme.image)}
aspectRatio={config.getIn(['product', 'image', 'aspectRatio'], 1)}
thumbnail={item.get('thumbnail_url')}
src={item.get('image_url') || item.get('thumbnail_url')}
alt={item.get('title')}
/>
<div display-if={config.getIn(['product', 'stickers', 'display'])}>
<DiscountSticker
config={config}
className={theme.discountSticker}
discount={item.get('discount')}
display-if={
config.getIn(['stickers', 'discount']) &&
config.getIn(['product', 'stickers', 'display']) &&
item.get('discount', List()).size &&
item.getIn(['stickers', 'discount'])
} />
</div>
</div>
<div
display-if={
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.variants}
display-if={
config.getIn(['product', 'variants', 'display']) &&
item.get('variants', List()).size > 1
}
>
{
template(config.getIn(['product', 'i18n', 'variants'], 'Available in %s variants'))(
item.get('variants', List()).size
)
}
</div>
<div className={theme.content}>
<Title
theme={theme}
display-if={config.getIn(['product', 'title', 'display'])}
text={item.get('title')}
config={config.getIn(['product', 'title'])} />
<Description
theme={theme}
display-if={config.getIn(['product', 'description', 'display'])}
text={item.get('description')}
config={config.getIn(['product', 'description'])} />
<Price
className={theme.priceWrapper}
display-if={config.getIn(['product', 'price', 'display'])}
price={item.get('price')}
oldPrice={item.get('compare_at')}
discount={item.get('discount')}
currency={config.get('currency_config').toJS()} />
<OutOfStockSticker
display-if={item.getIn(['stickers', 'out-of-stock'])}
config={config} />
</div>
</a>
)
export default ProductCardView;
import React from 'react';
const PromoCard = ({ item }) => (
<div className="findify-promo_card">
<div className="findify-promo-card-content-wrapper">
<div className="findify-promo-card-text-wrapper">
<div display-if={item.get('top-header')} className="findify-promo-title">{item.get('top-header')}</div>
<p display-if={item.get('sub-header')} className='findify-promo-card-text'>{item.get('sub-header')}</p>
</div>
<a
href={item.get('url')}
className='findify-promo-card-image-container'
dangerouslySetInnerHTML={{__html: item.get('image_')}}
/>
<a
display-if={item.get('cta')}
href={item.get('url')}
className="findify-promo-card-button"
>
{item.get('cta')}
</a>
<div
display-if={item.get('overlay')}
className="findify-promo-card-overlay"
style={{
background: item.get('overlay')
}}
/>
</div>
</div>
)
export default PromoCard;
Updated over 1 year ago