Untitled
unknown
plain_text
2 years ago
19 kB
11
Indexable
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import { AlertStatus, Box, Button, HStack, Text } from '@chakra-ui/react'
import { Money } from '@Types/product/Money'
import { Product } from '@Types/product/Product'
import { Variant } from '@Types/product/Variant'
import { CustomToast } from 'components/core-ui-controls/CustomToast'
import {
AlertBox,
getCartProductBySlug,
getCartProductByVariantSku,
PdpLayout,
Price,
QuantityPicker,
} from 'composable'
import { SwatchSelectorConfiguration, SwatchSelectorGrouped } from 'composable/components/types'
import { CurrencyHelpers } from 'helpers/currencyHelpers'
import { useFormat } from 'helpers/hooks/useFormat'
import groupBy from 'lodash/groupBy'
import keys from 'lodash/keys'
import mapKeys from 'lodash/mapKeys'
import toast, { Toast } from 'react-hot-toast'
import { GiTrousers } from 'react-icons/gi'
import { useCart } from 'frontastic'
import { addToWishlist } from 'frontastic/actions/wishlist'
export interface UIColor {
name?: string
key?: string
bgColor?: string
selectedColor?: string
}
export interface UIImage {
id?: string
src?: string
alt?: string
}
export interface UIDetail {
name: string
items: string[]
}
export type UIProduct = {
productId: string
name: string
variants: Variant[]
price?: Money
images?: UIImage[]
colors?: UIColor[]
sizes?: UISize[]
description: string
details?: UIDetail[]
isOnWishlist?: boolean
_url?: string
}
export interface UISize {
label: string
key: string
}
function ProductDetailsTastic({ data }) {
const router = useRouter()
const intl = useFormat({ name: 'common' })
const { data: cart, addItem, updateItem } = useCart()
const {
product,
childProducts,
cartPromotions,
priceRange,
isProductInStock,
}: {
product: Product
childProducts?: Product[]
cartPromotions: any[]
priceRange: any
isProductInStock: boolean
} = data.data.dataSource
const urlSwatch: string = (router.query.swatch as string) ?? ''
const urlSwatchChildProduct = childProducts?.find((childProduct: Product) => {
const childProductSwatch = (
childProduct?.variants?.[0]?.attributes?.colorDisplayName ||
childProduct?.variants?.[0]?.attributes?.ecommColor ||
''
)
.toLowerCase()
.replaceAll(' ', '_')
const urlSwatchLower = urlSwatch.toLowerCase().replaceAll(' ', '_')
return childProductSwatch === urlSwatchLower
})
console.log('urlSwatch', urlSwatch)
console.log('childProducts', childProducts)
const firstAvailableChildProduct = childProducts?.find((childProduct: Product) => {
return childProduct?.variants?.some((variant: Variant) => variant?.availableQuantity > 0)
})
const initialChildProduct =
urlSwatch && urlSwatchChildProduct ? urlSwatchChildProduct : firstAvailableChildProduct ?? childProducts?.[0]
const initialSwatch =
initialChildProduct?.variants?.[0]?.attributes?.colorDisplayName ||
initialChildProduct?.variants?.[0]?.attributes?.ecommColor
const [currentChildProduct, setCurrentChildProduct] = useState<Product>(initialChildProduct)
const [currentVariant, setCurrentVariant] = useState<Variant>(initialChildProduct?.variants?.[0])
const [quantity, setQuantity] = useState(1)
const [isLoading, setIsLoading] = useState(false)
const [selectedSwatch, setSelectedSwatch] = useState<string>(initialSwatch)
const [selectedAttributes, setSelectedAttributes] = useState<{ [key: string]: string }>({})
const [showMissingSelections, setShowMissingSelections] = useState<boolean>(false)
const variantCountInCart = useMemo(
() => getCartProductByVariantSku({ variantSku: currentVariant?.sku, cart: cart })?.count ?? 0,
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentVariant?.sku, cart?.lineItems],
)
useEffect(() => {
setCurrentVariant(initialChildProduct?.variants?.[0])
setSelectedSwatch(initialSwatch)
}, [product.slug])
function setSelectedAttribute(attribute: string, value: string) {
setSelectedAttributes((prev) => {
const isAttributeAlreadySelected = prev[attribute] === value
if (isAttributeAlreadySelected) {
const newSelectedAttributes = { ...prev }
delete newSelectedAttributes[attribute]
return newSelectedAttributes
} else {
return { ...prev, [attribute]: value }
}
})
}
function findVariantBySelectedAttributes(
selectedAttributes: { [key: string]: string },
currentChildProduct?: Product,
) {
const selectedVariant = currentChildProduct?.variants?.find((variant: Variant) => {
const condition = Object.entries(selectedAttributes).every(([key, value]) => {
const condition = variant.attributes[key] === value
return condition
})
return condition && variant?.availableQuantity > 0
})
return selectedVariant
}
function triggerVariantChange(selectedAttributes: { [key: string]: string }, currentChildProduct?: Product) {
setShowMissingSelections(false)
const selectedVariant = findVariantBySelectedAttributes(selectedAttributes, currentChildProduct)
const variantChangeCondition = selectedVariant && selectedVariant?.id !== currentVariant?.id
if (variantChangeCondition) setCurrentVariant(selectedVariant)
}
useEffect(() => {
triggerVariantChange(selectedAttributes, currentChildProduct)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAttributes])
function triggerSwatchChange(selectedSwatch: string) {
setShowMissingSelections(false)
const selectedChildProduct = childProducts?.find((childProduct: Product) => {
return childProduct.variants?.every((variant: Variant) => {
const colorMatch =
variant.attributes?.colorDisplayName === selectedSwatch || variant.attributes?.ecommColor === selectedSwatch
return colorMatch
})
})
const swatchChangeCondition =
selectedChildProduct && selectedChildProduct?.productId !== currentChildProduct?.productId
if (swatchChangeCondition) setCurrentChildProduct(selectedChildProduct)
triggerVariantChange(selectedAttributes, selectedChildProduct)
}
useEffect(() => {
triggerSwatchChange(selectedSwatch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSwatch])
const availableSwatches = useMemo(() => {
const initialAccumulator = {
attribute: {
label: 'Color',
value:
currentChildProduct?.variants?.[0]?.attributes?.colorDisplayName ||
currentChildProduct?.variants?.[0]?.attributes?.ecommColor,
},
options: [],
}
const swatches = childProducts?.reduce((acc: SwatchSelectorConfiguration, childProduct: Product) => {
const swatchValue =
childProduct?.variants?.[0]?.attributes?.colorDisplayName ||
currentChildProduct?.variants?.[0]?.attributes?.ecommColor
const swatch = childProduct?.swatch
const swatchUrl = childProduct?.variants?.[0]?.attributes?.swatchUrl
const isSelectedAttributesMatchingVariantAvailable = findVariantBySelectedAttributes(
selectedAttributes,
childProduct,
)
const swatchOption = {
value: swatchValue,
inStock: isSelectedAttributesMatchingVariantAvailable,
swatchUrl,
swatch,
priceGroup: {
price: childProduct?.variants?.[0].price,
discountPercent: childProduct?.variants?.[0].discountPercent,
discountedPrice: childProduct?.variants?.[0].discountedPrice,
},
}
const newOptions = [...acc.options, swatchOption]
const newAccumulator = { ...acc, options: newOptions }
return newAccumulator
}, initialAccumulator)
return swatches
}, [childProducts, selectedAttributes, currentChildProduct?.variants])
console.log('availableSwatches', availableSwatches)
const swatchSelectorGroupedByPrice = useMemo(() => {
if (!availableSwatches) return []
const swatcheSelectorGroupedByDiscounts = groupBy(availableSwatches?.options, 'priceGroup.discountPercent')
return mapKeys(swatcheSelectorGroupedByDiscounts, (value, key) => {
return key === 'undefined' ? '0' : key
}) as SwatchSelectorGrouped
}, [availableSwatches])
let availableAttributesTextSelectorsLables = {}
if (product.attributes?.segment2Label) {
availableAttributesTextSelectorsLables['segment2'] = product.attributes?.segment2Label
}
if (product.attributes?.segment3Label) {
availableAttributesTextSelectorsLables['segment3'] = product.attributes?.segment3Label
}
const textSelectorAttributes = Object.keys(availableAttributesTextSelectorsLables)
const availableAttributesOptions = useMemo(() => {
const childProductsAttributes = childProducts?.map((childProduct) =>
childProduct?.variants?.reduce((acc: { [key: string]: string[] }, variant: Variant) => {
Object.entries(variant.attributes).forEach(([key, value]: [string, string]) => {
if (
value &&
typeof value !== 'object' &&
textSelectorAttributes.includes(key) &&
!acc[key]?.includes(value)
) {
acc[key] = acc[key] ? [...acc[key], value] : [value]
}
})
return acc
}, {}),
)
const finalAttributes = childProductsAttributes?.reduce(
(acc: { [key: string]: string[] }, attributes: { [key: string]: string[] }) => {
Object.entries(attributes).forEach(([key, value]: [string, string[]]) => {
acc[key] = acc[key] ? [...acc[key], ...value] : [...value]
})
return acc
},
{},
)
Object.entries(finalAttributes).forEach(([key, value]: [string, string[]]) => {
finalAttributes[key] = [...new Set(value)].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
})
return finalAttributes
}, [childProducts])
const availableAttributesTextSelectors = useMemo(() => {
const attributesTextSelectors = Object.keys(availableAttributesOptions ?? {})
const orderedAttributesTextSelectors = attributesTextSelectors.reduce((accumulator, current) => {
const options = availableAttributesOptions[current].map((option: string) => {
const attributeOptionVariant = currentChildProduct?.variants?.find((variant: Variant) => {
const doAttributesMatch = Object.entries(selectedAttributes).every(([key, value]) => {
const isOptionCurrentAttribute = key === current
const variantAttributeValue = variant.attributes[key]
return isOptionCurrentAttribute ? true : variantAttributeValue === value
})
const doesCurrentAttributeMatch = variant.attributes[current] === option
const inStock = variant?.availableQuantity > 0
return doAttributesMatch && doesCurrentAttributeMatch && inStock
})
const result = {
value: option,
inStock: !!attributeOptionVariant,
}
return result
})
const attributeValue = selectedAttributes[current]
return {
...accumulator,
[current]: {
attribute: {
label: current,
value: attributeValue,
},
options,
},
}
}, {})
return orderedAttributesTextSelectors
}, [availableAttributesOptions, currentChildProduct?.variants, selectedAttributes])
const textSelectorsMissingSelection = useMemo<string[]>(
() =>
Object.values(availableAttributesTextSelectors)
.filter(
(textSelector: { attribute?: { value?: string | undefined | null } }) => !textSelector?.attribute?.value,
)
.map((textSelector: { attribute?: { label?: string | undefined | null } }) => textSelector?.attribute?.label),
[availableAttributesTextSelectors],
)
const stockIsSet = currentVariant?.availableQuantity !== undefined
const canAddToCart = stockIsSet ? quantity <= currentVariant?.availableQuantity - variantCountInCart : GiTrousers
useEffect(() => {
setQuantity(1)
}, [product?.key, childProducts])
const launchToast = (t: Toast, status: AlertStatus, message: string) => {
const onClose = () => toast.remove(t.id)
return toast.custom(<CustomToast status={status} description={message} onClose={onClose} />, { id: t.id })
}
const handleAddToCart = async (variant: Variant, quantity: number) => {
setIsLoading(true)
try {
let res
if (variantCountInCart > 0) {
const lineItem = getCartProductByVariantSku({ variantSku: variant.sku, cart: cart })
const newQuantity = lineItem.count + quantity
res = await updateItem(lineItem.lineItemId, newQuantity)
} else {
res = await addItem(variant, quantity)
}
if (res.ok === false) {
toast((t) =>
launchToast(t, 'error', intl.formatMessage({ id: 'cart.item.add.error', values: { name: product.name } })),
)
} else {
toast((t) =>
launchToast(
t,
'success',
intl.formatMessage({ id: 'cart.item.add.success', values: { name: product.name } }),
),
)
}
} catch (err) {
toast((t) =>
launchToast(t, 'error', intl.formatMessage({ id: 'cart.item.add.error', values: { name: product.name } })),
)
console.log(err)
}
setIsLoading(false)
}
const handleAddToWishList = () => {
toast((t) => launchToast(t, 'success', 'Added to wishlist'))
addToWishlist(currentVariant?.sku, 1)
}
const pdpAccordions = useMemo(() => {
const productAndFit = product?.attributes?.productAndFit
return productAndFit
? [
{
defaultOpen: false,
label: intl.formatMessage({ id: 'product.specifications.title' }),
content: productAndFit,
},
]
: []
}, [product?.attributes?.productAndFit])
const cartPromotion = useMemo(
() =>
cartPromotions?.find((cartPromotion) => {
const allVariants = childProducts?.flatMap((product) => product?.variants)
const promotionVariant = allVariants?.find((variant) => variant?.promotion === cartPromotion.key)
return promotionVariant
}),
[cartPromotions, childProducts],
)
const promotionVariant = useMemo(() => {
const allVariants = childProducts?.flatMap((product) => product?.variants)
const promotionVariant = allVariants?.find((variant) => variant?.promotion === cartPromotion?.key)
return promotionVariant
}, [cartPromotion, childProducts])
if (!product || !currentChildProduct || !currentVariant) return null
const main = (
<>
{isProductInStock && (
<>
<Box flex="1">
<QuantityPicker
value={quantity}
onChange={(val) => setQuantity(val)}
min={1}
max={stockIsSet ? currentVariant?.availableQuantity - variantCountInCart : 30}
buttonProps={{
size: 'sm',
}}
hideLabel={true}
/>
</Box>
<Button
mt={6}
size="lg"
variant={'solid'}
width={'full'}
onClick={() => {
if (textSelectorsMissingSelection.length > 0) {
setShowMissingSelections(true)
} else {
setShowMissingSelections(false)
handleAddToCart(currentVariant, quantity)
}
}}
isDisabled={!canAddToCart}
isLoading={isLoading}
>
{intl.formatMessage({ id: 'action.addToCart' })}
</Button>
</>
)}
{!canAddToCart && (
<AlertBox
description={intl.formatMessage({
id: 'product.outOfStock.info',
})}
rootProps={{
bg: '#FDE5D8',
status: 'error',
mt: 0,
}}
closeButtonProps={{ display: 'none' }}
/>
)}
</>
)
// Capture the value first, and if it is 0 then do not render.
const promotionPriceValue = promotionVariant
? promotionVariant?.discountedPrice?.centAmount
: currentVariant?.discountedPrice?.centAmount
const promotionPrice = promotionPriceValue ? CurrencyHelpers.formatForCurrency(promotionPriceValue) : undefined
const price = (
<>
<Price
priceProps={{ textStyle: 'body-300' }}
price={
promotionVariant
? CurrencyHelpers.formatForCurrency(promotionVariant?.price?.centAmount)
: CurrencyHelpers.formatForCurrency(currentVariant?.price?.centAmount)
}
discountedPrice={promotionPrice}
discountPercent={currentVariant?.discountPercent}
promoPrice={cartPromotion ? CurrencyHelpers.formatForCurrency(cartPromotion?.discountPrice[0]) : undefined}
priceRange={
priceRange?.minPrice && priceRange?.maxPrice
? {
minPrice: CurrencyHelpers.formatForCurrency(priceRange?.minPrice),
maxPrice: CurrencyHelpers.formatForCurrency(priceRange?.maxPrice),
}
: undefined
}
priceGroupQuantity={swatchSelectorGroupedByPrice ? keys(swatchSelectorGroupedByPrice).length : 1}
/>
</>
)
return (
<PdpLayout
title={product?.name}
productId={product.productId}
description={product.description}
isLoaded={true}
accordions={pdpAccordions}
currentVariant={currentVariant}
currentChildProduct={currentChildProduct}
product={product}
main={main}
price={price}
textSelectors={availableAttributesTextSelectors}
textSelectorsLabels={availableAttributesTextSelectorsLables}
swatchSelector={availableSwatches}
swatchSelectorGroupedByPrice={swatchSelectorGroupedByPrice}
childProducts={childProducts}
showMissingSelections={showMissingSelections}
setSelectedSwatch={setSelectedSwatch}
setSelectedAttribute={setSelectedAttribute}
mainStackProps={{
position: 'sticky',
height: 'fit-content',
top: '12',
}}
stackProps={{
direction: { base: 'column-reverse', lg: 'row-reverse' },
}}
isProductInStock={isProductInStock}
category={product.categories?.length > 0 ? product.categories[0] : undefined}
slug={product.slug}
/>
)
}
export default ProductDetailsTastic
Editor is loading...
Leave a Comment