Skip to main content

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

  1. Create a new React component in the components folder. You can use components/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.

  1. Create another React Native component in the components folder. You can use components/ImageVariation.js as a reference. This is our main component. It will use the ScrollBlocks 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.

  1. 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 };
  1. 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;