diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index 5074cb9..6bfb0da 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -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"; @@ -623,19 +624,20 @@ export default function Dashboard() { columnWrapperStyle={{gap: 16}} renderItem={({ item, index }) => ( - - router.push( - `/(drawer)/(tabs)/home/product-detail/${item.id}` - ) - } - showDeliveryInfo={true} - miniView={false} + + router.push( + `/(drawer)/(tabs)/home/product-detail/${item.id}` + ) + } + showDeliveryInfo={true} + miniView={false} + useAddToCartDialog={true} - key={item.id} - /> + key={item.id} + /> )} initialNumToRender={4} @@ -665,6 +667,7 @@ export default function Dashboard() { + diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/search-results/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/search-results/index.tsx index 5cc2c7a..8bcb128 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/home/search-results/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/search-results/index.tsx @@ -135,12 +135,9 @@ export default function SearchResults() { renderItem={({ item }) => ( router.push(`/(drawer)/(tabs)/home/product-detail/${item.id}`)} showDeliveryInfo={false} - iconType="flash" /> )} keyExtractor={(item, index) => index.toString()} diff --git a/apps/user-ui/components/ProductCard.tsx b/apps/user-ui/components/ProductCard.tsx index 5310e8a..e1cfb69 100644 --- a/apps/user-ui/components/ProductCard.tsx +++ b/apps/user-ui/components/ProductCard.tsx @@ -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 | React.JSXElementConstructor; + useAddToCartDialog?: boolean; } const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => { @@ -40,9 +43,11 @@ const ProductCard: React.FC = ({ 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 = ({ 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 = {}; + 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 = ({ } 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 = ({ Quantity: {formatQuantity(item.productQuantity || 1, item.unitNotation).display} - {showDeliveryInfo && item.nextDeliveryDate && ( + {showDeliveryInfo && displayDeliveryDate && ( - {dayjs(item.nextDeliveryDate).format("ddd, DD MMM • h:mm A")} + {dayjs(displayDeliveryDate).format("ddd, DD MMM • h:mm A")} )} diff --git a/apps/user-ui/src/components/AddToCartDialog.tsx b/apps/user-ui/src/components/AddToCartDialog.tsx new file mode 100644 index 0000000..9ed7d60 --- /dev/null +++ b/apps/user-ui/src/components/AddToCartDialog.tsx @@ -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(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 = {}; + const productSlotIdsMap: Record = {}; + + 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 ( + + + + + + + Select Delivery Slot + + + + {availableSlots.map((slot: any) => ( + setSelectedSlotId(slot.id)} + activeOpacity={0.7} + > + + + + {dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A')} + + + {selectedSlotId === slot.id ? ( + + ) : ( + + )} + + ))} + + + + Quantity + + + + + + + {addToCart.isLoading ? 'Adding...' : 'Add to Cart'} + + + + Cancel + + + + + ); +} diff --git a/apps/user-ui/src/store/cartStore.ts b/apps/user-ui/src/store/cartStore.ts new file mode 100644 index 0000000..3c3394a --- /dev/null +++ b/apps/user-ui/src/store/cartStore.ts @@ -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((set) => ({ + addedToCartProduct: null, + setAddedToCartProduct: (product) => set({ addedToCartProduct: product }), + clearAddedToCartProduct: () => set({ addedToCartProduct: null }), +}));