Product Pages

🚧

This guide is outdated and will be updated shortly.

The Product page

The Product page, commonly known as PDP, will display all relevant product-specific information. Customers might land on this page directly or via the homepage. This page will allow customers to select between one or more product variants, change quantity, and add a product to the cart.

Products template

The Product CMS group, imported and updated via BigCommerce or Shopify, will be the focus of this portion of the guide.

Unlike the homepage, there is no need to create a page for each product. Attaching the Product CMS group to a template will auto-generate a page for each content item within the group.

With that in mind, we will create our first template:

  1. Navigate to Templates by clicking the icon on the sidebar.
  2. Click ADD TEMPLATE.
  3. Name the template Products.
  4. Under USE THIS TEMPLATE FOR... select Products.
  5. Click SAVE TEMPLATE.

A CMS Group can only be linked to one template. In this example, there is already a Products template, and Products is grayed out in the screenshot above.

Saving the template will auto-generate the product pages. They will be blank until we create the necessary components and sections and add those to the template.

If you open any of these product pages you’ll see a blank page, and we will address this next. First, mark some pages as Ready to Publish, so that the published store will have pages to navigate.

Creating the ProductBox section

We will now create a Section that will hold all the product details, named ProductBox.

// ProductBox section
import * as React from 'react'
import Container from 'Components/Container'
import Flex from 'Components/Flex'
import Divider from 'Components/Divider'
import Heading from 'Components/Heading'
import Text from 'Components/Text'
import Icon from 'Components/Icon'
import Carousel from 'Components/Carousel'
import Select from 'Components/Select'
import NumberInput from 'Components/NumberInput'

const ProductBox = ({ product }) => {
  const { name, variants, media, descriptionHtml = '' } = product || {}
  // Local state to deal with product quantity that will added to the Cart
  const [productQuantity, setProductQuantity] = React.useState(1)
  // Local state to handle the product variant select 
  const [currentVariant, setCurrentVariant] = React.useState(variants && variants[0])
  const { price, storefrontId } = currentVariant || {}
  
  // Map through `media` array to create our `productMedia` array that will
  // be passed to Carousel as prop
  const productMedia =
    media &&
    media.map(({ details }) => ({
      name: details.name,
      src: details.src,
      alt: details.alt,
      width: details.width,
      height: details.height,
    }))

    // Array that we will pass to our Select component to allow users
  // select between product's variant
  const variantOptions =
    variants &&
    variants.map(variant => ({
      value: variant.storefrontId,
      text: variant.name,
    }))

  const handleProductQuantity = (_, quantityAsNumber) => {
    setProductQuantity(quantityAsNumber)
  }

  const handleVariantsSelect = event => {
    if (!variants) return

        // We want to find the variant that match with our local selected variant
    const selectedVariant = variants.find(
      variant => variant.storefrontId === event.currentTarget.value,
    )

    if (!selectedVariant) return
    setCurrentVariant(selectedVariant)
  }

  return (
    <Container constrainContent maxW="1220px">
      <Flex flexDirection={{ base: 'column', lg: 'row' }} pt={10}>
        <Container w={{ base: '100%', lg: '65%' }}>
          <Carousel media={productMedia} />
        </Container>
        <Container w={{ base: '100%', lg: '35%' }}>
          <Heading as="h1" size="lg" mb="3">
            {name}
          </Heading>
          <Text as="strong" display="block" fontSize="lg" mb="3">
            ${price}
          </Text>
          <Container
            mb={{ base: '5', md: '8' }}
            dangerouslySetInnerHTML={{ __html: descriptionHtml }}
          />
          <Divider />
          <Container mt={4}>
            <Text>Variants</Text>
            <Select options={variantOptions} onChange={handleVariantsSelect} />
          </Container>
          <Container mt={4}>
            <Text>Quantity</Text>
            <NumberInput
              inputProps={{ 'aria-label': 'Product quantity' }}
            />
          </Container>
          <Container mt={{ base: '5', md: '8' }} mb={{ base: '5', md: '10' }}>
            <Button>Add to Cart</Button>
          </Container>
        </Container>
      </Flex>
    </Container>
  )
}

export default ProductBox

Next, we will create a variable matching the prop that the section will receive.

  1. Set the TYPE as Reference
  2. Under CONTENT TYPE select Products.
  3. Under variants select externalId, storefrontId, name and price.
  4. Select details under media and finally descriptionHtml.

Navigate back to the Product template, and we will add the ProductBox section to our template.

  1. Click on ProductBox in the sidebar and connect the variable PRODUCT to Current Products. This ensures that each product page will pull in dynamic content from the Product CMS Group.
  2. Save the template.

Each product page will now display product-specific details in the ProductBox section. However, if you try to add the given product to the cart, nothing will happen. We will next connect everything with the frontend-checkout package.

Creating the AddToCartButton

We need to create a new component to handle the add to cart action: create a new component called AddToCartButton or a name of your preference.

This component will handle the logic of adding a product to the cart. In the example code below, we take into account the product's inventory level and trigger the CartDrawer to open. We will cover that in the next step.

// AddToCartButton component
import * as React from 'react'
import { useCartActions, useCartState } from 'frontend-checkout'
import Button from 'Components/Button'

const PRODUCT_AVAILABLE_TEXT = 'Add to Cart'
const SOLD_OUT_PRODUCT_TEXT = 'Sold out'

