Compare commits

..

5 commits

Author SHA1 Message Date
shafi54
aa7035b77c enh 2026-02-04 20:50:44 +05:30
shafi54
02cb713dd1 enh 2026-02-04 16:06:07 +05:30
shafi54
a83168b324 enh 2026-02-04 15:55:17 +05:30
shafi54
c02f2c84f5 enh 2026-02-04 15:34:38 +05:30
shafi54
b0e84b7089 enh 2026-02-04 14:45:07 +05:30
18 changed files with 527 additions and 415 deletions

View file

@ -32,4 +32,6 @@ DELIVERY_CHARGE=20
# Telegram Configuration # Telegram Configuration
TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
TELEGRAM_CHAT_IDS=-5075171894 TELEGRAM_CHAT_IDS=
# TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
# TELEGRAM_CHAT_IDS=-5075171894

File diff suppressed because one or more lines are too long

View file

@ -824,7 +824,7 @@ export const orderRouter = router({
updateUserNotes: protectedProcedure updateUserNotes: protectedProcedure
.input( .input(
z.object({ z.object({
id: z.string(), id: z.number(),
userNotes: z.string(), userNotes: z.string(),
}) })
) )

View file

@ -21,6 +21,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
import AddToCartDialog from "@/src/components/AddToCartDialog";
import MyFlatList from "common-ui/src/components/flat-list"; import MyFlatList from "common-ui/src/components/flat-list";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
@ -421,6 +422,7 @@ export default function Dashboard() {
) )
} }
showDeliveryInfo={false} showDeliveryInfo={false}
useAddToCartDialog={true}
miniView={true} miniView={true}
/> />
</View> </View>
@ -623,19 +625,20 @@ export default function Dashboard() {
columnWrapperStyle={{gap: 16}} columnWrapperStyle={{gap: 16}}
renderItem={({ item, index }) => ( renderItem={({ item, index }) => (
<ProductCard <ProductCard
item={item} item={item}
itemWidth={gridItemWidth} itemWidth={gridItemWidth}
onPress={() => onPress={() =>
router.push( router.push(
`/(drawer)/(tabs)/home/product-detail/${item.id}` `/(drawer)/(tabs)/home/product-detail/${item.id}`
) )
} }
showDeliveryInfo={true} showDeliveryInfo={true}
miniView={false} miniView={false}
useAddToCartDialog={true}
key={item.id} key={item.id}
/> />
)} )}
initialNumToRender={4} initialNumToRender={4}
@ -665,6 +668,7 @@ export default function Dashboard() {
</View> </View>
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." /> <LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
<AddToCartDialog />
<View style={tw`h-16`}></View> <View style={tw`h-16`}></View>
</ScrollView> </ScrollView>
<View style={tw`absolute bottom-2 left-4 right-4`}> <View style={tw`absolute bottom-2 left-4 right-4`}>

View file

@ -1,34 +1,21 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { import { View, Dimensions } from "react-native";
View,
Dimensions,
Image,
Alert,
Platform,
} from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router"; import { useRouter, useLocalSearchParams } from "expo-router";
import { import {
theme,
tw, tw,
useManualRefresh, useManualRefresh,
useMarkDataFetchers, useMarkDataFetchers,
LoadingDialog,
AppContainer,
MyFlatList, MyFlatList,
MyText, MyText,
MyTextInput, SearchBar,
MyTouchableOpacity, } from "common-ui";
SearchBar } from "common-ui";
import dayjs from "dayjs";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
import { useGetCart, useAddToCart } from '@/hooks/cart-query-hooks';
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2; // 48 = padding horizontal (16*2) + gap (16) const itemWidth = (screenWidth - 48) / 2;
export default function SearchResults() { export default function SearchResults() {
const router = useRouter(); const router = useRouter();
@ -36,29 +23,20 @@ export default function SearchResults() {
const query = (q as string) || ""; const query = (q as string) || "";
const [inputQuery, setInputQuery] = useState(query); const [inputQuery, setInputQuery] = useState(query);
const [searchQuery, setSearchQuery] = useState(query); const [searchQuery, setSearchQuery] = useState(query);
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const searchInputRef = useRef<any>(null); const searchInputRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
// Auto-focus search bar on mount
setTimeout(() => { setTimeout(() => {
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}, 100); }, 100);
}, []); }, []);
const { const { data: productsData, isLoading, error, refetch } =
data: productsData, trpc.common.product.getAllProductsSummary.useQuery({
isLoading, searchQuery: searchQuery || undefined,
error, });
refetch,
} = trpc.common.product.getAllProductsSummary.useQuery({
searchQuery: searchQuery || undefined,
});
const { data: cartData, refetch: refetchCart } = useGetCart();
const products = productsData?.products || []; const products = productsData?.products || [];
const addToCart = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true });
useManualRefresh(() => { useManualRefresh(() => {
refetch(); refetch();
@ -68,43 +46,6 @@ export default function SearchResults() {
refetch(); refetch();
}); });
const handleAddToCart = (productId: number) => {
setIsLoadingDialogOpen(true);
addToCart.mutate(
{ productId, quantity: 1 },
{
onSuccess: () => {
Alert.alert("Success", "Item added to cart!");
refetchCart();
},
onError: (error: any) => {
Alert.alert("Error", error.message || "Failed to add item to cart");
},
onSettled: () => {
setIsLoadingDialogOpen(false);
},
}
);
};
const handleBuyNow = (productId: number) => {
setIsLoadingDialogOpen(true);
addToCart.mutate(
{ productId, quantity: 1 },
{
onSuccess: () => {
router.push(`/(drawer)/(tabs)/home/cart?select=${productId}`);
},
onError: (error: any) => {
Alert.alert("Error", error.message || "Failed to add item to cart");
},
onSettled: () => {
setIsLoadingDialogOpen(false);
},
}
);
};
const handleSearch = () => { const handleSearch = () => {
setSearchQuery(inputQuery); setSearchQuery(inputQuery);
}; };
@ -135,20 +76,19 @@ export default function SearchResults() {
renderItem={({ item }) => ( renderItem={({ item }) => (
<ProductCard <ProductCard
item={item} item={item}
handleAddToCart={handleAddToCart}
handleBuyNow={handleBuyNow}
itemWidth={itemWidth} itemWidth={itemWidth}
onPress={() => router.push(`/(drawer)/(tabs)/home/product-detail/${item.id}`)} onPress={() =>
router.push(`/(drawer)/(tabs)/home/product-detail/${item.id}`)
}
showDeliveryInfo={false} showDeliveryInfo={false}
iconType="flash" useAddToCartDialog={true}
/> />
)} )}
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
columnWrapperStyle={{ gap: 16, justifyContent: 'center' }} columnWrapperStyle={{ gap: 16, justifyContent: "center" }}
contentContainerStyle={[tw`pb-24`, { gap: 16 }]} contentContainerStyle={[tw`pb-24`, { gap: 16 }]}
ListHeaderComponent={ ListHeaderComponent={
<View style={tw`pt-4 pb-2 px-4`}> <View style={tw`pt-4 pb-2 px-4`}>
{/* Search Bar */}
<SearchBar <SearchBar
ref={searchInputRef} ref={searchInputRef}
value={inputQuery} value={inputQuery}
@ -156,21 +96,19 @@ export default function SearchResults() {
onSubmitEditing={handleSearch} onSubmitEditing={handleSearch}
returnKeyType="search" returnKeyType="search"
/> />
{/* Section Title */}
<View style={tw`flex-row justify-between items-center mb-2`}> <View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`text-lg font-bold text-gray-900`}> <MyText style={tw`text-lg font-bold text-gray-900`}>
{searchQuery ? `Search Results for "${searchQuery}"` : 'All Products'} {searchQuery
? `Search Results for "${searchQuery}"`
: "All Products"}
</MyText> </MyText>
</View> </View>
</View> </View>
} }
/> />
<View style={tw`absolute bottom-2 left-4 right-4`}>
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
<View style={tw`absolute bottom-2 left-4 right-4`}>
<FloatingCartBar /> <FloatingCartBar />
</View> </View>
</View> </View>
); );
} }

