UNPKG

@shopgate/engage

Version:
385 lines (382 loc) • 13.9 kB
import React, { useCallback, useMemo, useLayoutEffect, useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { css } from 'glamor'; import { MODAL_VARIANT_SELECT } from '@shopgate/pwa-ui-shared/Dialog/constants'; import { ProductImage, ITEM_PATH, PriceInfo, isBaseProduct as isBaseProductSelector, isProductOrderable, hasProductVariants, ProductListEntryProvider } from '@shopgate/engage/product'; import { bin2hex, showModal as showModalAction, historyPush as historyPushAction, getThemeSettings, i18n } from '@shopgate/engage/core'; import { hasNewServices } from '@shopgate/engage/core/helpers'; import { Link, TextLink, SurroundPortals } from '@shopgate/engage/components'; import { makeIsRopeProductOrderable, getPreferredLocation, StockInfoLists } from '@shopgate/engage/locations'; import { FAVORITES_PRODUCT_NAME, FAVORITES_PRODUCT_PRICE, FAVORITES_ADD_TO_CART, FAVORITES_AVAILABILITY_TEXT } from '@shopgate/engage/favorites'; import { broadcastLiveMessage } from '@shopgate/engage/a11y'; import { responsiveMediaQuery } from '@shopgate/engage/styles'; import Price from '@shopgate/pwa-ui-shared/Price'; import PriceStriked from '@shopgate/pwa-ui-shared/PriceStriked'; import AddToCart from '@shopgate/pwa-ui-shared/AddToCartButton'; import { themeConfig } from '@shopgate/pwa-common/helpers/config'; import { updateFavorite } from '@shopgate/pwa-common-commerce/favorites/actions/toggleFavorites'; import { openFavoritesCommentDialog } from '@shopgate/pwa-common-commerce/favorites/action-creators'; import AvailableText from '@shopgate/pwa-ui-shared/Availability'; import classNames from 'classnames'; import Remove from "../RemoveButton"; import ItemCharacteristics from "./ItemCharacteristics"; import ItemQuantity from "./ItemQuantity"; import ItemNotes from "./ItemNotes"; import { FAVORITES_LIST_ITEM, FAVORITES_NOTES, FAVORITES_QUANTITY } from "../../constants/Portals"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const { variables } = themeConfig; /** * @return {Function} The extended component props. */ const makeMapStateToProps = () => { const isRopeProductOrderable = makeIsRopeProductOrderable((state, props) => getPreferredLocation(state, props)?.code, (state, props) => props.variantId || props.productId || null); return (state, props) => ({ isBaseProduct: isBaseProductSelector(state, props), hasVariants: hasProductVariants(state, props), isOrderable: isProductOrderable(state, props), isRopeProductOrderable: isRopeProductOrderable(state, props) }); }; /** * @param {Function} dispatch Dispatch. * @returns {Object} */ const mapDispatchToProps = dispatch => ({ showModal: (...args) => dispatch(showModalAction.apply(void 0, args)), historyPush: (...args) => dispatch(historyPushAction.apply(void 0, args)), updateFavoriteItem: (productId, listId, quantity, notes) => { dispatch(updateFavorite(productId, listId, quantity, notes)); }, openCommentDialog: (productId, listId) => dispatch(openFavoritesCommentDialog(productId, listId)) }); const styles = { root: css({ display: 'flex', position: 'relative', '&:not(:last-child)': { marginBottom: 16 } }).toString(), imageContainer: css({ flex: 0.4, marginRight: 18, [responsiveMediaQuery('>=xs', { appAlways: true })]: { maxWidth: 120, minWidth: 80 }, [responsiveMediaQuery('>=md', { webOnly: true })]: { maxWidth: 120, minWidth: 80 }, [responsiveMediaQuery('>=md', { webOnly: true })]: { width: 120, flex: 'none' } }).toString(), infoContainer: css({ flex: 1, display: 'flex', flexDirection: 'column', flexWrap: 'wrap', gap: 8 }).toString(), infoContainerRow: css({ flexDirection: 'row', display: 'flex', justifyContent: 'space-between' }).toString(), quantityContainer: css({ flexDirection: 'row', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 16 }).toString(), priceContainer: css({ minWidth: 100 }).toString(), priceContainerInner: css({ display: 'inline-block', textAlign: 'right' }), price: css({ justifyContent: 'flex-end' }).toString(), priceInfo: css({ wordBreak: 'break-word', fontSize: '0.875rem', lineHeight: '0.875rem', color: 'var(--color-text-low-emphasis)', padding: `${variables.gap.xsmall}px 0` }).toString(), titleWrapper: css({ display: 'flex', flexDirection: 'column', gap: 8 }).toString(), titleContainer: css({ marginRight: 10, flex: 1 }).toString(), title: css({ fontSize: 17, fontWeight: 600 }).toString(), removeContainer: css({ display: 'flex', flexShrink: 0, alignItems: 'flex-start' }) }; /** * Favorite Item component * @return {JSX.Element} */ const FavoriteItem = ({ listId, product, notes, quantity, remove, addToCart, isBaseProduct, isOrderable, isRopeProductOrderable, hasVariants, showModal, historyPush, updateFavoriteItem, openCommentDialog }) => { const { ListImage: gridResolutions } = getThemeSettings('AppImages') || {}; const [isDisabled, setIsDisabled] = useState(!isOrderable && !hasVariants); const currency = product.price?.currency || 'EUR'; const defaultPrice = product.price?.unitPrice || 0; const specialPrice = product.price?.unitPriceStriked; const hasStrikePrice = typeof specialPrice === 'number' && specialPrice > defaultPrice; const characteristics = product?.characteristics || []; const productLink = `${ITEM_PATH}/${bin2hex(product.id)}`; const notesButtonRef = useRef(); const [internalQuantity, setInternalQuantity] = useState(quantity); useEffect(() => { setInternalQuantity(quantity); }, [quantity]); useLayoutEffect(() => { setIsDisabled(!isOrderable && !hasVariants); }, [hasVariants, isOrderable]); const handleOpenComment = useCallback(e => { e.preventDefault(); e.stopPropagation(); openCommentDialog(product.id, listId); }, [listId, openCommentDialog, product.id]); const handleAddToCart = useCallback(e => { e.preventDefault(); e.stopPropagation(); if (isBaseProduct && hasVariants) { // Called for a parent product. User needs to confirm the navigation to the PDP showModal({ title: null, type: MODAL_VARIANT_SELECT, message: 'favorites.modal.message', confirm: 'favorites.modal.confirm', dismiss: 'common.cancel', params: { productId: product.id } }); return false; } if (hasNewServices() && !isRopeProductOrderable) { // Product is not orderable for ROPE. So users need to do some corrections. Just redirect. historyPush({ pathname: productLink }); return false; } broadcastLiveMessage('product.adding_item', { params: { count: 1 } }); return addToCart(e); }, [addToCart, hasVariants, historyPush, isBaseProduct, isRopeProductOrderable, product.id, productLink, showModal]); const commonPortalProps = useMemo(() => { const { availability, id, name } = product; return { availability, characteristics, id, name, price: defaultPrice, listId }; }, [characteristics, defaultPrice, listId, product]); const ctaPortalProps = useMemo(() => ({ isLoading: false, noShadow: false, listId, isBaseProduct, isDisabled, productId: product.id, handleRemoveFromCart: remove, handleAddToCart }), [handleAddToCart, isBaseProduct, isDisabled, listId, product.id, remove]); const handleChangeQuantity = useCallback(newQuantity => { // Do nothing when quantity didn't change if (newQuantity === quantity) return; updateFavoriteItem(product.id, listId, newQuantity, notes); }, [listId, notes, product.id, quantity, updateFavoriteItem]); const handleDeleteComment = useCallback(event => { event.preventDefault(); event.stopPropagation(); updateFavoriteItem(product.id, listId, quantity, ''); setTimeout(() => { if (notesButtonRef?.current) { // Focus the add button after item deletion to improve a11y notesButtonRef.current.focus(); } broadcastLiveMessage('favorites.comments.removed'); }, 300); }, [listId, product.id, quantity, updateFavoriteItem]); return /*#__PURE__*/_jsx(ProductListEntryProvider, { productId: product.id, children: /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_LIST_ITEM, portalProps: product, children: /*#__PURE__*/_jsxs("div", { className: classNames(styles.root, 'engage__favorites__item'), children: [/*#__PURE__*/_jsx(Link, { className: classNames(styles.imageContainer, 'engage__favorites__item__image-container'), component: "div", href: productLink, "aria-hidden": true, children: /*#__PURE__*/_jsx(ProductImage, { className: classNames('engage__favorites__item__image'), src: product.featuredImageBaseUrl, resolutions: gridResolutions }) }), /*#__PURE__*/_jsxs("div", { className: classNames(styles.infoContainer, 'engage__favorites__item__info-container'), children: [/*#__PURE__*/_jsxs("div", { className: classNames(styles.infoContainerRow), children: [/*#__PURE__*/_jsx("div", { className: styles.titleWrapper, children: /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_PRODUCT_NAME, portalProps: commonPortalProps, children: /*#__PURE__*/_jsx(TextLink, { href: productLink, tag: "span", className: classNames(styles.titleContainer, 'engage__favorites__item__title-container'), children: /*#__PURE__*/_jsx("span", { className: styles.title // eslint-disable-next-line react/no-danger , dangerouslySetInnerHTML: { __html: `${product.name}` } }) }) }) }), /*#__PURE__*/_jsx("div", { className: styles.removeContainer, children: /*#__PURE__*/_jsx(Remove, { onClick: remove }) })] }), /*#__PURE__*/_jsx(ItemCharacteristics, { characteristics: characteristics }), !hasNewServices() ? /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_AVAILABILITY_TEXT, portalProps: commonPortalProps, children: /*#__PURE__*/_jsx(AvailableText, { text: commonPortalProps.availability.text, state: commonPortalProps.availability.state, showWhenAvailable: true, className: styles.availability }) }) : /*#__PURE__*/_jsx(StockInfoLists, { product: product }), /*#__PURE__*/_jsxs("div", { className: styles.infoContainerRow, children: [/*#__PURE__*/_jsxs("div", { className: styles.quantityContainer, children: [/*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_QUANTITY, portalProps: commonPortalProps, children: /*#__PURE__*/_jsx(ItemQuantity, { quantity: internalQuantity, onChange: handleChangeQuantity }) }), /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_PRODUCT_PRICE, portalProps: commonPortalProps, children: /*#__PURE__*/_jsxs("div", { className: styles.priceContainer, children: [/*#__PURE__*/_jsxs("div", { className: styles.priceContainerInner, children: [hasStrikePrice ? /*#__PURE__*/_jsx(PriceStriked, { value: specialPrice, currency: currency }) : null, /*#__PURE__*/_jsx(Price, { currency: currency, discounted: hasStrikePrice, unitPrice: defaultPrice, className: styles.price })] }), /*#__PURE__*/_jsx(PriceInfo, { product: product, currency: currency, className: styles.priceInfo })] }) })] }), /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_ADD_TO_CART, portalProps: ctaPortalProps, children: /*#__PURE__*/_jsx(AddToCart, { onClick: handleAddToCart, isLoading: false, isDisabled: isDisabled, "aria-label": i18n.text('product.add_to_cart') }) })] }), /*#__PURE__*/_jsx(SurroundPortals, { portalName: FAVORITES_NOTES, portalProps: commonPortalProps, children: /*#__PURE__*/_jsx(ItemNotes, { notes: notes, onClickDeleteComment: handleDeleteComment, onClickOpenComment: handleOpenComment, notesButtonRef: notesButtonRef }) })] })] }) }) }); }; FavoriteItem.defaultProps = { isBaseProduct: true, isOrderable: true, isRopeProductOrderable: true, hasVariants: false, notes: undefined, quantity: 1 }; export default connect(makeMapStateToProps, mapDispatchToProps)(FavoriteItem);