Untitled

 avatar
unknown
plain_text
a year ago
19 kB
2
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
Leave a Comment