Untitled
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
Leave a Comment