freshyo/apps/user-ui/components/floating-cart-bar.tsx
2026-03-11 16:31:23 +05:30

392 lines
17 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { View, ScrollView, Dimensions, Alert } from "react-native";
import { Image } from 'expo-image';
import { useRouter } from "expo-router";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import {
StorageServiceCasual,
tw,
MyText,
MyTouchableOpacity,
MiniQuantifier,
BottomDropdown,
BottomDialog,
theme,
updateStatusBarColor,
} from "common-ui";
import {
useGetCart,
useUpdateCartItem,
useRemoveFromCart,
useAddToCart,
type CartType,
} from "@/hooks/cart-query-hooks";
import { useGetEssentialConsts, useSlots } from "@/src/hooks/prominent-api-hooks"
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import dayjs from "dayjs";
import { LinearGradient } from "expo-linear-gradient";
const { height: screenHeight } = Dimensions.get("window");
interface FloatingCartBarProps {
isFlashDelivery?: boolean;
isExpanded?: boolean;
setIsExpanded?: (value: boolean) => void;
}
// Smart time window formatting function
const formatTimeRange = (deliveryTime: string | Date) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');
const endPeriod = endTime.format('A');
let timeRange;
if (startPeriod === endPeriod) {
timeRange = `${time.format('h')}-${endTime.format('h')} ${startPeriod}`;
} else {
timeRange = `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`;
}
return `${time.format('ddd, DD MMM ')}${timeRange}`;
};
// Product name component with quantity
const ProductNameWithQuantity = ({ name, productQuantity, unitNotation }: { name: string; productQuantity: number; unitNotation: string }) => {
const truncatedName = name.length > 25 ? name.substring(0, 25) + '...' : name;
const unit = unitNotation ? ` ${unitNotation}` : '';
return (
<MyText style={tw`text-slate-900 font-extrabold text-sm flex-1`} numberOfLines={1}>
{truncatedName} <MyText style={tw`text-slate-500 font-medium text-xs`}>({productQuantity}{unit})</MyText>
</MyText>
);
};
const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
isFlashDelivery = false,
isExpanded: controlledIsExpanded,
setIsExpanded: controlledSetIsExpanded,
}) => {
const cartBarColor = isFlashDelivery ? '#f81260' : theme.colors.brand600;
const cartBarBorderColor = isFlashDelivery ? '#e11d48' : theme.colors.brand500;
const router = useRouter();
const [localIsExpanded, setLocalIsExpanded] = useState(false);
const [quantities, setQuantities] = useState<Record<number, number>>({});
const cartType: CartType = isFlashDelivery ? "flash" : "regular";
const isExpanded = controlledIsExpanded ?? localIsExpanded;
const setIsExpanded = controlledSetIsExpanded ?? setLocalIsExpanded;
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
const { data: constsData } = useGetEssentialConsts();
const { data: slotsData } = useSlots();
const productsById = useCentralProductStore((state) => state.productsById);
const { productSlotsMap } = useProductSlotIdentifier();
const cartItems = cartData?.items || [];
const itemCount = cartItems.length;
const updateCartItem = useUpdateCartItem({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
}, cartType);
const removeFromCart = useRemoveFromCart({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
}, cartType);
const addToCartHook = useAddToCart({
showSuccessAlert: false,
showErrorAlert: false,
refetchCart: true,
}, cartType);
useEffect(() => {
const initial: Record<number, number> = {};
cartItems.forEach((item) => {
initial[item.id] = item.quantity;
});
setQuantities(initial);
}, [cartData]);
useEffect(() => {
if (!cartItems.length || !slotsData?.slots || !productSlotsMap) return;
const itemsToUpdate = cartItems.filter(item => {
if (isFlashDelivery || !item.slotId) return false;
const availableSlots = productSlotsMap[item.productId]?.slots || [];
const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId);
return !isSlotAvailable;
});
itemsToUpdate.forEach((item) => {
const availableSlots = productSlotsMap[item.productId]?.slots || [];
if (availableSlots.length > 0 && !isFlashDelivery) {
const nearestSlotId = availableSlots[0].id;
removeFromCart.mutate({ itemId: item.id });
addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId);
}
});
}, []);
const firstItem = cartItems[0];
const expandedHeight = screenHeight * 0.7;
// Calculate total cart value and free delivery info
const totalCartValue = cartItems.reduce(
(sum, item) => {
const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return sum + price * item.quantity;
},
0
);
const freeDeliveryThreshold = isFlashDelivery
? constsData?.flashFreeDeliveryThreshold
: constsData?.freeDeliveryThreshold;
const remainingForFreeDelivery = Math.max(
0,
freeDeliveryThreshold - totalCartValue
);
return (
<>
<View
style={[
tw`rounded-2xl shadow-2xl overflow-hidden`,
{ backgroundColor: cartBarColor, borderColor: cartBarBorderColor, borderWidth: 1 },
]}
>
{!isExpanded && (
// --- Collapsed View ---
<MyTouchableOpacity
style={tw`flex-row items-center justify-between px-5 py-3`}
onPress={() => itemCount > 0 && setIsExpanded(true)}
activeOpacity={0.9}
>
<View style={tw`flex-row items-center flex-1`}>
<View style={tw`flex-1`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-white font-extrabold text-sm mr-2`}>
{itemCount === 0 ? (isFlashDelivery ? "No Flash Items" : "No Items In Cart") : (
<>
<MyText style={tw`text-white font-black text-base`}>
{totalCartValue}
{`${itemCount} ${itemCount === 1 ? "Item" : "Items"}`}
</MyText>
</>
)}
</MyText>
{itemCount > 0 && <MaterialIcons name="expand-less" size={18} color="white" />}
</View>
{remainingForFreeDelivery > 0 ? (
<MyText style={tw`text-[10px] text-brand100 mt-1 font-bold`}>
{remainingForFreeDelivery} more for <MyText style={tw`text-emerald-300`}>FREE Delivery</MyText>
</MyText>
) : itemCount > 0 ? (
<View style={tw`flex-row items-center mt-0.5`}>
<MaterialIcons name="verified" size={14} color="#10B981" />
<MyText style={tw`text-emerald-300 text-[10px] font-black uppercase ml-1 tracking-tighter`}>
Free Delivery Unlocked
</MyText>
</View>
) : (
<MyText style={tw`text-brand200 text-[10px] mt-0.5`}>Shop for {freeDeliveryThreshold}+ for free shipping</MyText>
)}
</View>
</View>
<MyTouchableOpacity
style={tw`bg-white px-3 py-2 rounded-2xl shadow-lg shadow-black/20`}
onPress={() => router.push(
isFlashDelivery
? "/(drawer)/(tabs)/flash-delivery/(cart)/cart"
: "/(drawer)/(tabs)/home/cart"
)}
>
<MyText style={[tw`font-bold text-sm`, { color: cartBarColor }]}>Go to Cart</MyText>
</MyTouchableOpacity>
</MyTouchableOpacity>
)}
</View>
<BottomDialog open={isExpanded} onClose={() => setIsExpanded(false)} enableDismiss={true}>
<ScrollView style={{ maxHeight: Dimensions.get('window').height * 0.8 }}>
{/* Header */}
<View style={tw`px-6 py-5 border-b border-slate-100 flex-row items-center justify-between`}>
<View>
<MyText style={tw`text-slate-900 font-black text-xl tracking-tight`}>
Your Cart
</MyText>
<MyText style={tw`text-slate-400 text-xs font-bold uppercase tracking-widest`}>
{itemCount} Items
</MyText>
</View>
<MyTouchableOpacity
style={tw`w-10 h-10 rounded-2xl bg-slate-100 items-center justify-center`}
onPress={() => setIsExpanded(false)}
>
<MaterialIcons name="close" size={24} color="#64748B" />
</MyTouchableOpacity>
</View>
{/* Progress Bar Header */}
{remainingForFreeDelivery > 0 && (
<View style={tw`px-6 py-3 bg-emerald-50/50 flex-row items-center justify-between`}>
<View style={tw`flex-1 mr-4`}>
<View style={tw`flex-row items-center justify-between mb-1.5`}>
<MyText style={tw`text-[10px] font-black text-emerald-700 uppercase`}>Free Delivery Progress</MyText>
<MyText style={tw`text-[10px] font-black text-emerald-700`}>{Math.round((totalCartValue / freeDeliveryThreshold) * 100)}%</MyText>
</View>
<View style={tw`h-1.5 bg-white rounded-full overflow-hidden border border-emerald-100`}>
<View style={[tw`h-full bg-emerald-500`, { width: `${(totalCartValue / freeDeliveryThreshold) * 100}%` }]} />
</View>
</View>
<View style={tw`items-end`}>
<MyText style={tw`text-[10px] font-bold text-slate-500`}>Needed</MyText>
<MyText style={tw`text-sm font-black text-emerald-600`}>+{remainingForFreeDelivery}</MyText>
</View>
</View>
)}
{/* Items List */}
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`px-6 py-4`}>
{cartItems.map((item, index) => (
<React.Fragment key={item.id}>
<View style={tw`py-4`}>
<View style={tw`flex-row items-center`}>
<Image
source={{ uri: productsById[item.productId]?.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/>
<View style={tw`flex-1 ml-4`}>
<View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity
name={productsById[item.productId]?.name || ''}
productQuantity={productsById[item.productId]?.productQuantity || 0}
unitNotation={productsById[item.productId]?.unitNotation || ''}
/>
<MiniQuantifier
value={quantities[item.id] || item.quantity}
onChange={(value) => {
if (value === 0) {
removeFromCart.mutate({ itemId: item.id });
} else {
setQuantities((prev) => ({ ...prev, [item.id]: value }));
updateCartItem.mutate({ itemId: item.id, quantity: value });
}
}}
step={productsById[item.productId]?.incrementStep || 1}
showUnits={true}
unit={productsById[item.productId]?.unitNotation}
/>
</View>
<View style={tw`flex-row items-center justify-between`}>
{item.slotId && slotsData && productSlotsMap[item.productId] && (
<BottomDropdown
label="Select Delivery Slot"
value={item.slotId}
options={(productSlotsMap[item.productId]?.slots || []).map((slot) => {
return {
label: slot ? formatTimeRange(slot.deliveryTime) : "N/A",
value: slot.id,
};
})}
onValueChange={async (val) => {
const newSlot = slotsData.slots.find(s => s.id === val);
if (!newSlot) return;
const productId = item.productId;
const quantity = item.quantity;
const itemId = item.id;
const slotId = typeof val === 'number' ? val : Number(val);
// await removeFromCart.mutateAsync(itemId);
addToCartHook.addToCart(productId, 0, slotId, () => {
refetchCart();
Alert.alert("Delivery Updated", `Scheduled for ${formatTimeRange(newSlot?.deliveryTime || '')}`);
});
}}
triggerComponent={({ onPress, displayText }) => (
<MyTouchableOpacity
onPress={onPress}
style={tw`flex-row items-center bg-blue-50 px-2 py-1 rounded-lg border border-blue-100`}
>
<MaterialIcons name="schedule" size={10} color={theme.colors.brand600} />
<MyText style={tw`text-[9px] font-black text-blue-700 ml-1 uppercase`}>
{displayText}
</MyText>
<MaterialIcons name="keyboard-arrow-down" size={12} color={theme.colors.brand500} />
</MyTouchableOpacity>
)}
/>
)}
<MyText style={tw`text-slate-900 text-sm font-bold`}>
{(() => {
const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return price * item.quantity;
})()}
</MyText>
</View>
</View>
</View>
</View>
{index < cartItems.length - 1 && (
<View style={tw`h-px bg-slate-200 w-full`} />
)}
</React.Fragment>
))}
</ScrollView>
{/* Fancy Footer */}
<View style={tw`p-6 bg-white border-t border-slate-100`}>
<View style={tw`flex-row justify-between items-center mb-5`}>
<View>
<MyText style={tw`text-slate-400 text-[10px] font-black uppercase tracking-widest`}>Subtotal</MyText>
<MyText style={tw`text-2xl font-black text-slate-900`}>{totalCartValue}</MyText>
</View>
{remainingForFreeDelivery === 0 && (
<View style={tw`bg-emerald-50 px-3 py-1.5 rounded-xl border border-emerald-100 flex-row items-center`}>
<MaterialIcons name="local-shipping" size={14} color="#059669" />
<MyText style={tw`text-emerald-700 text-[10px] font-black ml-1.5 uppercase`}>Free Delivery</MyText>
</View>
)}
</View>
<MyTouchableOpacity
onPress={() => router.push(
isFlashDelivery
? "/(drawer)/(tabs)/flash-delivery/(cart)/cart"
: "/(drawer)/(tabs)/home/cart"
)}
activeOpacity={0.9}
>
<LinearGradient
colors={isFlashDelivery ? ['#f81260', '#c40e50'] : ['#1570EF', '#194185']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={tw`py-4 rounded-2xl items-center shadow-lg shadow-brand200 flex-row justify-center`}
>
<MyText style={tw`text-white font-black text-base uppercase tracking-widest`}>
Go to cart
</MyText>
<MaterialIcons name="chevron-right" size={20} color="white" style={tw`ml-1`} />
</LinearGradient>
</MyTouchableOpacity>
</View>
</ScrollView>
</BottomDialog>
</>
);
};
export default FloatingCartBar;