import React, { useState, useCallback, useMemo, memo, useRef, useEffect } from "react";
import { View, Dimensions, Image, RefreshControl } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import {
theme,
tw,
useManualRefresh,
useMarkDataFetchers,
LoadingDialog,
MyTouchableOpacity,
MyText, SearchBar
} 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, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import FloatingCartBar from "@/components/floating-cart-bar";
import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime);
const { width: screenWidth } = Dimensions.get("window");
const itemWidth = screenWidth * 0.45;
const heroItemWidth = (screenWidth - 72) / 3;
const gridItemWidth = (screenWidth - 48) / 2;
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: { justifyContent: 'space-between', paddingHorizontal: 16 },
slotsListContent: { paddingBottom: 24 },
} as const;
const TAG_COLORS = [
{ pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose
{ pageBg: '#FFFCF1', bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', dot: '#D97706' }, // amber
{ pageBg: '#F6FEF9', bg: '#F0FDF4', border: '#BBF7D0', text: '#166534', dot: '#16A34A' }, // green
{ pageBg: '#F5FAFF', bg: '#EFF6FF', border: '#BFDBFE', text: '#1E3A8A', dot: '#2563EB' }, // blue
{ pageBg: '#FAF8FF', bg: '#F5F3FF', border: '#DDD6FE', text: '#5B21B6', dot: '#7C3AED' }, // violet
{ pageBg: '#FFF9F2', bg: '#FFF7ED', border: '#FFD6B0', text: '#9A3412', dot: '#EA580C' }, // orange
{ pageBg: '#F3FEFF', bg: '#ECFEFF', border: '#A5F3FC', text: '#155E75', dot: '#0891B2' }, // cyan
{ pageBg: '#FFF7FB', bg: '#FDF2F8', border: '#FBCFE8', text: '#9D174D', dot: '#DB2777' }, // pink
];
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length];
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 TagTabStripProps {
dashboardTags: any[];
activeTagId: number | null;
onSelectTag: (id: number) => void;
}
// Fixed, horizontally scrollable tab strip (plain ScrollView — no nesting,
// so it scrolls reliably on Android). Sits OUTSIDE the vertical FlatList.
const TagTabStrip = memo(({
dashboardTags,
activeTagId,
onSelectTag,
}: TagTabStripProps) => {
const scrollRef = useRef(null);
const positions = useRef([]);
const routes = useMemo(
() => dashboardTags.map((tag) => ({ key: String(tag.id), title: tag.tagName })),
[dashboardTags]
);
const index = useMemo(() => {
const idx = dashboardTags.findIndex((tag) => tag.id === activeTagId);
return idx < 0 ? 0 : idx;
}, [dashboardTags, activeTagId]);
useEffect(() => {
const x = positions.current[index];
if (x != null) {
scrollRef.current?.scrollTo({ x: Math.max(0, x - 16), animated: true });
}
}, [index]);
return (
{routes.map((route, i) => {
const isActive = i === index;
return (
{
positions.current[i] = e.nativeEvent.layout.x;
}}
onPress={() => onSelectTag(Number(route.key))}
style={tw`h-12 px-3 justify-center`}
activeOpacity={0.7}
>
{route.title}
);
})}
);
});
interface TagSceneProps {
dashboardTags: any[];
activeTagId: number | null;
productsByTagId: Record;
expandedByTagId: Record;
onToggleExpand: (tagId: number) => void;
onProductPress: (id: number) => void;
}
// Active tag's products grid — rendered inside the vertical FlatList so it
// scrolls with the page.
const TagScene = memo(({
dashboardTags,
activeTagId,
productsByTagId,
expandedByTagId,
onToggleExpand,
onProductPress,
}: TagSceneProps) => {
const tagId = activeTagId ?? dashboardTags[0]?.id ?? null;
if (tagId == null) return null;
const products = productsByTagId[tagId] || [];
const expanded = !!expandedByTagId[tagId];
const visible = expanded ? products : products.slice(0, 6);
const hasMore = products.length > 6 && !expanded;
return (
{products.length > 0 ? (
{visible.map((product: any) => (
))}
) : (
No products in this category yet
)}
{hasMore && (
onToggleExpand(tagId)}
>
Show More
)}
);
});
interface ExploreProductItemProps {
item: any;
onPress: (id: number) => void;
}
const ExploreProductItem = memo(({ item, onPress }: ExploreProductItemProps) => {
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 {
dashboardTags: any[];
activeTagId: number | null;
productsByTagId: Record;
expandedByTagId: Record;
onToggleExpand: (tagId: number) => void;
onProductPress: (id: number) => void;
onSelectTag: (id: number) => void;
storesData: any;
sortedSlots: any[];
}
const ListHeader = memo(({
dashboardTags,
activeTagId,
productsByTagId,
expandedByTagId,
onToggleExpand,
onProductPress,
onSelectTag,
storesData,
sortedSlots,
}: ListHeaderProps) => {
const renderSlotItem = useCallback(({ item }: { item: any }) => (
), []);
return (
<>
{dashboardTags.length > 0 && (
)}
{dashboardTags.length > 0 && (
)}
{storesData?.stores && storesData.stores.length > 0 && (
Our Stores
Fresh from our locations
{storesData.stores.map((store: any) => (
))}
)}
{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 [displayedProducts, setDisplayedProducts] = useState([]);
const [visibleCount, setVisibleCount] = useState(21);
const [hasMore, setHasMore] = useState(true);
const { getQuickestSlot } = useProductSlotIdentifier();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
const refetchSlotsFromStore = useCentralSlotStore((state) => state.refetchSlots);
const [isRefreshing, setIsRefreshing] = useState(false);
const {
data: productsData,
isLoading,
error,
} = useAllProducts();
const { data: essentialConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
const { data: storesData, refetch: refetchStores } = useStores();
const { data: slotsData } = useSlots();
const products = productsData?.products || [];
const dashboardTags = productsData?.tags || [];
const [selectedTagId, setSelectedTagId] = useState(null);
const activeTagId = selectedTagId ?? dashboardTags[0]?.id ?? null;
const [expandedByTagId, setExpandedByTagId] = useState>({});
const handleToggleExpand = useCallback((tagId: number) => {
setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }));
}, []);
React.useEffect(() => {
if (products.length === 0) {
setDisplayedProducts([])
setHasMore(false)
return
}
const allSorted = products
.filter(p => typeof p.id === "number")
.sort((a, b) => {
const slotA = getQuickestSlot(a.id)
const slotB = getQuickestSlot(b.id)
const aOutOfStock = Boolean(productSlotsMap[a.id]?.isOutOfStock) || !slotA
const bOutOfStock = Boolean(productSlotsMap[b.id]?.isOutOfStock) || !slotB
if (aOutOfStock && !bOutOfStock) return 1
if (!aOutOfStock && bOutOfStock) return -1
return 0
})
setDisplayedProducts(allSorted)
setVisibleCount(21)
setHasMore(allSorted.length > 21)
}, [productsData, productSlotsMap]);
const handleShowMore = useCallback(() => {
setVisibleCount((prev) => {
const next = prev + 21;
setHasMore(next < displayedProducts.length);
return next;
});
}, [displayedProducts.length]);
const sortedSlots = useMemo(() => {
if (!slotsData?.slots) return [];
const now = dayjs();
return [...slotsData.slots]
.filter(slot => dayjs(slot.deliveryTime).isAfter(now))
.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 productsByTagId = useMemo(() => {
const map: Record = {};
for (const tag of dashboardTags) {
const productById = new Map();
for (const product of products) {
productById.set(product.id, product);
}
// Respect the admin-curated order — tag.productIds is already ordered by
// the tag's sortOrder (backend reorders it when building the cache).
map[tag.id] = (tag.productIds || [])
.map((id: number) => productById.get(id))
.filter(Boolean) as any[];
}
return map;
}, [dashboardTags, products]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
const promises = [];
if (refetchProducts) {
promises.push(refetchProducts());
}
if (refetchSlotsFromStore) {
promises.push(refetchSlotsFromStore());
}
promises.push(refetchStores());
promises.push(refetchConsts());
await Promise.all(promises);
} finally {
setIsRefreshing(false);
}
}, [refetchProducts, refetchSlotsFromStore, refetchStores, refetchConsts]);
useManualRefresh(() => {
handleRefresh();
});
useMarkDataFetchers(() => {
handleRefresh();
});
const handleProductPress = useCallback((id: number) => {
router.push(`/(drawer)/(tabs)/home/product-detail/${id}`);
}, [router]);
const handleSearchPress = useCallback(() => {
router.push("/(drawer)/(tabs)/home/search-results");
}, [router]);
const renderProductItem = useCallback(({ item }: { item: any }) => (
//
), [handleProductPress]);
const listHeader = useMemo(() => (
), [dashboardTags, activeTagId, productsByTagId, expandedByTagId, handleToggleExpand, handleProductPress, storesData, sortedSlots]);
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
const pageTintStyle = useMemo(() => [
tw`flex-1`,
{ backgroundColor: '#FFFFFF' }
], [pageTint]);
const searchBarContainerStyle = useMemo(() => [
tw`w-full px-4 pt-4 pb-0`,
{ backgroundColor: '#FFFFFF' }
], [pageTint]);
const listContentContainerStyle = useMemo(() => [
tw`pb-24`,
staticStyles.flatListContent
], []);
if (isLoading) {
return (
Loading products...
);
}
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={3}
style={{ backgroundColor: '#FFFFFF' }}
contentContainerStyle={listContentContainerStyle}
columnWrapperStyle={staticStyles.columnWrapper}
renderItem={renderProductItem}
ListHeaderComponent={listHeader}
ListFooterComponent={
hasMore ? (
Show More
) : null
}
refreshControl={
}
ListEmptyComponent={
No products available
}
// NOTE: do not add removeClippedSubviews here - it breaks touch
// delivery to the nested horizontal scrollables in ListHeaderComponent
// (tag tabs / slots) on Android.
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
/>
);
}