ProductList.js
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...