ProductList.js

 avatar
unknown
jsx
2 years ago
26 kB
7
Indexable
import React from "react";
import {
    View,
    SectionList,
    TouchableOpacity,
    Text,
    Dimensions,
    Image,
    Animated,
} from "react-native";
import PropTypes from "prop-types";
import styled from "styled-components/native";
import { useSelector } from "react-redux";
import { colors, regularFont, mediumFont } from "@utils/constants";
import { MIX_MATCH_LIMIT } from "@config/constants";
import {
    trackMenuClick,
    trackAddToCartClick,
    setCategoryUnavailable,
} from "@services/Restaurant/actions";
import { removeItems } from "@root/services/Cart/actions";
import { useDispatch } from "react-redux";
import ProductItem from "./ProductItem";
import ConfirmModal from "@components/ConfirmModal";
import { translate } from "@localization/LocalProvider";
import { imageURLToLoad } from "@utils/urlUtils";

const WindowWidth = Dimensions.get("window").width;
const windowHeight = Dimensions.get("window").height;

const ProductListWrapper = styled.View({
    position: "relative",
    flex: 1,
    backgroundColor: colors.white,
});

const propTypes = {
    currency: PropTypes.string,
    restaurant: PropTypes.object,
    showMixNMatch: PropTypes.bool,
    ListHeaderComponent: PropTypes.node,
    renderSectionHeader: PropTypes.func,
    categories: PropTypes.array,
    cartInformation: PropTypes.object,
    cartProducts: PropTypes.array,
    cartPromotions: PropTypes.array,
    cartStoreGroups: PropTypes.array,
    hasScroll: PropTypes.bool,
    onHasScrollChange: PropTypes.func,
    cartActionPending: PropTypes.func,
    cartErrorMsg: PropTypes.string,
    summaryLayout: PropTypes.object,
    onPress: PropTypes.func,
    onPressSearch: PropTypes.func,
    selectedCategory: PropTypes.number,
};

const defaultProps = {
    currency: "MYR",
    restaurant: {},
    categories: [],
    cartInformation: {},
    cartProducts: [],
    cartPromotions: [],
    cartStoreGroups: [],
};

const ProductList = (props) => {
    const {
        restaurant,
        currency,
        locale,
        showMixNMatch,
        scrollToLocationOffset = 10,
        ListHeaderComponent,
        renderSectionHeader,
        cartInformation,
        cartProducts,
        cartPromotions,
        hasScroll,
        onHasScrollChange,
        cartErrorMsg,
        summaryLayout,
        onPress,
        onPressSearch,
        selectedCategory,
        foodId,
        loading,
        headerHeights,
    } = props;

    const [_, setCategories] = React.useState(props.categories);
    const [sections, setSections] = React.useState([
        { title: "Tab", data: [], index: 0 },
    ]);
    const dispatch = useDispatch();

    const [currentIndex, setCurrentIndex] = React.useState(0);
    const clearCartConfirmModalRef = React.useRef();

    const [sectionHeights, setSectionHeights] = React.useState([]);

    const _tabsMeasurements = React.useRef({});
    const _sectionListRef = React.useRef();
    const _blockUpdateIndex = React.useRef(true);

    const heightOfSearch = React.useRef(new Animated.Value(0)).current;

    const calculateCartProductQuantity = (category, productItem) => {
        try {
            const productsInCart = (cartProducts || []).filter(
                (item) =>
                    (item?.type === "product" &&
                        item?.product?.[0]?.uuid === productItem?.uuid &&
                        item?.product?.[0]?.stores?.[0]?.uuid ===
                            restaurant?.uuid &&
                        item?.store_category_uuid === category?.uuid) ||
                    (item?.type === "combo" &&
                        item?.parent?.uuid === productItem?.uuid &&
                        item?.parent?.stores?.[0]?.uuid === restaurant?.uuid &&
                        item?.store_category_uuid === category?.uuid)
            );

            return productsInCart.reduce(
                (accu, item) => accu + item?.quantity,
                0
            );
        } catch (e) {
            console.error(e);
        }
    };

    React.useEffect(() => {
        if (
            cartProducts &&
            cartProducts.length > 0 &&
            props.categories &&
            props.categories.length > 0
        ) {
            let cartKeys = new Array();
            let productNames = new Array();
            cartProducts.forEach((cartProduct) => {
                props.categories.forEach((category) => {
                    category.products.forEach((item) => {
                        if (item.is_set_quantity === 1) {
                            // check if quantity is 0
                            if (item.quantity === 0) {
                                // filter combo type
                                // search if item is added to cart
                                const foundProduct =
                                    cartProduct.type === "combo"
                                        ? cartProduct.parent.uuid === item.uuid
                                            ? cartProduct.parent
                                            : undefined
                                        : cartProduct.product.find(
                                              (foodProduct) => {
                                                  return (
                                                      foodProduct.uuid ===
                                                      item.uuid
                                                  );
                                              }
                                          );

                                // if item is addded to cart
                                if (foundProduct) {
                                    // get the cart product key
                                    cartKeys.push(cartProduct.key);
                                    // get the product name
                                    productNames.push(
                                        cartProduct.quantity +
                                            "x " +
                                            foundProduct.name
                                    );
                                }
                            }
                        }
                    });
                });
            });

            // display error modal
            if (productNames.length > 0) {
                // remove the item from cart
                dispatch(
                    removeItems({
                        keys: cartKeys,
                        isFromRestaurant: true,
                    })
                );

                dispatch(
                    setCategoryUnavailable({
                        productNames,
                        outOfStock: true,
                    })
                );
            }
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    React.useEffect(() => {
        if (restaurant && props.categories && cartProducts) {
            let catSections = [];
            if (props.categories && props.categories.length > 0) {
                catSections = props.categories.map((cat, index) => {
                    const sectionsData = cat.products[index]
                        ?.store_categories[0]?.is_out_of_stock
                        ? cat.products
                              .filter((item) => {
                                  // if auto renew is disabled
                                  if (item.is_set_quantity === 1) {
                                      // shows item with quantity more than 0
                                      return item.quantity > 0;
                                  } else {
                                      return item;
                                  }
                              })
                              .map((prd) => {
                                  let defaultOptionGroup = null;
                                  if (prd.option_groups) {
                                      defaultOptionGroup = prd.option_groups.find(
                                          (item) => item.quantity > 0
                                      );
                                  }
                                  return {
                                      uuid: prd.uuid,
                                      imgUrl: imageURLToLoad(prd.image) || "",
                                      title: prd.name,
                                      description: prd.description,
                                      currency: currency,
                                      price:
                                          (prd?.price || 0) +
                                          (defaultOptionGroup?.price || 0),
                                      promotionPrice: prd.discount_price,
                                      promotionLabel:
                                          prd.additional_description || "",
                                      quantity: 0,
                                      cartQuantity: calculateCartProductQuantity(
                                          cat,
                                          prd
                                      ),
                                      stockQuantity: prd.quantity,
                                      category: {
                                          uuid: cat.uuid,
                                          name: cat.name,
                                      },
                                      store_category_uuids:
                                          prd.store_category_uuids,
                                      parent: prd.parent,
                                      ...prd,
                                  };
                              })
                        : cat.products.map((prd) => {
                              let defaultOptionGroup = null;
                              if (prd.option_groups) {
                                  defaultOptionGroup = prd.option_groups.find(
                                      (item) => item.quantity > 0
                                  );
                              }

                              return {
                                  uuid: prd.uuid,
                                  imgUrl: imageURLToLoad(prd.image) || "",
                                  title: prd.name,
                                  description: prd.description,
                                  currency: currency,
                                  price:
                                      (prd?.price || 0) +
                                      (defaultOptionGroup?.price || 0),
                                  promotionPrice: prd.discount_price,
                                  promotionLabel:
                                      prd.additional_description || "",
                                  quantity: 0,
                                  cartQuantity: calculateCartProductQuantity(
                                      cat,
                                      prd
                                  ),
                                  stockQuantity: prd.quantity,
                                  category: {
                                      uuid: cat.uuid,
                                      name: cat.name,
                                  },
                                  store_category_uuids:
                                      prd.store_category_uuids,
                                  parent: prd.parent,
                                  ...prd,
                              };
                          });
                    return {
                        uuid: cat.uuid,
                        title: cat.name,
                        data: sectionsData,
                        // need this to render the section open string
                        is_use_special_open_hours:
                            cat.is_use_special_open_hours,
                        opening_times: cat.opening_times,
                    };
                });
            }

            catSections = catSections
                .filter(
                    (item) => Array.isArray(item.data) && item.data.length > 0
                )
                .map((item, index) => ({
                    ...item,
                    index: index + 1,
                }))
                .map((item, index) => {
                    item.showMixNMatch = false;

                    // slice array to 5 elements if mix and match is enabled
                    if (
                        restaurant.is_mix_match_experience === 1 &&
                        item.data.length > MIX_MATCH_LIMIT &&
                        !showMixNMatch
                    ) {
                        item.data = item.data.slice(0, MIX_MATCH_LIMIT);
                        item.showMixNMatch = true;
                    }

                    return item;
                });
            catSections.unshift({
                title: translate("restaurant.overview"),
                data: [],
                index: 0,
            });

            setSections(catSections);
            setCategories(props.categories);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [restaurant, props.categories, cartProducts]);

    React.useEffect(() => {
        if (selectedCategory < sections.length) {
            handleTabPress(selectedCategory);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [selectedCategory]);

    React.useEffect(() => {
        if (cartErrorMsg) {
            handleTabPress(0);
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [cartErrorMsg]);

    React.useEffect(() => {
        if (!loading && Object.keys(summaryLayout).length !== 0) {
            if (_sectionListRef?.current && sections.length > 1 && foodId) {
                let sectionIndex = -1;
                let itemIndex = -1;
                for (let i = 0; i < sections.length; i++) {
                    const index = sections[i].data.findIndex(
                        (section) => section.uuid === foodId
                    );
                    if (index > -1) {
                        sectionIndex = i;
                        itemIndex = index;
                    }
                }

                if (sectionIndex >= 0 && itemIndex >= 0) {
                    try {
                        _sectionListRef?.current?.scrollToLocation({
                            animated: false,
                            itemIndex: itemIndex + 1,
                            viewOffset: -56,
                            sectionIndex,
                            viewPosition: 0,
                        });
                    } catch (e) {
                        console.log(e);
                    }
                }
            }
        }
    }, [sections, foodId, loading, summaryLayout]);

    const _handleScroll = (event) => {
        if (hasScroll !== event.nativeEvent.contentOffset.y > 2) {
            onHasScrollChange(event.nativeEvent.contentOffset.y > 2);
        }
    };

    const _handleTrackMenuPress = (index) => {
        const payload = {
            category: `Food|${restaurant?.name ?? ""}|menu`, // 'Food|Kubis & Kale|menu'
            label: `${sections?.[index]?.title || ""}`, // 'Coffee'
            lineofbusiness: "Food",
            screenName: "Restaurant Page",
        };
        dispatch(trackMenuClick(payload));
    };

    const handleItemPress = (product, quantity, productCategory) => {
        if (onPress) {
            onPress(product, quantity, productCategory);
        }
    };

    const handleTabPress = (index) => {
        _blockUpdateIndex.current = true;
        setCurrentIndex(index);

        const sectionList = _sectionListRef.current;
        if (sectionList && sectionList.scrollToLocation) {
            sectionList.scrollToLocation({
                animated: false,
                itemIndex: 0,
                viewOffset: 0,
                sectionIndex: index,
            });
        }

        _handleTrackMenuPress(index);
    };

    const renderItem = ({ item, section, index }) => (
        <ProductItem
            product={item}
            productCategory={section}
            onPress={(product, quantity) =>
                handleItemPress(product, quantity, section)
            }
            locale={locale}
            currency={currency}
            index={index}
            setSectionHeights={setSectionHeights}
            sectionHeights={sectionHeights}
        />
    );

    const RenderTopSearchHeader = () => {
        return currentIndex > 0 ? (
            <View
                style={{
                    height: 56,
                    width: WindowWidth,
                    position: "absolute",
                    backgroundColor: "white",
                    flexDirection: "column",
                }}
            >
                <TouchableOpacity
                    style={{
                        width: WindowWidth - 32,
                        paddingVertical: 5,
                        paddingHorizontal: 15,
                        borderRadius: 17,
                        borderWidth: 1,
                        borderColor: colors.greyLight,
                        marginTop: 8,
                        flexDirection: "row",
                        height: 32,
                        marginHorizontal: 15,
                        justifyContent: "space-between",
                    }}
                    onPress={() => onPressSearch(sections, currentIndex)}
                >
                    <Text
                        style={{
                            fontSize: 14,
                            fontWeight: "400",
                            lineHeight: 21,
                            fontFamily: "DMSans-Regular",
                            color: colors.greyDarker,
                            alignSelf: "center",
                            justifyContent: "flex-start",
                        }}
                    >
                        {sections?.[currentIndex]?.title || ""}
                    </Text>
                    <View
                        style={{
                            justifyContent: "flex-end",
                            width: 10,
                            height: 5,
                            alignSelf: "center",
                            marginRight: 5,
                        }}
                    >
                        <Image
                            source={require("@root/assets/images/arrow_drop_down.png")}
                            style={{
                                height: "100%",
                                width: "100%",
                            }}
                        />
                    </View>
                </TouchableOpacity>

                <View
                    style={{
                        height: 1,
                        top: 16,
                        backgroundColor: colors.greyLight,
                        justifyContent: "flex-end",
                        shadowColor: "#000",
                        shadowOffset: {
                            width: 0,
                            height: 1,
                        },
                        shadowOpacity: 0.2,
                        shadowRadius: 0.8,
                        elevation: 8,
                    }}
                />
            </View>
        ) : null;
    };

    return (
        <ProductListWrapper>
            <SectionList
                bounces={false}
                sections={sections}
                initialNumToRender={6}
                removeClippedSubviews={true}
                onViewableItemsChanged={({ viewableItems }) => {
                    if (viewableItems[0]) {
                        const newCurrentIndex = viewableItems[0].section.index;
                        if (
                            currentIndex !== newCurrentIndex &&
                            _blockUpdateIndex.current === false
                        ) {
                            setCurrentIndex(newCurrentIndex);
                        }
                    }
                }}
                viewabilityConfig={{
                    minimumViewTime: 10,
                    itemVisiblePercentThreshold: 10,
                }}
                ref={_sectionListRef}
                onScroll={_handleScroll}
                onMomentumScrollBegin={() => {
                    _blockUpdateIndex.current = false;
                }}
                onMomentumScrollEnd={() => {
                    _blockUpdateIndex.current = false;
                }}
                renderSectionHeader={renderSectionHeader}
                renderItem={renderItem}
                ListHeaderComponent={ListHeaderComponent}
                ListEmptyComponent={
                    <View>
                        <Text>No Sections</Text>
                    </View>
                }
                keyExtractor={(item, index) => `${item.uuid}_${index}`}
                stickySectionHeadersEnabled={false}
                contentContainerStyle={{
                    backgroundColor: colors.white,
                }}
                getItemLayout={calculateItemLayout({
                    // getItemHeight: (rowData, sectionIndex, rowIndex) => sectionIndex === 0 ? 300 : 150,
                    getItemHeight: (rowData, sectionIndex, rowIndex) =>
                        sectionHeights.length > 0
                            ? sectionHeights[sectionIndex]?.itemHeights[
                                  rowIndex
                              ] || 120
                            : 120,
                    getSeparatorHeight: () => 0,
                    // getSectionHeaderHeight: () => 40,
                    getSectionHeaderHeight: (
                        rowData,
                        sectionIndex,
                        rowIndex
                    ) => {
                        if (sectionIndex === 0) {
                            return summaryLayout.height + 100 || 300;
                        }
                        if (headerHeights.length > 0) {
                            return (
                                headerHeights[sectionIndex]?.sectionHeight || 55
                            );
                        }
                        return 55;
                    },
                    getSectionFooterHeight: () => 0,
                    listHeaderHeight: 0,
                })}
                onScrollToIndexFailed={(info) => {
                    const wait = new Promise((resolve) =>
                        setTimeout(resolve, 500)
                    );
                    wait.then(() => {
                        handleTabPress(currentIndex);
                    });
                }}
            />
            {!showMixNMatch && <RenderTopSearchHeader />}
            <ConfirmModal
                ref={clearCartConfirmModalRef}
                titleText={translate("cart.popupClearCartTitle")}
                captionText={translate("cart.popupClearCartContent")}
                confirmText={translate("restaurant.confirmLabel")}
                cancelText={translate("restaurant.cancelLabel")}
            />
        </ProductListWrapper>
    );
};

const calculateItemLayout = ({
    getItemHeight,
    getSeparatorHeight = () => 0,
    getSectionHeaderHeight = () => 0,
    getSectionFooterHeight = () => 0,
    listHeaderHeight = 0,
}) => (data, index) => {
    let i = 0;
    let sectionIndex = 0;
    let elementPointer = { type: "SECTION_HEADER" };
    let offset =
        typeof listHeaderHeight === "function"
            ? listHeaderHeight()
            : listHeaderHeight;

    while (i < index) {
        switch (elementPointer.type) {
            case "SECTION_HEADER": {
                const sectionData = data[sectionIndex].data;
                const rowIndex = elementPointer.index;

                // offset += getSectionHeaderHeight(sectionIndex)
                offset += getSectionHeaderHeight(
                    sectionData[rowIndex],
                    sectionIndex,
                    rowIndex
                );

                // If this section is empty, we go right to the footer...
                if (sectionData.length === 0) {
                    elementPointer = { type: "SECTION_FOOTER" };
                    // ...otherwise we make elementPointer point at the first row in this section
                } else {
                    elementPointer = { type: "ROW", index: 0 };
                }

                break;
            }
            case "ROW": {
                const sectionData = data[sectionIndex].data;

                const rowIndex = elementPointer.index;

                offset += getItemHeight(
                    sectionData[rowIndex],
                    sectionIndex,
                    rowIndex
                );
                elementPointer.index += 1;

                if (rowIndex === sectionData.length - 1) {
                    elementPointer = { type: "SECTION_FOOTER" };
                } else {
                    offset += getSeparatorHeight(sectionIndex, rowIndex);
                }

                break;
            }
            case "SECTION_FOOTER": {
                offset += getSectionFooterHeight(sectionIndex);
                sectionIndex += 1;
                elementPointer = { type: "SECTION_HEADER" };
                break;
            }
        }

        i += 1;
    }

    let length;
    switch (elementPointer.type) {
        case "SECTION_HEADER":
            // length = getSectionHeaderHeight(sectionIndex)
            length = getSectionHeaderHeight(
                data[sectionIndex].data[rowIndex],
                sectionIndex,
                rowIndex
            );
            break;
        case "ROW":
            const rowIndex = elementPointer.index;
            length = getItemHeight(
                data[sectionIndex].data[rowIndex],
                sectionIndex,
                rowIndex
            );
            break;
        case "SECTION_FOOTER":
            length = getSectionFooterHeight(sectionIndex);
            break;
        default:
            throw new Error("Unknown elementPointer.type");
    }
    offset -= 56;

    return { length, offset, index };
};

ProductList.propTypes = propTypes;
ProductList.defaultProps = defaultProps;

export default ProductList;
Editor is loading...