Promo Cards: Activation (using Shopify meta fields)
#1. Background
Promo cards are non product assets displayed alongside products within the grid.
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 about 1 year ago