Add a custom variation block to the PDP
Introduction
In this guide, we will show you how to add a custom variation block to the Product Detail Page (PDP).
Prerequisites
You can only add a custom variation block to the PDP if you have a custom PDP page. If you don't have a custom PDP page, you can refer here to know how to replace the existing PDP page with a custom PDP page.
Steps
- Create a new React component in the
components
folder. You can usecomponents/ScrollBlocks.js
as a reference.
import React from 'react';
import { AppImage } from '@appmaker-xyz/uikit';
import { useApThemeState } from '@appmaker-xyz/uikit/src/index';
import { AppTouchable, Layout } from '@appmaker-xyz/uikit/src/components/index';
import { ScrollView, StyleSheet, FlatList } from 'react-native';
// import { FlatList } from 'react-navigation';
function ImageItem({ selected, imageUrl, appmakerAction, onAction }) {
const { spacing, color } = useApThemeState();
const styles = allStyles({ spacing, color });
const variationItemSelectedStyle = [styles.variationItemSelected];
return (
<AppTouchable
style={[styles.variationItem, selected && variationItemSelectedStyle]}
onPress={() => onAction(appmakerAction)}>
<AppImage uri={imageUrl} style={styles.image} />
</AppTouchable>
);
}
export const ScrollBlocks = ({ attributes, onAction }) => {
let { products, __appmakerCustomStyles = {} } = attributes;
const { spacing, color } = useApThemeState();
const styles = allStyles({ spacing, color });
const variationItemSelectedStyle = [styles.variationItemSelected];
if (__appmakerCustomStyles) {
variationItemSelectedStyle.push(
__appmakerCustomStyles?.variation_block
?.variation_item_selected_container,
);
}
function renderItem({ item }) {
return (
<ImageItem
imageUrl={item.imageUrl}
appmakerAction={item.appmakerAction}
onAction={onAction}
/>
);
}
// console.log(products);
return (
<ScrollView
horizontal={true}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
style={styles.container}>
<FlatList
horizontal={true}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
data={products}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
<Layout style={{ marginRight: spacing.base }} />
</ScrollView>
);
};
export const allStyles = ({ spacing, color }) =>
StyleSheet.create({
container: {
paddingBottom: spacing.small,
},
variationItem: {
position: 'relative',
backgroundColor: color.light,
borderRadius: spacing.nano,
marginLeft: spacing.base,
borderColor: color.light,
borderWidth: 1,
marginVertical: spacing.nano,
},
variationText: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.base,
},
variationItemSelected: {
borderColor: color.primary,
overflow: 'hidden',
},
listVariationItem: {
width: '100%',
marginLeft: 0,
marginVertical: spacing.mini,
},
selectBlock: {
marginHorizontal: spacing.base,
marginBottom: spacing.base,
backgroundColor: color.light,
padding: spacing.base,
borderRadius: spacing.nano,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
colorSelector: {
width: spacing.lg * 2,
height: spacing.lg * 2,
borderRadius: spacing.xl,
borderColor: color.light,
borderWidth: spacing.nano / 2,
marginLeft: spacing.base,
justifyContent: 'center',
alignItems: 'center',
},
colorSelectedIndicator: {
paddingBottom: spacing.base,
paddingHorizontal: spacing.base,
},
modal: {
justifyContent: 'flex-end',
marginVertical: 0,
marginHorizontal: 1,
},
modalBody: {
backgroundColor: color.white,
padding: spacing.md,
justifyContent: 'flex-start',
alignItems: 'flex-start',
borderTopStartRadius: spacing.md,
borderTopEndRadius: spacing.md,
flexDirection: 'column',
maxHeight: '80%',
},
modalHeader: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: color.light,
paddingBottom: spacing.base,
},
modalContent: {
paddingVertical: spacing.base,
flexDirection: 'column',
width: '100%',
},
muted: {
backgroundColor: color.white,
borderColor: color.grey,
},
muteImage: {
width: '100%',
height: '100%',
position: 'absolute',
zIndex: 10,
resizeMode: 'stretch',
},
imageContainer: {
position: 'relative',
backgroundColor: color.light,
borderRadius: spacing.nano,
marginLeft: spacing.base,
borderColor: color.light,
borderWidth: 1,
marginVertical: spacing.nano,
},
image: {
height: 75,
width: 75,
resizeMode: 'cover',
},
});
This component will be used to render the list of images. Check next step to see how to use it.
- Create another React Native component in the
components
folder. You can usecomponents/ImageVariation.js
as a reference. This is our main component. It will use theScrollBlocks
component we created in the previous step.
import { View, Text } from 'react-native';
import React, { useEffect } from 'react';
import { runDataSource, appPluginStoreApi } from '@appmaker-xyz/core';
import { ScrollBlocks } from './ScrollBlocks';
import { useState } from 'react';
import BlockCard from '@appmaker-xyz/uikit/src/components/organisms/card/BlockCard';
// helper function to resize the image
function imageResize(imageURL, size) {
if (imageURL && size) {
let separatorIndex = imageURL.lastIndexOf('.');
const resizedImage = `${imageURL.slice(
0,
separatorIndex,
)}_${size.toLowerCase()}${imageURL.slice(separatorIndex)}`;
return resizedImage;
}
}
// helper function to load products
async function loadProducts(ids) {
const dataSource = {
attributes: {},
source: 'shopify',
};
const [response] = await runDataSource(
{
dataSource,
},
{
methodName: 'products',
params: {
ids, // product ids, eg: ['gid://shopify/Product/1234567890', 'gid://shopify/Product/0987654321']
},
},
);
const products = response.data.data.nodes;
// hide out of stock products
const hideOutOfStock =
appPluginStoreApi()?.getState()?.plugins?.['shopify-custom-variations']
?.settings?.hide_out_of_stock || false;
let finalProducts = [];
products.map((product) => {
if (!product?.availableForSale && hideOutOfStock) {
return null;
}
finalProducts.push({
id: product.id,
appmakerAction: {
pageId: 'productDetail',
params: {
replacePage: true, // By adding this, the product variation will be opened in the same page
pageData: { node: product },
},
action: 'OPEN_PRODUCT_DETAIL',
},
imageUrl: imageResize(product.images.edges[0].node.src, 'x150'),
});
});
return finalProducts;
}
// main component
export default function ImageVariation({ attributes, onAction }) {
const { productIds: productIdString, title } = attributes;
const [products, setProducts] = useState([]);
useEffect(() => {
async function loadData() {
try {
const productsArray = await loadProducts(JSON.parse(productIdString));
setProducts(productsArray);
} catch (error) {
console.log(error);
}
}
loadData();
}, [productIdString]);
return products.length === 0 ? null : (
<BlockCard
attributes={{
title,
}}>
<ScrollBlocks // this is the component we created in step 1
onAction={onAction}
attributes={{
products,
}}
/>
</BlockCard>
);
}
This block is a simple block to show the product images in a horizontal scroll view. The block triggers an appmakerAction when a product image is pressed. You can set replacePage
as true
in appmakerAction
to open the product variation in the same page.
- Register the block in
blocks\index.js
file.
import ImageVariation from '../components/ImageVariation';
const blocks = [
{
name: 'namespace/custom-block', // This is a custom block name
component: ImageVariation,
},
];
export { blocks };
- Add the block to your custom PDP page. You can refer sample code below.
const CustomPage = {
title: 'Custom Page',
blocks: [
{
name: 'namespace/custom-block',// custom block name that you have registered in blocks/index.js
attributes: {
title: 'Title from app',
},
},
],
};
export default CustomPage;