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 +}