From ed7318f9eef7c2b8b321e767082c8e47afe58d2b Mon Sep 17 00:00:00 2001
From: shafi54 <108669266+shafi-aviz@users.noreply.github.com>
Date: Wed, 4 Mar 2026 00:17:52 +0530
Subject: [PATCH] enh
---
.../app/(drawer)/(tabs)/home/index.tsx | 925 ++++++++----------
apps/user-ui/app/(drawer)/_layout.tsx | 1 +
apps/user-ui/components/cart-page.tsx | 29 +-
3 files changed, 434 insertions(+), 521 deletions(-)
diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
index f07d5e0..ac7860a 100755
--- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
+++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
@@ -1,6 +1,5 @@
-import React, { useState } from "react";
-import { View, Dimensions, Image, Alert, ScrollView, StatusBar as RNStatusBar, RefreshControl } from "react-native";
-import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
+import React, { useState, useCallback, useMemo, memo } from "react";
+import { View, Dimensions, Image, RefreshControl } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import {
@@ -9,88 +8,34 @@ import {
useManualRefresh,
useMarkDataFetchers,
LoadingDialog,
- AppContainer,
MyTouchableOpacity,
- MyText, MyTextInput, SearchBar, useStatusBarStore,
+ MyText, SearchBar, useStatusBarStore,
colors
} from "common-ui";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
-import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
-import { Ionicons } from "@expo/vector-icons";
import ProductCard from "@/components/ProductCard";
import AddToCartDialog from "@/src/components/AddToCartDialog";
import MyFlatList from "common-ui/src/components/flat-list";
import { trpc } from "@/src/trpc-client";
-import { useGetCart } from "@/hooks/cart-query-hooks";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import FloatingCartBar from "@/components/floating-cart-bar";
-import AddressSelector from "@/components/AddressSelector";
import BannerCarousel from "@/components/BannerCarousel";
-import TestingPhaseNote from "@/components/TestingPhaseNote";
import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime);
-// import { StatusBar } from "expo-status-bar";
const { width: screenWidth } = Dimensions.get("window");
-const itemWidth = screenWidth * 0.45; // 45% of screen width
-const gridItemWidth = (screenWidth - 48) / 2; // Half of screen width minus padding
-
-const RenderStore = ({
- item,
-}: {
- item: any;
-}) => {
- const router = useRouter();
- const { setNavigatedFromHome, setSelectedStoreId } = useNavigationStore();
-
- const handlePress = () => {
- setNavigatedFromHome(true);
- setSelectedStoreId(item.id);
- router.push('/(drawer)/(tabs)/stores');
- };
-
- return (
-
-
- {item.signedImageUrl ? (
-
- ) : (
-
- )}
-
-
-
- {item.name.replace(/^The\s+/i, "")}
-
-
- );
-};
-
-
+const itemWidth = screenWidth * 0.45;
+const gridItemWidth = (screenWidth - 48) / 2;
const headerColor = colors.secondaryPink;
-// Format time range helper
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
@@ -104,6 +49,312 @@ const formatTimeRange = (deliveryTime: string) => {
}
};
+const staticStyles = {
+ flatListContent: { gap: 16 },
+ columnWrapper: { gap: 16, paddingHorizontal: 16 },
+ popularListContent: { paddingBottom: 16 },
+ slotsListContent: { paddingBottom: 24 },
+};
+
+interface RenderStoreProps {
+ item: any;
+}
+
+const RenderStore = memo(({ item }: RenderStoreProps) => {
+ const router = useRouter();
+ const { setNavigatedFromHome, setSelectedStoreId } = useNavigationStore();
+
+ const handlePress = useCallback(() => {
+ setNavigatedFromHome(true);
+ setSelectedStoreId(item.id);
+ router.push('/(drawer)/(tabs)/stores');
+ }, [item.id, router, setNavigatedFromHome, setSelectedStoreId]);
+
+ return (
+
+
+ {item.signedImageUrl ? (
+
+ ) : (
+
+ )}
+
+
+ {item.name.replace(/^The\s+/i, "")}
+
+
+ );
+});
+RenderStore.displayName = 'RenderStore';
+
+interface SlotCardProps {
+ slot: any;
+}
+
+const SlotCard = memo(({ slot }: SlotCardProps) => {
+ const router = useRouter();
+ const now = dayjs();
+ const freezeTime = dayjs(slot.freezeTime);
+ const isClosingSoon = freezeTime.diff(now, "hour") < 4 && freezeTime.isAfter(now);
+
+ const handlePress = useCallback(() => {
+ router.push(`/(drawer)/(tabs)/home/slot-view?slotId=${slot.id}`);
+ }, [router, slot.id]);
+
+ return (
+
+
+ {isClosingSoon && (
+
+
+ CLOSING SOON
+
+ )}
+
+
+
+
+
+
+
+
+ Delivery At
+
+ {formatTimeRange(slot.deliveryTime)}
+ {dayjs(slot.deliveryTime).format("ddd, MMM DD")}
+
+
+
+
+
+
+
+ Order By
+
+ {dayjs(slot.freezeTime).format("h:mm A")}
+ {dayjs(slot.freezeTime).format("ddd, MMM DD")}
+
+
+
+
+
+ {slot.products.slice(0, 3).map((p: any, i: number) => (
+ 0 && tw`-ml-3`]}
+ >
+ {p.images?.[0] ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ View all {slot.products.length} items
+
+
+
+ );
+});
+
+interface PopularProductItemProps {
+ item: any;
+ onPress: (id: number) => void;
+}
+
+const PopularProductItem = memo(({ item, onPress }: PopularProductItemProps) => {
+ const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
+
+ return (
+
+
+
+ );
+});
+
+interface SlotItemProps {
+ item: any;
+}
+
+const SlotItem = memo(({ item }: SlotItemProps) => );
+
+interface ProductItemProps {
+ item: any;
+ onPress: (id: number) => void;
+}
+
+const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
+ const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
+
+ return (
+
+ );
+});
+
+interface ListHeaderProps {
+ gradientHeight: number;
+ onGradientLayout: (height: number) => void;
+ storesData: any;
+ popularProducts: any[];
+ sortedSlots: any[];
+ onProductPress: (id: number) => void;
+}
+
+const ListHeader = memo(({
+ gradientHeight,
+ onGradientLayout,
+ storesData,
+ popularProducts,
+ sortedSlots,
+ onProductPress,
+}: ListHeaderProps) => {
+ const handleLayout = useCallback((event: any) => {
+ const { y, height } = event.nativeEvent.layout;
+ onGradientLayout(y + height);
+ }, [onGradientLayout]);
+
+ const renderPopularItem = useCallback(({ item }: { item: any }) => (
+
+ ), [onProductPress]);
+
+ const renderSlotItem = useCallback(({ item }: { item: any }) => (
+
+ ), []);
+
+ const gradientStyle = useMemo(() => [
+ tw`absolute left-0 right-0 shadow-lg`,
+ { height: gradientHeight + 32, zIndex: -1 }
+ ], [gradientHeight]);
+
+ return (
+ <>
+
+
+
+ {storesData?.stores && storesData.stores.length > 0 && (
+
+
+
+
+ Our Stores
+
+ Fresh from our locations
+
+
+
+
+ {storesData.stores.map((store: any) => (
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Popular Items
+ Trending fresh picks just for you
+
+
+
+ item.id.toString()}
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ contentContainerStyle={staticStyles.popularListContent}
+ renderItem={renderPopularItem}
+ removeClippedSubviews={true}
+ />
+
+
+
+ {sortedSlots.length > 0 && (
+
+
+
+ Upcoming Delivery Slots
+ Plan your fresh deliveries ahead
+
+
+ item.id.toString()}
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ contentContainerStyle={staticStyles.slotsListContent}
+ decelerationRate="fast"
+ snapToInterval={280 + 16}
+ renderItem={renderSlotItem}
+ removeClippedSubviews={true}
+ />
+
+ )}
+
+
+
+
+ All Available Products
+ Browse our complete selection
+
+
+
+
+ >
+ );
+});
+
export default function Dashboard() {
const router = useRouter();
const userDetails = useUserDetails();
@@ -112,10 +363,7 @@ export default function Dashboard() {
const [selectedTagId, setSelectedTagId] = useState(null);
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const [gradientHeight, setGradientHeight] = useState(0);
- const [stickyBarLayout, setStickyBarLayout] = useState({ y: 0, height: 0 });
- const [whiteSectionLayout, setWhiteSectionLayout] = useState({ y: 0 });
const [displayedProducts, setDisplayedProducts] = useState([]);
- const [endIndex, setEndIndex] = useState(10);
const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore();
@@ -134,46 +382,31 @@ export default function Dashboard() {
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
-
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery();
const products = productsData?.products || [];
- // Initialize with the first batch of products (only those with available slots)
React.useEffect(() => {
if (products.length > 0 && displayedProducts.length === 0) {
- const initialBatchRaw = products;
-
- // Filter to include only products with available slots
- // const initialBatch = initialBatchRaw.filter(product => {
- // const slot = getQuickestSlot(product.id);
- // return !product.isOutOfStock;
- // });
+ const initialBatch = products
+ .filter(p => typeof p.id === "number")
+ .sort((a, b) => {
+ const slotA = getQuickestSlot(a.id);
+ const slotB = getQuickestSlot(b.id);
+ if (slotA && !slotB) return -1;
+ if (!slotA && slotB) return 1;
+ if (a.isOutOfStock && !b.isOutOfStock) return 1;
+ if (!a.isOutOfStock && b.isOutOfStock) return -1;
+ return 0;
+ });
- const initialBatch = initialBatchRaw
- .filter(p => typeof p.id === "number")
- .sort((a, b) => {
- const slotA = getQuickestSlot(a.id);
- const slotB = getQuickestSlot(b.id);
-
- if (slotA && !slotB) return -1;
- if (!slotA && slotB) return 1;
-
- if(a.isOutOfStock && !b.isOutOfStock) return 1;
- if(!a.isOutOfStock && b.isOutOfStock) return -1;
- return 0;
- })
-
-
setDisplayedProducts(initialBatch);
setHasMore(products.length > 10);
- setEndIndex(10);
- }
+ }
}, [productsData]);
- // Extract popular items IDs as an array to preserve order
- const popularItemIds = (() => {
+ const popularItemIds = useMemo(() => {
const popularItems = essentialConsts?.popularItems;
if (!popularItems) return [];
@@ -186,37 +419,31 @@ export default function Dashboard() {
.filter((id: number) => !isNaN(id));
}
return [];
- })();
+ }, [essentialConsts?.popularItems]);
- const sortedSlots = React.useMemo(() => {
+ const sortedSlots = useMemo(() => {
if (!slotsData?.slots) return [];
- return slotsData.slots.sort((a, b) => {
+ return [...slotsData.slots].sort((a, b) => {
const deliveryDiff = dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime));
if (deliveryDiff !== 0) return deliveryDiff;
return dayjs(a.freezeTime).diff(dayjs(b.freezeTime));
});
}, [slotsData]);
- // Filter products to only include those whose ID exists in popularItemIds, preserving order
- // Only filter when both products and essentialConsts are loaded
- const popularProducts = popularItemIds
- .map(id => products.find(product => product.id === id))
- .filter((product): product is NonNullable => product != null);
-
+ const popularProducts = useMemo(() => {
+ return popularItemIds
+ .map(id => products.find(product => product.id === id))
+ .filter((product): product is NonNullable => product != null);
+ }, [popularItemIds, products]);
- const handleRefresh = async () => {
+ const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
- await Promise.all([
- refetch(),
- refetchStores(),
- refetchSlots(),
- refetchConsts(),
- ]);
+ await Promise.all([refetch(), refetchStores(), refetchSlots(), refetchConsts()]);
} finally {
setIsRefreshing(false);
}
- };
+ }, [refetch, refetchStores, refetchSlots, refetchConsts]);
useManualRefresh(() => {
handleRefresh();
@@ -226,8 +453,42 @@ export default function Dashboard() {
handleRefresh();
});
- const handleScroll = (event: any) => {
- };
+ const handleProductPress = useCallback((id: number) => {
+ router.push(`/(drawer)/(tabs)/home/product-detail/${id}`);
+ }, [router]);
+
+ const handleGradientLayout = useCallback((height: number) => {
+ setGradientHeight(height);
+ }, []);
+
+ const handleSearchPress = useCallback(() => {
+ router.push("/(drawer)/(tabs)/home/search-results");
+ }, [router]);
+
+ const renderProductItem = useCallback(({ item }: { item: any }) => (
+
+ ), [handleProductPress]);
+
+ const listHeader = useMemo(() => (
+
+ ), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress]);
+
+ const searchBarContainerStyle = useMemo(() => [
+ tw`w-full px-4 pt-4 pb-2`,
+ { backgroundColor }
+ ], [backgroundColor]);
+
+ const listContentContainerStyle = useMemo(() => [
+ tw`pb-24`,
+ staticStyles.flatListContent
+ ], []);
if (isLoading || isLoadingConsts) {
return (
@@ -251,14 +512,32 @@ export default function Dashboard() {
);
}
-
-
return (
- {/* */}
-
+ { }}
+ onPress={handleSearchPress}
+ editable={false}
+ containerStyle={tw`bg-white`}
+ onSubmitEditing={() => {
+ if (inputQuery.trim()) {
+ router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
+ }
+ }}
+ returnKeyType="search"
+ />
+
+
+ item.id.toString()}
+ numColumns={2}
+ contentContainerStyle={listContentContainerStyle}
+ columnWrapperStyle={staticStyles.columnWrapper}
+ renderItem={renderProductItem}
+ ListHeaderComponent={listHeader}
refreshControl={
}
- onScroll={(e) => {
- handleScroll(e);
-
- // Check if we're near the end of the scroll for vertical loading
- const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent;
- const paddingToBottom = 40;
-
- if (layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom) {
- // Load more products when reaching near the end
- if (!isLoadingMore && hasMore) {
- // loadMoreProducts();
- }
- }
- }}
- scrollEventThrottle={16}
- >
-
-
-
-
-
- router.push("/(drawer)/(tabs)/me")}
- style={tw`bg-white/10 rounded-full border border-white/10`}
- >
- {userDetails?.profileImage ? (
-
- ) : (
-
- )}
-
-
-
- {
- const { y, height } = event.nativeEvent.layout;
- setStickyBarLayout({ y, height });
- }}
- >
- { }}
- onPress={() => router.push("/(drawer)/(tabs)/home/search-results")}
- editable={false}
- containerStyle={tw` bg-white`}
- onSubmitEditing={() => {
- if (inputQuery.trim()) {
- router.push(
- `/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(
- inputQuery.trim()
- )}`
- );
- }
- }}
- returnKeyType="search"
- />
-
-
- {
- const { y, height } = event.nativeEvent.layout;
- setGradientHeight(y + height);
- }}
- >
- {/* Stores Section */}
- {storesData?.stores && storesData.stores.length > 0 && (
-
-
-
-
- Our Stores
-
-
- Fresh from our locations
-
-
-
-
-
- {storesData.stores.map((store, index) => (
-
-
-
- ))}
-
-
-
- )}
-
-
- {/* Banner Carousel */}
-
-
-
-
-
- {/* White Section */}
- {
- const { y } = event.nativeEvent.layout;
- setWhiteSectionLayout({ y });
- }}
- >
-
-
-
-
- {/* Section Title */}
-
-
- Popular Items
-
-
- Trending fresh picks just for you
-
+ ListEmptyComponent={
+
+ No products available
+ }
+ removeClippedSubviews={true}
+ maxToRenderPerBatch={10}
+ windowSize={5}
+ initialNumToRender={10}
+ updateCellsBatchingPeriod={50}
+ />
-
-
- {popularProducts.map((item, index: number) => (
-
-
- router.push(
- `/(drawer)/(tabs)/home/product-detail/${item.id}`
- )
- }
- showDeliveryInfo={false}
- useAddToCartDialog={true}
- miniView={true}
- />
-
- ))}
-
-
-
-
- {/* Upcoming Deliveries Section */}
- {sortedSlots.length > 0 && (
-
-
-
-
- Upcoming Delivery Slots
-
-
- Plan your fresh deliveries ahead
-
-
-
-
-
- {sortedSlots.slice(0, 5).map((slot) => {
- const now = dayjs();
- const freezeTime = dayjs(slot.freezeTime);
- const isClosingSoon =
- freezeTime.diff(now, "hour") < 4 && freezeTime.isAfter(now);
-
- return (
-
- router.push(
- `/(drawer)/(tabs)/home/slot-view?slotId=${slot.id}`
- )
- }
- activeOpacity={0.9}
- >
- {/* Status Badge */}
-
- {isClosingSoon && (
-
-
-
- CLOSING SOON
-
-
- )}
-
-
- {/* Main Time Grid */}
-
-
-
-
-
-
-
- Delivery At
-
-
-
- {formatTimeRange(slot.deliveryTime)}
-
-
- {dayjs(slot.deliveryTime).format("ddd, MMM DD")}
-
-
-
-
-
-
-
-
-
- Order By
-
-
-
- {dayjs(slot.freezeTime).format("h:mm A")}
-
-
- {dayjs(slot.freezeTime).format("ddd, MMM DD")}
-
-
-
-
- {/* Product Teaser */}
-
-
- {slot.products.slice(0, 3).map((p, i) => (
- 0 && tw`-ml-3`,
- ]}
- >
- {p.images?.[0] ? (
-
- ) : (
-
- )}
-
- ))}
-
-
- View all {slot.products.length} items
-
-
-
-
- );
- })}
-
-
- )}
-
- {/* All Products Section - Vertical Infinite Scroll */}
-
-
-
-
- All Available Products
-
-
- Browse our complete selection
-
-
-
-
- {/* Product List */}
- item.id}
- numColumns={2}
- // contentContainerStyle={tw`pb-8`}
- contentContainerStyle={[tw` pb-24`, { gap: 16 }]}
- columnWrapperStyle={{gap: 16}}
- renderItem={({ item, index }) => (
-
-
- router.push(
- `/(drawer)/(tabs)/home/product-detail/${item.id}`
- )
- }
- showDeliveryInfo={true}
- miniView={false}
- useAddToCartDialog={true}
-
- key={item.id}
- />
-
- )}
- initialNumToRender={4}
- maxToRenderPerBatch={4}
- windowSize={4}
- // onEndReached={() => {
- // if (!isLoadingMore && hasMore) {
- // loadMoreProducts();
- // }
- // }}
- onEndReachedThreshold={0.5}
- ListFooterComponent={
- isLoadingMore ? (
-
- Loading more...
-
- ) : null
- }
- ListEmptyComponent={
-
- No products available
-
- }
- />
-
-
-
-
-
-
-
-
+
+
diff --git a/apps/user-ui/app/(drawer)/_layout.tsx b/apps/user-ui/app/(drawer)/_layout.tsx
index b7233d0..1e43d1d 100755
--- a/apps/user-ui/app/(drawer)/_layout.tsx
+++ b/apps/user-ui/app/(drawer)/_layout.tsx
@@ -37,6 +37,7 @@ export default function Layout() {
>({});
@@ -260,6 +262,15 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
[finalTotal, constsData, isFlashDelivery]
);
+ const freeDeliveryThreshold = useMemo(
+ () => {
+ return isFlashDelivery
+ ? constsData?.flashFreeDeliveryThreshold
+ : constsData?.freeDeliveryThreshold;
+ },
+ [constsData, isFlashDelivery]
+ );
+
const finalTotalWithDelivery = finalTotal + deliveryCharge;
const hasAvailableItems = cartItems.some(item => !item.product?.isOutOfStock);
@@ -786,6 +797,18 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
)}
+
+ {!isAuthenticated && (
+ router.push("/(drawer)/(tabs)/me")}
+ >
+
+ Log In
+ To find offers and coupons
+
+
+ )}
)}
@@ -842,11 +865,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{/* Free Delivery Nudge */}
- {deliveryCharge > 0 && (constsData?.freeDeliveryThreshold || 0) > 0 && finalTotal < (constsData?.freeDeliveryThreshold || 0) && (
+ {deliveryCharge > 0 && (freeDeliveryThreshold || 0) > 0 && finalTotal < (freeDeliveryThreshold || 0) && (
- Add products worth ₹{((constsData?.freeDeliveryThreshold || 0) - finalTotal).toFixed(0)} for free delivery
+ Add products worth ₹{((freeDeliveryThreshold || 0) - finalTotal).toFixed(0)} for free delivery
)}
@@ -1035,4 +1058,4 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
);
-}
\ No newline at end of file
+}