Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import { useCallback, useState, useMemo } from 'react';
- import { useIntl } from 'react-intl';
- import { useMutation, useQuery, gql } from '@apollo/client';
- import { useCartContext } from '@magento/peregrine/lib/context/cart';
- import { useUserContext } from '@magento/peregrine/lib/context/user';
- import { appendOptionsToPayload } from '@magento/peregrine/lib/util/appendOptionsToPayload';
- import { findMatchingVariant } from '@magento/peregrine/lib/util/findMatchingProductVariant';
- import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
- import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
- import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
- import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
- import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
- import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
- import apiClient from '../../api';
- import { CartTriggerFragment } from '@magento/peregrine/lib/talons/Header/cartTriggerFragments.gql';
- import { MiniCartFragment } from '@magento/peregrine/lib/talons/MiniCart/miniCartFragments.gql';
- import { BrowserPersistence } from '@magento/peregrine/lib/util';
- import { GTMEventTrigger } from '../../lib/util/GTMEventTrigger';
- const INITIAL_OPTION_CODES = new Map();
- const INITIAL_OPTION_SELECTIONS = new Map();
- const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
- const deriveOptionCodesFromProduct = product => {
- // If this is a simple product it has no option codes.
- if (!isProductConfigurable(product)) {
- return INITIAL_OPTION_CODES;
- }
- // Initialize optionCodes based on the options of the product.
- const initialOptionCodes = new Map();
- for (const {
- attribute_id,
- attribute_code
- } of product.configurable_options) {
- initialOptionCodes.set(attribute_id, attribute_code);
- }
- return initialOptionCodes;
- };
- // Similar to deriving the initial codes for each option.
- const deriveOptionSelectionsFromProduct = product => {
- if (!isProductConfigurable(product)) {
- return INITIAL_OPTION_SELECTIONS;
- }
- const initialOptionSelections = new Map();
- for (const { attribute_id } of product.configurable_options) {
- initialOptionSelections.set(attribute_id, undefined);
- }
- return initialOptionSelections;
- };
- const getIsMissingOptions = (product, optionSelections) => {
- // Non-configurable products can't be missing options.
- if (!isProductConfigurable(product)) {
- return false;
- }
- // Configurable products are missing options if we have fewer
- // option selections than the product has options.
- const { configurable_options } = product;
- const numProductOptions = configurable_options.length;
- const numProductSelections = Array.from(optionSelections.values()).filter(
- value => !!value
- ).length;
- return numProductSelections < numProductOptions;
- };
- const getIsOutOfStock = (product, optionCodes, optionSelections) => {
- const { stock_status, variants } = product;
- const isConfigurable = isProductConfigurable(product);
- const optionsSelected =
- Array.from(optionSelections.values()).filter(value => !!value).length >
- 0;
- if (isConfigurable && optionsSelected) {
- const item = findMatchingVariant({
- optionCodes,
- optionSelections,
- variants
- });
- const stockStatus = item?.product?.stock_status;
- return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
- }
- return stock_status === OUT_OF_STOCK_CODE;
- };
- const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
- let value = [];
- const { media_gallery_entries, variants } = product;
- const isConfigurable = isProductConfigurable(product);
- // Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
- const optionsSelected =
- Array.from(optionSelections.values()).filter(value => !!value).length >
- 0;
- if (!isConfigurable || !optionsSelected) {
- value = media_gallery_entries;
- } else {
- // If any of the possible variants matches the selection add that
- // variant's image to the media gallery. NOTE: This _can_, and does,
- // include variants such as size. If Magento is configured to display
- // an image for a size attribute, it will render that image.
- const item = findMatchingVariant({
- optionCodes,
- optionSelections,
- variants
- });
- value = item
- ? [...item.product.media_gallery_entries, ...media_gallery_entries]
- : media_gallery_entries;
- }
- return value;
- };
- // We only want to display breadcrumbs for one category on a PDP even if a
- // product has multiple related categories. This function filters and selects
- // one category id for that purpose.
- const getBreadcrumbCategoryId = categories => {
- // Exit if there are no categories for this product.
- if (!categories || !categories.length) {
- return;
- }
- const breadcrumbSet = new Set();
- categories.forEach(({ breadcrumbs }) => {
- // breadcrumbs can be `null`...
- (breadcrumbs || []).forEach(({ category_id }) =>
- breadcrumbSet.add(category_id)
- );
- });
- // Until we can get the single canonical breadcrumb path to a product we
- // will just return the first category id of the potential leaf categories.
- const leafCategory = categories.find(
- category => !breadcrumbSet.has(category.uid)
- );
- // If we couldn't find a leaf category then just use the first category
- // in the list for this product.
- return leafCategory.uid || categories[0].uid;
- };
- const getConfigPrice = (product, optionCodes, optionSelections) => {
- let value;
- const { variants } = product;
- const isConfigurable = isProductConfigurable(product);
- const optionsSelected =
- Array.from(optionSelections.values()).filter(value => !!value).length >
- 0;
- if (!isConfigurable || !optionsSelected) {
- value = product.price_range?.maximum_price;
- } else {
- const item = findMatchingVariant({
- optionCodes,
- optionSelections,
- variants
- });
- value = item
- ? item.product.price_range?.maximum_price
- : product.price_range?.maximum_price;
- }
- return value;
- };
- const attributeLabelCompare = (attribute1, attribute2) => {
- const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
- const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
- if (label1 < label2) return -1;
- else if (label1 > label2) return 1;
- else return 0;
- };
- const getCustomAttributes = (product, optionCodes, optionSelections) => {
- const { custom_attributes, variants } = product;
- const isConfigurable = isProductConfigurable(product);
- const optionsSelected =
- Array.from(optionSelections.values()).filter(value => !!value).length >
- 0;
- if (isConfigurable && optionsSelected) {
- const item = findMatchingVariant({
- optionCodes,
- optionSelections,
- variants
- });
- return item && item.product
- ? [...item.product.custom_attributes].sort(attributeLabelCompare)
- : [];
- }
- return custom_attributes
- ? [...custom_attributes].sort(attributeLabelCompare)
- : [];
- };
- /**
- * @param {GraphQLDocument} props.addConfigurableProductToCartMutation - configurable product mutation
- * @param {GraphQLDocument} props.addSimpleProductToCartMutation - configurable product mutation
- * @param {Object.<string, GraphQLDocument>} props.operations - collection of operation overrides merged into defaults
- * @param {Object} props.product - the product, see RootComponents/Product
- *
- * @returns {{
- * breadcrumbCategoryId: string|undefined,
- * errorMessage: string|undefined,
- * handleAddToCart: func,
- * handleSelectionChange: func,
- * handleSetQuantity: func,
- * isAddToCartDisabled: boolean,
- * isSupportedProductType: boolean,
- * mediaGalleryEntries: array,
- * productDetails: object,
- * quantity: number
- * }}
- */
- export const useProductFullDetail = props => {
- const {
- addConfigurableProductToCartMutation,
- addSimpleProductToCartMutation,
- product
- } = props;
- const GET_CROSS_SELL_PRODUCTS = gql`
- query getProductDetailForProductPage($urlKey: String!) {
- products(filter: { url_key: { eq: $urlKey } }) {
- items {
- uid
- categories {
- uid
- }
- crosssell_products {
- small_image {
- url
- }
- uid
- stock_status
- rating_summary
- __typename
- url_key
- sku
- name
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- discount {
- amount_off
- }
- }
- }
- }
- }
- }
- }
- `;
- const ADD_CUSTOM_PRODUCT = gql`
- mutation addSimpleProductsToCart(
- $cartId: String!
- $sku: String!
- $custom_options: [CustomizableOptionInput]
- ) {
- addSimpleProductsToCart(
- input: {
- cart_id: $cartId
- cart_items: [
- {
- data: { quantity: 1, sku: $sku }
- customizable_options: $custom_options
- }
- ]
- }
- ) {
- cart {
- id
- ...CartTriggerFragment
- ...MiniCartFragment
- items {
- ... on SimpleCartItem {
- product {
- name
- sku
- }
- quantity
- customizable_options {
- label
- type
- values {
- label
- value
- price {
- value
- }
- }
- }
- }
- }
- }
- }
- }
- ${CartTriggerFragment}
- ${MiniCartFragment}
- `;
- const GET_ADD_DATA = gql`
- mutation addSimpleProductsToCart(
- $cartId: String!
- $sku: String!
- $optId: Int!
- $value: String!
- ) {
- addSimpleProductsToCart(
- input: {
- cart_id: $cartId
- cart_items: [
- {
- data: { quantity: 1, sku: $sku }
- customizable_options: [
- { id: $optId, value_string: $value }
- ]
- }
- ]
- }
- ) {
- cart {
- items {
- ... on SimpleCartItem {
- product {
- name
- sku
- }
- quantity
- customizable_options {
- label
- type
- values {
- label
- value
- price {
- value
- }
- }
- }
- }
- }
- }
- }
- }
- `;
- const [, { dispatch }] = useEventingContext();
- const [isAddedItem, setIsAddedItem] = useState(false);
- const [pincode, setPincode] = useState('');
- const [deliveryDate, setDeliveryDate] = useState('');
- const [isInvalidPincode, setInvalidPincode] = useState(false);
- const hasDeprecatedOperationProp = !!(
- addConfigurableProductToCartMutation || addSimpleProductToCartMutation
- );
- const operations = mergeOperations(defaultOperations, props.operations);
- const productType = product.__typename;
- const isSupportedProductType = isSupported(productType);
- const [{ cartId }] = useCartContext();
- const [{ isSignedIn }] = useUserContext();
- const { formatMessage } = useIntl();
- const { data: storeConfigData } = useQuery(
- operations.getWishlistConfigQuery,
- {
- fetchPolicy: 'cache-and-network'
- }
- );
- const { data: crossSellProductsData } = useQuery(GET_CROSS_SELL_PRODUCTS, {
- fetchPolicy: 'cache-and-network',
- variables: {
- urlKey: product.url_key
- }
- });
- const [
- addConfigurableProductToCart,
- {
- error: errorAddingConfigurableProduct,
- loading: isAddConfigurableLoading
- }
- ] = useMutation(
- addConfigurableProductToCartMutation ||
- operations.addConfigurableProductToCartMutation
- );
- const [
- addSimpleProductToCart,
- { error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
- ] = useMutation(
- addSimpleProductToCartMutation ||
- operations.addSimpleProductToCartMutation
- );
- const [
- addSolitaireProductToCart,
- { error: errorAddingSolitaireProduct, loading: isAddSolitaireLoading }
- ] = useMutation(GET_ADD_DATA);
- const [
- addCustomizableProductToCart,
- {
- error: errorAddingCustomizableProduct,
- loading: isAddCustomizableLoading
- }
- ] = useMutation(ADD_CUSTOM_PRODUCT);
- const [
- addProductToCart,
- { error: errorAddingProductToCart, loading: isAddProductLoading }
- ] = useMutation(operations.addProductToCartMutation);
- const breadcrumbCategoryId = useMemo(
- () => getBreadcrumbCategoryId(product.categories),
- [product.categories]
- );
- const derivedOptionSelections = useMemo(
- () => deriveOptionSelectionsFromProduct(product),
- [product]
- );
- const [optionSelections, setOptionSelections] = useState(
- derivedOptionSelections
- );
- const derivedOptionCodes = useMemo(
- () => deriveOptionCodesFromProduct(product),
- [product]
- );
- const [optionCodes] = useState(derivedOptionCodes);
- const isMissingOptions = useMemo(
- () => getIsMissingOptions(product, optionSelections),
- [product, optionSelections]
- );
- const isOutOfStock = useMemo(
- () => getIsOutOfStock(product, optionCodes, optionSelections),
- [product, optionCodes, optionSelections]
- );
- const mediaGalleryEntries = useMemo(
- () => getMediaGalleryEntries(product, optionCodes, optionSelections),
- [product, optionCodes, optionSelections]
- );
- const customAttributes = useMemo(
- () => getCustomAttributes(product, optionCodes, optionSelections),
- [product, optionCodes, optionSelections]
- );
- const productPrice = useMemo(
- () => getConfigPrice(product, optionCodes, optionSelections),
- [product, optionCodes, optionSelections]
- );
- // The map of ids to values (and their uids)
- // For example:
- // { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
- const attributeIdToValuesMap = useMemo(() => {
- const map = new Map();
- // For simple items, this will be an empty map.
- const options = product.configurable_options || [];
- for (const { attribute_id, values } of options) {
- map.set(attribute_id, values);
- }
- return map;
- }, [product.configurable_options]);
- // An array of selected option uids. Useful for passing to mutations.
- // For example:
- // ["abc", "def"]
- const selectedOptionsArray = useMemo(() => {
- const selectedOptions = [];
- optionSelections.forEach((value, key) => {
- const values = attributeIdToValuesMap.get(key);
- const selectedValue = values.find(
- item => item.value_index === value
- );
- if (selectedValue) {
- selectedOptions.push(selectedValue.uid);
- }
- });
- return selectedOptions;
- }, [attributeIdToValuesMap, optionSelections]);
- const handleAddToCart = useCallback(
- async formValues => {
- const { quantity } = formValues;
- /*
- @deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
- */
- if (hasDeprecatedOperationProp) {
- const payload = {
- item: product,
- productType,
- quantity
- };
- if (isProductConfigurable(product)) {
- appendOptionsToPayload(
- payload,
- optionSelections,
- optionCodes
- );
- }
- if (isSupportedProductType) {
- const variables = {
- cartId,
- parentSku: payload.parentSku,
- product: payload.item,
- quantity: payload.quantity,
- sku: payload.item.sku
- };
- // Use the proper mutation for the type.
- if (productType === 'SimpleProduct') {
- try {
- await addSimpleProductToCart({
- variables
- });
- GTMEventTrigger({
- route: window.location.pathname,
- event: 'add_to_cart',
- title: document.title
- });
- } catch {
- return;
- }
- } else if (productType === 'ConfigurableProduct') {
- try {
- await addConfigurableProductToCart({
- variables
- });
- GTMEventTrigger({
- route: window.location.pathname,
- event: 'add_to_cart',
- title: document.title
- });
- } catch {
- return;
- }
- }
- } else {
- console.error(
- 'Unsupported product type. Cannot add to cart.'
- );
- }
- } else {
- const variables = {
- cartId,
- product: {
- sku: product.sku,
- quantity
- },
- entered_options: [
- {
- uid: product.uid,
- value: product.name
- }
- ]
- };
- if (selectedOptionsArray.length) {
- variables.product.selected_options = selectedOptionsArray;
- }
- try {
- await addProductToCart({ variables });
- const selectedOptionsLabels =
- selectedOptionsArray?.map((uid, i) => ({
- attribute: product.configurable_options[i].label,
- value:
- product.configurable_options[i].values.findLast(
- x => x.uid === uid
- )?.label || null
- })) || null;
- GTMEventTrigger({
- route: window.location.pathname,
- event: 'add_to_cart',
- title: document.title
- });
- dispatch({
- type: 'CART_ADD_ITEM',
- payload: {
- cartId,
- sku: product.sku,
- name: product.name,
- priceTotal: productPrice.final_price.value,
- currencyCode: productPrice.final_price.currency,
- discountAmount: productPrice.discount.amount_off,
- selectedOptions: selectedOptionsLabels,
- quantity
- }
- });
- setIsAddedItem(true);
- } catch {
- setIsAddedItem(false);
- return;
- }
- }
- },
- [
- addConfigurableProductToCart,
- addProductToCart,
- addSimpleProductToCart,
- cartId,
- dispatch,
- hasDeprecatedOperationProp,
- isSupportedProductType,
- optionCodes,
- optionSelections,
- product,
- productPrice,
- productType,
- selectedOptionsArray
- ]
- );
- const handleCustomizableProductToCart = useCallback(
- async params => {
- const { value, optId } = params;
- const storage = new BrowserPersistence();
- const custom_options = [
- {
- id: storage.getItem('size_id'),
- value_string: storage.getItem('size'),
- },
- {
- id: storage.getItem('color_id'),
- value_string: storage.getItem('color'),
- }
- ].filter(a => a.id && a.value_string);
- try {
- await addCustomizableProductToCart({
- variables: {
- cartId,
- sku: product.sku,
- custom_options,
- }
- });
- const selectedOptionsLabels = value || null;
- GTMEventTrigger({
- route: window.location.pathname,
- event: 'add_to_cart',
- title: document.title
- });
- dispatch({
- type: 'CART_ADD_ITEM',
- payload: {
- cartId,
- sku: product.sku,
- name: product.name,
- priceTotal: productPrice.final_price.value,
- currencyCode: productPrice.final_price.currency,
- discountAmount: productPrice.discount.amount_off,
- selectedOptions: selectedOptionsLabels,
- quantity: 1
- }
- });
- setIsAddedItem(true);
- } catch {
- setIsAddedItem(false);
- return;
- }
- },
- [
- addConfigurableProductToCart,
- addProductToCart,
- cartId,
- dispatch,
- hasDeprecatedOperationProp,
- isSupportedProductType,
- optionCodes,
- optionSelections,
- product,
- productPrice,
- productType,
- selectedOptionsArray
- ]
- );
- const solitaireAddToCart = useCallback(
- async params => {
- console.log("params", params);
- const { value, optId } = params;
- const storage = new BrowserPersistence();
- const custom_options = [
- {
- id: storage.getItem('size_id'),
- value_string: storage.getItem('size'),
- },
- {
- id: storage.getItem('color_id'),
- value_string: storage.getItem('color'),
- },
- {
- id: optId,
- value_string:value
- }
- ].filter(a => a.id && a.value_string);
- console.log("custom options", custom_options);
- try {
- await addCustomizableProductToCart({
- variables: {
- cartId,
- sku: product.sku,
- custom_options,
- }
- });
- const selectedOptionsLabels = value || null;
- dispatch({
- type: 'CART_ADD_ITEM',
- payload: {
- cartId,
- sku: product.sku,
- name: product.name,
- priceTotal: productPrice.final_price.value,
- currencyCode: productPrice.final_price.currency,
- discountAmount: productPrice.discount.amount_off,
- selectedOptions: selectedOptionsLabels,
- quantity: 1
- }
- });
- setIsAddedItem(true);
- } catch {
- setIsAddedItem(false);
- return;
- }
- },
- [
- addConfigurableProductToCart,
- addProductToCart,
- cartId,
- dispatch,
- hasDeprecatedOperationProp,
- isSupportedProductType,
- optionCodes,
- optionSelections,
- product,
- productPrice,
- productType,
- selectedOptionsArray
- ]
- );
- const handleSelectionChange = useCallback(
- (optionId, selection) => {
- // We must create a new Map here so that React knows that the value
- // of optionSelections has changed.
- const nextOptionSelections = new Map([...optionSelections]);
- nextOptionSelections.set(optionId, selection);
- setOptionSelections(nextOptionSelections);
- },
- [optionSelections]
- );
- const handlePincode = useCallback(
- value => {
- setPincode(value);
- },
- [setPincode]
- );
- const checkDeliveryDate = useCallback(
- async (sku, ringSize = null, goldColor = null) => {
- try {
- if (pincode.length == 0) {
- setInvalidPincode(false);
- setDeliveryDate('Please Enter Pincode');
- return;
- } else if (pincode.length !== 6) {
- setInvalidPincode(true);
- setDeliveryDate('Invalid Pincode');
- return;
- }
- const response = await apiClient.get(
- apiClient.Urls.checkDeliveryDate,
- { sku, pin: pincode, ringSize, goldColor },
- {}
- );
- if (response.success) {
- if (response.data && response.data.success) {
- setInvalidPincode(false);
- setDeliveryDate(response.data && response.data.message);
- } else {
- setInvalidPincode(true);
- setDeliveryDate(response.data && response.data.message);
- }
- }
- } catch {
- return;
- }
- },
- [pincode]
- );
- // Normalization object for product details we need for rendering.
- const productDetails = {
- description: product.description,
- shortDescription: product.short_description,
- name: product.name,
- regularPrice: product.price_range,
- price: productPrice?.final_price,
- sku: product.sku
- };
- const derivedErrorMessage = useMemo(
- () =>
- deriveErrorMessage([
- errorAddingSimpleProduct,
- errorAddingConfigurableProduct,
- errorAddingProductToCart
- ]),
- [
- errorAddingConfigurableProduct,
- errorAddingProductToCart,
- errorAddingSimpleProduct
- ]
- );
- const wishlistItemOptions = useMemo(() => {
- const options = {
- quantity: 1,
- sku: product.sku
- };
- if (productType === 'ConfigurableProduct') {
- options.selected_options = selectedOptionsArray;
- }
- return options;
- }, [product, productType, selectedOptionsArray]);
- const wishlistButtonProps = {
- buttonText: isSelected =>
- isSelected
- ? formatMessage({
- id: 'wishlistButton.addedText',
- defaultMessage: 'Added to Favorites'
- })
- : formatMessage({
- id: 'wishlistButton.addText',
- defaultMessage: 'Add to Favorites'
- }),
- item: wishlistItemOptions,
- storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
- };
- return {
- breadcrumbCategoryId,
- errorMessage: derivedErrorMessage,
- handleAddToCart,
- handleCustomizableProductToCart,
- solitaireAddToCart,
- handleSelectionChange,
- isOutOfStock,
- isAddToCartDisabled:
- isOutOfStock ||
- isMissingOptions ||
- isAddConfigurableLoading ||
- isAddSimpleLoading ||
- isAddProductLoading,
- isSupportedProductType,
- mediaGalleryEntries,
- shouldShowWishlistButton:
- isSignedIn &&
- storeConfigData &&
- !!storeConfigData.storeConfig.magento_wishlist_general_is_enabled,
- productDetails,
- customAttributes,
- wishlistButtonProps,
- wishlistItemOptions,
- isAddedItem,
- setIsAddedItem,
- handlePincode,
- pincode,
- checkDeliveryDate,
- deliveryDate,
- isInvalidPincode,
- parentCategoryId:
- crossSellProductsData?.products?.items[0]?.categories[0]?.uid,
- designFamilyValue:
- crossSellProductsData?.products?.items[0]?.design_family,
- crossSellProducts:
- crossSellProductsData?.products?.items[0]?.crosssell_products
- };
- };
Add Comment
Please, Sign In to add comment