import React, { useState, useCallback, useMemo, memo } from "react";
import { View, Dimensions, Image, RefreshControl } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import {
theme,
tw,
useManualRefresh,
useMarkDataFetchers,
LoadingDialog,
MyTouchableOpacity,
MyText, SearchBar, useStatusBarStore,
colors
} from "common-ui";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
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";
import { useAllProducts } from "@/src/hooks/prominent-api-hooks";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import FloatingCartBar from "@/components/floating-cart-bar";
import BannerCarousel from "@/components/BannerCarousel";
import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
import { useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime);
const { width: screenWidth } = Dimensions.get("window");
const itemWidth = screenWidth * 0.45;
const gridItemWidth = (screenWidth - 48) / 2;
const headerColor = colors.secondaryPink;
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');
const endPeriod = endTime.format('A');
if (startPeriod === endPeriod) {
return `${time.format('h')}-${endTime.format('h')} ${startPeriod}`;
} else {
return `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`;
}
};
const staticStyles = {
flatListContent: { gap: 16 },
columnWrapper: { gap: 16, paddingHorizontal: 16 },
popularListContent: { paddingBottom: 16 },
slotsListContent: { paddingBottom: 24 },
};
interface RenderStoreProps {
item: any;
}
const RenderStore = memo(({ item }: RenderStoreProps) => {
const router = useRouter();
const { setNavigatedFromHome, setSelectedStoreId } = useNavigationStore();
const handlePress = useCallback(() => {
setNavigatedFromHome(true);
setSelectedStoreId(item.id);
router.push('/(drawer)/(tabs)/stores');
}, [item.id, router, setNavigatedFromHome, setSelectedStoreId]);
return (
{item.signedImageUrl ? (
) : (
)}
{item.name.replace(/^The\s+/i, "")}
);
});
RenderStore.displayName = 'RenderStore';
interface SlotCardProps {
slot: any;
}
const SlotCard = memo(({ slot }: SlotCardProps) => {
const router = useRouter();
const now = dayjs();
const freezeTime = dayjs(slot.freezeTime);
const isClosingSoon = freezeTime.diff(now, "hour") < 4 && freezeTime.isAfter(now);
const handlePress = useCallback(() => {
router.push(`/(drawer)/(tabs)/home/slot-view?slotId=${slot.id}`);
}, [router, slot.id]);
return (
{isClosingSoon && (
CLOSING SOON
)}
Delivery At
{formatTimeRange(slot.deliveryTime)}
{dayjs(slot.deliveryTime).format("ddd, MMM DD")}
Order By
{dayjs(slot.freezeTime).format("h:mm A")}
{dayjs(slot.freezeTime).format("ddd, MMM DD")}
{slot.products.slice(0, 3).map((p: any, i: number) => (
0 && tw`-ml-3`]}
>
{p.images?.[0] ? (
) : (
)}
))}
View all {slot.products.length} items
);
});
interface PopularProductItemProps {
item: any;
onPress: (id: number) => void;
}
const PopularProductItem = memo(({ item, onPress }: PopularProductItemProps) => {
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
return (
);
});
interface SlotItemProps {
item: any;
}
const SlotItem = memo(({ item }: SlotItemProps) => );
interface ProductItemProps {
item: any;
onPress: (id: number) => void;
}
const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
return (
);
});
interface ListHeaderProps {
gradientHeight: number;
onGradientLayout: (height: number) => void;
storesData: any;
popularProducts: any[];
sortedSlots: any[];
onProductPress: (id: number) => void;
}
const ListHeader = memo(({
gradientHeight,
onGradientLayout,
storesData,
popularProducts,
sortedSlots,
onProductPress,
}: ListHeaderProps) => {
const handleLayout = useCallback((event: any) => {
const { y, height } = event.nativeEvent.layout;
onGradientLayout(y + height);
}, [onGradientLayout]);
const renderPopularItem = useCallback(({ item }: { item: any }) => (
), [onProductPress]);
const renderSlotItem = useCallback(({ item }: { item: any }) => (
), []);
const gradientStyle = useMemo(() => [
tw`absolute left-0 right-0 shadow-lg`,
{ height: gradientHeight + 32, zIndex: -1 }
], [gradientHeight]);
return (
<>
{storesData?.stores && storesData.stores.length > 0 && (
Our Stores
Fresh from our locations
{storesData.stores.map((store: any) => (
))}
)}
Popular Items
Trending fresh picks just for you
item.id.toString()}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={staticStyles.popularListContent}
renderItem={renderPopularItem}
removeClippedSubviews={true}
/>
{sortedSlots.length > 0 && (
Upcoming Delivery Slots
Plan your fresh deliveries ahead
item.id.toString()}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={staticStyles.slotsListContent}
decelerationRate="fast"
snapToInterval={280 + 16}
renderItem={renderSlotItem}
removeClippedSubviews={true}
/>
)}
All Available Products
Browse our complete selection
>
);
});
export default function Dashboard() {
const router = useRouter();
const userDetails = useUserDetails();
const [inputQuery, setInputQuery] = useState("");
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const [gradientHeight, setGradientHeight] = useState(0);
const [displayedProducts, setDisplayedProducts] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore();
const { getQuickestSlot } = useProductSlotIdentifier();
const [isRefreshing, setIsRefreshing] = useState(false);
const {
data: productsData,
isLoading,
error,
refetch,
} = useAllProducts();
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery();
const products = productsData?.products || [];
React.useEffect(() => {
if (products.length > 0 && displayedProducts.length === 0) {
const initialBatch = products
.filter(p => typeof p.id === "number")
.sort((a, b) => {
const slotA = getQuickestSlot(a.id);
const slotB = getQuickestSlot(b.id);
if (slotA && !slotB) return -1;
if (!slotA && slotB) return 1;
if (a.isOutOfStock && !b.isOutOfStock) return 1;
if (!a.isOutOfStock && b.isOutOfStock) return -1;
return 0;
});
setDisplayedProducts(initialBatch);
setHasMore(products.length > 10);
}
}, [productsData]);
const popularItemIds = useMemo(() => {
const popularItems = essentialConsts?.popularItems;
if (!popularItems) return [];
if (Array.isArray(popularItems)) {
return popularItems.map((id: any) => parseInt(id)).filter((id: number) => !isNaN(id));
} else if (typeof popularItems === 'string') {
return popularItems
.split(',')
.map((id: string) => parseInt(id.trim()))
.filter((id: number) => !isNaN(id));
}
return [];
}, [essentialConsts?.popularItems]);
const sortedSlots = useMemo(() => {
if (!slotsData?.slots) return [];
return [...slotsData.slots].sort((a, b) => {
const deliveryDiff = dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime));
if (deliveryDiff !== 0) return deliveryDiff;
return dayjs(a.freezeTime).diff(dayjs(b.freezeTime));
});
}, [slotsData]);
const popularProducts = useMemo(() => {
return popularItemIds
.map(id => products.find(product => product.id === id))
.filter((product): product is NonNullable => product != null);
}, [popularItemIds, products]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
await Promise.all([refetch(), refetchStores(), refetchSlots(), refetchConsts()]);
} finally {
setIsRefreshing(false);
}
}, [refetch, refetchStores, refetchSlots, refetchConsts]);
useManualRefresh(() => {
handleRefresh();
});
useMarkDataFetchers(() => {
handleRefresh();
});
const handleProductPress = useCallback((id: number) => {
router.push(`/(drawer)/(tabs)/home/product-detail/${id}`);
}, [router]);
const handleGradientLayout = useCallback((height: number) => {
setGradientHeight(height);
}, []);
const handleSearchPress = useCallback(() => {
router.push("/(drawer)/(tabs)/home/search-results");
}, [router]);
const renderProductItem = useCallback(({ item }: { item: any }) => (
), [handleProductPress]);
const listHeader = useMemo(() => (
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress]);
const searchBarContainerStyle = useMemo(() => [
tw`w-full px-4 pt-4 pb-2`,
{ backgroundColor }
], [backgroundColor]);
const listContentContainerStyle = useMemo(() => [
tw`pb-24`,
staticStyles.flatListContent
], []);
if (isLoading || isLoadingConsts) {
return (
{isLoading ? 'Loading products...' : 'Loading app settings...'}
);
}
if (error || constsError) {
return (
Oops!
{error ? 'Failed to load products' : 'Failed to load app settings'}
);
}
return (
{ }}
onPress={handleSearchPress}
editable={false}
containerStyle={tw`bg-white`}
onSubmitEditing={() => {
if (inputQuery.trim()) {
router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
}
}}
returnKeyType="search"
/>
item.id.toString()}
numColumns={2}
contentContainerStyle={listContentContainerStyle}
columnWrapperStyle={staticStyles.columnWrapper}
renderItem={renderProductItem}
ListHeaderComponent={listHeader}
refreshControl={
}
ListEmptyComponent={
No products available
}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
/>
);
}