enh
This commit is contained in:
parent
6a0bc18a8d
commit
8e2e863b84
1 changed files with 58 additions and 155 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useCallback, useMemo, memo, useRef } from "react";
|
||||
import { View, Dimensions, Image, RefreshControl, ScrollView } from "react-native";
|
||||
import React, { useState, useCallback, useMemo, memo } 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 { useRouter } from "expo-router";
|
||||
|
|
@ -39,7 +39,7 @@ const gridItemWidth = (screenWidth - 48) / 2;
|
|||
|
||||
// Hero product card geometry (used to size the tab scene heights)
|
||||
const heroCardImageHeight = heroItemWidth * 0.82;
|
||||
const heroCardTextBlock = 86;
|
||||
const heroCardTextBlock = 92;
|
||||
const heroCardHeight = heroCardImageHeight + heroCardTextBlock;
|
||||
const heroRowHeight = heroCardHeight + 12; // + mb-3
|
||||
const TAG_GRID_COLUMNS = 3;
|
||||
|
|
@ -60,9 +60,9 @@ const formatTimeRange = (deliveryTime: string) => {
|
|||
|
||||
const staticStyles = {
|
||||
flatListContent: { gap: 16 },
|
||||
columnWrapper: { gap: 16, paddingHorizontal: 16 },
|
||||
columnWrapper: { justifyContent: 'space-between', paddingHorizontal: 16 },
|
||||
slotsListContent: { paddingBottom: 24 },
|
||||
};
|
||||
} as const;
|
||||
|
||||
const TAG_COLORS = [
|
||||
{ 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 {
|
||||
dashboardTags: any[];
|
||||
activeTagId: number | null;
|
||||
|
|
@ -261,17 +222,18 @@ const TagTabView = memo(({
|
|||
return idx < 0 ? 0 : idx;
|
||||
}, [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(() => {
|
||||
let maxRows = 1;
|
||||
for (const tag of dashboardTags) {
|
||||
const count = (productsByTagId[tag.id] || []).length;
|
||||
const visible = expandedByTagId[tag.id] ? count : Math.min(count, 6);
|
||||
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));
|
||||
maxRows = Math.max(maxRows, rows);
|
||||
}
|
||||
return maxRows * heroRowHeight + 16;
|
||||
}, [dashboardTags, productsByTagId, expandedByTagId]);
|
||||
const showMore = count > 6 && !expanded;
|
||||
return rows * heroRowHeight + 24 + (showMore ? 52 : 0);
|
||||
}, [activeTagKey, productsByTagId, expandedByTagId]);
|
||||
|
||||
const renderTabBar = useCallback((props: any) => (
|
||||
<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 {
|
||||
item: any;
|
||||
onPress: (id: number) => void;
|
||||
|
|
@ -408,14 +339,17 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
|
|||
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
|
||||
|
||||
return (
|
||||
<View style={tw`mb-3`}>
|
||||
<ProductCard
|
||||
item={item}
|
||||
itemWidth={gridItemWidth}
|
||||
itemWidth={heroItemWidth}
|
||||
onPress={handlePress}
|
||||
showDeliveryInfo={true}
|
||||
miniView={false}
|
||||
showDeliveryInfo={false}
|
||||
miniView={true}
|
||||
useAddToCartDialog={true}
|
||||
variant="hero"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -429,7 +363,6 @@ interface ListHeaderProps {
|
|||
activeTagId: number | null;
|
||||
productsByTagId: Record<number, any[]>;
|
||||
onSelectTag: (id: number) => void;
|
||||
onTabsSectionLayout: (layout: { y: number; height: number }) => void;
|
||||
}
|
||||
|
||||
const ListHeader = memo(({
|
||||
|
|
@ -442,7 +375,6 @@ const ListHeader = memo(({
|
|||
activeTagId,
|
||||
productsByTagId,
|
||||
onSelectTag,
|
||||
onTabsSectionLayout,
|
||||
}: ListHeaderProps) => {
|
||||
const handleLayout = useCallback((event: any) => {
|
||||
const { y, height } = event.nativeEvent.layout;
|
||||
|
|
@ -472,10 +404,6 @@ const ListHeader = memo(({
|
|||
|
||||
{dashboardTags.length > 0 && (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
const { y, height } = event.nativeEvent.layout;
|
||||
onTabsSectionLayout({ y, height });
|
||||
}}
|
||||
style={[
|
||||
tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-[30px]`,
|
||||
{ backgroundColor: theme.colors.brand25 },
|
||||
|
|
@ -549,8 +477,8 @@ const ListHeader = memo(({
|
|||
</View>
|
||||
)}
|
||||
|
||||
<View style={tw`mt-2 mb-4`}>
|
||||
<View style={tw`flex-row items-center justify-between px-1 mb-4`}>
|
||||
<View style={tw`mt-2 mb-[11px]`}>
|
||||
<View style={tw`flex-row items-center justify-between px-1 mb-2`}>
|
||||
<View>
|
||||
<View style={tw`flex-row items-center mb-1.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 [gradientHeight, setGradientHeight] = useState(0);
|
||||
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
|
||||
const [visibleCount, setVisibleCount] = useState(21);
|
||||
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 productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
|
||||
|
|
@ -608,7 +533,7 @@ export default function Dashboard() {
|
|||
return
|
||||
}
|
||||
|
||||
const initialBatch = products
|
||||
const allSorted = products
|
||||
.filter(p => typeof p.id === "number")
|
||||
.sort((a, b) => {
|
||||
const slotA = getQuickestSlot(a.id)
|
||||
|
|
@ -622,10 +547,19 @@ export default function Dashboard() {
|
|||
return 0
|
||||
})
|
||||
|
||||
setDisplayedProducts(initialBatch)
|
||||
setHasMore(products.length > 10)
|
||||
setDisplayedProducts(allSorted)
|
||||
setVisibleCount(21)
|
||||
setHasMore(allSorted.length > 21)
|
||||
}, [productsData, productSlotsMap]);
|
||||
|
||||
const handleShowMore = useCallback(() => {
|
||||
setVisibleCount((prev) => {
|
||||
const next = prev + 21;
|
||||
setHasMore(next < displayedProducts.length);
|
||||
return next;
|
||||
});
|
||||
}, [displayedProducts.length]);
|
||||
|
||||
const sortedSlots = useMemo(() => {
|
||||
if (!slotsData?.slots) return [];
|
||||
const now = dayjs();
|
||||
|
|
@ -716,19 +650,6 @@ export default function Dashboard() {
|
|||
router.push("/(drawer)/(tabs)/home/search-results");
|
||||
}, [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 }) => (
|
||||
<ProductItem item={item} onPress={handleProductPress} />
|
||||
// <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} />
|
||||
|
|
@ -745,9 +666,8 @@ export default function Dashboard() {
|
|||
activeTagId={activeTagId}
|
||||
productsByTagId={productsByTagId}
|
||||
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 pageTintStyle = useMemo(() => [
|
||||
|
|
@ -791,7 +711,6 @@ export default function Dashboard() {
|
|||
<View style={pageTintStyle}>
|
||||
<View
|
||||
style={searchBarContainerStyle}
|
||||
onLayout={(event) => setSearchBarHeight(event.nativeEvent.layout.height)}
|
||||
>
|
||||
<SearchBar
|
||||
value={""}
|
||||
|
|
@ -809,16 +728,27 @@ export default function Dashboard() {
|
|||
</View>
|
||||
|
||||
<MyFlatList
|
||||
data={displayedProducts}
|
||||
data={displayedProducts.slice(0, visibleCount)}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
numColumns={2}
|
||||
numColumns={3}
|
||||
style={{ backgroundColor: '#FFFFFF' }}
|
||||
onScroll={handleListScroll}
|
||||
scrollEventThrottle={16}
|
||||
contentContainerStyle={listContentContainerStyle}
|
||||
columnWrapperStyle={staticStyles.columnWrapper}
|
||||
renderItem={renderProductItem}
|
||||
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
|
||||
refreshing={isRefreshing}
|
||||
|
|
@ -839,33 +769,6 @@ export default function Dashboard() {
|
|||
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..." />
|
||||
<AddToCartDialog />
|
||||
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue