- {item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
-
+ {/* Delivery date hidden for now
{showDeliveryInfo && displayDeliveryDate && (
-
-
-
- {dayjs(displayDeliveryDate).format('ddd, DD MMM • h:mm A')}
+
+
+
+
+ {dayjs(displayDeliveryDate).format('ddd, DD MMM')}
+
+
+
+ {dayjs(displayDeliveryDate).format('h:mm A')}
)}
+ */}
{!miniView && (
- <>
+
{displayIsOutOfStock ? (
-
+
Unavailable
@@ -204,19 +221,18 @@ export function ProductCard({
/>
) : (
)}
- >
+
)}
diff --git a/apps/web-ui/src/components/shell/Sidebar.tsx b/apps/web-ui/src/components/shell/Sidebar.tsx
new file mode 100644
index 0000000..d48109c
--- /dev/null
+++ b/apps/web-ui/src/components/shell/Sidebar.tsx
@@ -0,0 +1,136 @@
+import React from 'react'
+import { useLocation, useNavigate } from '@tanstack/react-router'
+import { useAuth } from '../../lib/auth-context'
+import { Home, Store, Zap, Tag, User, LogOut, LogIn } from 'lucide-react'
+
+interface NavItem {
+ path: string
+ label: string
+ icon: React.ReactNode
+ match: (path: string) => boolean
+}
+
+const navItems: NavItem[] = [
+ {
+ path: '/home',
+ label: 'Home',
+ icon:
,
+ match: (p) => p === '/home' || p.startsWith('/home/'),
+ },
+ {
+ path: '/stores',
+ label: 'Stores',
+ icon:
,
+ match: (p) => p === '/stores' || p.startsWith('/stores/'),
+ },
+ {
+ path: '/flash',
+ label: '1 Hr Delivery',
+ icon:
,
+ match: (p) => p === '/flash' || p.startsWith('/flash/'),
+ },
+ {
+ path: '/offers',
+ label: 'Offers',
+ icon:
,
+ match: (p) => p === '/offers' || p.startsWith('/offers/'),
+ },
+ {
+ path: '/me',
+ label: 'My Account',
+ icon:
,
+ match: (p) => p === '/me' || p.startsWith('/me/'),
+ },
+]
+
+export function Sidebar() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const { user, logout } = useAuth()
+ const currentPath = location.pathname
+
+ return (
+
+ )
+}
diff --git a/apps/web-ui/src/components/shell/Topbar.tsx b/apps/web-ui/src/components/shell/Topbar.tsx
new file mode 100644
index 0000000..b2bc74c
--- /dev/null
+++ b/apps/web-ui/src/components/shell/Topbar.tsx
@@ -0,0 +1,82 @@
+import React from 'react'
+import { useNavigate } from '@tanstack/react-router'
+import { SearchBar } from 'web-components'
+import { useGetCart } from '../../hooks/cart-query-hooks'
+import { ShoppingCart } from 'lucide-react'
+
+interface TopbarProps {
+ isFlashDelivery?: boolean
+ onCartClick?: () => void
+}
+
+export function Topbar({ isFlashDelivery = false, onCartClick }: TopbarProps) {
+ const navigate = useNavigate()
+ const cartType = isFlashDelivery ? 'flash' : 'regular'
+ const { data: cartData } = useGetCart(cartType)
+ const itemCount = cartData?.items?.length || 0
+
+ const handleCartClick = () => {
+ if (onCartClick) {
+ onCartClick()
+ return
+ }
+ navigate({ to: isFlashDelivery ? '/flash/cart' : '/cart' })
+ }
+
+ const handleSearchClick = () => {
+ navigate({ to: '/home/search' as any })
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/web-ui/src/hooks/cart-query-hooks.ts b/apps/web-ui/src/hooks/cart-query-hooks.ts
index 0c314bd..789ff1b 100644
--- a/apps/web-ui/src/hooks/cart-query-hooks.ts
+++ b/apps/web-ui/src/hooks/cart-query-hooks.ts
@@ -12,6 +12,12 @@ interface CartItem {
deliveryDate?: string | null
}
+interface CartData {
+ items: CartItem[]
+ totalItems: number
+ totalAmount: number
+}
+
function getCartKey(cartType: CartType): string {
return `local-cart-${cartType}`
}
@@ -29,15 +35,16 @@ function writeCart(cartType: CartType, items: CartItem[]) {
localStorage.setItem(getCartKey(cartType), JSON.stringify(items))
}
+// Build the exact shape useGetCart returns, so setQueryData matches the cache.
+function toCartData(items: CartItem[]): CartData {
+ const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
+ return { items, totalItems, totalAmount: 0 }
+}
+
export function useGetCart(cartType: CartType = 'regular') {
return useQuery({
queryKey: [getCartKey(cartType)],
- queryFn: () => {
- const items = readCart(cartType)
- const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
- const totalAmount = 0 // Amount calculated in components with product data
- return { items, totalItems, totalAmount }
- },
+ queryFn: () => toCartData(readCart(cartType)),
})
}
@@ -76,6 +83,9 @@ export function useAddToCart(cartType: CartType = 'regular') {
})
}
writeCart(cartType, items)
+ // Optimistically update the cache so the UI reflects the cart instantly,
+ // before any refetch completes.
+ queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
@@ -98,14 +108,19 @@ export function useUpdateCartItem(cartType: CartType = 'regular') {
slotId?: number | null
deliveryDate?: string | null
}) => {
- const items = readCart(cartType)
+ let items = readCart(cartType)
const existing = items.find((i) => i.productId === productId)
- if (existing) {
+ if (!existing) return
+ if (quantity <= 0) {
+ // Quantity hit 0 — remove the item from the cart entirely
+ items = items.filter((i) => i.productId !== productId)
+ } else {
existing.quantity = quantity
if (slotId !== undefined) existing.slotId = slotId
if (deliveryDate !== undefined) existing.deliveryDate = deliveryDate
}
writeCart(cartType, items)
+ queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
@@ -121,6 +136,7 @@ export function useRemoveFromCart(cartType: CartType = 'regular') {
let items = readCart(cartType)
items = items.filter((i) => i.productId !== productId)
writeCart(cartType, items)
+ queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
diff --git a/apps/web-ui/src/hooks/prominent-api-hooks.ts b/apps/web-ui/src/hooks/prominent-api-hooks.ts
index 820ec54..8b809fd 100644
--- a/apps/web-ui/src/hooks/prominent-api-hooks.ts
+++ b/apps/web-ui/src/hooks/prominent-api-hooks.ts
@@ -1,8 +1,10 @@
+import { useMemo, useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { trpc } from '../lib/trpc-client'
import type {
AllProductsApiType,
+ AvailabilityApiType,
StoresApiType,
SlotsApiType,
EssentialConstsApiType,
@@ -18,11 +20,86 @@ export const useGetEssentialConsts = () => {
return { ...query, refetch: query.refetch }
}
+// ---------------------------------------------------------------------------
+// Persisted cache helpers — store fetched JSON keyed by its versioned URL.
+// Only refetch + repersist when the version (URL) changes.
+// ---------------------------------------------------------------------------
+
+interface PersistedCache
{
+ version: string
+ data: T
+}
+
+const CACHE_STORAGE_KEYS = {
+ products: 'cache_products',
+ stores: 'cache_stores',
+ slots: 'cache_slots',
+ banners: 'cache_banners',
+ availability: 'cache_availability',
+ storeProducts: (storeId: number) => `cache_store_${storeId}`,
+} as const
+
+async function readPersistedCache(key: string): Promise | null> {
+ try {
+ const raw = localStorage.getItem(key)
+ if (!raw) return null
+ return JSON.parse(raw) as PersistedCache
+ } catch {
+ return null
+ }
+}
+
+async function writePersistedCache(key: string, version: string, data: T): Promise {
+ try {
+ localStorage.setItem(key, JSON.stringify({ version, data }))
+ } catch (e) {
+ console.error('writePersistedCache error:', e)
+ }
+}
+
+function usePersistedCache(storageKey: string, version: string | null) {
+ const [initialData, setInitialData] = useState(undefined)
+ const [isReady, setIsReady] = useState(false)
+
+ useEffect(() => {
+ let cancelled = false
+ setIsReady(false)
+ setInitialData(undefined)
+ if (!version) {
+ setIsReady(true)
+ return
+ }
+ readPersistedCache(storageKey).then((persisted) => {
+ if (cancelled) return
+ if (persisted && persisted.version === version) {
+ setInitialData(persisted.data)
+ }
+ setIsReady(true)
+ })
+ return () => { cancelled = true }
+ }, [storageKey, version])
+
+ return { initialData, isReady }
+}
+
type ProductsResponse = AllProductsApiType
type StoresResponse = StoresApiType
type SlotsResponse = SlotsApiType
type BannersResponse = BannersApiType
type StoreWithProductsResponse = StoreWithProductsApiType
+type AvailabilityResponse = AvailabilityApiType
+
+type BaseProduct = AllProductsApiType['products'][number]
+type AvailabilityEntry = AvailabilityApiType['availability'][number]
+
+export type MergedProduct = BaseProduct & {
+ price: number
+ marketPrice: number | null
+ flashPrice: string | null
+ isFlashAvailable: boolean
+ isOutOfStock: boolean
+ isSuspended: boolean
+}
function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts()
@@ -43,71 +120,169 @@ function useCacheUrl(filename: string): string | null {
return `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/${filename}`
}
+function useAvailabilityCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const availabilityVersionNum = essentialConsts?.availabilityVersionNum
+
+ if (!assetsDomain || availabilityVersionNum === undefined || availabilityVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}av-${availabilityVersionNum}/${CACHE_FILENAMES.availability}`
+}
+
+function useSlotsCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const slotsVersionNum = essentialConsts?.slotsVersionNum
+
+ if (!assetsDomain || slotsVersionNum === undefined || slotsVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}slots/v-${slotsVersionNum}/${CACHE_FILENAMES.slots}`
+}
+
export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
+ const { data: availabilityData } = useAvailability()
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.products, version)
- return useQuery({
- queryKey: ['all-products', cacheUrl],
+ const productsQuery = useQuery({
+ queryKey: ['all-products', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get(cacheUrl)
+ await writePersistedCache(CACHE_STORAGE_KEYS.products, version, response.data)
return response.data
},
staleTime: 60000,
- enabled: !!cacheUrl,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
+ })
+
+ const mergedProducts = useMemo(() => {
+ const rawProducts = productsQuery.data?.products || []
+ const availabilityById: Record = {}
+ availabilityData?.availability?.forEach((entry: AvailabilityEntry) => {
+ availabilityById[entry.id] = entry
+ })
+
+ return rawProducts.map((product) => {
+ const availability = availabilityById[product.id]
+ return {
+ ...product,
+ price: availability ? Number(availability.price) : 0,
+ marketPrice: availability?.marketPrice != null ? Number(availability.marketPrice) : null,
+ flashPrice: availability?.flashPrice ?? null,
+ isFlashAvailable: availability?.isFlashAvailable ?? false,
+ isOutOfStock: availability?.isOutOfStock ?? false,
+ isSuspended: availability?.isSuspended ?? false,
+ }
+ })
+ }, [productsQuery.data, availabilityData])
+
+ const mergedData = useMemo(() => {
+ if (!productsQuery.data) return undefined
+ return {
+ ...productsQuery.data,
+ products: mergedProducts,
+ } as ProductsResponse & { products: MergedProduct[] }
+ }, [productsQuery.data, mergedProducts])
+
+ return {
+ ...productsQuery,
+ data: mergedData,
+ }
+}
+
+export function useAvailability() {
+ const cacheUrl = useAvailabilityCacheUrl()
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.availability, version)
+
+ return useQuery({
+ queryKey: ['availability', version],
+ queryFn: async () => {
+ if (!cacheUrl) {
+ throw new Error('Cache URL not available')
+ }
+ const response = await axios.get(cacheUrl)
+ await writePersistedCache(CACHE_STORAGE_KEYS.availability, version, response.data)
+ return response.data
+ },
+ staleTime: 60000,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
})
}
export function useStores() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores)
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.stores, version)
return useQuery({
- queryKey: ['stores', cacheUrl],
+ queryKey: ['stores', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get(cacheUrl)
+ await writePersistedCache(CACHE_STORAGE_KEYS.stores, version, response.data)
return response.data
},
staleTime: 60000,
- enabled: !!cacheUrl,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
})
}
export function useSlots() {
- const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots)
+ const cacheUrl = useSlotsCacheUrl()
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.slots, version)
return useQuery({
- queryKey: ['slots', cacheUrl],
+ queryKey: ['slots', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get(cacheUrl + '?v=123')
+ await writePersistedCache(CACHE_STORAGE_KEYS.slots, version, response.data)
return response.data
},
staleTime: 60000,
- enabled: !!cacheUrl,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
})
}
export function useBanners() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners)
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.banners, version)
return useQuery({
- queryKey: ['banners', cacheUrl],
+ queryKey: ['banners', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get(cacheUrl)
+ await writePersistedCache(CACHE_STORAGE_KEYS.banners, version, response.data)
return response.data
},
staleTime: 60000,
- enabled: !!cacheUrl,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
})
}
@@ -122,17 +297,21 @@ export function useStoreWithProducts(storeId: number) {
assetsDomain && apiCacheKey
? `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/stores/${storeId}.json`
: null
+ const version = cacheUrl ?? ''
+ const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version)
return useQuery({
- queryKey: ['store-with-products', storeId, cacheUrl],
+ queryKey: ['store-with-products', storeId, version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get(cacheUrl)
+ await writePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version, response.data)
return response.data
},
staleTime: 60000,
- enabled: !!cacheUrl,
+ enabled: !!cacheUrl && isReady,
+ placeholderData: initialData,
})
}
diff --git a/apps/web-ui/src/hooks/usePopulateCentralStores.ts b/apps/web-ui/src/hooks/usePopulateCentralStores.ts
index 4e506ee..31d7a88 100644
--- a/apps/web-ui/src/hooks/usePopulateCentralStores.ts
+++ b/apps/web-ui/src/hooks/usePopulateCentralStores.ts
@@ -34,8 +34,9 @@ export function usePopulateCentralStores() {
// Check if flash delivery is available
const isFlashAvailable = product.isFlashAvailable || false
- // Check if out of stock (no slots available)
- const isOutOfStock = productSlots.length === 0 && !isFlashAvailable
+ // Use the availability-merged out-of-stock flag (from products cache + availability)
+ // instead of the "no slots" heuristic, so products aren't wrongly marked OOS on startup.
+ const isOutOfStock = product.isOutOfStock === true || (productSlots.length === 0 && !isFlashAvailable && product.isOutOfStock !== false)
productMap[product.id] = {
slotId: productSlots.length > 0 ? productSlots[0].id : null,
diff --git a/apps/web-ui/src/lib/query-client.ts b/apps/web-ui/src/lib/query-client.ts
index cb54ee1..47a05a1 100644
--- a/apps/web-ui/src/lib/query-client.ts
+++ b/apps/web-ui/src/lib/query-client.ts
@@ -1,13 +1,20 @@
import { QueryClient } from '@tanstack/react-query'
+// Module-level singleton — shared across SSR hydration, tRPC, and all
+// components so mutations/invalidations hit the same cache.
+let queryClient: QueryClient | undefined
+
export function getQueryClient() {
- return new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 30 * 1000,
- retry: 2,
- refetchOnWindowFocus: false,
+ if (!queryClient) {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 30 * 1000,
+ retry: 2,
+ refetchOnWindowFocus: false,
+ },
},
- },
- })
+ })
+ }
+ return queryClient
}
diff --git a/apps/web-ui/src/lib/trpc-client.ts b/apps/web-ui/src/lib/trpc-client.ts
index fe0efce..1dd230e 100644
--- a/apps/web-ui/src/lib/trpc-client.ts
+++ b/apps/web-ui/src/lib/trpc-client.ts
@@ -3,7 +3,8 @@ import { createTRPCClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from '@backend/trpc/router'
// const BASE_API_URL = 'http://192.168.100.111:8787'
-export const BASE_API_URL = 'https://worker.freshyo.in'
+export const BASE_API_URL =
+ (import.meta.env?.VITE_API_URL as string | undefined) || 'https://worker.freshyo.in'
export const trpc = createTRPCReact()
diff --git a/apps/web-ui/src/routeTree.gen.ts b/apps/web-ui/src/routeTree.gen.ts
index 5f7e39d..435d558 100644
--- a/apps/web-ui/src/routeTree.gen.ts
+++ b/apps/web-ui/src/routeTree.gen.ts
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as StoresRouteImport } from './routes/stores'
import { Route as SlotViewRouteImport } from './routes/slot-view'
import { Route as RegisterRouteImport } from './routes/register'
+import { Route as OffersRouteImport } from './routes/offers'
import { Route as MeRouteImport } from './routes/me'
import { Route as LoginRouteImport } from './routes/login'
import { Route as HomeRouteImport } from './routes/home'
@@ -55,6 +56,11 @@ const RegisterRoute = RegisterRouteImport.update({
path: '/register',
getParentRoute: () => rootRouteImport,
} as any)
+const OffersRoute = OffersRouteImport.update({
+ id: '/offers',
+ path: '/offers',
+ getParentRoute: () => rootRouteImport,
+} as any)
const MeRoute = MeRouteImport.update({
id: '/me',
path: '/me',
@@ -200,6 +206,7 @@ export interface FileRoutesByFullPath {
'/home': typeof HomeRouteWithChildren
'/login': typeof LoginRoute
'/me': typeof MeRouteWithChildren
+ '/offers': typeof OffersRoute
'/register': typeof RegisterRoute
'/slot-view': typeof SlotViewRoute
'/stores': typeof StoresRouteWithChildren
@@ -231,6 +238,7 @@ export interface FileRoutesByTo {
'/flash': typeof FlashRouteWithChildren
'/login': typeof LoginRoute
'/me': typeof MeRouteWithChildren
+ '/offers': typeof OffersRoute
'/register': typeof RegisterRoute
'/slot-view': typeof SlotViewRoute
'/stores': typeof StoresRouteWithChildren
@@ -264,6 +272,7 @@ export interface FileRoutesById {
'/home': typeof HomeRouteWithChildren
'/login': typeof LoginRoute
'/me': typeof MeRouteWithChildren
+ '/offers': typeof OffersRoute
'/register': typeof RegisterRoute
'/slot-view': typeof SlotViewRoute
'/stores': typeof StoresRouteWithChildren
@@ -298,6 +307,7 @@ export interface FileRouteTypes {
| '/home'
| '/login'
| '/me'
+ | '/offers'
| '/register'
| '/slot-view'
| '/stores'
@@ -329,6 +339,7 @@ export interface FileRouteTypes {
| '/flash'
| '/login'
| '/me'
+ | '/offers'
| '/register'
| '/slot-view'
| '/stores'
@@ -361,6 +372,7 @@ export interface FileRouteTypes {
| '/home'
| '/login'
| '/me'
+ | '/offers'
| '/register'
| '/slot-view'
| '/stores'
@@ -394,6 +406,7 @@ export interface RootRouteChildren {
HomeRoute: typeof HomeRouteWithChildren
LoginRoute: typeof LoginRoute
MeRoute: typeof MeRouteWithChildren
+ OffersRoute: typeof OffersRoute
RegisterRoute: typeof RegisterRoute
SlotViewRoute: typeof SlotViewRoute
StoresRoute: typeof StoresRouteWithChildren
@@ -422,6 +435,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RegisterRouteImport
parentRoute: typeof rootRouteImport
}
+ '/offers': {
+ id: '/offers'
+ path: '/offers'
+ fullPath: '/offers'
+ preLoaderRoute: typeof OffersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/me': {
id: '/me'
path: '/me'
@@ -715,6 +735,7 @@ const rootRouteChildren: RootRouteChildren = {
HomeRoute: HomeRouteWithChildren,
LoginRoute: LoginRoute,
MeRoute: MeRouteWithChildren,
+ OffersRoute: OffersRoute,
RegisterRoute: RegisterRoute,
SlotViewRoute: SlotViewRoute,
StoresRoute: StoresRouteWithChildren,
diff --git a/apps/web-ui/src/routes/cart.tsx b/apps/web-ui/src/routes/cart.tsx
index e3e97da..f896288 100644
--- a/apps/web-ui/src/routes/cart.tsx
+++ b/apps/web-ui/src/routes/cart.tsx
@@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks'
-import { p, MyButton, Quantifier, AppContainer, div } from 'web-components'
-import { Trash2 } from 'lucide-react'
+import { Quantifier } from 'web-components'
+import { AppLayout } from '../components/AppLayout'
+import { Trash2, ShoppingCart, Ticket, ChevronRight } from 'lucide-react'
export const Route = createFileRoute('/cart')({ component: CartPage })
@@ -28,71 +29,126 @@ function CartPage() {
})
return (
-
-
- Your Cart
-
-
- {cartItems.length === 0 ? (
-
-
Your cart is empty
-
navigate({ to: '/home' })}
- />
+
+
+
+
+ Review Before Delivery
+
+
Your Cart
- ) : (
- <>
-
- {cartItems.map((item) => {
- const product = productsById[item.productId]
- const price = product.price
- return (
-
-

-
-
- {product.name}
-
-
- ₹{price}
-
-
updateItem.mutate({ productId: item.productId, quantity: q })}
+
+ {cartItems.length === 0 ? (
+
+
+
+
+
Your cart is empty
+
Fresh cuts are waiting at the counter
+
+
+ ) : (
+
+ {/* Items */}
+
+ {cartItems.map((item) => {
+ const product = productsById[item.productId]
+ const price = product.price
+ return (
+
+

+
+
{product.name}
+
+ {product.productQuantity || 1}{product.unitNotation || ''} per unit
+
+
+
updateItem.mutate({ productId: item.productId, quantity: q })}
+ />
+ ₹{price}
+
+
+
+
₹{price * item.quantity}
+
+
-
removeItem.mutate(item.productId)}>
-
+ )
+ })}
+
+
+ {/* Coupons card */}
+
+
+
+
+
+
Offers & Coupons
+
+
+
+
+ {/* Summary */}
+
+
+
Bill Summary
+
+
+
Item Total
+
₹{total}
+
+
+
Delivery Fee
+
Calculated at checkout
- )
- })}
-
-
-
-
-
Total
-
- ₹{total}
-
+
+
+
+
-
navigate({ to: '/checkout' })}
- className="bg-brand-500 text-white"
- />
- >
- )}
-
+ )}
+
+
)
}
diff --git a/apps/web-ui/src/routes/checkout.tsx b/apps/web-ui/src/routes/checkout.tsx
index c680a23..bde7de6 100644
--- a/apps/web-ui/src/routes/checkout.tsx
+++ b/apps/web-ui/src/routes/checkout.tsx
@@ -10,8 +10,7 @@ import { AddressForm } from '../components/AddressForm'
import { Dialog } from '../components/Dialog'
import { useAddressStore } from '../lib/stores/address-store'
import { useQueryClient } from '@tanstack/react-query'
-import { p, div } from 'web-components'
-import { MapPin, ShoppingCart, ChevronLeft } from 'lucide-react'
+import { ShoppingCart, ChevronLeft, MapPin } from 'lucide-react'
export const Route = createFileRoute('/checkout')({ component: CheckoutPage })
@@ -45,66 +44,71 @@ function CheckoutContent() {
if (cartItems.length === 0) {
return (
-
-
+
+
+
+
Your cart is empty
-
+
Add some delicious items to your cart before checking out
-
navigate({ to: '/home' })}
- className="rounded-lg bg-brand-500 px-6 py-3"
+ className="rounded-lg bg-brand-600 px-6 py-3 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
-
- Back to Shopping
-
-
+ Back to Shopping
+
)
}
return (
-
+
{/* Checkout Header */}
-
-
-
+
+
+
+
-
-
-
-
+
Checkout
-
- {/* Address Selection */}
-
{
- setEditingAddress(null)
- setShowAddAddress(true)
- }}
- onEditAddress={(address) => {
- setEditingAddress(address)
- setShowAddAddress(true)
- }}
- />
+
+ {/* Left: Address Selection + Payment */}
+
+ {
+ setEditingAddress(null)
+ setShowAddAddress(true)
+ }}
+ onEditAddress={(address) => {
+ setEditingAddress(address)
+ setShowAddAddress(true)
+ }}
+ />
+
- {/* Payment and Order Summary */}
-
navigate({ to: '/cart' })}
- />
+ {/* Right: Order summary + payment */}
+
+
navigate({ to: '/cart' })}
+ />
+
{/* Add/Edit Address Dialog */}
diff --git a/apps/web-ui/src/routes/flash.cart.tsx b/apps/web-ui/src/routes/flash.cart.tsx
index 119018f..c3fa9b1 100644
--- a/apps/web-ui/src/routes/flash.cart.tsx
+++ b/apps/web-ui/src/routes/flash.cart.tsx
@@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useCentralProductStore } from '../lib/stores/central-product-store'
-import { p, MyButton, Quantifier, AppContainer, div } from 'web-components'
-import { Trash2, Zap } from 'lucide-react'
+import { Quantifier } from 'web-components'
+import { AppLayout } from '../components/AppLayout'
+import { Trash2, Zap, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/flash/cart')({ component: FlashCartPage })
@@ -24,74 +25,112 @@ function FlashCartPage() {
})
return (
-
-
-
- {cartItems.length === 0 ? (
-
-
Your flash cart is empty
-
navigate({ to: '/flash' })}
- />
+
+
+
+
+ 1 Hr Delivery
+
+
+
+ Flash Cart
+
- ) : (
- <>
-
- {cartItems.map((item) => {
- const product = productsById[item.productId]
- const price = product.discountedPrice ?? product.price
- return (
-
-

-
-
- {product.name}
-
-
₹{price}
-
- updateItem.mutate({ productId: item.productId, quantity: q })
- }
+
+ {cartItems.length === 0 ? (
+
+
+
+
+
Your flash cart is empty
+
Pick something from 1 hr delivery
+
+
+ ) : (
+
+ {/* Items */}
+
+ {cartItems.map((item) => {
+ const product = productsById[item.productId]
+ const price = product.discountedPrice ?? product.price
+ return (
+
+

+
+
{product.name}
+
+ {product.unitValue || 1}{product.unit || ''} per unit
+
+
+
updateItem.mutate({ productId: item.productId, quantity: q })}
+ />
+ ₹{price}
+
+
+
+
₹{price * item.quantity}
+
+
-
removeItem.mutate(item.productId)}>
-
+ )
+ })}
+
+
+ {/* Summary */}
+
+
+
Bill Summary
+
+
+
Item Total
+
₹{total}
+
+
+
Delivery Fee
+
Calculated at checkout
- )
- })}
-
-
-
-
-
Total
-
- ₹{total}
-
+
+
+
+
-
navigate({ to: '/flash/checkout' })}
- className="bg-brand-500 text-white"
- />
- >
- )}
-
+ )}
+
+
)
}
diff --git a/apps/web-ui/src/routes/flash.order-success.tsx b/apps/web-ui/src/routes/flash.order-success.tsx
index 70fc53f..db256c2 100644
--- a/apps/web-ui/src/routes/flash.order-success.tsx
+++ b/apps/web-ui/src/routes/flash.order-success.tsx
@@ -1,5 +1,4 @@
-import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
-import { p, MyButton } from 'web-components'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Zap } from 'lucide-react'
export const Route = createFileRoute('/flash/order-success')({
@@ -15,25 +14,27 @@ function FlashOrderSuccessPage() {
const { orderId, totalAmount } = Route.useSearch()
return (
-
-
-
+
+
+
-
- 1 Hr Order Placed!
+
+ 1 Hr Order Placed
Order ID: #{orderId}
Total: ₹{totalAmount}
-
navigate({ to: '/flash' })}
- className="mb-3 bg-brand-500 text-white"
- />
-
+ Continue Shopping
+
+
)
}
diff --git a/apps/web-ui/src/routes/flash.tsx b/apps/web-ui/src/routes/flash.tsx
index 954d930..a1a67c0 100644
--- a/apps/web-ui/src/routes/flash.tsx
+++ b/apps/web-ui/src/routes/flash.tsx
@@ -1,12 +1,14 @@
-import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
-import { useState, useMemo } from 'react'
-import { p, div, MiniQuantifier } from 'web-components'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+import { useState, useMemo, useRef, useEffect } from 'react'
+import { MiniQuantifier } from 'web-components'
import { useAllProducts, useStores } from '../hooks/prominent-api-hooks'
import { useCentralProductStore } from '../lib/stores/central-product-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
+import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores'
+import { usePopulateCentralProductStore } from '../hooks/usePopulateCentralProductStore'
import { AppLayout } from '../components/AppLayout'
-import { Store, Grid3X3, ChevronLeft, ShoppingCart, Zap } from 'lucide-react'
+import { Zap, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/flash')({
component: FlashDeliveryPage,
@@ -14,8 +16,10 @@ export const Route = createFileRoute('/flash')({
function FlashDeliveryPage() {
const navigate = useNavigate()
- const search = useSearch({ from: '/flash' }) as { storeId?: string }
- const storeId = search.storeId ? Number(search.storeId) : undefined
+
+ // Populate central stores so flash-eligible products resolve on direct load
+ usePopulateCentralStores()
+ usePopulateCentralProductStore()
const { data: storesData } = useStores()
const { productsById } = useCentralProductStore()
@@ -26,6 +30,36 @@ function FlashDeliveryPage() {
const addToCart = useAddToCart('flash')
const { data: cartData } = useGetCart('flash')
+ // Store tabs — one tab per store (same model as home's category tabs)
+ const storesWithFlash = useMemo(() => {
+ return stores.filter((store: any) =>
+ Object.values(productsById).some(
+ (p: any) =>
+ p?.storeId === store.id &&
+ productSlotsMap[p.id]?.isFlashAvailable &&
+ !productSlotsMap[p.id]?.isOutOfStock
+ )
+ )
+ }, [stores, productsById, productSlotsMap])
+
+ const [selectedStoreId, setSelectedStoreId] = useState
(null)
+ const activeStoreId = selectedStoreId ?? storesWithFlash[0]?.id ?? null
+
+ // Auto-scroll the active store tab into view (matches user-ui tab behavior)
+ const tabsScrollRef = useRef(null)
+ const tabPositions = useRef([])
+ const activeTabIndex = Math.max(
+ 0,
+ storesWithFlash.findIndex((s: any) => s.id === activeStoreId)
+ )
+
+ useEffect(() => {
+ const x = tabPositions.current[activeTabIndex]
+ if (x != null) {
+ tabsScrollRef.current?.scrollTo({ left: Math.max(0, x - 16), behavior: 'smooth' })
+ }
+ }, [activeTabIndex])
+
// Get flash products from central store
const allFlashProducts = useMemo(() => {
return Object.values(productsById).filter(
@@ -36,15 +70,18 @@ function FlashDeliveryPage() {
)
}, [productsById, productSlotsMap])
- // Filter by store if selected
- const filteredProducts = storeId
- ? allFlashProducts.filter((p: any) => p.storeId === storeId)
- : allFlashProducts
+ // Products to show: active store's flash items, or all flash items when no stores
+ const filteredProducts = useMemo(() => {
+ if (activeStoreId != null) {
+ return allFlashProducts.filter((p: any) => p.storeId === activeStoreId)
+ }
+ return allFlashProducts
+ }, [activeStoreId, allFlashProducts])
const handleAddToCart = (productId: number) => {
const item = filteredProducts.find((p: any) => p.id === productId)
addToCart.mutate(
- { productId, quantity: 1, storeId: item?.storeId },
+ { productId, quantity: 1, storeId: item?.storeId ?? 0 },
{
onSuccess: () => {
alert(`Added ${item?.name || 'item'} for 1 Hr Delivery`)
@@ -56,191 +93,103 @@ function FlashDeliveryPage() {
return (
- {/* Header - Flash Delivery Style */}
-
-
- {/* Back Button */}
-
-
- {/* Flash Delivery Title */}
-
-
-
- Delivery within 1 hour
+ {/* Page header */}
+
+
+
+
+
+ Express Counter
+
Delivery within 1 hour
-
- {/* StoreSidebar - Fixed width on left for both mobile and desktop */}
-
-
- navigate({
- to: '/flash',
- search: { storeId: newStoreId },
- })
- }
- onAllSelect={() =>
- navigate({ to: '/flash' })
- }
- />
+
+ {/* Info Banner */}
+
+
+ Get these products delivered within 1 hour! Only available for select items.
+
- {/* Products Grid */}
-
- {/* Info Banner */}
-
-
- Get these products delivered within 1 hour! Only available for select items.
-
+ {/* Store tabs — same model as home page tabs */}
+ {storesWithFlash.length > 0 && (
+
+ {storesWithFlash.map((store: any, i: number) => {
+ const active = activeStoreId === store.id
+ return (
+
+ )
+ })}
+ )}
-
-
- {storeId
- ? stores.find((s: any) => s.id === storeId)?.name ||
- 'Store Products'
- : 'All Products'}
-
-
- {filteredProducts.length} items
-
-
-
-
- {filteredProducts.map((product: any) => (
-
- navigate({
- to: '/flash/product/$id',
- params: { id: String(product.id) },
- })
- }
- />
- ))}
-
-
- {filteredProducts.length === 0 && (
-
-
- {storeId
- ? 'No flash delivery products from this store.'
- : 'No flash delivery products available.'}
-
-
- )}
+
+
+ {activeStoreId != null
+ ? stores.find((s: any) => s.id === activeStoreId)?.name.replace(/^The\s+/i, '') ||
+ 'Store Products'
+ : 'All Products'}
+
+
+ {filteredProducts.length} items
+
+
+
+ {filteredProducts.map((product: any) => (
+
+ navigate({
+ to: '/flash/product/$id',
+ params: { id: String(product.id) },
+ })
+ }
+ />
+ ))}
+
+
+ {filteredProducts.length === 0 && (
+
+
+ No flash delivery products available.
+
+
+ )}
)
}
-interface StoreSidebarProps {
- stores: any[];
- storeId?: number;
- onStoreSelect: (storeId: number) => void;
- onAllSelect: () => void;
-}
-
-function StoreSidebar({
- stores,
- storeId,
- onStoreSelect,
- onAllSelect,
-}: StoreSidebarProps) {
- return (
-
-
- {/* All Products Item */}
-
-
-
-
- {/* Store Items */}
- {stores.map((store: any) => {
- const isActive = storeId === store.id;
-
- return (
-
onStoreSelect(store.id)}
- className={`flex flex-col items-center rounded-2xl p-2 ${
- isActive
- ? 'bg-gradient-to-br from-[#f81260] to-[#d10f4f] text-white shadow-lg'
- : 'border border-gray-100 bg-white text-gray-500'
- }`}
- >
-
- {store.signedImageUrl ? (
-

- ) : (
-
- )}
-
-
- {store.name.replace(/^The\s+/i, '')}
-
-
- );
- })}
-
-
- );
-}
-
const formatQuantity = (
quantity: number,
unit: string,
@@ -291,16 +240,16 @@ function CompactProductCard({
return (

{isOutOfStock && (
-
+
)}
@@ -313,38 +262,35 @@ function CompactProductCard({
/>
) : (
{
e.stopPropagation();
handleQuantityChange(1);
}}
>
-
+
)}
-
-
{item.name}
+
+
{item.name}
-
-
-
₹{price}
- {item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
-
- ₹{item.marketPrice}
-
- )}
-
- Qty:{" "}
-
- {formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
-
+
+
₹{price}
+ {item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
+
+ ₹{item.marketPrice}
-
+ )}
+
+
+ {formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
+
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/web-ui/src/routes/home.cart.tsx b/apps/web-ui/src/routes/home.cart.tsx
index 3aea48d..99f09da 100644
--- a/apps/web-ui/src/routes/home.cart.tsx
+++ b/apps/web-ui/src/routes/home.cart.tsx
@@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks'
-import { p, MyButton, Quantifier, AppContainer, div } from 'web-components'
-import { Trash2 } from 'lucide-react'
+import { Quantifier } from 'web-components'
+import { AppLayout } from '../components/AppLayout'
+import { Trash2, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/home/cart')({ component: CartPage })
@@ -28,71 +29,109 @@ function CartPage() {
})
return (
-
-
- Your Cart
-
-
- {cartItems.length === 0 ? (
-
-
Your cart is empty
-
navigate({ to: '/home' })}
- />
+
+
+
+
+ Review Before Delivery
+
+
Your Cart
- ) : (
- <>
-
- {cartItems.map((item) => {
- const product = productsById[item.productId]
- const price = product.price
- return (
-
-

-
-
- {product.name}
-
-
- ₹{price}
-
-
updateItem.mutate({ productId: item.productId, quantity: q })}
+
+ {cartItems.length === 0 ? (
+