Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product sets: basic pdp (@W-12301851@) #897

Merged
merged 27 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
14ffcfb
Product set: render the children products with minimal UI
vmarta Jan 5, 2023
c5f71bb
No longer showing breadcrumbs in the child items
vmarta Jan 6, 2023
05cf172
Merge branch 'feature/product-sets' into product-sets-basic-pdp
vmarta Jan 6, 2023
77a2ba6
Individual swatches now have correct `value`
vmarta Jan 6, 2023
3842f9a
Merge branch 'feature/product-sets' into product-sets-basic-pdp
vmarta Jan 9, 2023
aa5fdb9
1st attempt at saving swatches state in the page url
vmarta Jan 10, 2023
8400a46
Avoid null in the url search params
vmarta Jan 10, 2023
868a5a3
Some refactoring
vmarta Jan 10, 2023
e108963
Add-to-cart now works for child items
vmarta Jan 10, 2023
31bcd93
Show accordion for each child item
vmarta Jan 11, 2023
7d30f54
Add todo
vmarta Jan 11, 2023
9a56c27
Rename variable for accuracy
vmarta Jan 11, 2023
a81034f
Optional argument
vmarta Jan 11, 2023
5f5edc6
Some refactoring and fix linting
vmarta Jan 11, 2023
b3bd3b2
Helper hook for url search params
vmarta Jan 11, 2023
3acf93d
Clean up
vmarta Jan 11, 2023
a670c64
Rename module for a specific use case (PDP)
vmarta Jan 11, 2023
1b2a480
Show/hide add-to-cart buttons accordingly
vmarta Jan 12, 2023
e3d27f3
Modal shows product set data
vmarta Jan 12, 2023
5ad9cf4
Parent product
vmarta Jan 14, 2023
9aed81f
Variant's product image shows up in add-to-cart modal
vmarta Jan 16, 2023
41cde1c
Rename variables for clarity
vmarta Jan 16, 2023
5a8f6ce
Fix linting error
vmarta Jan 16, 2023
4ddb22e
Add horizontal rules to separate the parent and child items
vmarta Jan 16, 2023
31aa735
Tweak whitespace
vmarta Jan 16, 2023
1607644
Localize the Starting At label
vmarta Jan 17, 2023
88fb12f
Add todo
vmarta Jan 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import {noop} from '../../utils/utils'
* Each Swatch is a link with will direct to a href passed to them
*/
const SwatchGroup = (props) => {
const {displayName, children, value, label = '', variant = 'square', onChange = noop} = props
const {
displayName,
children,
value: selectedValue,
label = '',
variant = 'square',
onChange = noop
} = props
const styles = useStyleConfig('SwatchGroup')
return (
<Flex {...styles.swatchGroup} role="radiogroup">
Expand All @@ -28,9 +35,7 @@ const SwatchGroup = (props) => {
const childValue = child.props.value

return React.cloneElement(child, {
selected: childValue === value,
key: childValue,
value,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: there was a subtle bug with the original code. Now the parent (SwatchGroup) no longer overriding the children's value. They should be distinct.

selected: childValue === selectedValue,
variant,
onChange
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {useLocation} from 'react-router-dom'

export const usePDPSearchParams = (productId) => {
const {search} = useLocation()

const allParams = new URLSearchParams(search)
const productParams = new URLSearchParams(allParams.get(productId) || '')

return [allParams, productParams]
}
9 changes: 5 additions & 4 deletions packages/template-retail-react-app/app/hooks/use-product.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ const OUT_OF_STOCK = 'OUT_OF_STOCK'
const UNFULFILLABLE = 'UNFULFILLABLE'

// TODO: This needs to be refactored.
export const useProduct = (product) => {
export const useProduct = (product, isSetProduct = false) => {
const showLoading = !product
const stockLevel = product?.inventory?.stockLevel || 0
const stepQuantity = product?.stepQuantity || 1
const minOrderQuantity = stockLevel > 0 ? product?.minOrderQuantity || 1 : 0
const initialQuantity = product?.quantity || product?.minOrderQuantity || 1

const intl = useIntl()
const variant = useVariant(product)
const variationParams = useVariationParams(product)
const variationAttributes = useVariationAttributes(product)
const variant = useVariant(product, isSetProduct)
const variationParams = useVariationParams(product, isSetProduct)
const variationAttributes = useVariationAttributes(product, isSetProduct)
// console.log('--- variationAttributes', variationAttributes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to remove this 👍

const [quantity, setQuantity] = useState(initialQuantity)

// A product is considered out of stock if the stock level is 0 or if we have all our
Expand Down
4 changes: 2 additions & 2 deletions packages/template-retail-react-app/app/hooks/use-variant.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {useVariationParams} from './use-variation-params'
* @param {Object} product
* @returns {Object} the currently selected `Variant` object.
*/
export const useVariant = (product = {}) => {
export const useVariant = (product = {}, isSetProduct = false) => {
const {variants = []} = product
const variationParams = useVariationParams(product)
const variationParams = useVariationParams(product, isSetProduct)

// Get a filtered array of variants. The resulting array will only have variants
// which have all the current variation params values set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {useLocation} from 'react-router-dom'
import {useVariationParams} from './use-variation-params'

// Utils
import {rebuildPathWithParams} from '../utils/url'
import {updateSearchParams} from '../utils/url'
import {usePDPSearchParams} from './use-pdp-search-params'

/**
* Return the first image in the `swatch` type image group for a given
Expand Down Expand Up @@ -47,8 +48,17 @@ const getVariantValueSwatch = (product, variationValue) => {
* @param {Object} location
* @returns {String} a product url for the current variation value.
*/
const buildVariantValueHref = (product, params, location) => {
return rebuildPathWithParams(`${location.pathname}${location.search}`, params)
const buildVariantValueHref = ({pathname, existingParams, newParams, productId, isSetProduct}) => {
const [allParams, productParams] = existingParams

if (isSetProduct) {
updateSearchParams(productParams, newParams)
allParams.set(productId, productParams.toString())
} else {
updateSearchParams(allParams, newParams)
}

return `${pathname}?${allParams.toString()}`
}

/**
Expand Down Expand Up @@ -80,10 +90,12 @@ const isVariantValueOrderable = (product, variationParams) => {
* @returns {Array} a decorated variation attributes list.
*
*/
export const useVariationAttributes = (product = {}) => {
export const useVariationAttributes = (product = {}, isSetProduct = false) => {
const {variationAttributes = []} = product
const location = useLocation()
const variationParams = useVariationParams(product)
const variationParams = useVariationParams(product, isSetProduct)

const existingParams = usePDPSearchParams(product.id)

return useMemo(
() =>
Expand All @@ -104,7 +116,13 @@ export const useVariationAttributes = (product = {}) => {
return {
...value,
image: getVariantValueSwatch(product, value),
href: buildVariantValueHref(product, params, location),
href: buildVariantValueHref({
pathname: location.pathname,
existingParams,
newParams: params,
productId: product.id,
isSetProduct
}),
orderable: isVariantValueOrderable(product, params)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {useLocation} from 'react-router-dom'
import {usePDPSearchParams} from './use-pdp-search-params'

/*
* This hook will return only the params that are also product attributes for the
* passed in product object.
*/
export const useVariationParams = (product = {}) => {
export const useVariationParams = (product = {}, isSetProduct = false) => {
const {variationAttributes = [], variationValues = {}} = product
const variationParams = variationAttributes.map(({id}) => id)

const {search} = useLocation()
const params = new URLSearchParams(search)
// Using all the variation attribute id from the array generated above, get
const [allParams, productParams] = usePDPSearchParams(product.id)
const params = isSetProduct ? productParams : allParams
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to reuse the existing React hooks. The main difference is for set products, the hooks focus in on a specific subset of the page url params, rather than all of the params.


// Using all the variation attribute id from the array generated below, get
// the value if there is one from the location search params and add it to the
// accumulator.
const filteredVariationParams = variationParams.reduce((acc, key) => {
let value = params.get(`${key}`) || variationValues?.[key]
return value ? {...acc, [key]: value} : acc
}, {})
const variationParams = variationAttributes
.map(({id}) => id)
.reduce((acc, key) => {
let value = params.get(`${key}`) || variationValues?.[key]
return value ? {...acc, [key]: value} : acc
}, {})

return filteredVariationParams
return variationParams
}
143 changes: 33 additions & 110 deletions packages/template-retail-react-app/app/pages/product-detail/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,13 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React, {useEffect, useState} from 'react'
import React, {Fragment, useEffect, useState} from 'react'
import PropTypes from 'prop-types'
import {Helmet} from 'react-helmet'
import {FormattedMessage, useIntl} from 'react-intl'

// Components
import {
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Box,
Button,
Stack
} from '@chakra-ui/react'
import {Box, Button, Stack} from '@chakra-ui/react'

// Hooks
import useBasket from '../../commerce-api/hooks/useBasket'
Expand All @@ -32,6 +23,7 @@ import useEinstein from '../../commerce-api/hooks/useEinstein'
// Project Components
import RecommendedProducts from '../../components/recommended-products'
import ProductView from '../../partials/product-view'
import InformationAccordion from './partials/information-accordion'

// Others/Utils
import {HTTPNotFound} from 'pwa-kit-react-sdk/ssr/universal/errors'
Expand Down Expand Up @@ -155,105 +147,36 @@ const ProductDetail = ({category, product, isLoading}) => {
</Helmet>

<Stack spacing={16}>
<ProductView
product={product}
category={primaryCategory?.parentCategoryTree || []}
addToCart={(variant, quantity) => handleAddToCart(variant, quantity)}
addToWishlist={(_, quantity) => handleAddToWishlist(quantity)}
isProductLoading={isLoading}
isCustomerProductListLoading={!wishlist.isInitialized}
/>

{/* Information Accordion */}
<Stack direction="row" spacing={[0, 0, 0, 16]}>
<Accordion allowMultiple allowToggle maxWidth={'896px'} flex={[1, 1, 1, 5]}>
{/* Details */}
<AccordionItem>
<h2>
<AccordionButton height="64px">
<Box flex="1" textAlign="left" fontWeight="bold" fontSize="lg">
{formatMessage({
defaultMessage: 'Product Detail',
id: 'product_detail.accordion.button.product_detail'
})}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel mb={6} mt={4}>
<div
dangerouslySetInnerHTML={{
__html: product?.longDescription
}}
/>
</AccordionPanel>
</AccordionItem>

{/* Size & Fit */}
<AccordionItem>
<h2>
<AccordionButton height="64px">
<Box flex="1" textAlign="left" fontWeight="bold" fontSize="lg">
{formatMessage({
defaultMessage: 'Size & Fit',
id: 'product_detail.accordion.button.size_fit'
})}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel mb={6} mt={4}>
{formatMessage({
defaultMessage: 'Coming Soon',
id: 'product_detail.accordion.message.coming_soon'
})}
</AccordionPanel>
</AccordionItem>

{/* Reviews */}
<AccordionItem>
<h2>
<AccordionButton height="64px">
<Box flex="1" textAlign="left" fontWeight="bold" fontSize="lg">
{formatMessage({
defaultMessage: 'Reviews',
id: 'product_detail.accordion.button.reviews'
})}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel mb={6} mt={4}>
{formatMessage({
defaultMessage: 'Coming Soon',
id: 'product_detail.accordion.message.coming_soon'
})}
</AccordionPanel>
</AccordionItem>

{/* Questions */}
<AccordionItem>
<h2>
<AccordionButton height="64px">
<Box flex="1" textAlign="left" fontWeight="bold" fontSize="lg">
{formatMessage({
defaultMessage: 'Questions',
id: 'product_detail.accordion.button.questions'
})}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel mb={6} mt={4}>
{formatMessage({
defaultMessage: 'Coming Soon',
id: 'product_detail.accordion.message.coming_soon'
})}
</AccordionPanel>
</AccordionItem>
</Accordion>
<Box display={['none', 'none', 'none', 'block']} flex={4}></Box>
</Stack>
{product?.type.set ? (
// Product Set: render the child products
product.setProducts.map((childProduct) => (
<Fragment key={childProduct.id}>
<ProductView
product={childProduct}
isSetProduct={true}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this isSetProduct for the ProductView component (and also the related React hooks) because given solely the data for a child product, it is not possible to identify whether the product belongs to a set or not.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about other product types that we currently don't support that we might in the future. And how using isSetProduct as a prop will age?

Side note: Is isSetProduct short for isProductSetProduct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bendvc Good questions.. I don't know yet at this time, but there's a separate refactoring ticket for supporting other product types.🤔 However, I'll think about it a bit more at this stage anyways.

Copy link
Contributor Author

@vmarta vmarta Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is isSetProduct short for isProductSetProduct

Yes, isSetProduct comes from the way SCAPI calls the child products. The data for a product set has setProducts: [...].

addToCart={(variant, quantity) =>
handleAddToCart(variant, quantity)
}
addToWishlist={(_, quantity) => handleAddToWishlist(quantity)}
isProductLoading={isLoading}
isCustomerProductListLoading={!wishlist.isInitialized}
/>
<InformationAccordion product={childProduct} />
</Fragment>
))
) : (
<Fragment>
<ProductView
product={product}
category={primaryCategory?.parentCategoryTree || []}
addToCart={(variant, quantity) => handleAddToCart(variant, quantity)}
addToWishlist={(_, quantity) => handleAddToWishlist(quantity)}
isProductLoading={isLoading}
isCustomerProductListLoading={!wishlist.isInitialized}
/>
<InformationAccordion product={product} />
</Fragment>
)}

{/* Product Recommendations */}
<Stack spacing={16}>
Expand Down
Loading