510 lines
No EOL
19 KiB
TypeScript
510 lines
No EOL
19 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Drawer } from 'expo-router/drawer';
|
|
import { DrawerContentComponentProps } from '@react-navigation/drawer';
|
|
import { View, ScrollView, Alert, Dimensions } from 'react-native';
|
|
import { Image } from 'expo-image';
|
|
import { useRouter, usePathname } from 'expo-router';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier } from 'common-ui';
|
|
import { trpc } from '@/src/trpc-client';
|
|
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
|
|
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
|
import { useHideTabNav } from '@/src/hooks/useHideTabNav';
|
|
import CartIcon from '@/components/icons/CartIcon';
|
|
import { useSlotStore } from '@/components/stores/slotStore';
|
|
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import FloatingCartBar from '@/components/floating-cart-bar';
|
|
import QuickDeliveryAddressSelector from '@/components/QuickDeliveryAddressSelector';
|
|
import dayjs from 'dayjs';
|
|
|
|
const { width: screenWidth } = Dimensions.get("window");
|
|
const drawerWidth = 85; // From layout drawerStyle
|
|
const itemWidth = (screenWidth - drawerWidth - 48) / 2; // Account for drawer width
|
|
|
|
interface SlotLayoutProps {
|
|
slotId?: number;
|
|
storeId?: number;
|
|
baseUrl: string;
|
|
isForFlashDelivery?: boolean;
|
|
}
|
|
|
|
function CustomDrawerContent(baseUrl: string, drawerProps: DrawerContentComponentProps, slotIdParent?: number, storeIdParent?: number) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const { data: storesData } = trpc.user.stores.getStores.useQuery();
|
|
const setStoreId = useSlotStore(state => state.setStoreId);
|
|
|
|
const { slotId, storeId } = useSlotStore();
|
|
|
|
// Get current pathname to determine active item
|
|
const currentPath = pathname;
|
|
// Check if we are on the main 'quick-delivery' page (All products)
|
|
// const isAllActive = !currentPath.includes('/store/');
|
|
const isAllActive = isNaN(storeId as number);
|
|
|
|
|
|
const allSlotsUrl = `${baseUrl}${slotId ? `?slotId=${slotId}` : ''}`;
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50 px-2 border-r border-gray-200`}>
|
|
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={tw`pb-20`}>
|
|
<View style={tw`flex-col gap-3`}>
|
|
{/* All Products Item */}
|
|
<MyTouchableOpacity
|
|
style={[
|
|
tw`items-center w-full mb-2 rounded-2xl overflow-hidden`,
|
|
isAllActive ? tw`shadow-lg` : tw``
|
|
]}
|
|
onPress={() => {
|
|
router.replace(allSlotsUrl as any);
|
|
}}
|
|
activeOpacity={0.8}
|
|
>
|
|
{isAllActive ? (
|
|
<LinearGradient
|
|
colors={[theme.colors.brand400, theme.colors.brand600]}
|
|
style={tw`w-full p-3 items-center`}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
>
|
|
<View style={tw`w-10 h-10 rounded-full items-center justify-center bg-white/20 mb-1 border border-white/30`}>
|
|
<MaterialIcons name="grid-view" size={20} color="white" />
|
|
</View>
|
|
<MyText style={tw`text-[10px] font-bold text-white text-center`}>
|
|
ALL
|
|
</MyText>
|
|
</LinearGradient>
|
|
) : (
|
|
<View style={tw`w-full p-3 items-center bg-white border border-gray-100 rounded-2xl`}>
|
|
<View style={tw`w-10 h-10 rounded-full items-center justify-center bg-gray-50 mb-1`}>
|
|
<MaterialIcons name="grid-view" size={20} color="#6B7280" />
|
|
</View>
|
|
<MyText style={tw`text-[10px] font-medium text-gray-500 text-center`}>
|
|
ALL
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</MyTouchableOpacity>
|
|
|
|
<View style={tw`h-[1px] bg-gray-200 my-1 mx-2`} />
|
|
|
|
{/* Store Items */}
|
|
{storesData?.stores?.map(store => {
|
|
// Check if this specific store is active
|
|
|
|
const isStoreActive = storeId === store.id;
|
|
// const isStoreActive = currentPath.includes(`/store/${store.id}`);
|
|
|
|
return (
|
|
<MyTouchableOpacity
|
|
key={store.id}
|
|
style={[
|
|
tw`items-center w-full mb-3 rounded-2xl overflow-hidden`,
|
|
isStoreActive ? tw`shadow-lg` : tw``
|
|
]}
|
|
onPress={() => {
|
|
setStoreId(store.id);
|
|
const targetUrl = `${baseUrl}${slotId ? `?slotId=${slotId}&storeId=${store.id}` : `?storeId=${store.id}`}`;
|
|
|
|
router.replace(targetUrl as any);
|
|
}}
|
|
activeOpacity={0.8}
|
|
>
|
|
{isStoreActive ? (
|
|
<LinearGradient
|
|
colors={[theme.colors.brand400, theme.colors.brand600]}
|
|
style={tw`w-full p-2 py-3 items-center`}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
>
|
|
<View style={tw`w-12 h-12 rounded-full items-center justify-center bg-white border-2 border-white mb-2 shadow-sm overflow-hidden`}>
|
|
{store.signedImageUrl ? (
|
|
<Image
|
|
source={{ uri: store.signedImageUrl }}
|
|
style={tw`w-12 h-12 rounded-full`}
|
|
resizeMode="cover"
|
|
/>
|
|
) : (
|
|
<MaterialIcons name="storefront" size={24} color={theme.colors.brand500} />
|
|
)}
|
|
</View>
|
|
<MyText style={tw`text-[10px] font-bold text-white text-center w-full leading-tight`} numberOfLines={2}>
|
|
{store.name.replace(/^The\s+/i, '')}
|
|
</MyText>
|
|
|
|
{/* Active Pip/Indicator */}
|
|
<View style={tw`absolute right-0 top-0 bottom-0 w-1 bg-white/30`} />
|
|
</LinearGradient>
|
|
) : (
|
|
<View style={tw`w-full p-2 py-3 items-center bg-white border border-gray-100 rounded-2xl`}>
|
|
<View style={tw`w-12 h-12 rounded-full items-center justify-center bg-gray-50 mb-2 border border-gray-100 overflow-hidden`}>
|
|
{store.signedImageUrl ? (
|
|
<Image
|
|
source={{ uri: store.signedImageUrl }}
|
|
style={tw`w-12 h-12 rounded-full`}
|
|
resizeMode="cover"
|
|
/>
|
|
) : (
|
|
<MaterialIcons name="storefront" size={24} color="#9CA3AF" />
|
|
)}
|
|
</View>
|
|
<MyText style={tw`text-[10px] font-medium text-gray-500 text-center w-full leading-tight`} numberOfLines={2}>
|
|
{store.name.replace(/^The\s+/i, '')}
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</MyTouchableOpacity>
|
|
);
|
|
}) || (
|
|
<MyText style={tw`text-xs text-gray-400 text-center py-4`}>Loading...</MyText>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function SlotLayout({ slotId, storeId, baseUrl, isForFlashDelivery }: SlotLayoutProps) {
|
|
const router = useRouter();
|
|
|
|
// const { slotId: paramsSlotId } = useLocalSearchParams();
|
|
const isDrawerHidden = useQuickDeliveryStore(state => state.isDrawerHidden);
|
|
const setSelectedSlotId = useQuickDeliveryStore(state => state.setSelectedSlotId);
|
|
const setSlotId = useSlotStore(state => state.setSlotId);
|
|
|
|
const handleSlotChange = (newSlotId: number) => {
|
|
setSelectedSlotId(newSlotId);
|
|
setSlotId(newSlotId);
|
|
router.replace(`${baseUrl}?slotId=${newSlotId}` as any);
|
|
};
|
|
|
|
const slotQuery = slotId
|
|
? trpc.user.slots.getSlotById.useQuery({ slotId: Number(slotId) })
|
|
: trpc.user.slots.nextMajorDelivery.useQuery();
|
|
const deliveryTime = dayjs(slotQuery.data?.deliveryTime).format('DD MMM hh:mm A');
|
|
|
|
|
|
return (
|
|
<>
|
|
<View style={tw` w-full flex-row bg-white px-4 py-2 mb-1`}>
|
|
<QuickDeliveryAddressSelector
|
|
deliveryTime={deliveryTime}
|
|
slotId={Number(slotId)}
|
|
onSlotChange={handleSlotChange}
|
|
isForFlashDelivery={isForFlashDelivery}
|
|
/>
|
|
</View>
|
|
<Drawer
|
|
drawerContent={(props) => CustomDrawerContent(baseUrl, props, slotId, storeId)}
|
|
screenOptions={{
|
|
headerShown: false,
|
|
drawerType: 'permanent',
|
|
drawerStyle: {
|
|
width: 85,
|
|
...(isDrawerHidden && { display: 'none' }),
|
|
},
|
|
headerStyle: {
|
|
// width: 220,
|
|
backgroundColor: 'white',
|
|
},
|
|
headerShadowVisible: false,
|
|
}}
|
|
>
|
|
</Drawer>
|
|
|
|
{/* <FloatingCartBar /> */}
|
|
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
|
<FloatingCartBar isFlashDelivery={isForFlashDelivery} />
|
|
{/* <View style={tw`h-12 w-full bg-green-600`}></View> */}
|
|
</View>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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 CompactProductCard = ({
|
|
item,
|
|
handleAddToCart,
|
|
onPress,
|
|
cartType = "regular",
|
|
}: {
|
|
item: any;
|
|
handleAddToCart: (productId: number) => void;
|
|
onPress?: () => void;
|
|
cartType?: "regular" | "flash";
|
|
}) => {
|
|
|
|
// Cart management for miniView
|
|
const { data: cartData } = useGetCart({}, cartType);
|
|
const updateCartItem = useUpdateCartItem({
|
|
showSuccessAlert: false,
|
|
showErrorAlert: false,
|
|
refetchCart: true,
|
|
}, cartType);
|
|
const removeFromCart = useRemoveFromCart({
|
|
showSuccessAlert: false,
|
|
showErrorAlert: false,
|
|
refetchCart: true,
|
|
}, cartType);
|
|
|
|
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
|
|
const quantity = cartItem?.quantity || 0;
|
|
|
|
const handleQuantityChange = (newQuantity: number) => {
|
|
if (newQuantity === 0 && cartItem) {
|
|
removeFromCart.mutate({ itemId: cartItem.id });
|
|
} else if (newQuantity === 1 && !cartItem) {
|
|
handleAddToCart(item.id);
|
|
} else if (cartItem) {
|
|
updateCartItem.mutate({ itemId: cartItem.id, quantity: newQuantity });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<MyTouchableOpacity
|
|
style={[
|
|
tw`bg-white rounded-lg shadow-sm mb-2 overflow-hidden border border-gray-100`,
|
|
{ width: itemWidth },
|
|
]}
|
|
onPress={onPress}
|
|
activeOpacity={0.9}
|
|
>
|
|
<View style={tw`relative`}>
|
|
<Image
|
|
source={{ uri: item.images?.[0] }}
|
|
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
|
/>
|
|
{item.isOutOfStock && (
|
|
<View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}>
|
|
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
|
|
</View>
|
|
)}
|
|
<View style={tw`absolute bottom-2 right-2`}>
|
|
{quantity > 0 ? (
|
|
<MiniQuantifier
|
|
value={quantity}
|
|
onChange={handleQuantityChange}
|
|
step={item.incrementStep}
|
|
showUnits={true}
|
|
unit={item.unit}
|
|
/>
|
|
) : (
|
|
<MyTouchableOpacity
|
|
style={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={16} color="#2E90FA" />
|
|
</MyTouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={tw`p-2`}>
|
|
<MyText style={tw`text-gray-900 font-medium text-xs mb-1`} numberOfLines={2}>
|
|
{item.name}
|
|
</MyText>
|
|
|
|
<View style={tw`flex-row items-center justify-between`}>
|
|
<View style={tw`flex-row items-baseline flex-wrap`}>
|
|
<MyText style={tw`text-brand500 font-bold text-sm`}>₹{cartType === "flash" ? (item.flashPrice ?? item.price) : item.price}</MyText>
|
|
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
|
|
<MyText style={tw`text-gray-400 text-xs ml-1 line-through`}>₹{item.marketPrice}</MyText>
|
|
)}
|
|
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unit).display}</MyText></MyText>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
</MyTouchableOpacity>
|
|
);
|
|
};
|
|
|
|
interface SlotProductsProps {
|
|
slotId?: number;
|
|
storeId?: number;
|
|
baseUrl: string;
|
|
}
|
|
|
|
export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseUrl, }: SlotProductsProps) {
|
|
useHideTabNav('quick_delivery');
|
|
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = React.useState(false);
|
|
const router = useRouter();
|
|
const slotId = slotIdParent;
|
|
const storeId = storeIdParent;
|
|
const storeIdNum = storeId;
|
|
// const { storeId, slotId: slotIdRaw } = useLocalSearchParams();
|
|
// const slotId = Number(slotIdRaw);
|
|
|
|
|
|
// const storeIdNum = storeId ? Number(storeId) : undefined;
|
|
|
|
const slotQuery = trpc.user.slots.getSlotById.useQuery({ slotId: slotId! }, { enabled: !!slotId });
|
|
|
|
const productsQuery = trpc.user.product.getAllProductsSummary.useQuery();
|
|
|
|
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
|
|
|
|
const handleAddToCart = (productId: number) => {
|
|
setIsLoadingDialogOpen(true);
|
|
addToCart(productId, 1, slotId || 0, () => setIsLoadingDialogOpen(false));
|
|
};
|
|
|
|
if (slotQuery.isLoading || (storeIdNum && productsQuery?.isLoading)) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MyText style={tw`text-gray-500 font-medium`}>Loading slot delivery...</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
if (slotQuery.error || (storeIdNum && productsQuery?.error)) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MaterialIcons name="error-outline" size={48} color="#D84343" />
|
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
|
|
<MyText style={tw`text-gray-500 mt-2`}>Failed to load slot delivery</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
if (!slotQuery.data) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<MyText style={tw`text-2xl font-bold text-gray-900 mb-4`}>Quick Delivery</MyText>
|
|
<MyText style={tw`text-gray-600`}>No delivery slot available.</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
const filteredProducts: any[] = storeIdNum ? productsQuery?.data?.filter(p => p.storeId === storeIdNum) || [] : slotQuery.data.products;
|
|
|
|
return (
|
|
<View style={tw`flex-1`}>
|
|
<MyFlatList
|
|
data={filteredProducts}
|
|
numColumns={2}
|
|
renderItem={({ item }) => (
|
|
<CompactProductCard
|
|
item={item}
|
|
handleAddToCart={handleAddToCart}
|
|
onPress={() => router.push(`/(drawer)/(tabs)/home/product-detail/${item.id}`)}
|
|
cartType="regular"
|
|
/>
|
|
)}
|
|
keyExtractor={(item, index) => index.toString()}
|
|
columnWrapperStyle={{ gap: 16, justifyContent: 'flex-start' }}
|
|
contentContainerStyle={[tw`pb-24 px-4`, { gap: 16 }]}
|
|
onRefresh={() => slotQuery.refetch()}
|
|
ListEmptyComponent={
|
|
storeIdNum ? (
|
|
<View style={tw`items-center justify-center py-10`}>
|
|
<MyText style={tw`text-gray-400 font-medium`}>No products from this store in this slot.</MyText>
|
|
</View>
|
|
) : null
|
|
}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface FlashDeliveryProductsProps {
|
|
storeId?: number;
|
|
baseUrl: string;
|
|
onProductPress?: (productId: number) => void;
|
|
}
|
|
|
|
export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProductPress }: FlashDeliveryProductsProps) {
|
|
useHideTabNav('quick_delivery');
|
|
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = React.useState(false);
|
|
const router = useRouter();
|
|
const storeId = storeIdParent;
|
|
const storeIdNum = storeId;
|
|
|
|
const productsQuery = trpc.user.product.getAllProductsSummary.useQuery();
|
|
|
|
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
|
|
|
|
const handleAddToCart = (productId: number) => {
|
|
setIsLoadingDialogOpen(true);
|
|
|
|
addToCart(productId, 1, 0, () => setIsLoadingDialogOpen(false));
|
|
};
|
|
|
|
if (storeIdNum && productsQuery?.isLoading) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MyText style={tw`text-gray-500 font-medium`}>Loading Flash Delivery...</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
if (storeIdNum && productsQuery?.error) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MaterialIcons name="error-outline" size={48} color="#D84343" />
|
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
|
|
<MyText style={tw`text-gray-500 mt-2`}>Failed to load Flash Delivery</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
// Filter products to only include those eligible for flash delivery
|
|
let flashProducts: any[] = [];
|
|
if (storeIdNum) {
|
|
// Filter by store and flash availability
|
|
flashProducts = productsQuery?.data?.filter(p => p.storeId === storeIdNum && p.isFlashAvailable) || [];
|
|
} else {
|
|
// Show all flash-available products (no slot filtering)
|
|
flashProducts = productsQuery?.data?.filter(p => p.isFlashAvailable) || [];
|
|
}
|
|
|
|
return (
|
|
<View style={tw`flex-1`}>
|
|
<MyFlatList
|
|
data={flashProducts}
|
|
numColumns={2}
|
|
renderItem={({ item }) => (
|
|
<CompactProductCard
|
|
item={item}
|
|
handleAddToCart={handleAddToCart}
|
|
onPress={() => {
|
|
if (onProductPress) {
|
|
onProductPress(item.id);
|
|
} else {
|
|
router.push(`/(drawer)/(tabs)/flash-delivery/product-detail/${item.id}`);
|
|
}
|
|
}}
|
|
cartType="flash"
|
|
/>
|
|
)}
|
|
keyExtractor={(item, index) => index.toString()}
|
|
columnWrapperStyle={{ gap: 16, justifyContent: 'flex-start' }}
|
|
contentContainerStyle={[tw`pb-24 px-4`, { gap: 16 }]}
|
|
onRefresh={() => productsQuery.refetch()}
|
|
ListEmptyComponent={
|
|
<View style={tw`items-center justify-center py-10`}>
|
|
<MyText style={tw`text-gray-400 font-medium`}>No Flash Delivery products available.</MyText>
|
|
</View>
|
|
}
|
|
/>
|
|
</View>
|
|
);
|
|
} |