View file

@ -236,7 +236,7 @@ export default function OrderDetails() {
</MyTouchableOpacity> </MyTouchableOpacity>
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => { onPress={() => {
updateNotesMutation.mutate({ id: order.orderId, userNotes: notesInput }); updateNotesMutation.mutate({ id: order.id, userNotes: notesInput });
}} }}
disabled={updateNotesMutation.isPending} disabled={updateNotesMutation.isPending}
style={tw`px-3 py-1 bg-brand500 rounded-lg`} style={tw`px-3 py-1 bg-brand500 rounded-lg`}
@ -435,7 +435,7 @@ export default function OrderDetails() {
setComplaintDialogOpen(false); setComplaintDialogOpen(false);
refetch(); refetch();
}} }}
orderId={order.orderId} orderId={order.id}
/> />
</BottomDialog> </BottomDialog>
</View> </View>

View file

@ -1,54 +1,31 @@
import React, { useState } from "react"; import React from "react";
import { import { View, Dimensions, ScrollView } from "react-native";
View,
Dimensions,
Image,
Platform,
Alert,
ScrollView,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { import {
theme,
tw, tw,
useManualRefresh, useManualRefresh,
useMarkDataFetchers, useMarkDataFetchers,
LoadingDialog,
AppContainer, AppContainer,
MyFlatList, MyFlatList,
MyText, MyText,
} from "common-ui"; } from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
import { useGetCart, useAddToCart } from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
import TabLayoutWrapper from "@/components/TabLayoutWrapper"; import TabLayoutWrapper from "@/components/TabLayoutWrapper";
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2; // 2 items per row with padding const itemWidth = (screenWidth - 48) / 2;
export default function OrderAgain() { export default function OrderAgain() {
const router = useRouter(); const router = useRouter();
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const { const { data: recentProductsData, isLoading, error, refetch } =
data: recentProductsData, trpc.user.order.getRecentlyOrderedProducts.useQuery({
isLoading, limit: 20,
error, });
refetch,
} = trpc.user.order.getRecentlyOrderedProducts.useQuery({
limit: 20,
});
const { data: cartData, refetch: refetchCart } = useGetCart();
const { addToCart = () => {} } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }) || {};
const { getQuickestSlot } = useProductSlotIdentifier();
const recentProducts = recentProductsData?.products || []; const recentProducts = recentProductsData?.products || [];
@ -60,37 +37,14 @@ export default function OrderAgain() {
refetch(); refetch();
}); });
const handleAddToCart = (productId: number) => {
const slotId = getQuickestSlot(productId);
if (!slotId) {
Alert.alert("Error", "No available delivery slot for this product");
return;
}
setIsLoadingDialogOpen(true);
addToCart(productId, 1, slotId, () => setIsLoadingDialogOpen(false));
};
const handleBuyNow = (productId: number) => {
const slotId = getQuickestSlot(productId);
if (!slotId) {
Alert.alert("Error", "No available delivery slot for this product");
return;
}
setIsLoadingDialogOpen(true);
addToCart(productId, 1, slotId, () => {
setIsLoadingDialogOpen(false);
router.push(`/(drawer)/(tabs)/home/cart?select=${productId}`);
});
};
if (isLoading) { if (isLoading) {
return ( return (
<AppContainer> <AppContainer>
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<MaterialIcons name="refresh" size={48} color="#3B82F6" /> <MaterialIcons name="refresh" size={48} color="#3B82F6" />
<MyText style={tw`text-gray-500 font-medium mt-4`}>Loading your recent orders...</MyText> <MyText style={tw`text-gray-500 font-medium mt-4`}>
Loading your recent orders...
</MyText>
</View> </View>
</AppContainer> </AppContainer>
); );
@ -102,7 +56,9 @@ export default function OrderAgain() {
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" /> <MaterialIcons name="error-outline" size={48} color="#EF4444" />
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText> <MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
<MyText style={tw`text-gray-500 mt-2`}>Failed to load recent orders</MyText> <MyText style={tw`text-gray-500 mt-2`}>
Failed to load recent orders
</MyText>
</View> </View>
</AppContainer> </AppContainer>
); );
@ -112,7 +68,7 @@ export default function OrderAgain() {
<TabLayoutWrapper> <TabLayoutWrapper>
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-20`}> <ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-20`}>
<LinearGradient <LinearGradient
colors={['#194185', '#1570EF']} // brand900 to brand600 colors={["#194185", "#1570EF"]}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
style={tw`pb-8 pt-6 px-5 rounded-b-[32px] shadow-lg mb-4`} style={tw`pb-8 pt-6 px-5 rounded-b-[32px] shadow-lg mb-4`}
@ -123,22 +79,27 @@ export default function OrderAgain() {
<View style={tw`bg-white/20 p-1 rounded-full mr-2`}> <View style={tw`bg-white/20 p-1 rounded-full mr-2`}>
<MaterialIcons name="refresh" size={14} color="#FFF" /> <MaterialIcons name="refresh" size={14} color="#FFF" />
</View> </View>
<MyText style={tw`text-brand100 text-xs font-bold uppercase tracking-widest`}> <MyText
style={tw`text-brand100 text-xs font-bold uppercase tracking-widest`}
>
Order Again Order Again
</MyText> </MyText>
</View> </View>
<MyText style={tw`text-white text-sm font-medium opacity-90 ml-1`} numberOfLines={1}> <MyText
style={tw`text-white text-sm font-medium opacity-90 ml-1`}
numberOfLines={1}
>
Reorder your favorite items quickly Reorder your favorite items quickly
</MyText> </MyText>
</View> </View>
</View> </View>
</LinearGradient> </LinearGradient>
{/* White Section */}
<View style={tw`bg-white`}> <View style={tw`bg-white`}>
{/* Section Title */}
<View style={tw`flex-row items-center mb-2 px-4 pt-4`}> <View style={tw`flex-row items-center mb-2 px-4 pt-4`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>Recently Ordered</MyText> <MyText style={tw`text-lg font-bold text-gray-900`}>
Recently Ordered
</MyText>
</View> </View>
{recentProducts.length === 0 ? ( {recentProducts.length === 0 ? (
@ -152,22 +113,27 @@ export default function OrderAgain() {
No recent orders No recent orders
</MyText> </MyText>
<MyText style={tw`text-gray-500 mt-2 text-center px-8`}> <MyText style={tw`text-gray-500 mt-2 text-center px-8`}>
Items you&apos;ve ordered recently will appear here Items you've ordered recently will appear here
</MyText> </MyText>
</View> </View>
) : ( ) : (
<View style={tw`px-4 pb-4`}> <View style={tw`px-4 pb-4`}>
<View style={tw`flex-row flex-wrap justify-between`}> <View style={tw`flex-row flex-wrap justify-between`}>
{recentProducts.map((item, index) => ( {recentProducts.map((item, index) => (
<View key={item.id} style={tw`mb-4 ${index % 2 === 0 ? 'mr-2' : ''}`}> <View
key={item.id}
style={tw`mb-4 ${index % 2 === 0 ? "mr-2" : ""}`}
>
<ProductCard <ProductCard
item={item} item={item}
// handleAddToCart={handleAddToCart}
// handleBuyNow={handleBuyNow}
itemWidth={itemWidth} itemWidth={itemWidth}
onPress={() => router.push(`/(drawer)/(tabs)/order-again/product-detail/${item.id}`)} onPress={() =>
router.push(
`/(drawer)/(tabs)/order-again/product-detail/${item.id}`
)
}
showDeliveryInfo={false} showDeliveryInfo={false}
// iconType="flash" useAddToCartDialog={true}
/> />
</View> </View>
))} ))}
@ -177,7 +143,6 @@ export default function OrderAgain() {
</View> </View>
</ScrollView> </ScrollView>
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
<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 file

@ -1,76 +1,44 @@
import React, { useState } from 'react'; import React from "react";
import { View, Dimensions } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router";
import { import {
View, theme,
Dimensions, tw,
Image, useManualRefresh,
TouchableOpacity, useMarkDataFetchers,
Alert, MyFlatList,
Platform, useDrawerTitle,
} from 'react-native'; MyText,
import { useRouter, useLocalSearchParams } from 'expo-router'; } from "common-ui";
import { theme, tw, useManualRefresh, MyFlatList, useDrawerTitle, useMarkDataFetchers, LoadingDialog, MyText, MyTouchableOpacity } from 'common-ui'; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import dayjs from 'dayjs'; import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { trpc } from "@/src/trpc-client";
import FontAwesome5 from '@expo/vector-icons/FontAwesome5'; import ProductCard from "@/components/ProductCard";
import { trpc } from '@/src/trpc-client'; import FloatingCartBar from "@/components/floating-cart-bar";
import { useAddToCart } from '@/hooks/cart-query-hooks';
import ProductCard from '@/components/ProductCard';
import FloatingCartBar from '@/components/floating-cart-bar';
const { width: screenWidth } = Dimensions.get('window');
const itemWidth = (screenWidth - 48) / 2; // 48 = padding horizontal (16*2) + gap (16)
const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2;
export default function StoreDetail() { export default function StoreDetail() {
const router = useRouter(); const router = useRouter();
// const { storeId } = useLocalSearchParams();
const { id: storeId } = useLocalSearchParams(); const { id: storeId } = useLocalSearchParams();
const storeIdNum = parseInt(storeId as string); const storeIdNum = parseInt(storeId as string);
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const { data: storeData, isLoading, refetch, error } = trpc.user.stores.getStoreWithProducts.useQuery( const { data: storeData, isLoading, refetch, error } =
{ storeId: storeIdNum }, trpc.user.stores.getStoreWithProducts.useQuery(
{ enabled: !!storeIdNum } { storeId: storeIdNum },
); { enabled: !!storeIdNum }
);
useManualRefresh(() => {
refetch();
});
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
refetch(); refetch();
}); });
useDrawerTitle(storeData?.store?.name || 'Store', [storeData?.store?.name]); useDrawerTitle(storeData?.store?.name || "Store", [storeData?.store?.name]);
const addToCart = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true });
const handleAddToCart = (productId: number) => {
setIsLoadingDialogOpen(true);
addToCart.mutate({ productId, quantity: 1 }, {
onSuccess: () => {
Alert.alert('Success', 'Item added to cart!');
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to add item to cart');
},
onSettled: () => {
setIsLoadingDialogOpen(false);
},
});
};
const handleBuyNow = (productId: number) => {
setIsLoadingDialogOpen(true);
addToCart.mutate({ productId, quantity: 1 }, {
onSuccess: () => {
router.push(`/(drawer)/(tabs)/home/cart?select=${productId}`);
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to add item to cart');
},
onSettled: () => {
setIsLoadingDialogOpen(false);
},
});
};
if (isLoading) { if (isLoading) {
return ( return (
@ -85,7 +53,9 @@ export default function StoreDetail() {
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" /> <MaterialIcons name="error-outline" size={48} color="#EF4444" />
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText> <MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
<MyText style={tw`text-gray-500 mt-2`}>Store not found or error loading</MyText> <MyText style={tw`text-gray-500 mt-2`}>
Store not found or error loading
</MyText>
</View> </View>
); );
} }
@ -96,36 +66,58 @@ export default function StoreDetail() {
data={storeData?.products || []} data={storeData?.products || []}
numColumns={2} numColumns={2}
renderItem={({ item }) => ( renderItem={({ item }) => (
<ProductCard <ProductCard
item={item} item={item}
itemWidth={itemWidth} itemWidth={itemWidth}
onPress={() => router.push(`/(drawer)/(tabs)/stores/store-detail/product-detail/${item.id}`)} onPress={() =>
showDeliveryInfo={false} router.push(
miniView={true} `/(drawer)/(tabs)/stores/store-detail/product-detail/${item.id}`
/> )
}
showDeliveryInfo={false}
miniView={true}
useAddToCartDialog={true}
/>
)} )}
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
columnWrapperStyle={{ gap: 16 }} columnWrapperStyle={{ gap: 16 }}
contentContainerStyle={[tw`px-4 pb-24`, { gap: 16 }]} contentContainerStyle={[tw`px-4 pb-24`, { gap: 16 }]}
ListHeaderComponent={ ListHeaderComponent={
<View style={tw`pt-4 pb-6`}> <View style={tw`pt-4 pb-6`}>
<View style={tw`bg-white p-6 rounded-2xl shadow-sm border border-gray-100 items-center`}> <View
<View style={tw`w-16 h-16 bg-pink-50 rounded-full items-center justify-center mb-4`}> style={tw`bg-white p-6 rounded-2xl shadow-sm border border-gray-100 items-center`}
<FontAwesome5 name="store" size={28} color={theme.colors.brand500} /> >
<View
style={tw`w-16 h-16 bg-pink-50 rounded-full items-center justify-center mb-4`}
>
<FontAwesome5
name="store"
size={28}
color={theme.colors.brand500}
/>
</View> </View>
<MyText style={tw`text-2xl font-bold text-gray-900 text-center mb-2`}>{storeData?.store?.name}</MyText> <MyText
style={tw`text-2xl font-bold text-gray-900 text-center mb-2`}
>
{storeData?.store?.name}
</MyText>
{storeData?.store?.description && ( {storeData?.store?.description && (
<MyText style={tw`text-gray-500 text-center leading-5 px-4`}>{storeData?.store?.description}</MyText> <MyText
style={tw`text-gray-500 text-center leading-5 px-4`}
>
{storeData?.store?.description}
</MyText>
)} )}
</View> </View>
<View style={tw`flex-row items-center mt-6 mb-2`}> <View style={tw`flex-row items-center mt-6 mb-2`}>
<MaterialIcons name="grid-view" size={20} color="#374151" /> <MaterialIcons name="grid-view" size={20} color="#374151" />
<MyText style={tw`text-lg font-bold text-gray-900 ml-2`}>Products from this Store</MyText> <MyText style={tw`text-lg font-bold text-gray-900 ml-2`}>
Products from this Store
</MyText>
</View> </View>
</View> </View>
} }
/> />
<LoadingDialog open={isLoadingDialogOpen} message="Processing..." />
<View style={tw`absolute bottom-2 left-4 right-4`}> <View style={tw`absolute bottom-2 left-4 right-4`}>
<FloatingCartBar /> <FloatingCartBar />

View file

@ -25,6 +25,7 @@ import UpdateChecker from "@/components/UpdateChecker";
import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context"; import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context";
import WebViewWrapper from "@/components/WebViewWrapper"; import WebViewWrapper from "@/components/WebViewWrapper";
import BackHandlerWrapper from "@/components/BackHandler"; import BackHandlerWrapper from "@/components/BackHandler";
import AddToCartDialog from "@/src/components/AddToCartDialog";
import React from "react"; import React from "react";
export default function RootLayout() { export default function RootLayout() {
@ -61,6 +62,7 @@ export default function RootLayout() {
<RefreshProvider queryClient={queryClient}> <RefreshProvider queryClient={queryClient}>
<BackHandlerWrapper /> <BackHandlerWrapper />
<Stack screenOptions={{ headerShown: false }} /> <Stack screenOptions={{ headerShown: false }} />
<AddToCartDialog />
</RefreshProvider> </RefreshProvider>
</LocationTestWrapper> </LocationTestWrapper>
</PaperProvider> </PaperProvider>

View file

@ -45,6 +45,9 @@ interface LocationTestWrapperProps {
children: ReactNode; children: ReactNode;
} }
// Feature flag to enable/disable location warning dialogs
const ENABLE_LOCATION_WARNINGS = false;
const LocationTestWrapper: React.FC<LocationTestWrapperProps> = ({ children }) => { const LocationTestWrapper: React.FC<LocationTestWrapperProps> = ({ children }) => {
// Skip location checks entirely for emulators // Skip location checks entirely for emulators
if (isEmulator()) { if (isEmulator()) {
@ -62,7 +65,7 @@ const LocationTestWrapper: React.FC<LocationTestWrapperProps> = ({ children }) =
); );
useEffect(() => { useEffect(() => {
if (locationCheck && !locationCheck.isInside) { if (ENABLE_LOCATION_WARNINGS && locationCheck && !locationCheck.isInside) {
setLocationErrorDialogOpen(true); setLocationErrorDialogOpen(true);
} }
}, [locationCheck]); }, [locationCheck]);
@ -73,7 +76,7 @@ const LocationTestWrapper: React.FC<LocationTestWrapperProps> = ({ children }) =
if (status === 'granted') { if (status === 'granted') {
const location = await Location.getCurrentPositionAsync({}); const location = await Location.getCurrentPositionAsync({});
setUserLocation(location); setUserLocation(location);
} else { } else if (ENABLE_LOCATION_WARNINGS) {
setLocationDialogOpen(true); setLocationDialogOpen(true);
} }
})(); })();

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { View, Alert, TouchableOpacity, Text } from 'react-native'; import { View, Alert, TouchableOpacity, Text } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui'; import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui';
@ -13,6 +13,8 @@ import {
useAddToCart, useAddToCart,
} from '@/hooks/cart-query-hooks'; } from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useCartStore } from '@/src/store/cartStore';
import { trpc } from '@/src/trpc-client';
interface ProductCardProps { interface ProductCardProps {
@ -23,6 +25,7 @@ interface ProductCardProps {
miniView?: boolean; miniView?: boolean;
nullIfNotAvailable?: boolean; nullIfNotAvailable?: boolean;
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>; containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
useAddToCartDialog?: boolean;
} }
const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => { const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => {
@ -40,9 +43,11 @@ const ProductCard: React.FC<ProductCardProps> = ({
miniView = false, miniView = false,
nullIfNotAvailable = false, nullIfNotAvailable = false,
containerComp: ContainerComp = React.Fragment, containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false,
}) => { }) => {
const { data: cartData } = useGetCart(); const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const { setAddedToCartProduct } = useCartStore();
const updateCartItem = useUpdateCartItem({ const updateCartItem = useUpdateCartItem({
showSuccessAlert: false, showSuccessAlert: false,
showErrorAlert: false, showErrorAlert: false,
@ -63,6 +68,22 @@ const ProductCard: React.FC<ProductCardProps> = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const quantity = cartItem?.quantity || 0; const quantity = cartItem?.quantity || 0;
// Query all slots with products
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
// Create slot lookup map
const slotMap = useMemo(() => {
const map: Record<number, any> = {};
slotsData?.slots?.forEach((slot: any) => {
map[slot.id] = slot;
});
return map;
}, [slotsData]);
// Get cart item's slot delivery time if item is in cart
const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null;
const displayDeliveryDate = cartSlot?.deliveryTime || item.nextDeliveryDate;
// Precompute the next slot and determine display out of stock status // Precompute the next slot and determine display out of stock status
const slotId = getQuickestSlot(item.id); const slotId = getQuickestSlot(item.id);
const displayIsOutOfStock = item.isOutOfStock || !slotId; const displayIsOutOfStock = item.isOutOfStock || !slotId;
@ -73,7 +94,9 @@ const ProductCard: React.FC<ProductCardProps> = ({
} }
const handleQuantityChange = (newQuantity: number) => { const handleQuantityChange = (newQuantity: number) => {
if (newQuantity === 0 && cartItem) { if (useAddToCartDialog) {
setAddedToCartProduct({ productId: item.id, product: item });
} else if (newQuantity === 0 && cartItem) {
removeFromCart.mutate({ itemId: cartItem.id }); removeFromCart.mutate({ itemId: cartItem.id });
} else if (newQuantity === 1 && !cartItem) { } else if (newQuantity === 1 && !cartItem) {
const slotId = getQuickestSlot(item.id); const slotId = getQuickestSlot(item.id);
@ -141,11 +164,11 @@ const ProductCard: React.FC<ProductCardProps> = ({
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText> <MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText>
</View> </View>
{showDeliveryInfo && item.nextDeliveryDate && ( {showDeliveryInfo && displayDeliveryDate && (
<View style={tw`flex-row items-center bg-brand50 px-2 py-1.5 rounded-lg self-start mb-2 border border-brand100`}> <View style={tw`flex-row items-center bg-brand50 px-2 py-1.5 rounded-lg self-start mb-2 border border-brand100`}>
<MaterialIcons name="local-shipping" size={12} color="#2E90FA" /> <MaterialIcons name="local-shipping" size={12} color="#2E90FA" />
<MyText style={tw`text-[10px] text-brand700 ml-1.5 font-bold`}> <MyText style={tw`text-[10px] text-brand700 ml-1.5 font-bold`}>
{dayjs(item.nextDeliveryDate).format("ddd, DD MMM • h:mm A")} {dayjs(displayDeliveryDate).format("ddd, DD MMM • h:mm A")}
</MyText> </MyText>
</View> </View>
)} )}

View file

@ -443,7 +443,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{/* Review Form - Moved above or keep below? Usually users want to read reviews first, but if few reviews, writing is good. The original had form then reviews. I will keep format but make it nicer. */} {/* Review Form - Moved above or keep below? Usually users want to read reviews first, but if few reviews, writing is good. The original had form then reviews. I will keep format but make it nicer. */}
<View style={tw`mb-6`}> <View style={tw`mb-6`}>
<ReviewForm productId={productDetail.id} onReviewSubmitted={handleReviewSubmitted} /> <ReviewForm productId={productDetail!.id} onReviewSubmitted={handleReviewSubmitted} />
</View> </View>
<View style={tw`bg-white rounded-3xl shadow-sm border border-gray-100 overflow-hidden`}> <View style={tw`bg-white rounded-3xl shadow-sm border border-gray-100 overflow-hidden`}>

View file

@ -121,174 +121,157 @@ const QuickDeliveryAddressSelector: React.FC<QuickDeliveryAddressSelectorProps>
)} )}
{!isForFlashDelivery && ( {!isForFlashDelivery && (
<View style={tw`flex-1 mr-2`}> <>
<View style={tw`flex-row items-center mb-1`}> <View style={tw`flex-1 flex-row items-center gap-2`}>
<MyTouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-2`}
activeOpacity={0.7}
>
<MaterialIcons name="chevron-left" size={24} />
</MyTouchableOpacity>
<MyText style={[tw`text-lg font-bold ml-2`]}>
Delivery At {getCurrentSlotDisplay()}
</MyText>
</View>
{/* Trigger Component with Separate Chevrons */}
<View style={tw`bg-brand50 border border-brand100 rounded-lg p-3`}>
{/* Regular Delivery Time Section */}
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => setDialogOpen(true)} onPress={() => router.back()}
style={tw`flex-row items-center justify-between mb-2`} style={tw`p-1`}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={tw`flex-1`}> <MaterialIcons name="chevron-left" size={24} />
{/* <MyText style={tw`text-xs text-brand500 font-bold uppercase tracking-wider leading-3`}>
Delivery Time
</MyText> */}
<MyText style={tw`text-sm font-bold text-brand900 leading-4`}>
Delivery at: {getCurrentSlotDisplay()}
</MyText>
</View>
<MaterialIcons name="keyboard-arrow-down" size={20} color={theme.colors.brand500} />
</MyTouchableOpacity> </MyTouchableOpacity>
<View style={tw`flex-1 gap-1`}>
<MyTouchableOpacity
onPress={() => setDialogOpen(true)}
style={tw`flex-row items-center bg-brand50 border border-brand100 rounded-lg px-3 py-2`}
activeOpacity={0.7}
>
<View style={tw`flex-1`}>
<MyText style={tw`text-xs text-brand500 font-bold uppercase`}>Delivery Time</MyText>
<MyText style={tw`text-sm font-bold text-brand900`}>
{getCurrentSlotDisplay()}
</MyText>
</View>
<MaterialIcons name="keyboard-arrow-down" size={18} color={theme.colors.brand500} />
</MyTouchableOpacity>
{/* Address Section */} {/* Address dropdown - temporarily hidden
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => setDialogOpen(true)} onPress={() => setDialogOpen(true)}
style={tw`flex-row items-center justify-between`} style={tw`flex-row items-center bg-brand50 border border-brand100 rounded-lg px-3 py-2`}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={tw`flex-1`}> <View style={tw`flex-1`}>
{/* <MyText style={tw`text-xs text-brand500 font-bold uppercase tracking-wider leading-3`}> <MyText style={tw`text-xs text-brand500 font-bold uppercase`}>Address</MyText>
Delivery Address <MyText style={tw`text-sm font-bold text-brand900`} numberOfLines={1}>
</MyText> */} {getCurrentAddressDisplay()}
<MyText style={tw`text-sm font-bold text-brand900 leading-4`} numberOfLines={1}> </MyText>
TO: {getCurrentAddressDisplay()} </View>
</MyText> <MaterialIcons name="keyboard-arrow-down" size={18} color={theme.colors.brand500} />
</View> </MyTouchableOpacity>
<MaterialIcons name="keyboard-arrow-down" size={20} color={theme.colors.brand500} /> */}
</MyTouchableOpacity>
</View>
{/* Consolidated Dialog - 80% height */}
<BottomDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
>
<View style={[tw`py-6`, { height: Dimensions.get('window').height * 0.8 }]}>
<MyText style={tw`text-xl font-bold text-gray-900 mb-6 text-center`}>
Select Delivery Options
</MyText>
<View style={tw`flex-1`}>
{/* Section 1: Delivery Time Selection */}
<View style={tw`mb-6`}>
<MyText style={tw`text-lg font-bold text-gray-900 mb-3`}>
Select Delivery Time
</MyText>
<ScrollView style={tw`max-h-40`} showsVerticalScrollIndicator={false}>
{isForFlashDelivery ? (
<View style={tw`p-3 bg-green-50 border border-green-200 rounded-lg mb-2`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="bolt" size={20} color="#16a34a" />
<MyText style={tw`text-green-800 font-medium ml-2`}>
1 Hr Delivery - within 1 hour
</MyText>
</View>
<MyText style={tw`text-green-700 text-xs mt-1`}>
Selected automatically for flash delivery
</MyText>
</View>
) : (
slotOptions.map(slot => (
<MyTouchableOpacity
key={slot.id}
style={tw`p-3 border border-gray-200 rounded-lg mb-2 ${
slot.id === (slotId || earliestSlot?.id) ? 'bg-brand50 border-brand500' : 'bg-white'
}`}
onPress={() => {
onSlotChange?.(slot.id);
setDialogOpen(false);
}}
activeOpacity={0.7}
>
<MyText style={tw`font-medium text-gray-900`}>
Delivery: {slot.deliveryTime}
</MyText>
<MyText style={tw`text-xs text-gray-500 mt-1`}>
Orders Close at: {slot.closeTime}
</MyText>
</MyTouchableOpacity>
))
)}
</ScrollView>
</View>
{/* Divider */}
<View style={tw`h-px bg-gray-200 mb-6`} />
{/* Section 2: Address Selection */}
<View style={tw`flex-1`}>
<MyText style={tw`text-lg font-bold text-gray-900 mb-3`}>
Select Delivery Address
</MyText>
<ScrollView style={tw`flex-1`} showsVerticalScrollIndicator={false}>
{!isAuthenticated ? (
<View style={tw`p-4 bg-red-50 border border-red-200 rounded-lg`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="error" size={20} color="#DC2626" />
<MyText style={tw`text-red-800 font-medium ml-2`}>
Authentication Required
</MyText>
</View>
<MyText style={tw`text-red-700 text-sm mt-2`}>
Please log in to select and manage delivery addresses.
</MyText>
</View>
) : addressOptions.length === 0 ? (
<View style={tw`p-4 bg-gray-50 border border-gray-200 rounded-lg`}>
<MyText style={tw`text-gray-600 text-center`}>
No delivery addresses available. Please add an address first.
</MyText>
</View>
) : (
addressOptions.map(address => (
<MyTouchableOpacity
key={address.id}
style={tw`p-3 border border-gray-200 rounded-lg mb-2 ${
address.id === (selectedAddressId || defaultAddress?.id) ? 'bg-brand50 border-brand500' : 'bg-white'
}`}
onPress={() => {
setSelectedAddressId(address.id);
setDialogOpen(false);
}}
activeOpacity={0.7}
>
<MyText style={tw`font-medium text-gray-900`}>
{address.name}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`} numberOfLines={2}>
{address.address}
</MyText>
<MyText style={tw`text-xs text-gray-500 mt-1`}>
Phone: {address.phone}
</MyText>
</MyTouchableOpacity>
))
)}
</ScrollView>
</View>
</View> </View>
</View> </View>
</BottomDialog>
</View>
)}
<BottomDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
>
<View style={[tw`py-6`, { height: Dimensions.get('window').height * 0.5 }]}>
<MyText style={tw`text-xl font-bold text-gray-900 mb-4 text-center`}>
Select Delivery Time
</MyText>
<ScrollView showsVerticalScrollIndicator={false}>
{isForFlashDelivery ? (
<View style={tw`p-3 bg-green-50 border border-green-200 rounded-lg mb-2`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="bolt" size={20} color="#16a34a" />
<MyText style={tw`text-green-800 font-medium ml-2`}>
1 Hr Delivery - within 1 hour
</MyText>
</View>
<MyText style={tw`text-green-700 text-xs mt-1`}>
Selected automatically for flash delivery
</MyText>
</View>
) : (
slotOptions.map(slot => (
<MyTouchableOpacity
key={slot.id}
style={tw`p-3 border border-gray-200 rounded-lg mb-2 ${
slot.id === (slotId || earliestSlot?.id) ? 'bg-brand50 border-brand500' : 'bg-white'
}`}
onPress={() => {
onSlotChange?.(slot.id);
setDialogOpen(false);
}}
activeOpacity={0.7}
>
<MyText style={tw`font-medium text-gray-900`}>
Delivery: {slot.deliveryTime}
</MyText>
<MyText style={tw`text-xs text-gray-500 mt-1`}>
Orders Close at: {slot.closeTime}
</MyText>
</MyTouchableOpacity>
))
)}
</ScrollView>
</View>
</BottomDialog>
{/* Address section - temporarily hidden
<BottomDialog
open={addressDialogOpen}
onClose={() => setAddressDialogOpen(false)}
>
<View style={[tw`py-6`, { height: Dimensions.get('window').height * 0.5 }]}>
<MyText style={tw`text-xl font-bold text-gray-900 mb-4 text-center`}>
Select Delivery Address
</MyText>
<ScrollView showsVerticalScrollIndicator={false}>
{!isAuthenticated ? (
<View style={tw`p-4 bg-red-50 border border-red-200 rounded-lg`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="error" size={20} color="#DC2626" />
<MyText style={tw`text-red-800 font-medium ml-2`}>
Authentication Required
</MyText>
</View>
<MyText style={tw`text-red-700 text-sm mt-2`}>
Please log in to select and manage delivery addresses.
</MyText>
</View>
) : addressOptions.length === 0 ? (
<View style={tw`p-4 bg-gray-50 border border-gray-200 rounded-lg`}>
<MyText style={tw`text-gray-600 text-center`}>
No delivery addresses available. Please add an address first.
</MyText>
</View>
) : (
addressOptions.map(address => (
<MyTouchableOpacity
key={address.id}
style={tw`p-3 border border-gray-200 rounded-lg mb-2 ${
address.id === (selectedAddressId || defaultAddress?.id) ? 'bg-brand50 border-brand500' : 'bg-white'
}`}
onPress={() => {
setSelectedAddressId(address.id);
setAddressDialogOpen(false);
}}
activeOpacity={0.7}
>
<MyText style={tw`font-medium text-gray-900`}>
{address.name}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`} numberOfLines={2}>
{address.address}
</MyText>
<MyText style={tw`text-xs text-gray-500 mt-1`}>
Phone: {address.phone}
</MyText>
</MyTouchableOpacity>
))
)}
</ScrollView>
</View>
</BottomDialog>
*/}
</>
)}
</> </>
); );
}; };

View file

@ -162,8 +162,8 @@ useEffect(() => {
<> <>
<MyText style={tw`text-white font-black text-base`}> <MyText style={tw`text-white font-black text-base`}>
{totalCartValue} {totalCartValue}
</MyText>
{`${itemCount} ${itemCount === 1 ? "Item" : "Items"}`} {`${itemCount} ${itemCount === 1 ? "Item" : "Items"}`}
</MyText>
</> </>
)} )}
</MyText> </MyText>

View file

@ -0,0 +1,171 @@
import React, { useState, useMemo, useEffect } from 'react';
import { View, ScrollView } from 'react-native';
import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useCartStore } from '@/src/store/cartStore';
import { trpc } from '@/src/trpc-client';
import { useAddToCart, useGetCart, useUpdateCartItem } from '@/hooks/cart-query-hooks';
import dayjs from 'dayjs';
export default function AddToCartDialog() {
const { addedToCartProduct, clearAddedToCartProduct } = useCartStore();
const [quantity, setQuantity] = useState(1);
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null);
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
const { data: cartData } = useGetCart();
const addToCart = useAddToCart({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
});
const updateCartItem = useUpdateCartItem({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
});
const isOpen = !!addedToCartProduct;
const product = addedToCartProduct?.product;
// Pre-select cart's slotId and quantity if item is already in cart
useEffect(() => {
if (isOpen && product) {
const cartItem = cartData?.items?.find((item: any) => item.productId === product.id);
const cartQuantity = cartItem?.quantity || 0;
// Set quantity: 0 → 1, >1 → keep as is
setQuantity(cartQuantity === 0 ? 1 : cartQuantity);
if (cartItem?.slotId) {
setSelectedSlotId(cartItem.slotId);
} else {
setSelectedSlotId(null);
}
}
}, [isOpen, cartData, product]);
const { slotMap, productSlotIdsMap } = useMemo(() => {
const slotMap: Record<number, any> = {};
const productSlotIdsMap: Record<number, number[]> = {};
if (slotsData?.slots) {
slotsData.slots.forEach((slot: any) => {
slotMap[slot.id] = slot;
slot.products?.forEach((p: any) => {
if (!productSlotIdsMap[p.id]) {
productSlotIdsMap[p.id] = [];
}
productSlotIdsMap[p.id].push(slot.id);
});
});
}
return { slotMap, productSlotIdsMap };
}, [slotsData]);
const availableSlotIds = productSlotIdsMap[product?.id] || [];
const availableSlots = availableSlotIds
.map((slotId) => slotMap[slotId])
.filter(Boolean);
// Find cart item for this product
const cartItem = cartData?.items?.find((item: any) => item.productId === product?.id);
// Determine if updating existing item (quantity > 1 means it's an update)
const isUpdate = (cartItem?.quantity || 0) > 1;
const handleAddToCart = () => {
if (isUpdate && cartItem?.id) {
updateCartItem.mutate(
{ itemId: cartItem.id, quantity },
{ onSuccess: () => clearAddedToCartProduct() }
);
} else {
const slotId = selectedSlotId ?? availableSlotIds[0] ?? 0;
addToCart.mutate(
{ productId: product.id, quantity, slotId },
{ onSuccess: () => clearAddedToCartProduct() }
);
}
};
if (!isOpen || !addedToCartProduct) return null;
return (
<BottomDialog open={isOpen} onClose={clearAddedToCartProduct}>
<View style={tw`p-6 max-h-[500px]`}>
<View style={tw`flex-row items-center mb-2`}>
<View style={tw`w-10 h-10 bg-blue-50 rounded-full items-center justify-center mr-3`}>
<MaterialIcons name="schedule" size={20} color="#3B82F6" />
</View>
<View>
<MyText style={tw`text-xl font-bold text-gray-900`}>Select Delivery Slot</MyText>
{product?.name && (
<MyText style={tw`text-sm text-gray-500`}>{product.name}</MyText>
)}
</View>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{availableSlots.map((slot: any) => (
<MyTouchableOpacity
key={slot.id}
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100 ${
selectedSlotId === slot.id ? 'border-brand500' : 'border-gray-100'
}`}
onPress={() => setSelectedSlotId(slot.id)}
activeOpacity={0.7}
>
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
<View style={tw`ml-3 flex-1`}>
<MyText style={tw`text-gray-900 font-bold text-base`}>
{dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A')}
</MyText>
</View>
{selectedSlotId === slot.id ? (
<MaterialIcons name="check-circle" size={24} color="#3B82F6" style={tw`mt-0.5`} />
) : (
<MaterialIcons name="check-box-outline-blank" size={24} color="#9CA3AF" style={tw`mt-0.5`} />
)}
</MyTouchableOpacity>
))}
</ScrollView>
<View style={tw`mt-4`}>
<MyText style={tw`text-sm font-bold text-gray-900 mb-2`}>Quantity</MyText>
<Quantifier
value={quantity}
setValue={setQuantity}
step={product.incrementStep}
unit={product.unitNotation}
/>
</View>
<View style={tw`flex-row gap-3 mt-4`}>
<MyTouchableOpacity
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center ${!selectedSlotId ? 'opacity-50' : ''}`}
onPress={handleAddToCart}
disabled={(addToCart.isLoading || updateCartItem.isLoading) || !selectedSlotId}
>
<MyText style={tw`text-white font-bold`}>
{addToCart.isLoading || updateCartItem.isLoading ? (isUpdate ? 'Updating...' : 'Adding...') : (isUpdate ? 'Update Item' : 'Add to Cart')}
</MyText>
</MyTouchableOpacity>
<MyTouchableOpacity
style={tw`flex-1 bg-gray-100 py-3.5 rounded-xl items-center`}
onPress={clearAddedToCartProduct}
>
<MyText style={tw`text-gray-700 font-bold`}>Cancel</MyText>
</MyTouchableOpacity>
</View>
</View>
</BottomDialog>
);
}

View file

@ -112,6 +112,16 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
> >
<MyText style={tw`text-xl font-bold mb-4`}>{isEdit ? 'Edit Address' : 'Add Address'}</MyText> <MyText style={tw`text-xl font-bold mb-4`}>{isEdit ? 'Edit Address' : 'Add Address'}</MyText>
{/* Service Area Notice */}
<View style={tw`bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-amber-600 mr-2`}></MyText>
<MyText style={tw`text-amber-800 text-sm flex-1`}>
We currently serve only in Mahabubnagar town
</MyText>
</View>
</View>
{/* Submit Error Message */} {/* Submit Error Message */}
{submitError && ( {submitError && (
<View style={tw`bg-red-50 border border-red-200 rounded-lg p-3 mb-4`}> <View style={tw`bg-red-50 border border-red-200 rounded-lg p-3 mb-4`}>

View file

@ -0,0 +1,18 @@
import { create } from 'zustand';
interface AddedToCartProduct {
productId: number;
product: any;
}
interface CartStore {
addedToCartProduct: AddedToCartProduct | null;
setAddedToCartProduct: (product: AddedToCartProduct | null) => void;
clearAddedToCartProduct: () => void;
}
export const useCartStore = create<CartStore>((set) => ({
addedToCartProduct: null,
setAddedToCartProduct: (product) => set({ addedToCartProduct: product }),
clearAddedToCartProduct: () => set({ addedToCartProduct: null }),
}));

View file

@ -4,7 +4,8 @@ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
retry: 3, retry: 3,
refetchOnWindowFocus: true, refetchOnWindowFocus: false,
staleTime: 1200000,
}, },
} }
}); });