343 lines
15 KiB
TypeScript
343 lines
15 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 { trpc } from "@/src/trpc-client";
|
|
import {
|
|
useGetCart,
|
|
useUpdateCartItem,
|
|
useRemoveFromCart,
|
|
useAddToCart,
|
|
type CartType,
|
|
} from "@/hooks/cart-query-hooks";
|
|
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
|
|
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
|
|
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;
|
|
}
|
|
|
|
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 } = trpc.user.slots.getSlotsWithProducts.useQuery();
|
|
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 { addToCart = () => { } } = 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 (!item.slotId) return true; // No slotId
|
|
const slotExists = slotsData.slots.some(slot => slot.id === item.slotId);
|
|
return !slotExists; // Slot doesn't exist
|
|
});
|
|
|
|
itemsToUpdate.forEach((item) => {
|
|
|
|
const availableSlots = productSlotsMap.get(item.productId) || [];
|
|
if (availableSlots.length > 0 && !isFlashDelivery) { // don't bother updating slot for flash delivery
|
|
const nearestSlotId = availableSlots[0];
|
|
removeFromCart.mutate({ itemId: item.id });
|
|
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 price = isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price;
|
|
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}
|
|
</MyText>
|
|
{` • ${itemCount} ${itemCount === 1 ? "Item" : "Items"}`}
|
|
</>
|
|
)}
|
|
</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: item.product.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`}>
|
|
<MyText style={tw`text-slate-900 font-extrabold text-sm flex-1`} numberOfLines={1}>
|
|
{item.product.name.length > 30 ? item.product.name.substring(0, 30) + '...' : item.product.name}
|
|
</MyText>
|
|
<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={item.product.incrementStep}
|
|
showUnits={true}
|
|
unit={item.product?.unitNotation}
|
|
/>
|
|
</View>
|
|
<View style={tw`flex-row items-center justify-between`}>
|
|
{item.slotId && slotsData && productSlotsMap.has(item.productId) && (
|
|
<BottomDropdown
|
|
label="Select Delivery Slot"
|
|
value={item.slotId}
|
|
options={(productSlotsMap.get(item.productId) || []).map(slotId => {
|
|
const slot = slotsData.slots.find(s => s.id === slotId);
|
|
return {
|
|
label: slot ? dayjs(slot.deliveryTime).format("ddd, MMM DD • h:mm A") : "N/A",
|
|
value: slotId,
|
|
};
|
|
})}
|
|
onValueChange={(val) => {
|
|
const newSlot = slotsData.slots.find(s => s.id === val);
|
|
Alert.alert("Delivery Updated", `Scheduled for ${dayjs(newSlot?.deliveryTime).format("MMM DD, h:mm A")}`);
|
|
}}
|
|
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`}>
|
|
₹{(isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.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;
|