Secondary image on hover for swatches using alt text
#1. Background
Seeing the color palette of a product, as well as the ability to get an alternative view of what the product looks like, are both essentials when it comes to convincing your client go for the purchase. Bringing him the ability to get as many possible angles on the product he might be interested is crucial.
In this guide we'll be showing you how you can combine the two, when the alternative angle on the product along with all the available color variation can be viewed on the listing page, without the user having to click into the product description page. And also, it looks cool :)
Let us show you how that can be achieved.
#2. Requirements
When you need to get a second image from a special shopify_images_alt field (or something similar), the first step is to activate the field in MD.
In the example we also need to activate old_colors and shopify_images_url fields.
In addition, you need to allow showing alternate images on hover of the product grid.
#3. Time Estimates
- Set up in Platform: 5 minutes
- Integration: 30 minutes
- Styling: 10 minutes
#4. Functional Overview
- [components/Cards/Product/index.tsx] (https://github.com/findify/findify-js/blob/develop/packages/react-components/src/components/Cards/Product/index.tsx)
#5. Integration Steps
At first you need to create two states - imageMain and imageHover.
Then we produce the function that returns initial values for the states. In instance we create getImage function. But you need to create the logic that will work for your store,.
Afterwards, push the initial values to the states.
import { useState } from 'react';
// default code
const getImage = (variant, item, selector) => {
const color = variant.getIn(['custom_fields','old_colors'], '');
const alt = item.getIn(['custom_fields','shopify_images_alt'], '');
const image = item.getIn(['custom_fields','shopify_images_url'], '');
const images = alt.reduce( (acc, value, index) => {
if(value.toLowerCase().indexOf(color) > -1 && value.toLowerCase().indexOf(selector) > -1 && image) acc.push(image.get(index));
return acc;
}
, [image])
return images[1] && images[1].replace('_medium','_large');;
}
const [imageMain, setImage] = useState(
getImage(variant, item, 'main')
);
const [imageHover, setSecondImage] = useState(
getImage(variant, item, 'hover')
);
Finally, add the images blocks to your code.
// default code
<Image
aspectRatio={config.getIn(['image', 'aspectRatio'])}
thumbnail={item.get('thumbnail_url')}
alt={item.get('title')}
lazy={config.getIn(['image', 'lazy'])}
offset={config.getIn(['image', 'lazyOffset'])}
src={
config.getIn(['image', 'multiple'])
? [imageMain, imageHover]
: imageMain || item.get('thumbnail_url')
}
/>
We have to push setImage and setSecondImage into ColorSwatches:
/**
* @module components/Cards/Product
*/
import cx from 'classnames';
import Image from 'components/common/Image';
import Rating from 'components/Cards/Product/Rating';
import Price from 'components/Cards/Product/Price';
import Title from 'components/Cards/Product/Title';
import Description from 'components/Cards/Product/Description';
import Variants from 'components/Cards/Product/Variants';
import styles from 'components/Cards/Product/styles.css';
import { List } from 'immutable';
import { IProduct, ThemedSFCProps } from 'types';
import { Immutable, Product } from '@findify/store-configuration';
import trackProductPosition from 'helpers/trackProductPosition';
import { useMemo, useState } from 'react';
import ColorSwatches from 'ColorSwatches';
import { Item } from 'react-connect/lib/immutable/item';
export interface IProductCardProps extends ThemedSFCProps {
item: IProduct;
config: Immutable.Factory<Product>;
Container?: React.ElementType;
}
const getImage = (color, alt, image, selector) => {
const images = alt.reduce( (acc, value, index) => {
if(value.toLowerCase().indexOf(color) > -1 && value.toLowerCase().indexOf(selector) > -1 && image) acc.push(image.get(index));
return acc;
}
, [image])
return images[1] && images[1].replace('_medium','_large');;
}
const useVariants = (
item
): [IProduct, React.Dispatch<React.SetStateAction<string>>] => {
const [currentVariant, setVariant] = useState<string>(
item.get('selected_variant_id')
);
const variant = useMemo(
() =>
item.merge(
item.get('variants')?.find((i) => i.get('id') === currentVariant)
),
[currentVariant]
);
return [variant, setVariant];
};
export default ({
item,
theme = styles,
className,
config,
Container = 'div',
}: any) => {
const container = trackProductPosition(item);
const [variant, setVariant] = useVariants(item);
const [imageMain, setImage] = useState(
getImage(
variant.getIn(['custom_fields','old_colors'], ''),
item.getIn(['custom_fields','shopify_images_alt'], ''),
item.getIn(['custom_fields','shopify_images_url'], ''),
'main'
)
);
const [imageHover, setSecondImage] = useState(
getImage(
variant.getIn(['custom_fields','old_colors'], ''),
item.getIn(['custom_fields','shopify_images_alt'], ''),
item.getIn(['custom_fields','shopify_images_url'], ''),
'hover'
)
);
return (
<Container
ref={container}
data-element="card"
className={cx(theme.root, theme[config.get('template')], className)}
>
<div className={theme.content}>
<ColorSwatches
variants={item.get('variants','')}
item={item}
currentId={variant.get('id')}
setVariant={setVariant}
setSecondImage={setSecondImage}
setImage={setImage}
/>
<Rating
className={theme.rating}
value={variant.getIn(['reviews', 'average_score'])}
count={
variant.getIn(['reviews', 'count']) ||
variant.getIn(['reviews', 'total_reviews'])
}
display-if={
!!variant.getIn(['reviews', 'count']) ||
!!variant.getIn(['reviews', 'total_reviews'])
}
/>
{/*
Link hack:
Title's "a" contains :after element with absolute position
what makes provide link effect to the rest of card
- To remove element from the effect set `position:relative`
- Or `z-index: 1`, but it may have side effects
*/}
<Title
display-if={!!variant.get('title')}
theme={theme}
onClick={variant.onClick}
href={variant.get('product_url')}
text={variant.get('title')}
/>
<Description
display-if={!!variant.get('description')}
theme={theme}
text={variant.get('description')}
/>
{/*<div className={theme.divider} />*/}
<Price
display-if={!!variant.get('price')}
className={theme.priceWrapper}
item={item}
/>
</div>
{/*
ADA specific hack:
We need to make image belong to content, so we move it under the title.
- flex order set to -1
*/}
<div className={theme.image} onClick={item.onClick}>
<div className='findify-product-main-image'>
<Image
aspectRatio={true && config.getIn(['image', 'aspectRatio'])}
thumbnail={variant.get('thumbnail_url')}
alt={variant.get('title')}
lazy={config.getIn(['image', 'lazy'])}
offset={config.getIn(['image', 'lazyOffset'])}
src={
imageMain || variant.get('image_url','')
}
/>
</div>
<div className='findify-product-image-on-hover'>
<Image
aspectRatio={true && config.getIn(['image', 'aspectRatio'])}
thumbnail={variant.get('thumbnail_url')}
alt={variant.get('title')}
lazy={config.getIn(['image', 'lazy'])}
offset={config.getIn(['image', 'lazyOffset'])}
src={imageHover || variant.get('image_url')}
/>
</div>
</div>
</Container>
);
};
ColorSwatches may look like this
import { useState } from 'react';
import { fromJS } from 'immutable';
import cx from 'classnames';
const getImage = (color, alt, image, selector) => {
const images = alt.reduce( (acc, value, index) => {
if(value.toLowerCase().indexOf(color) > -1 && value.toLowerCase().indexOf(selector) > -1 && image) acc.push(image.get(index));
return acc;
}
, [image])
return images[1] && images[1].replace('_medium','_large');
}
const getColorSwatches = (variants) => {
let arr = []
const reducedVariants = variants.reduce(
(acc, item, index) => {
if(!item.getIn(['custom_fields','old_colors'])) acc[index] = false;
if(item.getIn(['custom_fields','old_colors']) && arr.indexOf(item.getIn(['custom_fields','old_colors'])) < 0){
acc[index] = {
'id': item.get('id',''),
'color': item.getIn(['color', 0],''),
'swatchUrl': item.getIn(['custom_fields','swatch_url'],''),
'oldColors': item.getIn(['custom_fields','old_colors'],''),
'image_url': getImage(
item.getIn(['custom_fields','old_colors']),
item.getIn(['custom_fields','shopify_images_alt']),
item.getIn(['custom_fields','shopify_images_url']),
'main'
),
'imageHover': getImage(
item.getIn(['custom_fields','old_colors']),
item.getIn(['custom_fields','shopify_images_alt']),
item.getIn(['custom_fields','shopify_images_url']),
'hover'
)
}
arr.push(item.getIn(['custom_fields','old_colors']));
}
return acc;
}
, [variants])
return fromJS(reducedVariants.filter(i => i !== false));
}
const Item = ({currentId, item, id, setVariant, setSecondImage, setImage}) => {
const color=item.get("oldColors") && item.get("oldColors").toLowerCase()
if(!color){
return null
}
return (
<div
className={cx('findify-product-color-swatch-item',{['findify-product-color-swatch-item-selected']: id == currentId})}
tabIndex="0"
aria-label={color}
onClick={
(e) => {
e.stopPropagation();
e.preventDefault();
setVariant(item.get('id'));
setImage(item.get('image_url'));
setSecondImage(item.get('imageHover'));
}
}
/>
)
}
const ColorSwatches = ({variants, setVariant, currentId, setSecondImage, setImage}) => {
const colorSwatches = variants && getColorSwatches(variants);
return (
<div
display-if={colorSwatches}
className={cx("findify-product-color-swatches-wrapper")}
>
<div className='findify-swatches-wrapper'>
{colorSwatches.map(
(item, index) => <Item
id={item && item.get('id','')}
item={item}
currentId={currentId}
index={index}
setVariant={setVariant}
setSecondImage={setSecondImage}
setImage={setImage}
/>
)}
</div>
</div>
)
}
export default ColorSwatches;
#6. MJS Version
This module has been optimized for MJS version 7.1.38
Updated 12 months ago