Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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 @@ -5,6 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useState, useEffect, useMemo} from 'react'
import {useIntl} from 'react-intl'
import {
Modal,
ModalCloseButton,
Expand All @@ -20,18 +21,25 @@ import {
Skeleton,
SimpleGrid,
Button,
Checkbox
useDisclosure
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useProducts} from '@salesforce/commerce-sdk-react'
import {useHistory} from 'react-router-dom'
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image'
import ProductViewModal from '@salesforce/retail-react-app/app/components/product-view-modal'
import PropTypes from 'prop-types'
import {useBonusProductModalContext} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'
import {useShopperBasketsMutationHelper} from '@salesforce/commerce-sdk-react'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {findImageGroupBy} from '@salesforce/retail-react-app/app/utils/image-groups-utils'
import {FormattedMessage} from 'react-intl'
import {filterImageGroups} from '@salesforce/retail-react-app/app/utils/product-utils'
import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'

// Component to display individual bonus product with checkbox for selection
const BonusProductItem = ({product, productData, isSelected, onToggle, isLoading}) => {
const BonusProductItem = ({product, productData, onClick, isLoading}) => {
const productName = product?.productName || product?.title

// Get the appropriate image group from the passed product data
Expand Down Expand Up @@ -73,24 +81,18 @@ const BonusProductItem = ({product, productData, isSelected, onToggle, isLoading

return (
<VStack spacing={3} p={4} bg="white">
<AspectRatio
ratio={1}
width="150px"
minWidth="150px"
cursor="pointer"
onClick={() => onToggle(product)}
>
<AspectRatio ratio={1} width="150px" minWidth="150px" cursor="pointer">
{image && (
<DynamicImage
src={`${image.disBaseLink || image.link}[?sw={width}&q=60]`}
widths={{
base: '150px'
}}
widths={{base: '150px'}}
imageProps={{
alt: productName,
borderRadius: 'md',
objectFit: 'cover'
objectFit: 'cover',
style: {pointerEvents: 'all'}
}}
onClick={onClick}
/>
)}
</AspectRatio>
Expand All @@ -105,13 +107,6 @@ const BonusProductItem = ({product, productData, isSelected, onToggle, isLoading
>
{productName}
</Text>
<Box width="full" display="flex" justifyContent="center">
<Checkbox
isChecked={isSelected}
onChange={() => onToggle(product)}
cursor="pointer"
/>
</Box>
</VStack>
</VStack>
)
Expand All @@ -120,21 +115,46 @@ const BonusProductItem = ({product, productData, isSelected, onToggle, isLoading
BonusProductItem.propTypes = {
product: PropTypes.object.isRequired,
productData: PropTypes.object,
isSelected: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
isLoading: PropTypes.bool
}

export const BonusProductModal = () => {
const {isOpen, onClose, data} = useBonusProductModalContext()
const [selectedProducts, setSelectedProducts] = useState(new Set())
const {data: basket} = useCurrentBasket()
const einstein = useEinstein()
const history = useHistory()
const showToast = useToast()
const {formatMessage} = useIntl()
const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper()
const {isOpen, onClose, onClickClose, data} = useBonusProductModalContext()
const {
isOpen: isProductViewOpen,
onOpen: onProductViewOpen,
onClose: onProductViewClose
} = useDisclosure()
const [selectedProduct, setSelectedProduct] = useState()

// Extract bonus items from the response structure
const bonusDiscountLineItems = data?.newBonusItems || data?.allBonusItems || []
const currentPromotion = bonusDiscountLineItems[0] || {}
const bonusProducts = currentPromotion.bonusProducts || []
const maxBonusItems = currentPromotion.maxBonusItems || 1

// Get existing bonus products for this specific promotion
const existingBonusProducts = useMemo(() => {
if (!basket?.productItems || !currentPromotion.id) return []
return basket.productItems.filter(
(item) =>
item.bonusProductLineItem === true &&
item.bonusDiscountLineItemId === currentPromotion.id
)
}, [basket?.productItems, currentPromotion.id])
// Calculate how many more bonus items can be added
const currentBonusCount = existingBonusProducts.reduce(
(total, item) => total + item.quantity,
0
)

// Get all product IDs for batch fetching
const productIds = useMemo(() => {
return bonusProducts.map((product) => product.productId || product.id).filter(Boolean)
Expand Down Expand Up @@ -166,88 +186,123 @@ export const BonusProductModal = () => {
// Reset selections when modal opens
useEffect(() => {
if (isOpen) {
setSelectedProducts(new Set())
setSelectedProduct(null)
}
}, [isOpen])

const handleToggle = (product) => {
const productIdentifier = product.id || product.productId
const isSelected = selectedProducts.has(productIdentifier)
if (isSelected) {
setSelectedProducts((prev) => {
const newSet = new Set(prev)
newSet.delete(productIdentifier)
return newSet
})
} else if (selectedProducts.size < maxBonusItems) {
setSelectedProducts((prev) => {
const newSet = new Set(prev)
newSet.add(productIdentifier)
return newSet
})
}
const showError = () => {
showToast({
title: formatMessage(API_ERROR_MESSAGE),
status: 'error'
})
}

const handleNext = () => {
onClose()
const handleClickBonusProduct = (product) => {
setSelectedProduct(product.productId || product.id)
onProductViewOpen()
}

const selectedCount = selectedProducts.size
const handleAddToCart = async (productSelectionValues) => {
try {
const productItems = productSelectionValues.map(({variant, quantity}) => ({
productId: variant.productId,
bonusDiscountLineItemId: currentPromotion.id,
quantity
}))

await addItemToNewOrExistingBasket(productItems)

einstein.sendAddToCart(productItems)
} catch (error) {
console.log('error', error)
showError()
} finally {
onClickClose()
history.push('/cart')
}
}

const onProductViewHide = () => {
setSelectedProduct(null)
onProductViewClose()
}

// Calculate columns based on number of products
const productCount = bonusProducts.length
const columns = Math.min(productCount, 3) // Max 3 columns, but fewer if less products
const product = useMemo(() => {
const bonusProduct = bonusProducts.find((product) => product.productId === selectedProduct)
if (!bonusProduct) return null
// Get the full product data from the fetched products
const fullProductData = productsDataMap[bonusProduct.productId || bonusProduct.id]
// Merge bonus product data with full product data
return fullProductData ? {...fullProductData, ...bonusProduct} : null
}, [bonusProducts, selectedProduct, productsDataMap])

if (!isOpen) return null

return (
<Modal isOpen={isOpen} onClose={onClose} size="3xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Text fontSize="lg" fontWeight="bold">
Add Bonus Product ({selectedCount} of {maxBonusItems})
</Text>
</ModalHeader>
<ModalCloseButton />

<ModalBody bgColor="white" padding="6">
{bonusProducts.length > 0 ? (
<SimpleGrid columns={columns} spacing={8} justifyItems="start">
{bonusProducts.map((product) => {
const productId = product.productId || product.id
const productData = productsDataMap[productId]

return (
<BonusProductItem
key={productId}
product={product}
productData={productData}
isSelected={selectedProducts.has(productId)}
onToggle={handleToggle}
isLoading={isProductsLoading}
/>
)
})}
</SimpleGrid>
) : (
<Box textAlign="center" py={8}>
<Text color="gray.500">
<FormattedMessage
defaultMessage="No bonus products available"
id="bonus_product_modal.no_products_available"
/>
<>
{selectedProduct && product && (
<ProductViewModal
isOpen={isProductViewOpen}
onClose={onProductViewHide}
onOpen={onProductViewOpen}
product={product}
addToCart={(variant, quantity) =>
handleAddToCart([{product: product, variant, quantity: quantity}])
}
/>
)}
{!selectedProduct && (
<Modal isOpen={isOpen} onClose={onClose} size="3xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Text fontSize="lg" fontWeight="bold">
Add Bonus Product ({currentBonusCount} of {maxBonusItems})
</Text>
</Box>
)}
</ModalBody>

<ModalFooter justifyContent="center">
<Button colorScheme="blue" onClick={handleNext}>
Next
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</ModalHeader>
<ModalCloseButton onClick={onClickClose} />

<ModalBody bgColor="white" padding="6">
{bonusProducts.length > 0 ? (
<SimpleGrid columns={columns} spacing={8} justifyItems="start">
{bonusProducts.map((product) => {
const productId = product.productId || product.id
const productData = productsDataMap[productId]

return (
<BonusProductItem
key={productId}
product={product}
productData={productData}
onClick={() => handleClickBonusProduct(product)}
isLoading={isProductsLoading}
/>
)
})}
</SimpleGrid>
) : (
<Box textAlign="center" py={8}>
<Text color="gray.500">
<FormattedMessage
defaultMessage="No bonus products available"
id="bonus_product_modal.no_products_available"
/>
</Text>
</Box>
)}
</ModalBody>

<ModalFooter justifyContent="center">
<Button colorScheme="blue" onClick={onClickClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
</>
)
}
Loading
Loading