enh
This commit is contained in:
parent
9a47ff1a9b
commit
fa56fd4a82
3 changed files with 199 additions and 167 deletions
|
|
@ -9,8 +9,7 @@ import {
|
||||||
useMarkDataFetchers,
|
useMarkDataFetchers,
|
||||||
LoadingDialog,
|
LoadingDialog,
|
||||||
MyTouchableOpacity,
|
MyTouchableOpacity,
|
||||||
MyText, SearchBar, useStatusBarStore,
|
MyText, SearchBar
|
||||||
colors
|
|
||||||
} from "common-ui";
|
} from "common-ui";
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
@ -35,8 +34,8 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get("window");
|
const { width: screenWidth } = Dimensions.get("window");
|
||||||
const itemWidth = screenWidth * 0.45;
|
const itemWidth = screenWidth * 0.45;
|
||||||
|
const heroItemWidth = (screenWidth - 72) / 3;
|
||||||
const gridItemWidth = (screenWidth - 48) / 2;
|
const gridItemWidth = (screenWidth - 48) / 2;
|
||||||
const headerColor = colors.secondaryPink;
|
|
||||||
|
|
||||||
const formatTimeRange = (deliveryTime: string) => {
|
const formatTimeRange = (deliveryTime: string) => {
|
||||||
const time = dayjs(deliveryTime);
|
const time = dayjs(deliveryTime);
|
||||||
|
|
@ -58,16 +57,15 @@ const staticStyles = {
|
||||||
slotsListContent: { paddingBottom: 24 },
|
slotsListContent: { paddingBottom: 24 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Light/pastel color pairs for the Explore Products tabs.
|
|
||||||
const TAG_COLORS = [
|
const TAG_COLORS = [
|
||||||
{ bg: '#FFE4E6', border: '#FECDD3', text: '#BE123C' }, // rose
|
{ pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose
|
||||||
{ bg: '#FEF3C7', border: '#FDE68A', text: '#B45309' }, // amber
|
{ pageBg: '#FFFCF1', bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', dot: '#D97706' }, // amber
|
||||||
{ bg: '#DCFCE7', border: '#BBF7D0', text: '#15803D' }, // green
|
{ pageBg: '#F6FEF9', bg: '#F0FDF4', border: '#BBF7D0', text: '#166534', dot: '#16A34A' }, // green
|
||||||
{ bg: '#DBEAFE', border: '#BFDBFE', text: '#1D4ED8' }, // blue
|
{ pageBg: '#F5FAFF', bg: '#EFF6FF', border: '#BFDBFE', text: '#1E3A8A', dot: '#2563EB' }, // blue
|
||||||
{ bg: '#EDE9FE', border: '#DDD6FE', text: '#6D28D9' }, // violet
|
{ pageBg: '#FAF8FF', bg: '#F5F3FF', border: '#DDD6FE', text: '#5B21B6', dot: '#7C3AED' }, // violet
|
||||||
{ bg: '#FFE4CC', border: '#FFD6B0', text: '#C2410C' }, // orange
|
{ pageBg: '#FFF9F2', bg: '#FFF7ED', border: '#FFD6B0', text: '#9A3412', dot: '#EA580C' }, // orange
|
||||||
{ bg: '#CFFAFE', border: '#A5F3FC', text: '#0E7490' }, // cyan
|
{ pageBg: '#F3FEFF', bg: '#ECFEFF', border: '#A5F3FC', text: '#155E75', dot: '#0891B2' }, // cyan
|
||||||
{ bg: '#FCE7F3', border: '#FBCFE8', text: '#BE185D' }, // pink
|
{ pageBg: '#FFF7FB', bg: '#FDF2F8', border: '#FBCFE8', text: '#9D174D', dot: '#DB2777' }, // pink
|
||||||
];
|
];
|
||||||
|
|
||||||
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length];
|
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length];
|
||||||
|
|
@ -217,28 +215,34 @@ interface ExploreTabProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
|
const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
|
||||||
const color = getTagColor(tag.id);
|
|
||||||
const productCount = tag.productIds?.length || 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.75}
|
||||||
style={[
|
style={tw`px-2 pt-2 pb-1`}
|
||||||
tw`px-4 py-2.5 rounded-full border`,
|
|
||||||
isSelected
|
|
||||||
? { backgroundColor: color.bg, borderColor: color.text }
|
|
||||||
: { backgroundColor: '#FFFFFF', borderColor: color.border },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<MyText
|
<View style={tw`items-center`}>
|
||||||
style={[
|
<MyText
|
||||||
tw`text-sm font-semibold`,
|
style={[
|
||||||
{ color: color.text },
|
tw`text-base tracking-tight`,
|
||||||
]}
|
{
|
||||||
>
|
color: isSelected ? '#111827' : '#64748B',
|
||||||
{tag.tagName} ({productCount})
|
fontWeight: isSelected ? '700' : '500',
|
||||||
</MyText>
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{tag.tagName}
|
||||||
|
</MyText>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 999,
|
||||||
|
marginTop: 8,
|
||||||
|
backgroundColor: isSelected ? '#111827' : 'transparent',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -252,14 +256,15 @@ const ExploreProductItem = memo(({ item, onPress }: ExploreProductItemProps) =>
|
||||||
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
|
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`mr-4`}>
|
<View style={tw`mb-3`}>
|
||||||
<ProductCard
|
<ProductCard
|
||||||
item={item}
|
item={item}
|
||||||
itemWidth={itemWidth}
|
itemWidth={heroItemWidth}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
showDeliveryInfo={false}
|
showDeliveryInfo={false}
|
||||||
useAddToCartDialog={true}
|
useAddToCartDialog={true}
|
||||||
miniView={true}
|
miniView={true}
|
||||||
|
variant="hero"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -316,19 +321,20 @@ const ListHeader = memo(({
|
||||||
activeTagProducts,
|
activeTagProducts,
|
||||||
onSelectTag,
|
onSelectTag,
|
||||||
}: ListHeaderProps) => {
|
}: ListHeaderProps) => {
|
||||||
|
const [showAllActiveProducts, setShowAllActiveProducts] = useState(false);
|
||||||
const handleLayout = useCallback((event: any) => {
|
const handleLayout = useCallback((event: any) => {
|
||||||
const { y, height } = event.nativeEvent.layout;
|
const { y, height } = event.nativeEvent.layout;
|
||||||
onGradientLayout(y + height);
|
onGradientLayout(y + height);
|
||||||
}, [onGradientLayout]);
|
}, [onGradientLayout]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setShowAllActiveProducts(false);
|
||||||
|
}, [activeTagId]);
|
||||||
|
|
||||||
const renderPopularItem = useCallback(({ item }: { item: any }) => (
|
const renderPopularItem = useCallback(({ item }: { item: any }) => (
|
||||||
<PopularProductItem item={item} onPress={onProductPress} />
|
<PopularProductItem item={item} onPress={onProductPress} />
|
||||||
), [onProductPress]);
|
), [onProductPress]);
|
||||||
|
|
||||||
const renderExploreItem = useCallback(({ item }: { item: any }) => (
|
|
||||||
<ExploreProductItem item={item} onPress={onProductPress} />
|
|
||||||
), [onProductPress]);
|
|
||||||
|
|
||||||
const renderSlotItem = useCallback(({ item }: { item: any }) => (
|
const renderSlotItem = useCallback(({ item }: { item: any }) => (
|
||||||
<SlotItem item={item} />
|
<SlotItem item={item} />
|
||||||
), []);
|
), []);
|
||||||
|
|
@ -337,25 +343,92 @@ const ListHeader = memo(({
|
||||||
tw`absolute left-0 right-0 shadow-lg`,
|
tw`absolute left-0 right-0 shadow-lg`,
|
||||||
{ height: gradientHeight + 32, zIndex: -1 }
|
{ height: gradientHeight + 32, zIndex: -1 }
|
||||||
], [gradientHeight]);
|
], [gradientHeight]);
|
||||||
|
const activeColor = activeTagId == null ? null : getTagColor(activeTagId);
|
||||||
|
const pageTint = activeColor?.pageBg ?? '#FFFFFF';
|
||||||
|
const visibleActiveTagProducts = showAllActiveProducts ? activeTagProducts : activeTagProducts.slice(0, 6);
|
||||||
|
const hasMoreActiveTagProducts = activeTagProducts.length > visibleActiveTagProducts.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View onLayout={handleLayout}>
|
<View onLayout={handleLayout} style={{ backgroundColor: pageTint }}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[headerColor, headerColor]}
|
colors={[pageTint, pageTint]}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0.5 }}
|
end={{ x: 1, y: 0.5 }}
|
||||||
style={gradientStyle}
|
style={gradientStyle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{dashboardTags.length > 0 && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-2`,
|
||||||
|
{ backgroundColor: pageTint },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={tw`gap-3 py-1 px-1`}
|
||||||
|
>
|
||||||
|
{dashboardTags.map((tag) => (
|
||||||
|
<ExploreTab
|
||||||
|
key={tag.id}
|
||||||
|
tag={tag}
|
||||||
|
isSelected={activeTagId === tag.id}
|
||||||
|
onPress={() => onSelectTag(tag.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
{activeTagProducts.length > 0 ? (
|
||||||
|
<View style={[tw`mt-4 relative`, { backgroundColor: pageTint }]}>
|
||||||
|
<View style={tw`flex-row flex-wrap justify-between`}>
|
||||||
|
{visibleActiveTagProducts.map((product: any) => (
|
||||||
|
<ExploreProductItem
|
||||||
|
key={product.id}
|
||||||
|
item={product}
|
||||||
|
onPress={onProductPress}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{hasMoreActiveTagProducts && (
|
||||||
|
<MyTouchableOpacity
|
||||||
|
style={tw`self-center mt-1 mb-2 px-5 py-2.5 rounded-full bg-gray-900`}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={() => setShowAllActiveProducts(true)}
|
||||||
|
>
|
||||||
|
<MyText style={tw`text-white text-sm font-semibold`}>Show More</MyText>
|
||||||
|
</MyTouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={tw`py-6 items-center`}>
|
||||||
|
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||||
|
No products in this category yet
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={tw`py-4`}>
|
||||||
|
<BannerCarousel />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
tw`rounded-t-3xl px-4`,
|
||||||
|
{ backgroundColor: pageTint },
|
||||||
|
]}
|
||||||
|
>
|
||||||
{storesData?.stores && storesData.stores.length > 0 && (
|
{storesData?.stores && storesData.stores.length > 0 && (
|
||||||
<View style={tw`px-4 pb-2`}>
|
<View style={tw`mt-4 mb-5 rounded-[28px] bg-white/70 border border-white px-3 pt-4 pb-2`}>
|
||||||
<View style={tw`flex-row items-center justify-between mb-4 px-1`}>
|
<View style={tw`flex-row items-center justify-between mb-4 px-1`}>
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-xl font-extrabold text-white tracking-tight drop-shadow-md text-neutral-800`}>
|
<MyText style={tw`text-xl font-extrabold text-gray-900 tracking-tight`}>
|
||||||
Our Stores
|
Our Stores
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyText style={tw`text-xs font-medium mt-0.5 text-neutral-800`}>Fresh from our locations</MyText>
|
<MyText style={tw`text-xs font-medium mt-0.5 text-gray-500`}>Fresh from our locations</MyText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={tw`pb-2`}>
|
<View style={tw`pb-2`}>
|
||||||
|
|
@ -369,13 +442,7 @@ const ListHeader = memo(({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={tw`py-4`}>
|
|
||||||
<BannerCarousel />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={tw`bg-white rounded-t-3xl px-4`}>
|
|
||||||
<View style={tw`py-2`}>
|
<View style={tw`py-2`}>
|
||||||
<NextOrderGlimpse />
|
<NextOrderGlimpse />
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -404,55 +471,6 @@ const ListHeader = memo(({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{dashboardTags.length > 0 && (
|
|
||||||
<View style={tw`mb-4`}>
|
|
||||||
<View style={tw`px-1 mb-2`}>
|
|
||||||
<MyText style={tw`text-2xl font-extrabold text-gray-900 tracking-tight`}>Explore Products</MyText>
|
|
||||||
<MyText style={tw`text-sm text-gray-500 font-medium mt-1`}>Browse by category</MyText>
|
|
||||||
</View>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={tw`gap-2 py-2`}
|
|
||||||
>
|
|
||||||
{dashboardTags.map((tag) => (
|
|
||||||
<ExploreTab
|
|
||||||
key={tag.id}
|
|
||||||
tag={tag}
|
|
||||||
isSelected={activeTagId === tag.id}
|
|
||||||
onPress={() => onSelectTag(tag.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
{activeTagProducts.length > 0 ? (
|
|
||||||
<View style={tw`mt-3 relative`}>
|
|
||||||
<MyFlatList
|
|
||||||
data={activeTagProducts}
|
|
||||||
keyExtractor={(item) => item.id.toString()}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={staticStyles.popularListContent}
|
|
||||||
renderItem={renderExploreItem}
|
|
||||||
removeClippedSubviews={true}
|
|
||||||
/>
|
|
||||||
<LinearGradient
|
|
||||||
colors={["transparent", "rgba(0,0,0,0.08)"]}
|
|
||||||
start={{ x: 0, y: 0.5 }}
|
|
||||||
end={{ x: 1, y: 0.5 }}
|
|
||||||
style={tw`absolute right-0 top-0 bottom-4 w-12 rounded-l-xl`}
|
|
||||||
pointerEvents="none"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={tw`py-6 items-center`}>
|
|
||||||
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
|
||||||
No products in this category yet
|
|
||||||
</MyText>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sortedSlots.length > 0 && (
|
{sortedSlots.length > 0 && (
|
||||||
<View style={tw`mt-2 mb-4`}>
|
<View style={tw`mt-2 mb-4`}>
|
||||||
<View style={tw`flex-row items-center justify-between px-1 mb-6`}>
|
<View style={tw`flex-row items-center justify-between px-1 mb-6`}>
|
||||||
|
|
@ -497,7 +515,6 @@ export default function Dashboard() {
|
||||||
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
|
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const { backgroundColor } = useStatusBarStore();
|
|
||||||
const { getQuickestSlot } = useProductSlotIdentifier();
|
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
|
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
|
||||||
|
|
@ -660,10 +677,16 @@ export default function Dashboard() {
|
||||||
/>
|
/>
|
||||||
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts]);
|
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts]);
|
||||||
|
|
||||||
|
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
|
||||||
|
const pageTintStyle = useMemo(() => [
|
||||||
|
tw`flex-1`,
|
||||||
|
{ backgroundColor: pageTint }
|
||||||
|
], [pageTint]);
|
||||||
|
|
||||||
const searchBarContainerStyle = useMemo(() => [
|
const searchBarContainerStyle = useMemo(() => [
|
||||||
tw`w-full px-4 pt-4 pb-2`,
|
tw`w-full px-4 pt-4 pb-0`,
|
||||||
{ backgroundColor }
|
{ backgroundColor: pageTint }
|
||||||
], [backgroundColor]);
|
], [pageTint]);
|
||||||
|
|
||||||
const listContentContainerStyle = useMemo(() => [
|
const listContentContainerStyle = useMemo(() => [
|
||||||
tw`pb-24`,
|
tw`pb-24`,
|
||||||
|
|
@ -695,55 +718,58 @@ export default function Dashboard() {
|
||||||
displayedProducts.forEach(product => str += `${product.id}-`)
|
displayedProducts.forEach(product => str += `${product.id}-`)
|
||||||
// console.log(str)
|
// console.log(str)
|
||||||
return (
|
return (
|
||||||
<TabLayoutWrapper>
|
<TabLayoutWrapper style={{ backgroundColor: pageTint }}>
|
||||||
<View style={searchBarContainerStyle}>
|
<View style={pageTintStyle}>
|
||||||
<SearchBar
|
<View style={searchBarContainerStyle}>
|
||||||
value={""}
|
<SearchBar
|
||||||
onChangeText={() => { }}
|
value={""}
|
||||||
onPress={handleSearchPress}
|
onChangeText={() => { }}
|
||||||
editable={false}
|
onPress={handleSearchPress}
|
||||||
containerStyle={tw`bg-white`}
|
editable={false}
|
||||||
onSubmitEditing={() => {
|
containerStyle={tw`bg-white`}
|
||||||
if (inputQuery.trim()) {
|
onSubmitEditing={() => {
|
||||||
router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
|
if (inputQuery.trim()) {
|
||||||
}
|
router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
|
||||||
}}
|
}
|
||||||
returnKeyType="search"
|
}}
|
||||||
/>
|
returnKeyType="search"
|
||||||
</View>
|
|
||||||
|
|
||||||
<MyFlatList
|
|
||||||
data={displayedProducts}
|
|
||||||
keyExtractor={(item) => item.id.toString()}
|
|
||||||
numColumns={2}
|
|
||||||
contentContainerStyle={listContentContainerStyle}
|
|
||||||
columnWrapperStyle={staticStyles.columnWrapper}
|
|
||||||
renderItem={renderProductItem}
|
|
||||||
ListHeaderComponent={listHeader}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isRefreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
tintColor="#3b82f6"
|
|
||||||
colors={["#3b82f6"]}
|
|
||||||
/>
|
/>
|
||||||
}
|
</View>
|
||||||
ListEmptyComponent={
|
|
||||||
<View style={tw`items-center py-8`}>
|
|
||||||
<MyText style={tw`text-gray-500`}>No products available</MyText>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
removeClippedSubviews={true}
|
|
||||||
maxToRenderPerBatch={10}
|
|
||||||
windowSize={5}
|
|
||||||
initialNumToRender={10}
|
|
||||||
updateCellsBatchingPeriod={50}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
|
<MyFlatList
|
||||||
<AddToCartDialog />
|
data={displayedProducts}
|
||||||
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
keyExtractor={(item) => item.id.toString()}
|
||||||
<FloatingCartBar />
|
numColumns={2}
|
||||||
|
style={{ backgroundColor: pageTint }}
|
||||||
|
contentContainerStyle={listContentContainerStyle}
|
||||||
|
columnWrapperStyle={staticStyles.columnWrapper}
|
||||||
|
renderItem={renderProductItem}
|
||||||
|
ListHeaderComponent={listHeader}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={isRefreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor="#3b82f6"
|
||||||
|
colors={["#3b82f6"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View style={tw`items-center py-8`}>
|
||||||
|
<MyText style={tw`text-gray-500`}>No products available</MyText>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
maxToRenderPerBatch={10}
|
||||||
|
windowSize={5}
|
||||||
|
initialNumToRender={10}
|
||||||
|
updateCellsBatchingPeriod={50}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
|
||||||
|
<AddToCartDialog />
|
||||||
|
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
||||||
|
<FloatingCartBar />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TabLayoutWrapper>
|
</TabLayoutWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ interface ProductCardProps {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
showDeliveryInfo?: boolean;
|
showDeliveryInfo?: boolean;
|
||||||
miniView?: boolean;
|
miniView?: boolean;
|
||||||
|
variant?: 'default' | 'hero';
|
||||||
nullIfNotAvailable?: boolean;
|
nullIfNotAvailable?: boolean;
|
||||||
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
|
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
|
||||||
useAddToCartDialog?: boolean;
|
useAddToCartDialog?: boolean;
|
||||||
|
|
@ -42,10 +43,12 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
onPress,
|
onPress,
|
||||||
showDeliveryInfo = true,
|
showDeliveryInfo = true,
|
||||||
miniView = false,
|
miniView = false,
|
||||||
|
variant = 'default',
|
||||||
nullIfNotAvailable = false,
|
nullIfNotAvailable = false,
|
||||||
containerComp: ContainerComp = React.Fragment,
|
containerComp: ContainerComp = React.Fragment,
|
||||||
useAddToCartDialog = false,
|
useAddToCartDialog = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isHero = variant === 'hero';
|
||||||
const imageUri = item.images?.[0]
|
const imageUri = item.images?.[0]
|
||||||
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
|
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
|
||||||
const [imageError, setImageError] = React.useState<string | null>(null)
|
const [imageError, setImageError] = React.useState<string | null>(null)
|
||||||
|
|
@ -152,7 +155,8 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
<ContainerComp>
|
<ContainerComp>
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
tw`bg-white rounded-2xl overflow-hidden border border-gray-200 pb-2`,
|
tw`bg-white overflow-hidden border border-gray-200`,
|
||||||
|
isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`,
|
||||||
{ width: itemWidth },
|
{ width: itemWidth },
|
||||||
]}
|
]}
|
||||||
onPress={onPress || (() => {/* TODO: Navigate to product detail */})}
|
onPress={onPress || (() => {/* TODO: Navigate to product detail */})}
|
||||||
|
|
@ -162,7 +166,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
<RnImage
|
<RnImage
|
||||||
source={{ uri: imageUri }}
|
source={{ uri: imageUri }}
|
||||||
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
|
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
|
||||||
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
style={{ width: "100%", height: isHero ? itemWidth * 0.82 : itemWidth, resizeMode: "cover" }}
|
||||||
onLoadStart={() => {
|
onLoadStart={() => {
|
||||||
setImageStatus('loading')
|
setImageStatus('loading')
|
||||||
setImageError(null)
|
setImageError(null)
|
||||||
|
|
@ -194,36 +198,36 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{miniView && (
|
{miniView && (
|
||||||
<View style={tw`absolute bottom-2 right-2`}>
|
<View style={isHero ? tw`absolute bottom-1.5 right-1.5` : tw`absolute bottom-2 right-2`}>
|
||||||
{quantity > 0 ? (
|
{quantity > 0 ? (
|
||||||
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
|
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
|
||||||
) : (
|
) : (
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
|
style={isHero ? tw`w-7 h-7 rounded-full bg-white items-center justify-center shadow-md` : tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
|
||||||
onPress={() => handleQuantityChange(1)}
|
onPress={() => handleQuantityChange(1)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<CartIcon focused={false} size={16} color="#2E90FA" />
|
<CartIcon focused={false} size={isHero ? 14 : 16} color="#2E90FA" />
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={tw`px-3 pt-3`}>
|
<View style={isHero ? tw`px-2 pt-2` : tw`px-3 pt-3`}>
|
||||||
<MyText style={tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}>
|
<MyText style={isHero ? tw`text-gray-900 font-semibold text-xs mb-1 leading-4` : tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</MyText>
|
</MyText>
|
||||||
|
|
||||||
<View style={tw`flex-row items-baseline mb-2`}>
|
<View style={isHero ? tw`flex-row items-baseline mb-1` : tw`flex-row items-baseline mb-2`}>
|
||||||
<MyText style={tw`text-brand500 font-bold text-base`}>₹{item.price}</MyText>
|
<MyText style={isHero ? tw`text-brand500 font-bold text-sm` : tw`text-brand500 font-bold text-base`}>₹{item.price}</MyText>
|
||||||
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
|
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
|
||||||
<MyText style={tw`text-gray-400 text-xs ml-2 line-through`}>₹{item.marketPrice}</MyText>
|
<MyText style={isHero ? tw`text-gray-400 text-[10px] ml-1.5 line-through` : tw`text-gray-400 text-xs ml-2 line-through`}>₹{item.marketPrice}</MyText>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={tw`flex-row items-center mb-2`}>
|
<View style={isHero ? tw`flex-row items-center mb-1` : tw`flex-row items-center mb-2`}>
|
||||||
{item.productType !== 'combo' && (
|
{item.productType !== 'combo' && (
|
||||||
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
<MyText style={isHero ? tw`text-gray-500 text-[10px] font-medium` : tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type { StyleProp, ViewStyle } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { tw } from 'common-ui';
|
import { tw } from 'common-ui';
|
||||||
|
|
||||||
interface TabLayoutWrapperProps {
|
interface TabLayoutWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TabLayoutWrapper({ children }: TabLayoutWrapperProps) {
|
export default function TabLayoutWrapper({ children, style }: TabLayoutWrapperProps) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView edges={['top']} style={tw`flex-1 bg-white`}>
|
<SafeAreaView edges={['top']} style={[tw`flex-1 bg-white`, style]}>
|
||||||
{children}
|
{children}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue