@shopgate/engage
Version:
Shopgate's ENGAGE library.
385 lines (382 loc) • 13.9 kB
JavaScript
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);