const AddToCartButton = React.forwardRef((props, ref) => {
  const { productId, quantity, ...rest } = props
    // `addItems` is the function responsible to adding our products to the Cart
  // `showCart` will toggle an internal flag that allows us to show the CartDrawer
  const { addItems, showCart } = useCartActions()
  // `inventory` is an object containing all the store's inventory information
  const { inventory } = useCartState()
  // Local state to deal with transitional states
  const [isLoading, setLoading] = React.useState(true)
  // Local state to handle whether the AddToCartButton should be disabled or not
  const [disable, setDisable] = React.useState(false)
  // Local state to handle productAvailability
  const [availableForSale, setAvailableForSale] = React.useState(false)

  React.useEffect(() => {
    setLoading(inventory.status === 'loading')
        // If neither `productId` nor `productVariants` exists or are undefined
    // set the button as disabled
    if (!inventory.productVariants || !productId) return setDisable(true)
        // Get the product's availability from inventory
    const { availableForSale } = inventory.productVariants[productId]
        // Set if the product is available or not
    setAvailableForSale(availableForSale)
    // If the product is not available, set the button as disabled
    setDisable(!availableForSale)
  }, [inventory, productId])

  const clickHandler = () => {
    if (!productId) return
        // `addItems` expects an object containing `id` and `quantity`
    // refer to https://www.notion.so/shogunfrontend/Hooks-Components-and-Helpers-Guide-f828b973ada34c99ab3dea20dac994ee#50afc7e4076a4f54ab5f591a396e6855 for more information
    addItems({ id: productId, quantity })
    // At this point, we can trigger `showCart`
    showCart()
  }

  return (
    <Button
      ref={ref}
      disabled={disable}
      onClick={clickHandler}
      isFullWidth={true}
      isLoading={isLoading}
      {...rest}
    >
      {availableForSale ? PRODUCT_AVAILABLE_TEXT : SOLD_OUT_PRODUCT_TEXT}
    </Button>
  )
})

export default AddToCartButton

With the AddToCartButton ready, we will add it to the ProductBox:

// ProductBox section
import * as React from 'react'
import AddToCartButton from 'Components/AddToCartButton'
import Container from 'Components/Container'
import Flex from 'Components/Flex'
import Divider from 'Components/Divider'
import Heading from 'Components/Heading'
import Text from 'Components/Text'
import Icon from 'Components/Icon'
import Carousel from 'Components/Carousel'
import Select from 'Components/Select'
import NumberInput from 'Components/NumberInput'

const ProductBox = ({ product }) => {
  const { name, variants, media, descriptionHtml = '' } = product || {}
  // Local state to deal with product quantity that will added to the Cart
  const [productQuantity, setProductQuantity] = React.useState(1)
  // Local state to handle the product variant select 
  const [currentVariant, setCurrentVariant] = React.useState(variants && variants[0])
  const { price, storefrontId } = currentVariant || {}
  
    // Map through `media` array to create our `productMedia` array that will
  // be passed to Carousel as prop
  const productMedia =
    media &&
    media.map(({ details }) => ({
      name: details.name,
      src: details.src,
      alt: details.alt,
      width: details.width,
      height: details.height,
    }))

    // Array that we will pass to our Select component to allow users
  // select between product's variant
  const variantOptions =
    variants &&
    variants.map(variant => ({
      value: variant.storefrontId,
      text: variant.name,
    }))

  const handleProductQuantity = (quantityAsString, quantityAsNumber) => {
    setProductQuantity(quantityAsNumber)
  }

  const handleVariantsSelect = event => {
    if (!variants) return
        
        // We want to find the variant that match with our local selected variant 
    const selectedVariant = variants.find(
      variant => variant.storefrontId === event.currentTarget.value,
    )

    if (!selectedVariant) return
    setCurrentVariant(selectedVariant)
  }

  return (
    <Container constrainContent maxW="1220px">
      <Flex flexDirection={{ base: 'column', lg: 'row' }} pt={10}>
        <Container w={{ base: '100%', lg: '65%' }}>
          <Carousel media={productMedia} />
        </Container>
        <Container w={{ base: '100%', lg: '35%' }}>
          <Heading as="h1" size="lg" mb="3">
            {name}
          </Heading>
          <Text as="strong" display="block" fontSize="lg" mb="3">
            ${price}
          </Text>
          <Container
            mb={{ base: '5', md: '8' }}
            dangerouslySetInnerHTML={{ __html: descriptionHtml }}
          />
          <Divider />
          <Container mt={4}>
            <Text>Variants</Text>
            <Select options={variantOptions} onChange={handleVariantsSelect} />
          </Container>
          <Container mt={4}>
            <Text>Quantity</Text>
            <NumberInput
              inputProps={{ 'aria-label': 'Product quantity' }}
              onChange={handleProductQuantity}
            />
          </Container>
          <Container mt={{ base: '5', md: '8' }} mb={{ base: '5', md: '10' }}>
            <AddToCartButton productId={storefrontId} quantity={productQuantity} />
          </Container>
        </Container>
      </Flex>
    </Container>
  )
}

export default ProductBox

🚧

If your Shopify store is using the REST API instead of GraphQL, make sure to use externalId instead of storefrontId. If you are using a BigCommerce store, make sure to use the id from the product.


What’s Next
Did this page help you?