src/components/templates/ItemDetails/NestedModifiers/withNestedModifiers.jsx

import React, {useState, useReducer, useRef} from "react";
import {helpers, config, Copy, images as configImages} from "utils";
import {useMenuContext} from "components/providers/Menu/MenuContext";
import {
  modsReducer,
  mapModifications,
  mergeModifications,
  mapItemTabs,
  nestedModifierState,
} from "../utils";

/**
 * Nested modifier drawer hook. Parse properties of nested modifier drawers and return the same component with additional properties.
 * @alias     withNestedModifiers
 * @memberof! NestedModifiers
 * @type      {function}
 * @param     {ReactElement} Component - The {@link Component} from the parent nested drawer state.
 * @return    {ReactElement} The {@link Return} element (type from the selector).
 */
const withNestedModifiers = (Component) => (props) => {
  const {
    addModifier,
    onClose,
    parent,
    removeModifier,
    showQuantity,
    style,
  } = props;
  const {item, option} = parent;

  const {
    groupsHash,
    optionsHash,
    itemsHash,
    upsellsItemsHash,
    upsellsOptionsHash,
  } = useMenuContext();

  const [modifications, dispatch] = useReducer(modsReducer, parent?.modifiers);

  const optionRefs = useRef({});
  const [errors, setErrors] = useState([]);
  const [globalError, setGlobalError] = useState(null);
  const [nestedDrawer, setNestedDrawer] = useState({});

  const [quantity, setQuantity] = useState(parent?.quantity || 1);
  const increment = () => setQuantity(quantity + 1);
  const decrement = () => setQuantity(quantity > 1 ? quantity - 1 : quantity);

  const getOptionHash = () => optionsHash; // (isUpsell ? upsellsOptionsHash : optionsHash);

  const {items = []} = optionsHash[option] ?? {};
  const {name, options} = items?.filter((i) => i?.id === item)?.[0] ?? {};
  const payload = {
    ...parent,
    isNested: true,
    quantity,
    modifiers: modifications,
  };

  /**
   * Event Handlers
   * closeDrawer(event)
   * addNestedModifier(payload)
   * removeNestedModifier(payload)
   * onSubmit()
   */
  const closeDrawer = (e) => {
    e.preventDefault();
    setNestedDrawer({});
  };
  const addNestedModifier = (payload) => {
    const {option, item, isNested, price, quantity = 0, modifiers} = payload;

    if (isNested && !quantity) {
      // Adding new nested mod, open parent drawer
      return setNestedDrawer(payload);
    }
    // Saving a nested mod, close parent drawer
    setNestedDrawer({});

    const {max = 1, name: optionName} = getOptionHash()[option];
    // Remove first modification for this option user if selecting more than the max allowed
    if (modifications[option]?.length === max) {
      // If the RG is configured to so that the last mod item is not replace return immediately
      if (config.not_replace_mod) {
        setGlobalError(
          helpers.stringReplacer(Copy.ITEM_DETAILS_STATIC.MAX_ITEM_SELECTED, [
            {replaceTarget: "{option}", replaceValue: optionName},
          ]),
        );
        return;
      }
      dispatch({
        payload: {
          ...payload,
          item: modifications[option][0]?.item,
        },
        type: "REMOVE_MOD",
      });
    }

    setGlobalError(null);
    dispatch({
      payload,
      type: "ADD_MOD",
    });
  };
  const removeNestedModifier = (payload) => {
    setGlobalError(null);
    dispatch({
      payload,
      type: "REMOVE_MOD",
    });
  };

  /**
   * validateMods - cross check the itemHash options with the modifier state to validate quantities (min and max).
   *
   * @param {string} optionId - if the validation is nested, the option id to check
   * @param {string} modId - if the validation is nested, the item id to check
   */
  const validateMods = ({optionId, modId} = {}) => {
    // Start with no errors
    let newErrors = [];

    const optionsHashByType = getOptionHash();
    // Validate modifier state against options from nested item id within itemsHash
    let reductionObj = itemsHash?.[modId]?.options ?? [];

    // Get modifiers to validate from modifier state, recursively search for nesting
    const nestedModsState = nestedModifierState({
      modId,
      modsState: modifications,
      optionId,
    });

    // Reduce the options from itemsHash, tabs or itemInfo
    newErrors = reductionObj.reduce((accu, i) => {
      // Find this option in the optionsHash
      const optionId = i?.id;
      const option = optionId && optionsHashByType?.[optionId];

      if (!option) return accu;

      const {max = 0, min = 0} = option;

      // Look for this option in the modifiers state
      const currOptionModifiers = nestedModsState?.[optionId] ?? [];
      // How many modifiers were selected
      const selectedQty = currOptionModifiers?.length;

      // If there is a minimum (greater than 0) and nothing selected in the modifiers state
      // Or if there is a minimum and the length of our selected modifiers state of this option does not meet the minimum
      if ((min && !selectedQty) || (min && selectedQty < min)) {
        // Minimum was not met for this modifier
        accu.push({
          message: helpers.stringReplacer(
            Copy.ITEM_DETAILS_STATIC.SELECTION_MIN_ERROR,
            [
              {
                replaceTarget: "{option}",
                replaceValue: min,
              },
            ],
          ),
          optionId,
          type: "option",
        });
      }

      // If there is a maximum (greater than 0) and the length of our selected modifiers state is greater than the maximum
      if (max && selectedQty > max) {
        // Too many modifiers were chosen
        accu.push({
          message: helpers.stringReplacer(
            Copy.ITEM_DETAILS_STATIC.SELECTION_MAX_ERROR,
            [
              {
                replaceTarget: "{option}",
                replaceValue: max,
              },
            ],
          ),
          optionId,
          type: "option",
        });
      }

      return accu;
    }, []);

    // Update the error state with any new errors
    setErrors(newErrors);

    // Also return the errors in case it's needed
    return newErrors;
  };
  const onSubmit = () => {
    const errorList = validateMods({
      modId: item,
      optionId: option,
    });
    if (!errorList.length) {
      // Edit an existing nested modifier
      if (parent?.quantity) {
        // For now, we replace the existing modifier
        removeModifier(payload);
      }
      addModifier(payload);
    }
  };

  const additionalProps = {
    addNestedModifier,
    closeDrawer,
    decrement,
    errors,
    increment,
    modifications,
    name,
    nestedDrawer,
    onClose,
    onSubmit,
    options,
    optionsHash,
    quantity,
    removeNestedModifier,
    showQuantity,
    style,
  };

  return <Component {...additionalProps} />;
};

export default withNestedModifiers;