This commit is contained in:
shafi54 2026-08-04 23:06:27 +05:30
parent 9a47ff1a9b
commit fa56fd4a82
3 changed files with 199 additions and 167 deletions

View file

@ -9,8 +9,7 @@ import {
useMarkDataFetchers, useMarkDataFetchers,
LoadingDialog, LoadingDialog,
MyTouchableOpacity, MyTouchableOpacity,
MyText, SearchBar, useStatusBarStore, MyText, SearchBar
colors
} from "common-ui"; } from "common-ui";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -35,8 +34,8 @@ dayjs.extend(relativeTime);
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const itemWidth = screenWidth * 0.45; const itemWidth = screenWidth * 0.45;
const heroItemWidth = (screenWidth - 72) / 3;
const gridItemWidth = (screenWidth - 48) / 2; const gridItemWidth = (screenWidth - 48) / 2;
const headerColor = colors.secondaryPink;
const formatTimeRange = (deliveryTime: string) => { const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime); const time = dayjs(deliveryTime);
@ -58,16 +57,15 @@ const staticStyles = {
slotsListContent: { paddingBottom: 24 }, slotsListContent: { paddingBottom: 24 },
}; };
// Light/pastel color pairs for the Explore Products tabs.
const TAG_COLORS = [ const TAG_COLORS = [
{ bg: '#FFE4E6', border: '#FECDD3', text: '#BE123C' }, // rose { pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose
{ bg: '#FEF3C7', border: '#FDE68A', text: '#B45309' }, // amber { pageBg: '#FFFCF1', bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', dot: '#D97706' }, // amber
{ bg: '#DCFCE7', border: '#BBF7D0', text: '#15803D' }, // green { pageBg: '#F6FEF9', bg: '#F0FDF4', border: '#BBF7D0', text: '#166534', dot: '#16A34A' }, // green
{ bg: '#DBEAFE', border: '#BFDBFE', text: '#1D4ED8' }, // blue { pageBg: '#F5FAFF', bg: '#EFF6FF', border: '#BFDBFE', text: '#1E3A8A', dot: '#2563EB' }, // blue
{ bg: '#EDE9FE', border: '#DDD6FE', text: '#6D28D9' }, // violet { pageBg: '#FAF8FF', bg: '#F5F3FF', border: '#DDD6FE', text: '#5B21B6', dot: '#7C3AED' }, // violet
{ bg: '#FFE4CC', border: '#FFD6B0', text: '#C2410C' }, // orange { pageBg: '#FFF9F2', bg: '#FFF7ED', border: '#FFD6B0', text: '#9A3412', dot: '#EA580C' }, // orange
{ bg: '#CFFAFE', border: '#A5F3FC', text: '#0E7490' }, // cyan { pageBg: '#F3FEFF', bg: '#ECFEFF', border: '#A5F3FC', text: '#155E75', dot: '#0891B2' }, // cyan
{ bg: '#FCE7F3', border: '#FBCFE8', text: '#BE185D' }, // pink { pageBg: '#FFF7FB', bg: '#FDF2F8', border: '#FBCFE8', text: '#9D174D', dot: '#DB2777' }, // pink
]; ];
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length]; const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length];
@ -217,28 +215,34 @@ interface ExploreTabProps {
} }
const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => { const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
const color = getTagColor(tag.id);
const productCount = tag.productIds?.length || 0;
return ( return (
<MyTouchableOpacity <MyTouchableOpacity
onPress={onPress} onPress={onPress}
activeOpacity={0.8} activeOpacity={0.75}
style={[ style={tw`px-2 pt-2 pb-1`}
tw`px-4 py-2.5 rounded-full border`,
isSelected
? { backgroundColor: color.bg, borderColor: color.text }
: { backgroundColor: '#FFFFFF', borderColor: color.border },
]}
> >
<View style={tw`items-center`}>
<MyText <MyText
style={[ style={[
tw`text-sm font-semibold`, tw`text-base tracking-tight`,
{ color: color.text }, {
color: isSelected ? '#111827' : '#64748B',
fontWeight: isSelected ? '700' : '500',
},
]} ]}
> >
{tag.tagName} ({productCount}) {tag.tagName}
</MyText> </MyText>
<View
style={{
width: '100%',
height: 4,
borderRadius: 999,
marginTop: 8,
backgroundColor: isSelected ? '#111827' : 'transparent',
}}
/>
</View>
</MyTouchableOpacity> </MyTouchableOpacity>
); );
}); });
@ -252,14 +256,15 @@ const ExploreProductItem = memo(({ item, onPress }: ExploreProductItemProps) =>
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]); const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
return ( return (
<View style={tw`mr-4`}> <View style={tw`mb-3`}>
<ProductCard <ProductCard
item={item} item={item}
itemWidth={itemWidth} itemWidth={heroItemWidth}
onPress={handlePress} onPress={handlePress}
showDeliveryInfo={false} showDeliveryInfo={false}
useAddToCartDialog={true} useAddToCartDialog={true}
miniView={true} miniView={true}
variant="hero"
/> />
</View> </View>
); );
@ -316,19 +321,20 @@ const ListHeader = memo(({
activeTagProducts, activeTagProducts,
onSelectTag, onSelectTag,
}: ListHeaderProps) => { }: ListHeaderProps) => {
const [showAllActiveProducts, setShowAllActiveProducts] = useState(false);
const handleLayout = useCallback((event: any) => { const handleLayout = useCallback((event: any) => {
const { y, height } = event.nativeEvent.layout; const { y, height } = event.nativeEvent.layout;
onGradientLayout(y + height); onGradientLayout(y + height);
}, [onGradientLayout]); }, [onGradientLayout]);
React.useEffect(() => {
setShowAllActiveProducts(false);
}, [activeTagId]);
const renderPopularItem = useCallback(({ item }: { item: any }) => ( const renderPopularItem = useCallback(({ item }: { item: any }) => (
<PopularProductItem item={item} onPress={onProductPress} /> <PopularProductItem item={item} onPress={onProductPress} />
), [onProductPress]); ), [onProductPress]);
const renderExploreItem = useCallback(({ item }: { item: any }) => (
<ExploreProductItem item={item} onPress={onProductPress} />
), [onProductPress]);
const renderSlotItem = useCallback(({ item }: { item: any }) => ( const renderSlotItem = useCallback(({ item }: { item: any }) => (
<SlotItem item={item} /> <SlotItem item={item} />
), []); ), []);
@ -337,25 +343,92 @@ const ListHeader = memo(({
tw`absolute left-0 right-0 shadow-lg`, tw`absolute left-0 right-0 shadow-lg`,
{ height: gradientHeight + 32, zIndex: -1 } { height: gradientHeight + 32, zIndex: -1 }
], [gradientHeight]); ], [gradientHeight]);
const activeColor = activeTagId == null ? null : getTagColor(activeTagId);
const pageTint = activeColor?.pageBg ?? '#FFFFFF';
const visibleActiveTagProducts = showAllActiveProducts ? activeTagProducts : activeTagProducts.slice(0, 6);
const hasMoreActiveTagProducts = activeTagProducts.length > visibleActiveTagProducts.length;
return ( return (
<> <>
<View onLayout={handleLayout}> <View onLayout={handleLayout} style={{ backgroundColor: pageTint }}>
<LinearGradient <LinearGradient
colors={[headerColor, headerColor]} colors={[pageTint, pageTint]}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0.5 }} end={{ x: 1, y: 0.5 }}
style={gradientStyle} style={gradientStyle}
/> />
{dashboardTags.length > 0 && (
<View
style={[
tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-2`,
{ backgroundColor: pageTint },
]}
>
<ScrollView
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={() => onSelectTag(tag.id)}
/>
))}
</ScrollView>
{activeTagProducts.length > 0 ? (
<View style={[tw`mt-4 relative`, { backgroundColor: pageTint }]}>
<View style={tw`flex-row flex-wrap justify-between`}>
{visibleActiveTagProducts.map((product: any) => (
<ExploreProductItem
key={product.id}
item={product}
onPress={onProductPress}
/>
))}
</View>
{hasMoreActiveTagProducts && (
<MyTouchableOpacity
style={tw`self-center mt-1 mb-2 px-5 py-2.5 rounded-full bg-gray-900`}
activeOpacity={0.85}
onPress={() => setShowAllActiveProducts(true)}
>
<MyText style={tw`text-white text-sm font-semibold`}>Show More</MyText>
</MyTouchableOpacity>
)}
</View>
) : (
<View style={tw`py-6 items-center`}>
<MyText style={tw`text-sm text-gray-500 font-medium`}>
No products in this category yet
</MyText>
</View>
)}
</View>
)}
</View>
<View style={tw`py-4`}>
<BannerCarousel />
</View>
<View
style={[
tw`rounded-t-3xl px-4`,
{ backgroundColor: pageTint },
]}
>
{storesData?.stores && storesData.stores.length > 0 && ( {storesData?.stores && storesData.stores.length > 0 && (
<View style={tw`px-4 pb-2`}> <View style={tw`mt-4 mb-5 rounded-[28px] bg-white/70 border border-white px-3 pt-4 pb-2`}>
<View style={tw`flex-row items-center justify-between mb-4 px-1`}> <View style={tw`flex-row items-center justify-between mb-4 px-1`}>
<View> <View>
<MyText style={tw`text-xl font-extrabold text-white tracking-tight drop-shadow-md text-neutral-800`}> <MyText style={tw`text-xl font-extrabold text-gray-900 tracking-tight`}>
Our Stores Our Stores
</MyText> </MyText>
<MyText style={tw`text-xs font-medium mt-0.5 text-neutral-800`}>Fresh from our locations</MyText> <MyText style={tw`text-xs font-medium mt-0.5 text-gray-500`}>Fresh from our locations</MyText>
</View> </View>
</View> </View>
<View style={tw`pb-2`}> <View style={tw`pb-2`}>
@ -369,13 +442,7 @@ const ListHeader = memo(({
</View> </View>
</View> </View>
)} )}
</View>
<View style={tw`py-4`}>
<BannerCarousel />
</View>
<View style={tw`bg-white rounded-t-3xl px-4`}>
<View style={tw`py-2`}> <View style={tw`py-2`}>
<NextOrderGlimpse /> <NextOrderGlimpse />
</View> </View>
@ -404,55 +471,6 @@ const ListHeader = memo(({
/> />
</View> </View>
{dashboardTags.length > 0 && (
<View style={tw`mb-4`}>
<View style={tw`px-1 mb-2`}>
<MyText style={tw`text-2xl font-extrabold text-gray-900 tracking-tight`}>Explore Products</MyText>
<MyText style={tw`text-sm text-gray-500 font-medium mt-1`}>Browse by category</MyText>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw`gap-2 py-2`}
>
{dashboardTags.map((tag) => (
<ExploreTab
key={tag.id}
tag={tag}
isSelected={activeTagId === tag.id}
onPress={() => onSelectTag(tag.id)}
/>
))}
</ScrollView>
{activeTagProducts.length > 0 ? (
<View style={tw`mt-3 relative`}>
<MyFlatList
data={activeTagProducts}
keyExtractor={(item) => item.id.toString()}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={staticStyles.popularListContent}
renderItem={renderExploreItem}
removeClippedSubviews={true}
/>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.08)"]}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={tw`absolute right-0 top-0 bottom-4 w-12 rounded-l-xl`}
pointerEvents="none"
/>
</View>
) : (
<View style={tw`py-6 items-center`}>
<MyText style={tw`text-sm text-gray-500 font-medium`}>
No products in this category yet
</MyText>
</View>
)}
</View>
)}
{sortedSlots.length > 0 && ( {sortedSlots.length > 0 && (
<View style={tw`mt-2 mb-4`}> <View style={tw`mt-2 mb-4`}>
<View style={tw`flex-row items-center justify-between px-1 mb-6`}> <View style={tw`flex-row items-center justify-between px-1 mb-6`}>
@ -497,7 +515,6 @@ export default function Dashboard() {
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]); const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore();
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);
@ -660,10 +677,16 @@ export default function Dashboard() {
/> />
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts]); ), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts]);
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
const pageTintStyle = useMemo(() => [
tw`flex-1`,
{ backgroundColor: pageTint }
], [pageTint]);
const searchBarContainerStyle = useMemo(() => [ const searchBarContainerStyle = useMemo(() => [
tw`w-full px-4 pt-4 pb-2`, tw`w-full px-4 pt-4 pb-0`,
{ backgroundColor } { backgroundColor: pageTint }
], [backgroundColor]); ], [pageTint]);
const listContentContainerStyle = useMemo(() => [ const listContentContainerStyle = useMemo(() => [
tw`pb-24`, tw`pb-24`,
@ -695,7 +718,8 @@ export default function Dashboard() {
displayedProducts.forEach(product => str += `${product.id}-`) displayedProducts.forEach(product => str += `${product.id}-`)
// console.log(str) // console.log(str)
return ( return (
<TabLayoutWrapper> <TabLayoutWrapper style={{ backgroundColor: pageTint }}>
<View style={pageTintStyle}>
<View style={searchBarContainerStyle}> <View style={searchBarContainerStyle}>
<SearchBar <SearchBar
value={""} value={""}
@ -716,6 +740,7 @@ export default function Dashboard() {
data={displayedProducts} data={displayedProducts}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
numColumns={2} numColumns={2}
style={{ backgroundColor: pageTint }}
contentContainerStyle={listContentContainerStyle} contentContainerStyle={listContentContainerStyle}
columnWrapperStyle={staticStyles.columnWrapper} columnWrapperStyle={staticStyles.columnWrapper}
renderItem={renderProductItem} renderItem={renderProductItem}
@ -745,6 +770,7 @@ export default function Dashboard() {
<View style={tw`absolute bottom-2 left-4 right-4`}> <View style={tw`absolute bottom-2 left-4 right-4`}>
<FloatingCartBar /> <FloatingCartBar />
</View> </View>
</View>
</TabLayoutWrapper> </TabLayoutWrapper>
); );
} }

View file

@ -24,6 +24,7 @@ interface ProductCardProps {
onPress?: () => void; onPress?: () => void;
showDeliveryInfo?: boolean; showDeliveryInfo?: boolean;
miniView?: boolean; miniView?: boolean;
variant?: 'default' | 'hero';
nullIfNotAvailable?: boolean; nullIfNotAvailable?: boolean;
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>; containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
useAddToCartDialog?: boolean; useAddToCartDialog?: boolean;
@ -42,10 +43,12 @@ const ProductCard: React.FC<ProductCardProps> = ({
onPress, onPress,
showDeliveryInfo = true, showDeliveryInfo = true,
miniView = false, miniView = false,
variant = 'default',
nullIfNotAvailable = false, nullIfNotAvailable = false,
containerComp: ContainerComp = React.Fragment, containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false, useAddToCartDialog = false,
}) => { }) => {
const isHero = variant === 'hero';
const imageUri = item.images?.[0] const imageUri = item.images?.[0]
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading') const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
const [imageError, setImageError] = React.useState<string | null>(null) const [imageError, setImageError] = React.useState<string | null>(null)
@ -152,7 +155,8 @@ const ProductCard: React.FC<ProductCardProps> = ({
<ContainerComp> <ContainerComp>
<MyTouchableOpacity <MyTouchableOpacity
style={[ style={[
tw`bg-white rounded-2xl overflow-hidden border border-gray-200 pb-2`, tw`bg-white overflow-hidden border border-gray-200`,
isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`,
{ width: itemWidth }, { width: itemWidth },
]} ]}
onPress={onPress || (() => {/* TODO: Navigate to product detail */})} onPress={onPress || (() => {/* TODO: Navigate to product detail */})}
@ -162,7 +166,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
<RnImage <RnImage
source={{ uri: imageUri }} source={{ uri: imageUri }}
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}} // source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }} style={{ width: "100%", height: isHero ? itemWidth * 0.82 : itemWidth, resizeMode: "cover" }}
onLoadStart={() => { onLoadStart={() => {
setImageStatus('loading') setImageStatus('loading')
setImageError(null) setImageError(null)
@ -194,36 +198,36 @@ const ProductCard: React.FC<ProductCardProps> = ({
</View> </View>
)} )}
{miniView && ( {miniView && (
<View style={tw`absolute bottom-2 right-2`}> <View style={isHero ? tw`absolute bottom-1.5 right-1.5` : tw`absolute bottom-2 right-2`}>
{quantity > 0 ? ( {quantity > 0 ? (
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} /> <MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
) : ( ) : (
<MyTouchableOpacity <MyTouchableOpacity
style={tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`} style={isHero ? tw`w-7 h-7 rounded-full bg-white items-center justify-center shadow-md` : tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
onPress={() => handleQuantityChange(1)} onPress={() => handleQuantityChange(1)}
activeOpacity={0.8} activeOpacity={0.8}
> >
<CartIcon focused={false} size={16} color="#2E90FA" /> <CartIcon focused={false} size={isHero ? 14 : 16} color="#2E90FA" />
</MyTouchableOpacity> </MyTouchableOpacity>
)} )}
</View> </View>
)} )}
</View> </View>
<View style={tw`px-3 pt-3`}> <View style={isHero ? tw`px-2 pt-2` : tw`px-3 pt-3`}>
<MyText style={tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}> <MyText style={isHero ? tw`text-gray-900 font-semibold text-xs mb-1 leading-4` : tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}>
{item.name} {item.name}
</MyText> </MyText>
<View style={tw`flex-row items-baseline mb-2`}> <View style={isHero ? tw`flex-row items-baseline mb-1` : tw`flex-row items-baseline mb-2`}>
<MyText style={tw`text-brand500 font-bold text-base`}>{item.price}</MyText> <MyText style={isHero ? tw`text-brand500 font-bold text-sm` : tw`text-brand500 font-bold text-base`}>{item.price}</MyText>
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && ( {item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<MyText style={tw`text-gray-400 text-xs ml-2 line-through`}>{item.marketPrice}</MyText> <MyText style={isHero ? tw`text-gray-400 text-[10px] ml-1.5 line-through` : tw`text-gray-400 text-xs ml-2 line-through`}>{item.marketPrice}</MyText>
)} )}
</View> </View>
<View style={tw`flex-row items-center mb-2`}> <View style={isHero ? tw`flex-row items-center mb-1` : tw`flex-row items-center mb-2`}>
{item.productType !== 'combo' && ( {item.productType !== 'combo' && (
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText> <MyText style={isHero ? tw`text-gray-500 text-[10px] font-medium` : tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
)} )}
</View> </View>

View file

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { tw } from 'common-ui'; import { tw } from 'common-ui';
interface TabLayoutWrapperProps { interface TabLayoutWrapperProps {
children: React.ReactNode; children: React.ReactNode;
style?: StyleProp<ViewStyle>;
} }
export default function TabLayoutWrapper({ children }: TabLayoutWrapperProps) { export default function TabLayoutWrapper({ children, style }: TabLayoutWrapperProps) {
return ( return (
<SafeAreaView edges={['top']} style={tw`flex-1 bg-white`}> <SafeAreaView edges={['top']} style={[tw`flex-1 bg-white`, style]}>
{children} {children}
</SafeAreaView> </SafeAreaView>
); );