Create a product bundle page in React and connect with appmaker web sdk
Introduction
In this guide, we will learn how to create a product bundle page in React and connect with appmaker web sdk. Customer can create a customised product bundle by selecting the products from the product bundle page and add products to appmaker native cart. Below is a sample video of the product bundle page to cart flow.
Topics covered
- Create a sample react app using next.js
- Install and configure tailwindcss
- Create a product bundle page
- Install and configure appmaker web sdk
- Connect with appmaker web sdk
Create a sample react app using next.js
Create a new Next.js app by running the following command:
npx create-next-app@latest appmaker-docs-examples --typescript --eslint
cd appmaker-docs-examples
Install Tailwind CSS and its peer dependencies:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Now we need to configure Tailwind CSS in our project. Open the tailwind.config.js
file and add the following content:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
Add the following content to the globals.css
file:
@tailwind base;
@tailwind components;
@tailwind utilities;
Now start the development server by running the following command:
npm run dev
Now we have a sample react app with tailwindcss configured. Next we will create a product bundle page in react and connect with appmaker web sdk.
Create a product bundle page
Create a new file src/pages/example-product-bundle/index.jsx
and add the following content:
import React from "react";
export default function ProductBundlePage() {
return (
<div className="flex flex-col min-h-screen py-2">
<main className="flex flex-col w-full flex-1 px-20 text-center">
<h1 className="text-2xl font-bold">Perfect Gift Box</h1>
</main>
</div>
);
}
Now Lets create a an array of products and map it to the product bundle page.
const bundle = [
{
id: "gid://shopify/Collection/429493911574",
title: "Tops",
maxItems: 2,
products: {
nodes: [
{
id: "gid://shopify/Product/7983594962966",
title: "Workout Shirt",
featuredImage: {
id: "gid://shopify/ProductImage/39774607736854",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenWorkoutShirt_400x.jpg?v=1675455464",
},
},
{
id: "gid://shopify/Product/7983594340374",
title: "Puffer Jacket",
featuredImage: {
id: "gid://shopify/ProductImage/39774605475862",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenPufferjacket01_400x.jpg?v=1675455364",
},
},
{
id: "gid://shopify/Product/7983594176534",
title: "Puffer",
featuredImage: {
id: "gid://shopify/ProductImage/39774605279254",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenPuffer01_400x.jpg?v=1675455329",
},
},
],
},
},
{
id: "gid://shopify/Collection/429493944342",
title: "Bottoms",
maxItems: 2,
products: {
nodes: [
{
id: "gid://shopify/Product/7983594471446",
title: "Shorts",
featuredImage: {
id: "gid://shopify/ProductImage/39774605836310",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenShorts_400x.jpg?v=1675462426",
},
},
{
id: "gid://shopify/Product/7983593422870",
title: "Leggings",
featuredImage: {
id: "gid://shopify/ProductImage/39774604754966",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenLeggings03_400x.jpg?v=1675455256",
},
},
{
id: "gid://shopify/Product/7982856273942",
title: "Sweatpants",
featuredImage: {
id: "gid://shopify/ProductImage/39774602002454",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenSweatpants01_400x.jpg?v=1675455387",
},
},
],
},
},
{
id: "gid://shopify/Collection/429531037718",
title: "Shoes",
maxItems: 1,
products: {
nodes: [
{
id: "gid://shopify/Product/7983595487254",
title: "High Top Sneakers",
featuredImage: {
id: "gid://shopify/ProductImage/39774608883734",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Whiteleathersneakers01_400x.jpg?v=1675447604",
},
},
{
id: "gid://shopify/Product/7983595388950",
title: "Gray Runners",
featuredImage: {
id: "gid://shopify/ProductImage/39774608818198",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Greyrunners_400x.jpg?v=1675447483",
},
},
{
id: "gid://shopify/Product/7983595356182",
title: "Gray Leather Sneakers",
featuredImage: {
id: "gid://shopify/ProductImage/39774608785430",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Greyleathersneakers_400x.jpg?v=1675447462",
},
},
],
},
},
];
Now Lets create a product bundle component and map the products to it.
import React from "react";
export function ProductCard(props) {
const { item, selectProduct, isSelected } = props;
return (
<div className="relative rounded overflow-hidden bg-white border border-gray-100">
<label htmlFor={item.name}>
<img
src={item.featuredImage?.url}
alt={item.name}
className="h-48 w-full object-cover"
/>
<div className="p-2">
<p className="mb-2">{item.name}</p>
<span className="p-2 bg-indigo-600 rounded-sm text-white block text-center">
{isSelected ? "Remove Item" : "Add To Box"}
</span>
</div>
<input
type="checkbox"
id={item.name}
name={item.name}
checked={isSelected}
onChange={() => selectProduct(item)}
className="h-6 w-6 border-gray-300 rounded-sm text-indigo-600 focus:ring-indigo-600 absolute top-2 right-2"
/>
</label>
</div>
);
}
Now lets render the product bundle component.
export default function ProductBundle() {
return (
<div className="flex flex-col min-h-screen py-2">
<main className="flex flex-col w-full flex-1 px-20 text-center">
<h1 className="text-2xl font-bold">Perfect Gift Box</h1>
{/* <ProductCard /> */}
<div className="flex flex-col">
{bundle.map((item) => (
<div className="flex flex-col" key={item.id}>
<h2 className="text-xl font-bold text-center">{item.title}</h2>
<div className="flex flex-wrap">
{item.products?.nodes.map((product) => (
<ProductCard item={product} key={product.id} />
))}
</div>
</div>
))}
</div>
</main>
</div>
);
}
Now lets add the functionality to add and remove items from the bundle.
Customers can choose n
number of products from each collection. So we need to keep track of the selected products in a state. our state will look like this.
const state = {
bundleId1: {
totalItems: 2,
maxItems: 2,
products: {
productId1: true,
productId2: true,
},
},
bundleId2: {
totalItems: 1,
maxItems: 1,
products: {
productId1: true,
},
},
bundleId3: {
totalItems: 2,
maxItems: 3,
products: {
productId1: false,
},
},
};
Now lets create a custom hook with useImmer to manage the state.
import { useState } from "react";
import { useImmer } from "use-immer";
export function useProductBundle() {
// bundle
const [state, setState] = useImmer({
// bundleId: { maxItems: 0 },
bundles: {},
totalItems: 0,
});
const [bundleConfig, _setBundleConfig] = useState({});
function setProduct(bundleId, productId, isSelected) {
setState((draft) => {
if (draft.bundles[bundleId] === undefined) {
draft.bundles[bundleId] = {
limitExceeded: false,
totalItems: 0,
maxItems: bundleConfig[bundleId].maxItems,
products: {},
};
}
if (isSelected) {
draft.totalItems += 1;
draft.bundles[bundleId].totalItems += 1;
draft.bundles[bundleId].products[productId] = true;
} else {
draft.totalItems -= 1;
draft.bundles[bundleId].totalItems -= 1;
draft.bundles[bundleId].products[productId] = false;
}
if (
draft.bundles[bundleId].totalItems >= draft.bundles[bundleId].maxItems
) {
draft.bundles[bundleId].limitExceeded = true;
} else {
draft.bundles[bundleId].limitExceeded = false;
}
});
}
function setBundle(bundle) {
const _bundleConfig = {};
bundle.forEach((bundle) => {
_bundleConfig[bundle.id] = {
maxItems: bundle.maxItems,
};
});
_setBundleConfig(_bundleConfig);
}
function isSelected(bundleId, productId) {
if (state.bundles[bundleId] === undefined) {
return false;
}
return state.bundles[bundleId].products[productId] || false;
}
function isLimitExceeded(bundleId) {
if (state.bundles[bundleId] === undefined) {
return false;
}
return state.bundles[bundleId].limitExceeded;
}
function selectecItemsCount(bundleId) {
if (state.bundles[bundleId] === undefined) {
return 0;
}
return state.bundles[bundleId].totalItems;
}
return {
state,
setProduct,
isSelected,
setBundle,
isLimitExceeded,
selectecItemsCount,
};
}
Now lets use the custom hook in our product bundle page.
export default function ProductBundle() {
const {
state,
setProduct,
isSelected,
isLimitExceeded,
setBundle,
selectecItemsCount,
} = useProductBundle();
useEffect(() => {
setBundle(bundle);
}, []);
return (
<div className="flex flex-col min-h-screen py-2">
<main className="flex flex-col w-full flex-1 px-20 text-center">
<h1 className="text-2xl font-bold">
Perfect Gift Box - {state.totalItems}
</h1>
{/* <ProductCard /> */}
{/* <pre>{JSON.stringify(state, null, 2)}</pre> */}
<div className="flex flex-col">
{bundle.map((item) => (
<div className="flex flex-col" key={item.id}>
<h2 className="text-xl font-bold text-center">
{item.title} ({selectecItemsCount(item.id)}/{item.maxItems}))
</h2>
<div className="flex flex-wrap">
{item.products?.map((product) => (
<ProductCard
item={product}
isSelected={isSelected(item.id, product.id)}
key={product.id}
isLimitExceeded={isLimitExceeded(item.id)}
selectProduct={(isSelected) => {
setProduct(item.id, product.id, isSelected);
}}
/>
))}
</div>
</div>
))}
</div>
</main>
</div>
);
}
Now we have our product bundle page ready. We can add and remove products from the bundle. We can also limit the number of products that can be selected from each collection.
Now lets Design and add to cart button to add the selected products to cart.
Add below code to the bottom of the page. ( Add it after the main closing tag )
<div className="fixed bottom-0 max-w-md bg-white border-t border-gray-100 p-3 w-full flex items-center">
<div className="mr-4">
<h3 className="text-sm">{state.totalItems} Selected</h3>
<h3 className="font-bold text-lg">$124.00</h3>
</div>
<button
className="bg-gray-900 text-white px-4 py-2 flex-1 text-lg rounded-md">
ADD TO CART
</button>
</div>
Now let'sadd the functionality to add the bundle to the cart.
Let us assign the selected products in the bundle to a variable.
const customAttributes = Object.keys(state.bundles).map((bundleId) => {
const product_bundle = state.bundles[bundleId];
const products = Object.keys(product_bundle.products);
//match bundle id with collection id in bundles variable above and take the title of bundle
const bundle_title = bundle.find((item) => item.id === bundleId).title;
const product_titles = products.map((product) => {
//match product id with product id in bundles variable above and take the title of product
const product_title = bundle.map((item) => item.products).flat().find((item) => item.id === product).title;
return product_title;
});
return {
key: bundle_title,
value: product_titles.join(", ")
};
});
Then we can integrate with appmaker web sdk to create products to native cart.
First install the web sdk.
npm install @appmaker-xyz/web-sdk
Then import the web sdk in the product bundle page.
import AppmakerWebSdk from "@appmaker-xyz/web-sdk"
To add the product to cart , we can use the addProductToCart
method of the web sdk and to show the message after adding the product to cart, we can use the showMessage
method of the web sdk.
Refer to the web sdk documentation for more details on the web sdk methods.
Let's write a function to add the product to cart.
const handleAddToCart = () => {
AppmakerWebSdk.addProductToCart({
product: product,
variant: variant,
quantity: 1,
customAttributes: customAttributes,
})
AppmakerWebSdk.showMessage({title:"Product added to cart"});
}
We need to add an onclick event to the add to cart button to add the product to cart.
<button
className="bg-gray-900 text-white px-4 py-2 flex-1 text-lg rounded-md"
onClick={() =>
handleAddToCart()
}>
ADD TO CART
</button>
Let's check the final product bundle page.
import React, { useEffect } from "react";
import { ProductCard } from "@/comonents/ProductCard";
import { useProductBundle } from "@/hooks/useProductBundle";
// import { } from "./comonents/ProductCard";
import AppmakerWebSdk from "@appmaker-xyz/web-sdk"
const bundle = [
{
id: "gid://shopify/Collection/429493911574",
title: "Tops",
maxItems: 1,
products: [
{
id: "gid://shopify/Product/7983594962966",
title: "Workout Shirt",
featuredImage: {
id: "gid://shopify/ProductImage/39774607736854",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenWorkoutShirt_400x.jpg?v=1675455464",
},
},
{
id: "gid://shopify/Product/7983594340374",
title: "Puffer Jacket",
featuredImage: {
id: "gid://shopify/ProductImage/39774605475862",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenPufferjacket01_400x.jpg?v=1675455364",
},
},
{
id: "gid://shopify/Product/7983594176534",
title: "Puffer",
featuredImage: {
id: "gid://shopify/ProductImage/39774605279254",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenPuffer01_400x.jpg?v=1675455329",
},
},
{
id: "gid://shopify/ProductImage/1245458466",
title: "Half Zip",
featuredImage: {
id: "gid://shopify/ProductImage/1245458466",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenHalfzip01.jpg?v=1675455104",
},
},
],
},
{
id: "gid://shopify/Collection/429493944342",
title: "Bottoms",
maxItems: 2,
products: [
{
id: "gid://shopify/Product/7983594471446",
title: "Shorts",
featuredImage: {
id: "gid://shopify/ProductImage/39774605836310",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenShorts_400x.jpg?v=1675462426",
},
},
{
id: "gid://shopify/Product/7983593422870",
title: "Leggings",
featuredImage: {
id: "gid://shopify/ProductImage/39774604754966",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenLeggings03_400x.jpg?v=1675455256",
},
},
{
id: "gid://shopify/Product/7982856273942",
title: "Sweatpants",
featuredImage: {
id: "gid://shopify/ProductImage/39774602002454",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenSweatpants01_400x.jpg?v=1675455387",
},
},
],
},
{
id: "gid://shopify/Collection/429531037718",
title: "Shoes",
maxItems: 1,
products: [
{
id: "gid://shopify/Product/7983595487254",
title: "High Top Sneakers",
featuredImage: {
id: "gid://shopify/ProductImage/39774608883734",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Whiteleathersneakers01_400x.jpg?v=1675447604",
},
},
{
id: "gid://shopify/Product/7983595388950",
title: "Gray Runners XPs",
featuredImage: {
id: "gid://shopify/ProductImage/39774608818198",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Greyrunners_400x.jpg?v=1675447483",
},
},
{
id: "gid://shopify/Product/7983595356182",
title: "Gray Leather Sneakers",
featuredImage: {
id: "gid://shopify/ProductImage/39774608785430",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/Greyleathersneakers_400x.jpg?v=1675447462",
},
},
{
id: "gid://shopify/Product/52415645102314",
title: "Canvas Can Sneakers",
featuredImage: {
id: "gid://shopify/ProductImage/52415645102314",
url: "https://cdn.shopify.com/s/files/1/0688/1755/1382/products/GreenCanvasSneaker01.jpg?v=1675454881",
},
},
],
},
];
export default function ProductBundle() {
const {
state,
setProduct,
isSelected,
isLimitExceeded,
setBundle,
selectecItemsCount,
} = useProductBundle();
useEffect(() => {
setBundle(bundle);
}, []);
const product = {
"id": 'gid://shopify/Product/679604980957' // replace with your product id
}
const variant = {
"id": 'gid://shopify/ProductVariant/40026424869026' // replace with your variant id
}
//pass the selected bundle items as custom attributes
const customAttributes = Object.keys(state.bundles).map((bundleId) => {
const product_bundle = state.bundles[bundleId];
const products = Object.keys(product_bundle.products);
//match bundle id with collection id in bundles variable above and take the title of bundle
const bundle_title = bundle.find((item) => item.id === bundleId).title;
const product_titles = products.map((product) => {
//match product id with product id in bundles variable above and take the title of product
const product_title = bundle.map((item) => item.products).flat().find((item) => item.id === product).title;
return product_title;
});
return {
key: bundle_title,
value: product_titles.join(", ")
};
});
const handleAddToCart = () => {
AppmakerWebSdk.addProductToCart({
product: product,
variant: variant,
quantity: 1,
customAttributes: customAttributes,
})
AppmakerWebSdk.showMessage({ title: "Product added to cart" });
}
return (
<div className="flex flex-col min-h-screen pb-16 max-w-md relative mx-auto">
<main className="flex flex-col w-full flex-1 text-center">
<img
src="https://cdn.shopify.com/s/files/1/0688/1755/1382/files/DALL_E_2023-02-03_11.19.22_-_basketball_gym_5_1.png?v=1675445658&width=800"
alt=""
/>
<h1 className="text-2xl font-medium mb-4 pt-4">Perfect Gift Box</h1>
{bundle.map((item) => (
<div className="flex flex-col mb-4" key={item.id}>
<h2 className="text-xl mb-3 text-left px-2">
{item.title} ({selectecItemsCount(item.id)}/{item.maxItems})
</h2>
<div className="w-full flex gap-2 snap-x overflow-x-auto mx-auto pb-4 px-2">
{item.products?.map((product) => (
<ProductCard
item={product}
isSelected={isSelected(item.id, product.id)}
key={product.id}
isLimitExceeded={isLimitExceeded(item.id)}
selectProduct={(isSelected) => {
setProduct(item.id, product.id, isSelected);
}}
/>
))}
</div>
</div>
))}
</main>
<div className="fixed bottom-0 max-w-md bg-white border-t border-gray-100 p-3 w-full flex items-center">
<div className="mr-4">
<h3 className="text-sm">{state.totalItems} Selected</h3>
<h3 className="font-bold text-lg">$124.00</h3>
</div>
<button
className="bg-gray-900 text-white px-4 py-2 flex-1 text-lg rounded-md"
onClick={() =>
handleAddToCart()
}>
ADD TO CART
</button>
</div>
</div>
);
}
After adding to cart, you can see the custom attributes below the product.