This commit is contained in:
shafi54 2026-08-08 21:49:42 +05:30
parent 6a0bc18a8d
commit 8e2e863b84

View file

@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, memo, useRef } from "react"; import React, { useState, useCallback, useMemo, memo } from "react";
import { View, Dimensions, Image, RefreshControl, ScrollView } from "react-native"; import { View, Dimensions, Image, RefreshControl } from "react-native";
import { TabView, TabBar } from "react-native-tab-view"; import { TabView, TabBar } from "react-native-tab-view";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
@ -39,7 +39,7 @@ const gridItemWidth = (screenWidth - 48) / 2;
// Hero product card geometry (used to size the tab scene heights) // Hero product card geometry (used to size the tab scene heights)
const heroCardImageHeight = heroItemWidth * 0.82; const heroCardImageHeight = heroItemWidth * 0.82;
const heroCardTextBlock = 86; const heroCardTextBlock = 92;
const heroCardHeight = heroCardImageHeight + heroCardTextBlock; const heroCardHeight = heroCardImageHeight + heroCardTextBlock;
const heroRowHeight = heroCardHeight + 12; // + mb-3 const heroRowHeight = heroCardHeight + 12; // + mb-3
const TAG_GRID_COLUMNS = 3; const TAG_GRID_COLUMNS = 3;
@ -60,9 +60,9 @@ const formatTimeRange = (deliveryTime: string) => {
const staticStyles = { const staticStyles = {
flatListContent: { gap: 16 }, flatListContent: { gap: 16 },
columnWrapper: { gap: 16, paddingHorizontal: 16 }, columnWrapper: { justifyContent: 'space-between', paddingHorizontal: 16 },
slotsListContent: { paddingBottom: 24 }, slotsListContent: { paddingBottom: 24 },
}; } as const;
const TAG_COLORS = [ const TAG_COLORS = [
{ pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose { pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose
@ -195,45 +195,6 @@ const SlotCard = memo(({ slot }: SlotCardProps) => {
); );
}); });
interface ExploreTabProps {
tag: any;
isSelected: boolean;
onPress: () => void;
}
const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
return (
<MyTouchableOpacity
onPress={onPress}
activeOpacity={0.75}
style={tw`px-2 pt-2 pb-1`}
>
<View style={tw`items-center`}>
<MyText
style={[
tw`text-base tracking-tight`,
{
color: isSelected ? theme.colors.brand600 : '#64748B',
fontWeight: isSelected ? '700' : '500',
},
]}
>
{tag.tagName}
</MyText>
<View
style={{
width: '100%',
height: 4,
borderRadius: 999,
marginTop: 8,
backgroundColor: isSelected ? theme.colors.brand500 : 'transparent',
}}
/>
</View>
</MyTouchableOpacity>
);
});
interface TagTabViewProps { interface TagTabViewProps {
dashboardTags: any[]; dashboardTags: any[];
activeTagId: number | null; activeTagId: number | null;
@ -261,17 +222,18 @@ const TagTabView = memo(({
return idx < 0 ? 0 : idx; return idx < 0 ? 0 : idx;
}, [dashboardTags, activeTagId]); }, [dashboardTags, activeTagId]);
// Height: fit the max visible product rows across all tags (default 6 per tag). // 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 sceneHeight = useMemo(() => {
let maxRows = 1; const tagId = Number(activeTagKey);
for (const tag of dashboardTags) { const count = (productsByTagId[tagId]?.length) || 0;
const count = (productsByTagId[tag.id] || []).length; const expanded = expandedByTagId[tagId];
const visible = expandedByTagId[tag.id] ? count : Math.min(count, 6); const visible = expanded ? count : Math.min(count, 6);
const rows = Math.max(1, Math.ceil(visible / TAG_GRID_COLUMNS)); const rows = Math.max(1, Math.ceil(visible / TAG_GRID_COLUMNS));
maxRows = Math.max(maxRows, rows); const showMore = count > 6 && !expanded;
} return rows * heroRowHeight + 24 + (showMore ? 52 : 0);
return maxRows * heroRowHeight + 16; }, [activeTagKey, productsByTagId, expandedByTagId]);
}, [dashboardTags, productsByTagId, expandedByTagId]);
const renderTabBar = useCallback((props: any) => ( const renderTabBar = useCallback((props: any) => (
<TabBar <TabBar
@ -339,37 +301,6 @@ const TagTabView = memo(({
); );
}); });
interface StickyTabsRowProps {
dashboardTags: any[];
activeTagId: number | null;
onSelectTag: (id: number) => void;
}
const StickyTabsRow = memo(({ dashboardTags, activeTagId, onSelectTag }: StickyTabsRowProps) => {
const scrollRef = useRef<ScrollView>(null);
const handleSelect = (id: number) => {
onSelectTag(id);
const idx = dashboardTags.findIndex((tag) => tag.id === id);
if (idx >= 0) {
scrollRef.current?.scrollTo({ x: idx * 96, animated: true });
}
};
return (
<ScrollView
ref={scrollRef}
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={() => handleSelect(tag.id)} />
))}
</ScrollView>
);
});
interface ExploreProductItemProps { interface ExploreProductItemProps {
item: any; item: any;
onPress: (id: number) => void; onPress: (id: number) => void;
@ -408,14 +339,17 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]); const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
return ( return (
<ProductCard <View style={tw`mb-3`}>
item={item} <ProductCard
itemWidth={gridItemWidth} item={item}
onPress={handlePress} itemWidth={heroItemWidth}
showDeliveryInfo={true} onPress={handlePress}
miniView={false} showDeliveryInfo={false}
useAddToCartDialog={true} miniView={true}
/> useAddToCartDialog={true}
variant="hero"
/>
</View>
); );
}); });
@ -429,7 +363,6 @@ interface ListHeaderProps {
activeTagId: number | null; activeTagId: number | null;
productsByTagId: Record<number, any[]>; productsByTagId: Record<number, any[]>;
onSelectTag: (id: number) => void; onSelectTag: (id: number) => void;
onTabsSectionLayout: (layout: { y: number; height: number }) => void;
} }
const ListHeader = memo(({ const ListHeader = memo(({
@ -442,7 +375,6 @@ const ListHeader = memo(({
activeTagId, activeTagId,
productsByTagId, productsByTagId,
onSelectTag, onSelectTag,
onTabsSectionLayout,
}: ListHeaderProps) => { }: ListHeaderProps) => {
const handleLayout = useCallback((event: any) => { const handleLayout = useCallback((event: any) => {
const { y, height } = event.nativeEvent.layout; const { y, height } = event.nativeEvent.layout;
@ -472,10 +404,6 @@ const ListHeader = memo(({
{dashboardTags.length > 0 && ( {dashboardTags.length > 0 && (
<View <View
onLayout={(event) => {
const { y, height } = event.nativeEvent.layout;
onTabsSectionLayout({ y, height });
}}
style={[ style={[
tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-[30px]`, tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-[30px]`,
{ backgroundColor: theme.colors.brand25 }, { backgroundColor: theme.colors.brand25 },
@ -549,8 +477,8 @@ const ListHeader = memo(({
</View> </View>
)} )}
<View style={tw`mt-2 mb-4`}> <View style={tw`mt-2 mb-[11px]`}>
<View style={tw`flex-row items-center justify-between px-1 mb-4`}> <View style={tw`flex-row items-center justify-between px-1 mb-2`}>
<View> <View>
<View style={tw`flex-row items-center mb-1.5`}> <View style={tw`flex-row items-center mb-1.5`}>
<View style={tw`w-1 h-5 rounded-full bg-brand500 mr-2.5`} /> <View style={tw`w-1 h-5 rounded-full bg-brand500 mr-2.5`} />
@ -572,11 +500,8 @@ export default function Dashboard() {
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false); const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const [gradientHeight, setGradientHeight] = useState(0); const [gradientHeight, setGradientHeight] = useState(0);
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]); const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
const [visibleCount, setVisibleCount] = useState(21);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [searchBarHeight, setSearchBarHeight] = useState(0);
const [tabsSectionLayout, setTabsSectionLayout] = useState({ y: 0, height: 0 });
const [showStickyTabs, setShowStickyTabs] = useState(false);
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);
@ -608,7 +533,7 @@ export default function Dashboard() {
return return
} }
const initialBatch = products const allSorted = products
.filter(p => typeof p.id === "number") .filter(p => typeof p.id === "number")
.sort((a, b) => { .sort((a, b) => {
const slotA = getQuickestSlot(a.id) const slotA = getQuickestSlot(a.id)
@ -622,10 +547,19 @@ export default function Dashboard() {
return 0 return 0
}) })
setDisplayedProducts(initialBatch) setDisplayedProducts(allSorted)
setHasMore(products.length > 10) setVisibleCount(21)
setHasMore(allSorted.length > 21)
}, [productsData, productSlotsMap]); }, [productsData, productSlotsMap]);
const handleShowMore = useCallback(() => {
setVisibleCount((prev) => {
const next = prev + 21;
setHasMore(next < displayedProducts.length);
return next;
});
}, [displayedProducts.length]);
const sortedSlots = useMemo(() => { const sortedSlots = useMemo(() => {
if (!slotsData?.slots) return []; if (!slotsData?.slots) return [];
const now = dayjs(); const now = dayjs();
@ -716,19 +650,6 @@ export default function Dashboard() {
router.push("/(drawer)/(tabs)/home/search-results"); router.push("/(drawer)/(tabs)/home/search-results");
}, [router]); }, [router]);
const handleTabsSectionLayout = useCallback((layout: { y: number; height: number }) => {
setTabsSectionLayout(layout);
}, []);
const handleListScroll = useCallback((event: any) => {
const scrollY = event.nativeEvent.contentOffset.y;
const stickyStart = tabsSectionLayout.y;
const stickyEnd = tabsSectionLayout.y + tabsSectionLayout.height - 56;
const shouldShowStickyTabs = tabsSectionLayout.height > 0 && scrollY > stickyStart && scrollY < stickyEnd;
setShowStickyTabs((current) => current === shouldShowStickyTabs ? current : shouldShowStickyTabs);
}, [tabsSectionLayout]);
const renderProductItem = useCallback(({ item }: { item: any }) => ( const renderProductItem = useCallback(({ item }: { item: any }) => (
<ProductItem item={item} onPress={handleProductPress} /> <ProductItem item={item} onPress={handleProductPress} />
// <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} /> // <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} />
@ -745,9 +666,8 @@ export default function Dashboard() {
activeTagId={activeTagId} activeTagId={activeTagId}
productsByTagId={productsByTagId} productsByTagId={productsByTagId}
onSelectTag={setSelectedTagId} onSelectTag={setSelectedTagId}
onTabsSectionLayout={handleTabsSectionLayout}
/> />
), [gradientHeight, handleGradientLayout, storesData, sortedSlots, handleProductPress, dashboardTags, activeTagId, productsByTagId, handleTabsSectionLayout]); ), [gradientHeight, handleGradientLayout, storesData, sortedSlots, handleProductPress, dashboardTags, activeTagId, productsByTagId]);
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg; const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
const pageTintStyle = useMemo(() => [ const pageTintStyle = useMemo(() => [
@ -791,7 +711,6 @@ export default function Dashboard() {
<View style={pageTintStyle}> <View style={pageTintStyle}>
<View <View
style={searchBarContainerStyle} style={searchBarContainerStyle}
onLayout={(event) => setSearchBarHeight(event.nativeEvent.layout.height)}
> >
<SearchBar <SearchBar
value={""} value={""}
@ -809,16 +728,27 @@ export default function Dashboard() {
</View> </View>
<MyFlatList <MyFlatList
data={displayedProducts} data={displayedProducts.slice(0, visibleCount)}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
numColumns={2} numColumns={3}
style={{ backgroundColor: '#FFFFFF' }} style={{ backgroundColor: '#FFFFFF' }}
onScroll={handleListScroll}
scrollEventThrottle={16}
contentContainerStyle={listContentContainerStyle} contentContainerStyle={listContentContainerStyle}
columnWrapperStyle={staticStyles.columnWrapper} columnWrapperStyle={staticStyles.columnWrapper}
renderItem={renderProductItem} renderItem={renderProductItem}
ListHeaderComponent={listHeader} ListHeaderComponent={listHeader}
ListFooterComponent={
hasMore ? (
<View style={tw`items-center py-4`}>
<MyTouchableOpacity
style={tw`px-6 py-3 rounded-full bg-brand500 shadow-sm`}
activeOpacity={0.85}
onPress={handleShowMore}
>
<MyText style={tw`text-white text-sm font-semibold`}>Show More</MyText>
</MyTouchableOpacity>
</View>
) : null
}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={isRefreshing} refreshing={isRefreshing}
@ -839,33 +769,6 @@ export default function Dashboard() {
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
/> />
{showStickyTabs && dashboardTags.length > 0 && (
<View
pointerEvents="box-none"
style={[
tw`absolute left-0 right-0 z-20 px-4 pb-2`,
{
top: searchBarHeight,
backgroundColor: '#FFFFFF',
elevation: 8,
},
]}
>
<View
style={[
tw`rounded-[28px] px-3 pt-2 pb-1`,
{ backgroundColor: theme.colors.brand25 },
]}
>
<StickyTabsRow
dashboardTags={dashboardTags}
activeTagId={activeTagId}
onSelectTag={setSelectedTagId}
/>
</View>
</View>
)}
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." /> <LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
<AddToCartDialog /> <AddToCartDialog />
<View style={tw`absolute bottom-2 left-4 right-4`}> <View style={tw`absolute bottom-2 left-4 right-4`}>