From 1139bf0f46354e0456753e59aefe6914a9c09a6b Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:59:38 +0530 Subject: [PATCH] enh --- .../app/(drawer)/(tabs)/home/index.tsx | 315 +++++++++--------- .../app/(drawer)/(tabs)/order-again/index.tsx | 110 +++--- apps/user-ui/app/_layout.tsx | 9 +- 3 files changed, 231 insertions(+), 203 deletions(-) diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index 41c0c7f..3930108 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -1,7 +1,6 @@ -import React, { useState, useCallback, useMemo, memo } from "react"; +import React, { useState, useCallback, useMemo, memo, useRef, useEffect } from "react"; import { View, Dimensions, Image, RefreshControl } from "react-native"; -import { TabView, TabBar } from "react-native-tab-view"; -import { LinearGradient } from "expo-linear-gradient"; +import { ScrollView } from "react-native-gesture-handler"; import { useRouter } from "expo-router"; import { theme, @@ -37,14 +36,6 @@ const itemWidth = screenWidth * 0.45; const heroItemWidth = (screenWidth - 72) / 3; const gridItemWidth = (screenWidth - 48) / 2; -// Hero product card geometry (used to size the tab scene heights) -const heroCardImageHeight = heroItemWidth * 0.82; -const heroCardTextBlock = 92; -const heroCardHeight = heroCardImageHeight + heroCardTextBlock; -const heroRowHeight = heroCardHeight + 12; // + mb-3 -const TAG_GRID_COLUMNS = 3; -const TAB_BAR_HEIGHT = 48; - const formatTimeRange = (deliveryTime: string) => { const time = dayjs(deliveryTime); const endTime = time.add(1, 'hour'); @@ -195,22 +186,21 @@ const SlotCard = memo(({ slot }: SlotCardProps) => { ); }); -interface TagTabViewProps { +interface TagTabStripProps { dashboardTags: any[]; activeTagId: number | null; - productsByTagId: Record; onSelectTag: (id: number) => void; - onProductPress: (id: number) => void; } -const TagTabView = memo(({ +// Fixed, horizontally scrollable tab strip (plain ScrollView — no nesting, +// so it scrolls reliably on Android). Sits OUTSIDE the vertical FlatList. +const TagTabStrip = memo(({ dashboardTags, activeTagId, - productsByTagId, onSelectTag, - onProductPress, -}: TagTabViewProps) => { - const [expandedByTagId, setExpandedByTagId] = useState>({}); +}: TagTabStripProps) => { + const scrollRef = useRef(null); + const positions = useRef([]); const routes = useMemo( () => dashboardTags.map((tag) => ({ key: String(tag.id), title: tag.tagName })), @@ -222,82 +212,116 @@ const TagTabView = memo(({ return idx < 0 ? 0 : idx; }, [dashboardTags, activeTagId]); - // Height: fit the active tag's visible product rows (default 6 per tag), - // plus room for the "Show More" button when it has more products. - const activeTagKey = routes[index]?.key; - const sceneHeight = useMemo(() => { - const tagId = Number(activeTagKey); - const count = (productsByTagId[tagId]?.length) || 0; - const expanded = expandedByTagId[tagId]; - const visible = expanded ? count : Math.min(count, 6); - const rows = Math.max(1, Math.ceil(visible / TAG_GRID_COLUMNS)); - const showMore = count > 6 && !expanded; - return rows * heroRowHeight + 24 + (showMore ? 52 : 0); - }, [activeTagKey, productsByTagId, expandedByTagId]); - - const renderTabBar = useCallback((props: any) => ( - - ), []); - - const renderScene = useCallback(({ route }: any) => { - const tagId = Number(route.key); - const products = productsByTagId[tagId] || []; - const expanded = !!expandedByTagId[tagId]; - const visible = expanded ? products : products.slice(0, 6); - const hasMore = products.length > 6 && !expanded; - - return ( - - {products.length > 0 ? ( - - {visible.map((product: any) => ( - - ))} - - ) : ( - - - No products in this category yet - - - )} - {hasMore && ( - setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }))} - > - Show More - - )} - - ); - }, [productsByTagId, expandedByTagId, onProductPress]); + useEffect(() => { + const x = positions.current[index]; + if (x != null) { + scrollRef.current?.scrollTo({ x: Math.max(0, x - 16), animated: true }); + } + }, [index]); return ( - onSelectTag(Number(routes[i].key))} - renderTabBar={renderTabBar} - renderScene={renderScene} - swipeEnabled - lazy - style={{ height: TAB_BAR_HEIGHT + sceneHeight }} - /> + + {routes.map((route, i) => { + const isActive = i === index; + return ( + { + positions.current[i] = e.nativeEvent.layout.x; + }} + onPress={() => onSelectTag(Number(route.key))} + style={tw`h-12 px-3 justify-center`} + activeOpacity={0.7} + > + + + {route.title} + + + + + ); + })} + + ); +}); + +interface TagSceneProps { + dashboardTags: any[]; + activeTagId: number | null; + productsByTagId: Record; + expandedByTagId: Record; + onToggleExpand: (tagId: number) => void; + onProductPress: (id: number) => void; +} + +// Active tag's products grid — rendered inside the vertical FlatList so it +// scrolls with the page. +const TagScene = memo(({ + dashboardTags, + activeTagId, + productsByTagId, + expandedByTagId, + onToggleExpand, + onProductPress, +}: TagSceneProps) => { + const tagId = activeTagId ?? dashboardTags[0]?.id ?? null; + if (tagId == null) return null; + + const products = productsByTagId[tagId] || []; + const expanded = !!expandedByTagId[tagId]; + const visible = expanded ? products : products.slice(0, 6); + const hasMore = products.length > 6 && !expanded; + + return ( + + {products.length > 0 ? ( + + {visible.map((product: any) => ( + + ))} + + ) : ( + + + No products in this category yet + + + )} + {hasMore && ( + onToggleExpand(tagId)} + > + Show More + + )} + ); }); @@ -354,72 +378,58 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => { }); interface ListHeaderProps { - gradientHeight: number; - onGradientLayout: (height: number) => void; - storesData: any; - sortedSlots: any[]; - onProductPress: (id: number) => void; dashboardTags: any[]; activeTagId: number | null; productsByTagId: Record; + expandedByTagId: Record; + onToggleExpand: (tagId: number) => void; + onProductPress: (id: number) => void; onSelectTag: (id: number) => void; + storesData: any; + sortedSlots: any[]; } const ListHeader = memo(({ - gradientHeight, - onGradientLayout, - storesData, - sortedSlots, - onProductPress, dashboardTags, activeTagId, productsByTagId, + expandedByTagId, + onToggleExpand, + onProductPress, onSelectTag, + storesData, + sortedSlots, }: ListHeaderProps) => { - const handleLayout = useCallback((event: any) => { - const { y, height } = event.nativeEvent.layout; - onGradientLayout(y + height); - }, [onGradientLayout]); - const renderSlotItem = useCallback(({ item }: { item: any }) => ( ), []); - const gradientStyle = useMemo(() => [ - tw`absolute left-0 right-0 shadow-lg`, - { height: gradientHeight + 32, zIndex: -1 } - ], [gradientHeight]); - const activeColor = activeTagId == null ? null : getTagColor(activeTagId); - const pageTint = activeColor?.pageBg ?? '#FFFFFF'; - return ( <> - - 0 && ( + - - {dashboardTags.length > 0 && ( - - - - )} - - + )} + {dashboardTags.length > 0 && ( + + + + )} ([]); const [visibleCount, setVisibleCount] = useState(21); const [hasMore, setHasMore] = useState(true); @@ -524,6 +533,10 @@ export default function Dashboard() { const [selectedTagId, setSelectedTagId] = useState(null); const activeTagId = selectedTagId ?? dashboardTags[0]?.id ?? null; + const [expandedByTagId, setExpandedByTagId] = useState>({}); + const handleToggleExpand = useCallback((tagId: number) => { + setExpandedByTagId((prev) => ({ ...prev, [tagId]: true })); + }, []); React.useEffect(() => { @@ -642,10 +655,6 @@ export default function Dashboard() { 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]); @@ -657,17 +666,17 @@ export default function Dashboard() { const listHeader = useMemo(() => ( - ), [gradientHeight, handleGradientLayout, storesData, sortedSlots, handleProductPress, dashboardTags, activeTagId, productsByTagId]); + ), [dashboardTags, activeTagId, productsByTagId, expandedByTagId, handleToggleExpand, handleProductPress, storesData, sortedSlots]); const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg; const pageTintStyle = useMemo(() => [ @@ -762,7 +771,9 @@ export default function Dashboard() { No products available } - removeClippedSubviews={true} + // NOTE: do not add removeClippedSubviews here - it breaks touch + // delivery to the nested horizontal scrollables in ListHeaderComponent + // (tag tabs / slots) on Android. maxToRenderPerBatch={10} windowSize={5} initialNumToRender={10} diff --git a/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx index 5428556..04979cc 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx @@ -1,14 +1,13 @@ -import React from "react"; +import React, { useState } from "react"; import { View, Dimensions, ScrollView } from "react-native"; -import { LinearGradient } from "expo-linear-gradient"; import { useRouter } from "expo-router"; import { tw, useManualRefresh, useMarkDataFetchers, AppContainer, - MyFlatList, MyText, + MyTouchableOpacity, } from "common-ui"; import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import ProductCard from "@/components/ProductCard"; @@ -17,34 +16,35 @@ import FloatingCartBar from "@/components/floating-cart-bar"; import TabLayoutWrapper from "@/components/TabLayoutWrapper"; const { width: screenWidth } = Dimensions.get("window"); -const itemWidth = screenWidth * 0.45; +// Same 3-column hero card sizing used on the home page. +const heroItemWidth = (screenWidth - 72) / 3; +const SHOW_MORE_STEP = 6; -const rowListContent = { paddingBottom: 16, paddingHorizontal: 16 }; - -interface OffersRowProps { +interface OffersSectionProps { title: string; subtitle: string; products: any[]; + expanded: boolean; + onExpand: () => void; onProductPress: (id: number) => void; } -const OffersRow = ({ title, subtitle, products, onProductPress }: OffersRowProps) => { - const renderItem = ({ item }: { item: any }) => ( - - onProductPress(item.id)} - showDeliveryInfo={false} - useAddToCartDialog={true} - miniView={true} - /> - - ); +// Grid section (home page style): shows the first SHOW_MORE_STEP products +// with a "Show More" button to reveal the rest. +const OffersSection = ({ + title, + subtitle, + products, + expanded, + onExpand, + onProductPress, +}: OffersSectionProps) => { + const visible = expanded ? products : products.slice(0, SHOW_MORE_STEP); + const hasMore = products.length > SHOW_MORE_STEP && !expanded; return ( - - + + {title} @@ -61,24 +61,32 @@ const OffersRow = ({ title, subtitle, products, onProductPress }: OffersRowProps ) : ( - - item.id.toString()} - horizontal - showsHorizontalScrollIndicator={false} - contentContainerStyle={rowListContent} - renderItem={renderItem} - removeClippedSubviews={true} - /> - - + <> + + {visible.map((item: any) => ( + + onProductPress(item.id)} + showDeliveryInfo={false} + useAddToCartDialog={true} + miniView={true} + variant="hero" + /> + + ))} + + {hasMore && ( + + Show More + + )} + )} ); @@ -86,6 +94,8 @@ const OffersRow = ({ title, subtitle, products, onProductPress }: OffersRowProps export default function Offers() { const router = useRouter(); + const [expandedOffers, setExpandedOffers] = useState(false); + const [expandedCombos, setExpandedCombos] = useState(false); const { data, isLoading, error, refetch } = trpc.user.product.getOffersPage.useQuery(); @@ -135,17 +145,21 @@ export default function Offers() { return ( - - - setExpandedOffers(true)} + onProductPress={handleProductPress} + /> + + setExpandedCombos(true)} onProductPress={handleProductPress} /> diff --git a/apps/user-ui/app/_layout.tsx b/apps/user-ui/app/_layout.tsx index 2c02370..f6da538 100755 --- a/apps/user-ui/app/_layout.tsx +++ b/apps/user-ui/app/_layout.tsx @@ -28,6 +28,7 @@ import WebViewWrapper from "@/components/WebViewWrapper"; import BackHandlerWrapper from "@/components/BackHandler"; import AddToCartDialog from "@/src/components/AddToCartDialog"; import React from "react"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import NotifChecker from "@/services/notif-service/notif-checker"; export default function RootLayout() { @@ -47,9 +48,10 @@ export default function RootLayout() { } return ( - - - + + + + + ); }