This commit is contained in:
shafi54 2026-02-04 15:34:38 +05:30
parent b0e84b7089
commit c02f2c84f5
5 changed files with 203 additions and 19 deletions

View file

@ -21,6 +21,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import { Ionicons } from "@expo/vector-icons";
import ProductCard from "@/components/ProductCard";
import AddToCartDialog from "@/src/components/AddToCartDialog";
import MyFlatList from "common-ui/src/components/flat-list";
import { trpc } from "@/src/trpc-client";
@ -633,6 +634,7 @@ export default function Dashboard() {
}
showDeliveryInfo={true}
miniView={false}
useAddToCartDialog={true}
key={item.id}
/>
@ -665,6 +667,7 @@ export default function Dashboard() {
</View>
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
<AddToCartDialog />
<View style={tw`h-16`}></View>
</ScrollView>
<View style={tw`absolute bottom-2 left-4 right-4`}>

View file

@ -135,12 +135,9 @@ export default function SearchResults() {
renderItem={({ item }) => (
<ProductCard
item={item}
handleAddToCart={handleAddToCart}
handleBuyNow={handleBuyNow}
itemWidth={itemWidth}
onPress={() => router.push(`/(drawer)/(tabs)/home/product-detail/${item.id}`)}
showDeliveryInfo={false}
iconType="flash"
/>
)}
keyExtractor={(item, index) => index.toString()}

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { View, Alert, TouchableOpacity, Text } from 'react-native';
import { Image } from 'expo-image';
import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui';
@ -13,6 +13,8 @@ import {
useAddToCart,
} from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useCartStore } from '@/src/store/cartStore';
import { trpc } from '@/src/trpc-client';
interface ProductCardProps {
@ -23,6 +25,7 @@ interface ProductCardProps {
miniView?: boolean;
nullIfNotAvailable?: boolean;
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
useAddToCartDialog?: boolean;
}
const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => {
@ -40,9 +43,11 @@ const ProductCard: React.FC<ProductCardProps> = ({
miniView = false,
nullIfNotAvailable = false,
containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false,
}) => {
const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier();
const { setAddedToCartProduct } = useCartStore();
const updateCartItem = useUpdateCartItem({
showSuccessAlert: false,
showErrorAlert: false,
@ -63,6 +68,22 @@ const ProductCard: React.FC<ProductCardProps> = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
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
const slotId = getQuickestSlot(item.id);
const displayIsOutOfStock = item.isOutOfStock || !slotId;
@ -73,7 +94,9 @@ const ProductCard: React.FC<ProductCardProps> = ({
}
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 });
} else if (newQuantity === 1 && !cartItem) {
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>
</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`}>
<MaterialIcons name="local-shipping" size={12} color="#2E90FA" />
<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>
</View>
)}

View file

@ -0,0 +1,143 @@
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 } 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 isOpen = !!addedToCartProduct;
const product = addedToCartProduct?.product;
// Pre-select cart's slotId if item is already in cart
useEffect(() => {
if (isOpen && product) {
const cartItem = cartData?.items?.find((item: any) => item.productId === product.id);
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);
const handleAddToCart = () => {
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-6`}>
<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>
<MyText style={tw`text-xl font-bold text-gray-900`}>Select Delivery Slot</MyText>
</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 || !selectedSlotId}
>
<MyText style={tw`text-white font-bold`}>
{addToCart.isLoading ? 'Adding...' : '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

@ -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 }),
}));