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 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`}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue