freshyo/apps/user-ui/components/ProductCard.tsx
2026-08-23 20:38:12 +05:30

277 lines
11 KiB
TypeScript

import React from 'react';
import { View, Alert, ActivityIndicator } from 'react-native';
import { Image } from 'expo-image';
import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui';
import CartIcon from '@/components/icons/CartIcon';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import Ionicons from '@expo/vector-icons/Ionicons';
import dayjs from 'dayjs';
import {
useGetCart,
useUpdateCartItem,
useRemoveFromCart,
useAddToCart,
} from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useCartStore } from '@/src/store/cartStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Image as RnImage } from 'react-native'
interface ProductCardProps {
item: any;
itemWidth: number;
onPress?: () => void;
showDeliveryInfo?: boolean;
miniView?: boolean;
variant?: 'default' | 'hero';
nullIfNotAvailable?: boolean;
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
useAddToCartDialog?: boolean;
}
const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => {
if (unit?.toLowerCase() === 'kg' && quantity < 1) {
return { value: `${Math.round(quantity * 1000)} g`, display: `${Math.round(quantity * 1000)}g` };
}
return { value: `${quantity} ${unit}(s)`, display: `${quantity}${unit}` };
};
const ProductCard: React.FC<ProductCardProps> = ({
item,
itemWidth,
onPress,
showDeliveryInfo = true,
miniView = false,
variant = 'default',
nullIfNotAvailable = false,
containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false,
}) => {
const isHero = variant === 'hero';
const imageUri = item.images?.[0]
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
const [imageError, setImageError] = React.useState<string | null>(null)
const [updater, setUpdater] = React.useState(0)
React.useEffect(() => {
const intervalId = setInterval(() => {
setUpdater(prev => prev + 1)
}, 5000)
return () => clearInterval(intervalId)
}, [])
const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier();
const { setAddedToCartProduct } = useCartStore();
const updateCartItem = useUpdateCartItem({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
});
const removeFromCart = useRemoveFromCart({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
});
const { addToCart = () => {} } = useAddToCart({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
}) || {};
// Find current quantity from cart data
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.skuId === item.id);
const quantity = cartItem?.quantity || 0;
// Get slots data from central store
const slots = useCentralSlotStore((state) => state.slots);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Create slot lookup map
const slotMap = React.useMemo(() => {
const map: Record<number, any> = {};
slots?.forEach((slot: any) => {
map[slot.id] = slot;
});
return map;
}, [slots]);
// 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;
React.useEffect(() => {
if (imageUri) {
setImageStatus('loading')
setImageError(null)
return
}
setImageStatus('error')
setImageError('No image available')
}, [imageUri])
// Precompute the next slot and determine display out of stock status
const slotId = getQuickestSlot(item.id);
// Use isOutOfStock from productSlotsMap (all products now included)
const productSlotInfo = productSlotsMap[item.id];
const isOutOfStockFromSlots = productSlotInfo?.isOutOfStock;
const isSlotsLoaded = useCentralSlotStore((state) => state.isSlotsLoaded);
// Don't treat "no slot yet" as out of stock while slot data is still loading
const displayIsOutOfStock = isOutOfStockFromSlots || (isSlotsLoaded && !slotId);
// if(item.name.startsWith('Mutton Curry Cut')) {
// console.log({slotId, displayIsOutOfStock})
// }
// Return null if nullIfNotAvailable is true and the product is out of stock
if (nullIfNotAvailable && displayIsOutOfStock) {
return null;
}
const handleQuantityChange = (newQuantity: number) => {
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);
if (!slotId) {
Alert.alert("Error", "No available delivery slot for this product");
return;
}
const slot = slotMap[slotId];
const deliveryTime = slot ? dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A') : '';
addToCart(item.id, 1, slotId, () => {
Alert.alert('Added to Cart', `Added ${item.name} for delivery at ${deliveryTime}`);
});
} else if (cartItem) {
updateCartItem.mutate({ itemId: cartItem.id, quantity: newQuantity });
}
};
// console.log('rendering the product cart for id', item.id)
return (
<ContainerComp>
<MyTouchableOpacity
style={[
tw`bg-white overflow-hidden border border-gray-200`,
isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`,
{ width: itemWidth },
]}
onPress={onPress || (() => {/* TODO: Navigate to product detail */})}
activeOpacity={0.9}
>
<View style={tw`relative`}>
<RnImage
source={{ uri: imageUri }}
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
style={{ width: "100%", height: isHero ? itemWidth * 0.82 : itemWidth, resizeMode: "cover" }}
onLoadStart={() => {
setImageStatus('loading')
setImageError(null)
}}
// onLoadEnd={() => {
// setImageError('loading stopped indefinitely')
//
// }}
onLoad={() => setImageStatus('loaded')}
onError={(event) => {
setImageStatus('error')
setImageError( 'Image failed to load')
}}
/>
{imageStatus === 'error' && (
<View style={tw`absolute inset-0 items-center justify-center bg-gray-100`}>
<MaterialIcons name="broken-image" size={22} color="#94A3B8" />
<MyText style={tw`text-[10px] text-gray-500 mt-1`}>
{imageError || 'Image failed to load'}
</MyText>
</View>
)}
{displayIsOutOfStock && (
<View style={tw`absolute inset-0 bg-black/40 items-center justify-center`}>
<View style={tw`bg-red-500 px-3 py-1 rounded-full`}>
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
</View>
</View>
)}
{miniView && (
<View style={isHero ? tw`absolute bottom-1.5 right-1.5` : tw`absolute bottom-2 right-2`}>
{quantity > 0 ? (
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
) : (
<MyTouchableOpacity
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)}
activeOpacity={0.8}
>
<CartIcon focused={false} size={isHero ? 14 : 16} color="#2E90FA" />
</MyTouchableOpacity>
)}
</View>
)}
</View>
<View style={isHero ? tw`px-2 pt-2` : tw`px-3 pt-3`}>
<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}
</MyText>
<View style={isHero ? tw`flex-row items-baseline mb-1` : tw`flex-row items-baseline mb-2`}>
<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) && (
<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 style={isHero ? tw`flex-row items-center mb-1` : tw`flex-row items-center mb-2`}>
{item.productType !== 'combo' && (
<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>
{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(displayDeliveryDate).format("ddd, DD MMM • h:mm A")}
</MyText>
</View>
)}
{!miniView && (
<>
{displayIsOutOfStock ? (
<View style={tw`bg-gray-100 rounded-lg items-center mt-1`}>
<MyText style={tw`text-gray-400 text-xs font-bold uppercase tracking-wide`}>Unavailable</MyText>
</View>
) : quantity > 0 ? (
<Quantifier
value={quantity}
setValue={handleQuantityChange}
step={item.incrementStep}
unit={item.unitNotation}
/>
) : (
<MyTouchableOpacity
style={tw`bg-brand500 py-2 rounded-lg items-center mt-1`}
onPress={() => handleQuantityChange(1)}
>
<View style={tw`flex-row items-center`}>
<CartIcon focused={false} size={16} color="white" />
<MyText style={tw`text-white text-xs font-bold uppercase tracking-wide ml-1`}>Add to Cart</MyText>
</View>
</MyTouchableOpacity>
)}
</>
)}
</View>
</MyTouchableOpacity>
</ContainerComp>
);
};
export default ProductCard;