ProductList.js
unknown
jsx
3 years ago
26 kB
10
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...