This commit is contained in:
shafi54 2026-08-18 23:12:55 +05:30
parent cc540297bf
commit 7291bfe7ce
35 changed files with 24627 additions and 6020 deletions

View file

@ -19,7 +19,20 @@
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && python3 - <<'PY' # Move the product_info block (CREATE + INSERTs) above product_skus. with open('local_8_aug.sql', 'r') as f: lines = f.readlines() # 1-indexed boundaries pi_start = 8334 - 1 # CREATE TABLE product_info pi_end = 8478 # exclusive (up to line 8478 inclusive) -> slice [8333:8478] sku_start = 7900 - 1 # line where product_skus CREATE begins block = lines[pi_start:pi_end] # Remove the product_info block new_lines = lines[:pi_start] + lines[pi_end:] # Re-insert before product_skus (its index may have shifted by -len(block)) insert_at = sku_start new_lines = new_lines[:insert_at] + block + new_lines[insert_at:] with open('local_8_aug.sql', 'w') as f: f.writelines(new_lines) print(f\"Moved product_info block (lines {pi_start+1}-{pi_end}) to before line {sku_start+1}\") PY echo \"=== verify new order ===\" && grep -nE 'CREATE TABLE .(product_info|product_skus|sku_features|product_market_stats|cart_items).' local_8_aug.sql)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && echo \"=== order: product_info BEFORE product_skus? ===\" && grep -nE '^CREATE TABLE' local_8_aug.sql | grep -E \"product_info|product_skus|sku_features|product_market_stats|cart_items\" && echo \"=== doc check: every REFERENCES has parent CREATE before it ===\" && grep -nE '^CREATE TABLE|REFERENCES' local_8_aug.sql | grep -E \"REFERENCES\" | grep -oE \"REFERENCES \\`?[a-z_]+\" | sort | uniq -c)",
"Shell(sqlite3:*)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && sqlite3 :memory: <<'EOF' PRAGMA foreign_keys=ON; BEGIN; .read local_8_aug.sql COMMIT; EOF echo \"exit: $?\")"
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && sqlite3 :memory: <<'EOF' PRAGMA foreign_keys=ON; BEGIN; .read local_8_aug.sql COMMIT; EOF echo \"exit: $?\")",
"Shell(cd:*)",
"Shell(mv:*)",
"Shell(npm:*)",
"Shell(curl -s http://localhost:4174/home 2 >& 1)",
"Shell(curl -s https://assets.freshyo.in/api-cache/v-690/products.json 2 >& 1)",
"Shell(python3 -c import sys,json; d=json.load(sys.stdin); print('products:', len(d.get('products',[]))); print('tags:', [t['tagName'] for t in d.get('tags',[])][:10]) 2 >& 1)",
"Shell(lsof:*)",
"Shell(curl -s http://localhost:8787/api/trpc/common.essentialConsts 2 >& 1)",
"Shell(curl -s https://assets2.freshyo.in/api-cache-dev/v-684/products.json 2 >& 1)",
"Shell(python3 -c import sys,json; d=json.load(sys.stdin); print('products:', len(d.get('products',[]))); print('tags:', [t['tagName'] for t in d.get('tags',[])][:12]) 2 >& 1)",
"Shell(curl -s http://localhost:4174/src/styles.css 2 > /dev/null)",
"Shell(printf:*)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend && python3 - <<'EOF' import re with open('dumps/local_8_aug.sql') as f: content = f.read() # Find all CREATE TABLE positions and all REFERENCES target tables create_positions = {} # table name -> line for m in re.finditer(r'CREATE TABLE(?: IF NOT EXISTS)? \"?([A-Za-z_]+)\"? \\(', content): create_positions[m.group(1)] = content.count('\\n', 0, m.start()) + 1 # Verify each REFERENCES target is defined before the referencing CREATE errors = [] for m in re.finditer(r'CREATE TABLE(?: IF NOT EXISTS)? \"?([A-Za-z_]+)\"? \\(([^;]*?)\\)', content, re.S): table = m.group(1) table_line = content.count('\\n', 0, m.start()) + 1 body = m.group(2) for ref in re.findall(r'REFERENCES `?([A-Za-z_]+)`?\\(', body): if ref in create_positions and create_positions[ref] > table_line: errors.append(f\"{table} (line {table_line}) references {ref} (defined line {create_positions[ref]})\") if errors: print(\"FAIL - out of order:\") for e in errors: print(\" \", e) else: print(\"PASS - all REFERENCES resolve to tables defined earlier\") EOF)"
],
"deny": [],
"defaultMode": "default"

View file

@ -1,5 +1,6 @@
# Taste
- Prefers that the agent not run typechecks; instead, read the project's AGENTS.md for instructions on what to do/not do before finalizing work. Confidence: 0.9
- Judges completion by seeing the feature live in the running app ("I still don't see it on the /home route page"), so for data-driven features the agent must verify the app's actual runtime data source (API base URL, cache/backend) has the required data — not just that code builds and typechecks. Confirmed when the user signaled success only after the fix (pointing web-ui at the local backend with the tags data) made the section visible in the running app, and reconfirmed with the admin-ui product selector showing no products because it points at the production worker. Confidence: 0.9
- Prefers detailed analysis and explicit error/mistake checking before executing git operations (e.g., merges), rather than just performing the action. Confidence: 0.6
- Do not run `git stash` or other destructive git operations that would discard or modify working-tree changes without explicit permission. Confidence: 0.9
- When removing unused code (e.g., an unused tRPC procedure), prefers a complete cleanup: also remove internal helper methods used only by it, associated types, re-exports, orphaned hook/API files and their shared types, and even commented-out imports, leaving zero remaining references. Confidence: 0.9
@ -15,12 +16,13 @@
- When changing cache or storage keys, wants both the read and write paths verified to use the same single source of truth (e.g., a shared `CACHE_STORAGE_KEYS` constant). Confidence: 0.8
- In React Native with react-native-paper's `Text` component (wrapped as `MyText`), prefers avoiding mixed string/expression children; use template literals to produce a single string child to prevent "Text strings must be rendered within a <Text> component" warnings. Confidence: 0.8
- When requesting test plans, expects exhaustive coverage across all relevant apps/screens/routes, including every functionality and every edge case. Confidence: 0.9
- Verifies that changed code introduces no NEW type errors beyond a project's pre-existing baseline (e.g., by temporarily stashing changes to compare typecheck output), and fixes even pre-existing type errors in files he heavily edits. Confidence: 0.8
- Prefers test case documentation to be structured with preconditions, steps, explicit "things to test" checklist, and expected results. Confidence: 0.7
- Prefers test plans written for a non-technical audience, using plain-language, click-by-click instructions ("tap this", "type that", "check there") rather than technical terms or API names. Confidence: 0.9
- When updating or creating a document, prefers the agent first compare it against existing source documents, identify missing items or gaps, and add them in the same established format. Confidence: 0.9
- Dislikes nested headers in mobile/drawer navigation; prefers a single, shared header (e.g., the drawer header) and relies on device back buttons or gestures for returning to previous screens. Confidence: 0.9
- After reviewing a presented plan, prefers brief, action-oriented approval (e.g., "nice. go ahead") before implementation proceeds. Confidence: 0.7
- Wants feature parity maintained between the web-ui and user-ui apps (port logic/data patterns, rebuild UI per platform). Confidence: 0.85
- After reviewing a presented plan, prefers brief, action-oriented approval (e.g., "nice. go ahead", "go ahead and implement") before implementation proceeds, expecting the agent to autonomously execute the already-presented plan. Confidence: 0.85
- Wants feature parity maintained between the web-ui and user-ui apps (port logic/data patterns, rebuild UI per platform). Confidence: 0.95
- Prefers dead-code audits to be documented in a markdown file. Confidence: 0.8
- Prefers detailed technical documentation of system architecture, data models, flows, and integrations in markdown format. Confidence: 0.8
- Wants edge cases explicitly enumerated when documenting or analyzing existing code/systems. Confidence: 0.8
@ -29,3 +31,29 @@
- Prefers using sentinel values in existing fields (e.g., slotId = 0) to distinguish special-case items rather than creating separate fields or structures. Confidence: 0.9
- Prefers handling special-case logic locally in the relevant component/file rather than globally or via parallel flows. Confidence: 0.85
- Prefers lazy/virtualized list rendering (e.g., FlatList) for long lists, rather than rendering all items at once. Confidence: 0.8
- Wants true responsive web design for desktop, not just color/theme changes or mobile layouts stretched to desktop. Confidence: 0.95
- Does not want color/theme changes during redesigns — keep colors and theme consistent with the existing apps (e.g., @apps/user-ui): reuse the shared theme tokens (Untitled UI blue brand scale, flash pink accent, gray neutrals) rather than introducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95
roducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9
- Combo-only SKUs (`isComboOnly` on `product_skus`) must be hidden from all user-facing product surfaces — home page, offers page, and any other list where they could appear ("Don't display them. We're hiding them from home page too"). Confidence: 0.75
- Expects a filter/business rule already applied on one page to be applied consistently on every other page showing the same data, not just the page he happened to notice ("we need to hide them here too"). Confidence: 0.85
- Debugs by adding temporary console.log debug prints to inspect runtime data (e.g., `console.log({skusData})` to check a query result) and reports the observed values to the agent as ground truth (e.g., "the database is same. I've checked"). Confidence: 0.75
- When a bug reproduces only on a deployed API (e.g., devapi) but not against the local backend, expects the agent to investigate the environment difference — which API base URL the app actually points to, wrangler/D1 bindings, and the deployed bundle version — rather than assuming identical code paths. Confidence: 0.65
- Keeps operational runbooks / command documentation (e.g., `apps/backend/wrangler-commands.md` documenting the D1 export table-ordering trap) in repo markdown files and expects the agent to read them and apply the documented procedure when performing the related operation (e.g., fixing dump table order), treating the doc as the source of truth for the fix. Confidence: 0.65
- Prefers monetary amounts (totals, discounts, savings) displayed as whole numbers only — explicitly truncates decimals (e.g., `Math.trunc()`) rather than rounding, keeping internal calculations at full precision and truncating only at the render site ("Just truncate and show only the whole number"). Confidence: 0.7
- When displaying savings/benefit lines, hides the row entirely unless the amount exceeds a meaningful threshold (e.g., "show only if the savings is greater than rs 1") so the truncated whole-number display never shows a negligible ₹0/₹1 saving. Confidence: 0.55
- Prefers out-of-stock items pushed to the end of product list sections (e.g., home page tab sections) while preserving the existing/admin-curated order — a stable partition (in-stock first, out-of-stock last, relative order kept within each group), reusing the same out-of-stock detection rule used elsewhere on the page ("preserve the order but push the out of stock items to last"). Confidence: 0.7
- Tracks previously discussed/planned fixes and follows up to verify they were actually applied (e.g., "did we take care of the math.trunc"); expects the agent to check the current state of the code and apply any outstanding planned changes rather than assume they were completed. Confidence: 0.8
- Prevents contradictory/invalid states in admin forms by hiding the inapplicable control entirely — including its label/text row — rather than leaving it visible but disabled (e.g., the entire "Combo Only SKU" checkbox row is hidden when the SKU itself is a combo — has combo items — not just disabled). Confidence: 0.6
- Wants savings figures kept consistent across all order surfaces: a savings concept shown in the cart/checkout bill (market price offered price) must also be reflected in the order confirmation summary ("you saved n on this order"), aggregating all savings sources (coupon discount, market-price savings, free delivery) rather than only the coupon/discount component. Confidence: 0.85
- Cares about the exact user-facing wording of labels and explicitly dictates renames (e.g., "Total Savings" → "Total Discount" on the cart page), expecting the change to be applied exactly as specified. Confidence: 0.7
- When a button/feature is broken on one page but works on a sibling page (e.g., "Edit Notes" in the order-list three-dot menu vs. the order detail page), expects it fixed to behave like the working page — including calling the proper API with the correct parameter types (numeric order id matching the backend's zod schema, not a stringified id) rather than a no-op or wrong call. Confidence: 0.7
- Reports UI bugs by pointing at the specific file and describing the intended action plus the visible symptom (e.g., "submit not happening"), often with a hypothesis about the cause ("maybe we're not fulfilling conditions to enable"); expects the agent to trace the validation/submit path and find the root cause rather than just making the UI appear to work. Confidence: 0.6
- Expects form validation and submit-enable conditions to match the current UI mode: fields hidden by a mode toggle (e.g., the single coupon-code field when "reserved coupon" is selected) must not be required, and discount-type toggles should not pre-fill placeholder values (like 0) that fail the `> 0` validation — hidden/placeholder state must never block submission. Confidence: 0.55
e changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9
als) rather than introducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95
roducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9
tes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9

View file

@ -457,14 +457,16 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<MyText style={tw`text-gray-700 font-medium`}>Offer SKU</MyText>
</View>
<View style={tw`flex-row items-center mb-3`}>
<Checkbox
checked={variant.isComboOnly}
onPress={() => setFieldValue(`variants.${vIndex}.isComboOnly`, !variant.isComboOnly)}
style={tw`mr-3`}
/>
<MyText style={tw`text-gray-700 font-medium`}>Combo Only SKU</MyText>
</View>
{!(values.productType === 'combo' && (variant.comboItems?.length || 0) > 0) && (
<View style={tw`flex-row items-center mb-3`}>
<Checkbox
checked={variant.isComboOnly}
onPress={() => setFieldValue(`variants.${vIndex}.isComboOnly`, !variant.isComboOnly)}
style={tw`mr-3`}
/>
<MyText style={tw`text-gray-700 font-medium`}>Combo Only SKU</MyText>
</View>
)}
{mode === 'edit' && (
<View style={tw`flex-row items-center mb-3`}>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -595,12 +595,24 @@ export default function Dashboard() {
// 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 || [])
// Out-of-stock items are pushed to the end, preserving their relative order.
const ordered = (tag.productIds || [])
.map((id: number) => productById.get(id))
.filter(Boolean) as any[];
const inStock: any[] = [];
const outOfStock: any[] = [];
for (const product of ordered) {
const isOut =
Boolean(productSlotsMap[product.id]?.isOutOfStock) ||
!getQuickestSlot(product.id);
if (isOut) outOfStock.push(product);
else inStock.push(product);
}
map[tag.id] = [...inStock, ...outOfStock];
}
return map;
}, [dashboardTags, products]);
}, [dashboardTags, products, productSlotsMap, getQuickestSlot]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);

View file

@ -36,7 +36,7 @@ const OrderMenu: React.FC<OrderMenuProps> = ({ orderId, postActionHandler }) =>
const handleEditNotes = async () => {
try {
await updateNotesMutation.mutateAsync({
id: orderId.toString(),
id: orderId,
userNotes: editNotes.trim()
});
} catch (error) {
@ -106,8 +106,11 @@ const OrderMenu: React.FC<OrderMenuProps> = ({ orderId, postActionHandler }) =>
<MyTouchableOpacity
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl shadow-sm`}
onPress={() => {
setEditNotes('');
setEditNotesDialogOpen(true);
setOpen(false);
setTimeout(() => {
setEditNotes('');
setEditNotesDialogOpen(true);
}, 300);
}}
>
<View style={tw`w-10 h-10 rounded-full bg-blue-50 items-center justify-center mr-4`}>

View file

@ -16,6 +16,7 @@ interface PaymentAndOrderProps {
selectedCouponId: number | null;
cartItems: any[];
totalPrice: number;
totalSavings?: number;
discountAmount: number;
finalTotal: number;
finalTotalWithDelivery: number;
@ -32,6 +33,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
selectedCouponId,
cartItems,
totalPrice,
totalSavings = 0,
discountAmount,
finalTotal,
finalTotalWithDelivery,
@ -294,14 +296,22 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
{/* Item Total */}
<View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`text-gray-500`}>Item Total</MyText>
<MyText style={tw`text-gray-900 font-medium`}>{totalPrice}</MyText>
<MyText style={tw`text-gray-900 font-medium`}>{Math.trunc(totalPrice)}</MyText>
</View>
{/* Total Savings */}
{totalSavings > 1 && (
<View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`text-gray-500`}>Total Savings</MyText>
<MyText style={tw`text-green-600 font-medium`}>{Math.trunc(totalSavings)}</MyText>
</View>
)}
{/* Discount */}
{discountAmount > 0 && (
<View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`text-gray-500`}>Product Discount</MyText>
<MyText style={tw`text-green-600 font-medium`}>-{discountAmount}</MyText>
<MyText style={tw`text-green-600 font-medium`}>-{Math.trunc(discountAmount)}</MyText>
</View>
)}
@ -356,15 +366,15 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
</View>
{/* Savings Banner */}
{(discountAmount > 0 || deliveryCharge === 0) && (
{(discountAmount > 0 || deliveryCharge === 0 || totalSavings > 1) && (
<View style={tw`bg-green-50 rounded-lg p-2 mt-4 flex-row justify-center items-center`}>
<MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
<MyText style={tw`text-green-700 text-xs font-bold`}>
You saved {discountAmount + (deliveryCharge === 0 ? (
You saved {Math.trunc(discountAmount + totalSavings + (deliveryCharge === 0 ? (
isFlashDelivery
? constsData?.flashDeliveryCharge
: constsData?.deliveryCharge
) : 0)} on this order
) : 0))} on this order
</MyText>
</View>
)}

View file

@ -215,6 +215,18 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
return sum + Number(price) * quantity;
}, 0);
// Total savings = sum of (market price - offered price) * quantity, per in-stock item.
const totalSavings = cartItems
.filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock)
.reduce((sum, item) => {
const product = productsById[item.skuId];
const quantity = quantities[item.id] || item.quantity;
const marketPrice = Number(product?.marketPrice || 0);
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const saving = marketPrice - Number(price);
return sum + (saving > 0 ? saving : 0) * quantity;
}, 0);
const dropdownData = useMemo(
() =>
eligibleCoupons?.map((coupon) => {
@ -829,14 +841,22 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{/* Item Total */}
<View style={tw`flex-row justify-between items-center mb-3`}>
<MyText style={tw`text-gray-500`}>Item Total</MyText>
<MyText style={tw`text-gray-900 font-medium`}>{totalPrice}</MyText>
<MyText style={tw`text-gray-900 font-medium`}>{Math.trunc(totalPrice)}</MyText>
</View>
{/* Total Discount */}
{totalSavings > 1 && (
<View style={tw`flex-row justify-between items-center mb-3`}>
<MyText style={tw`text-gray-500`}>Total Discount</MyText>
<MyText style={tw`text-green-600 font-medium`}>{Math.trunc(totalSavings)}</MyText>
</View>
)}
{/* Discount */}
{discountAmount > 0 && (
<View style={tw`flex-row justify-between items-center mb-3`}>
<MyText style={tw`text-gray-500`}>Product Discount</MyText>
<MyText style={tw`text-green-600 font-medium`}>-{discountAmount}</MyText>
<MyText style={tw`text-green-600 font-medium`}>-{Math.trunc(discountAmount)}</MyText>
</View>
)}
@ -884,15 +904,15 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{/* Grand Total */}
<View style={tw`flex-row justify-between items-center mt-2`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>To Pay</MyText>
<MyText style={tw`text-xl font-bold text-gray-900`}>{finalTotalWithDelivery}</MyText>
<MyText style={tw`text-xl font-bold text-gray-900`}>{Math.trunc(finalTotalWithDelivery)}</MyText>
</View>
{/* Savings Banner */}
{(discountAmount > 0 || deliveryCharge === 0) && (
{(discountAmount > 0 || deliveryCharge === 0 || totalSavings > 1) && (
<View style={tw`bg-green-50 rounded-lg p-2 mt-4 flex-row justify-center items-center`}>
<MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
<MyText style={tw`text-green-700 text-xs font-bold`}>
You saved {discountAmount + (deliveryCharge === 0 ? (constsData?.deliveryCharge || 0) : 0)} on this order
You saved {Math.trunc(discountAmount + totalSavings + (deliveryCharge === 0 ? (constsData?.deliveryCharge || 0) : 0))} on this order
</MyText>
</View>
)}

View file

@ -142,6 +142,20 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
0
);
// Total savings = sum of (market price - offered price) * quantity, per in-stock item.
const totalSavings = selectedItems
.filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock)
.reduce(
(sum, item) => {
const product = productsById[item.skuId];
const marketPrice = Number(product?.marketPrice || 0);
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const saving = marketPrice - Number(price);
return sum + (saving > 0 ? saving : 0) * item.quantity;
},
0
);
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery(
undefined,
{ enabled: isAuthenticated }
@ -257,6 +271,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
selectedCouponId={selectedCouponId}
cartItems={selectedItems}
totalPrice={totalPrice}
totalSavings={totalSavings}
discountAmount={discountAmount}
finalTotal={finalTotal}
finalTotalWithDelivery={finalTotalWithDelivery}

View file

@ -1,39 +1,56 @@
import React from 'react'
import { useLocation } from '@tanstack/react-router'
import { BottomNavigation } from './BottomNavigation'
import { FloatingCartBar } from './FloatingCartBar'
import { useLocation } from '@tanstack/react-router'
import { Sidebar } from './shell/Sidebar'
import { Topbar } from './shell/Topbar'
interface AppLayoutProps {
children: React.ReactNode
showCartBar?: boolean
isFlashDelivery?: boolean
hideShell?: boolean
}
// Routes where bottom nav should be hidden
const hideBottomNavRoutes = ['/login', '/register', '/checkout', '/cart']
// Routes where the full shell (sidebar/topbar/bottom nav) should be hidden
const hideShellRoutes = ['/login', '/register', '/checkout', '/home/checkout', '/flash/checkout']
export function AppLayout({ children, showCartBar = true, isFlashDelivery = false }: AppLayoutProps) {
export function AppLayout({
children,
showCartBar = true,
isFlashDelivery = false,
hideShell = false,
}: AppLayoutProps) {
const location = useLocation()
const currentPath = location.pathname
const shouldShowBottomNav = !hideBottomNavRoutes.some((route) =>
currentPath === route || currentPath.startsWith(`${route}/`)
)
const shouldHideShell =
hideShell ||
hideShellRoutes.some((route) => currentPath === route || currentPath.startsWith(`${route}/`))
const shouldShowCartBar = showCartBar && shouldShowBottomNav
// On mobile the cart bar replaces the old floating pill
const shouldShowCartBar = showCartBar && !shouldHideShell
if (shouldHideShell) {
return <main className="min-h-screen">{children}</main>
}
return (
<div className="relative min-h-screen pb-20">
{/* Main Content */}
<main>{children}</main>
<div className="shell-grid">
{/* Desktop rail */}
<Sidebar />
{/* Floating Cart Bar - positioned above bottom nav */}
{shouldShowCartBar && (
<FloatingCartBar isFlashDelivery={isFlashDelivery} />
)}
{/* Top bar with search + cart */}
<Topbar isFlashDelivery={isFlashDelivery} />
{/* Bottom Navigation */}
{shouldShowBottomNav && <BottomNavigation />}
{/* Main content */}
<main className="shell-content min-w-0">{children}</main>
{/* Cart slide-over (desktop) / bottom bar (mobile) */}
{shouldShowCartBar && <FloatingCartBar isFlashDelivery={isFlashDelivery} />}
{/* Mobile bottom nav */}
<BottomNavigation />
</div>
)
}

View file

@ -1,6 +1,5 @@
import React from 'react'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { p, div } from 'web-components'
import { Home, Store, Zap, Tag, User } from 'lucide-react'
interface TabItem {
@ -9,7 +8,6 @@ interface TabItem {
label: string
icon: React.ReactNode
iconActive: React.ReactNode
isCenter?: boolean
}
export function BottomNavigation() {
@ -35,10 +33,9 @@ export function BottomNavigation() {
{
name: 'flash',
path: '/flash',
label: '1 Hr Delivery',
icon: <Zap className="h-6 w-6" />,
iconActive: <Zap className="h-6 w-6 fill-current" />,
isCenter: true,
label: '1 Hr',
icon: <Zap className="h-5 w-5" />,
iconActive: <Zap className="h-5 w-5 fill-current" />,
},
{
name: 'offers',
@ -60,56 +57,34 @@ export function BottomNavigation() {
if (path === '/home') return currentPath === '/home' || currentPath.startsWith('/home/')
if (path === '/stores') return currentPath === '/stores' || currentPath.startsWith('/stores/')
if (path === '/flash') return currentPath === '/flash' || currentPath.startsWith('/flash/')
if (path === '/me/orders') return currentPath === '/me/orders' || currentPath.startsWith('/me/orders/')
if (path === '/me') return currentPath === '/me' || (currentPath.startsWith('/me/') && !currentPath.startsWith('/me/orders/'))
if (path === '/me') return currentPath === '/me' || currentPath.startsWith('/me/')
return currentPath === path || currentPath.startsWith(`${path}/`)
}
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white pb-safe">
<div className="flex h-16 items-center justify-around px-2">
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white pb-safe md:hidden">
<div className="flex h-16 items-stretch">
{tabs.map((tab) => {
const active = isActive(tab.path)
if (tab.isCenter) {
// Center elevated button for Flash Delivery
return (
<div
key={tab.name}
onClick={() => navigate({ to: tab.path })}
className="relative -top-3 flex flex-col items-center"
>
<div
className={`flex h-14 w-14 items-center justify-center rounded-full border-4 border-white shadow-lg transition-all ${
'bg-gradient-to-br from-brand-400 to-brand-600'
}`}
>
<span className={active ? 'text-white' : 'text-white/80'}>{tab.icon}</span>
</div>
<p
className={`mt-0.5 text-[10px] font-bold ${active ? 'text-brand-600' : 'text-gray-500'}`}
>
{tab.label}
</p>
</div>
)
}
return (
<div
<button
key={tab.name}
onClick={() => navigate({ to: tab.path })}
className="flex flex-1 flex-col items-center justify-center py-2"
onClick={() => navigate({ to: tab.path as any })}
className="relative flex flex-1 flex-col items-center justify-center gap-1"
>
<span className={active ? 'text-brand-600' : 'text-gray-500'}>
{/* Flat thick underline for active state */}
{active && <span className="absolute inset-x-6 top-0 h-1 rounded-b-full bg-brand-600" />}
<span className={active ? 'text-brand-600' : 'text-gray-400'}>
{active ? tab.iconActive : tab.icon}
</span>
<p
className={`mt-1 text-xs font-medium ${active ? 'text-brand-600' : 'text-gray-500'}`}
<span
className={`text-[11px] font-semibold ${
active ? 'text-brand-700' : 'text-gray-500'
}`}
>
{tab.label}
</p>
</div>
</span>
</button>
)
})}
</div>

View file

@ -1,11 +1,10 @@
import React, { useState, useEffect, useMemo } from 'react'
import { useNavigate, useLocation } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart, useAddToCart } from '../hooks/cart-query-hooks'
import React, { useState, useEffect } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
import { p, div, MiniQuantifier } from 'web-components'
import { Dialog } from './Dialog'
import { ShoppingCart, ChevronRight, Package, X, Clock, MapPin, Home, Store, Zap, RotateCcw, User } from 'lucide-react'
import { MiniQuantifier } from 'web-components'
import { ShoppingCart, ChevronRight, Clock, X, Package } from 'lucide-react'
import dayjs from 'dayjs'
interface FloatingCartBarProps {
@ -35,12 +34,11 @@ export function FloatingCartBar({ isFlashDelivery = false }: FloatingCartBarProp
const [quantities, setQuantities] = useState<Record<number, number>>({})
const cartType = isFlashDelivery ? 'flash' : 'regular'
const { data: cartData, refetch: refetchCart } = useGetCart(cartType)
const { data: cartData } = useGetCart(cartType)
const { data: constsData } = useGetEssentialConsts()
const { data: productsData } = useAllProducts()
const updateCartItem = useUpdateCartItem(cartType)
const removeFromCart = useRemoveFromCart(cartType)
const addToCartHook = useAddToCart(cartType)
const products = productsData?.products || []
const productsById: Record<number, any> = {}
@ -76,220 +74,201 @@ export function FloatingCartBar({ isFlashDelivery = false }: FloatingCartBarProp
const cartBarColor = isFlashDelivery ? '#f81260' : 'var(--brand-600, #2563eb)'
const cartBarBorderColor = isFlashDelivery ? '#e11d48' : 'var(--brand-500, #3b82f6)'
return (
<>
{/* Collapsed Bar */}
<div
className="fixed bottom-18 left-4 right-4 z-40 rounded-lg border shadow-2xl"
style={{
backgroundColor: cartBarColor,
borderColor: cartBarBorderColor,
borderWidth: 1,
}}
>
<div
className="flex flex-row items-center justify-between py-3"
onClick={() => itemCount > 0 && setIsExpanded(true)}
>
<div className="flex flex-1 flex-row items-center px-2">
<div className="flex-1">
<div className="flex flex-row items-center">
<p className="font-bold mr-2 text-sm text-white">
{itemCount === 0 ? (
isFlashDelivery ? 'No Flash Items' : 'No Items In Cart'
) : (
<>
<span className="text-base font-black text-white">
{totalCartValue}
</span>
<span className="text-sm font-bold text-white">
{' '}&bull; {itemCount} {itemCount === 1 ? 'Item' : 'Items'}
</span>
</>
)}
</p>
{itemCount > 0 && <span className="text-white"></span>}
</div>
const goToCart = () => {
setIsExpanded(false)
navigate({ to: isFlashDelivery ? '/flash/cart' : '/cart' })
}
{remainingForFreeDelivery > 0 ? (
<p className="mt-1 text-[10px] font-bold text-white/80">
{remainingForFreeDelivery} more for <span className="text-emerald-300">FREE Delivery</span>
</p>
) : itemCount > 0 ? (
<div className="mt-0.5 flex flex-row items-center">
<span className="text-emerald-400"></span>
<p className="ml-1 text-[10px] font-black uppercase tracking-tighter text-emerald-300">
Free Delivery Unlocked
</p>
</div>
) : (
<p className="mt-0.5 text-[10px] text-white/60">
Shop for {freeDeliveryThreshold}+ for free shipping
</p>
)}
</div>
</div>
<div
className="rounded-2xl bg-white px-3 py-2 shadow-lg mr-2"
onClick={(e) => {
e.stopPropagation()
navigate({
to: isFlashDelivery ? '/flash/cart' : '/cart',
})
}}
>
<p className="font-bold text-sm" style={{ color: cartBarColor }}>
Go to Cart
</p>
</div>
/* ---------- Desktop: slide-over panel ---------- */
const slideOver = (
<div className="pointer-events-auto flex h-full w-full max-w-md flex-col border-l border-gray-200 bg-white">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
<div>
<p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
Your Cart
</p>
<p className="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-400">
{itemCount} {itemCount === 1 ? 'Item' : 'Items'}
</p>
</div>
<button
onClick={() => setIsExpanded(false)}
className="flex h-10 w-10 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-50 hover:text-gray-700"
aria-label="Close cart"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Expanded Dialog */}
<Dialog open={isExpanded} onClose={() => setIsExpanded(false)} title="">
<div className="flex max-h-[80vh] flex-col">
{/* Header */}
<div className="flex flex-row items-center justify-between border-b border-slate-100 px-6 py-5">
<div>
<p className="font-bold text-xl tracking-tight text-slate-900">
Your Cart
</p>
<p className="text-xs font-bold uppercase tracking-widest text-slate-400">
{itemCount} Items
</p>
</div>
<div
className="flex h-10 w-10 items-center justify-center rounded-2xl bg-slate-100"
onClick={() => setIsExpanded(false)}
>
<X className="h-6 w-6 text-slate-500" />
</div>
{/* Progress */}
{remainingForFreeDelivery > 0 && (
<div className="border-b border-gray-100 bg-brand-25 px-6 py-3">
<div className="mb-1.5 flex items-center justify-between">
<p className="text-[10px] font-black uppercase tracking-widest text-brand-700">
Free Delivery Progress
</p>
<p className="text-[10px] font-black text-brand-700">
{Math.round((totalCartValue / freeDeliveryThreshold) * 100)}%
</p>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-brand-100">
<div
className="h-full rounded-full bg-brand-600 transition-all"
style={{ width: `${Math.min(100, (totalCartValue / freeDeliveryThreshold) * 100)}%` }}
/>
</div>
<p className="mt-1.5 text-[11px] font-semibold text-gray-500">
Add {remainingForFreeDelivery} more for free delivery
</p>
</div>
)}
{/* Progress Bar Header */}
{remainingForFreeDelivery > 0 && (
<div className="flex flex-row items-center justify-between bg-emerald-50/50 px-6 py-3">
<div className="mr-4 flex-1">
<div className="mb-1.5 flex flex-row items-center justify-between">
<p className="text-[10px] font-black uppercase text-emerald-700">Free Delivery Progress</p>
<p className="text-[10px] font-black text-emerald-700">
{Math.round((totalCartValue / freeDeliveryThreshold) * 100)}%
</p>
</div>
<div className="h-1.5 overflow-hidden rounded-full border border-emerald-100 bg-white">
<div
className="h-full bg-emerald-500"
style={{ width: `${(totalCartValue / freeDeliveryThreshold) * 100}%` }}
/>
</div>
</div>
<div className="items-end">
<p className="text-[10px] font-bold text-slate-500">Needed</p>
<p className="text-sm font-black text-emerald-600">+{remainingForFreeDelivery}</p>
</div>
{/* Items */}
<div className="flex-1 overflow-y-auto px-6 py-2">
{cartItems.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center py-16 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
<Package className="h-7 w-7 text-gray-400" />
</div>
)}
{/* Items List */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{cartItems.map((item, index) => (
<React.Fragment key={item.id}>
<div className="py-4">
<div className="flex flex-row items-center">
<img
src={productsById[item.productId]?.images?.[0]}
alt=""
className="h-8 w-8 rounded-lg border border-slate-100 bg-slate-50 object-cover"
/>
<div className="ml-4 flex-1">
<div className="mb-1 flex flex-row items-center justify-between">
<p className="font-bold flex-1 text-sm text-slate-900">
{productsById[item.productId]?.name || ''}{' '}
<span className="text-xs font-medium text-slate-500">
({productsById[item.productId]?.productQuantity || 0}
{productsById[item.productId]?.unitNotation || ''})
</span>
</p>
<MiniQuantifier
value={quantities[item.id] || item.quantity}
setValue={(value) => {
if (value === 0) {
removeFromCart.mutate(item.id)
} else {
setQuantities((prev) => ({ ...prev, [item.id]: value }))
updateCartItem.mutate({ productId: item.id, quantity: value })
}
}}
step={productsById[item.productId]?.incrementStep || 1}
/>
</div>
<div className="flex flex-row items-center justify-between">
{item.slotId && (
<div className="flex flex-row items-center rounded-lg border border-blue-100 bg-blue-50 px-2 py-1">
<Clock className="h-3 w-3 text-blue-600" />
<p className="ml-1 text-[9px] font-black uppercase text-blue-700">
{formatTimeRange(item.deliveryDate || new Date())}
</p>
</div>
)}
<p className="font-bold text-sm text-slate-900">
{(() => {
const product = productsById[item.productId]
const basePrice = product?.price ?? 0
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice
return price * item.quantity
})()}
</p>
</div>
<p className="mt-4 font-bold text-gray-800">
{isFlashDelivery ? 'No flash items yet' : 'Your cart is empty'}
</p>
<p className="mt-1 text-sm text-gray-500">
{isFlashDelivery
? 'Pick something from 1 hr delivery'
: 'Add fresh products to get started'}
</p>
</div>
) : (
cartItems.map((item, index) => (
<React.Fragment key={item.id}>
<div className="flex items-center gap-4 py-4">
<img
src={productsById[item.productId]?.images?.[0]}
alt=""
className="h-14 w-14 shrink-0 rounded-lg border border-gray-100 bg-gray-50 object-cover"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold text-gray-900">
{productsById[item.productId]?.name || ''}
</p>
<p className="text-xs font-medium text-gray-500">
{productsById[item.productId]?.productQuantity || 0}
{productsById[item.productId]?.unitNotation || ''}
</p>
{item.slotId && (
<div className="mt-1 flex items-center gap-1 rounded border border-brand-100 bg-brand-25 px-1.5 py-0.5">
<Clock className="h-3 w-3 text-brand-600" />
<p className="text-[10px] font-bold text-brand-700">
{formatTimeRange(item.deliveryDate || new Date())}
</p>
</div>
)}
<div className="mt-1.5 flex items-center justify-between">
<MiniQuantifier
value={quantities[item.id] || item.quantity}
setValue={(value) => {
if (value === 0) {
removeFromCart.mutate(item.id)
} else {
setQuantities((prev) => ({ ...prev, [item.id]: value }))
updateCartItem.mutate({ productId: item.id, quantity: value })
}
}}
step={productsById[item.productId]?.incrementStep || 1}
/>
<p className="text-sm font-bold text-gray-900">
{(() => {
const product = productsById[item.productId]
const basePrice = product?.price ?? 0
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice
return price * item.quantity
})()}
</p>
</div>
</div>
{index < cartItems.length - 1 && <div className="h-px w-full bg-slate-200" />}
</React.Fragment>
))}
</div>
{/* Footer */}
<div className="border-t border-slate-100 bg-white p-6">
<div className="mb-5 flex flex-row items-center justify-between">
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400">Subtotal</p>
<p className="font-bold text-2xl text-slate-900">
{totalCartValue}
</p>
</div>
{remainingForFreeDelivery === 0 && (
<div className="flex flex-row items-center rounded-xl border border-emerald-100 bg-emerald-50 px-3 py-1.5">
<Package className="h-4 w-4 text-emerald-600" />
<p className="ml-1.5 text-[10px] font-black uppercase text-emerald-700">Free Delivery</p>
</div>
)}
</div>
{index < cartItems.length - 1 && <div className="h-px w-full bg-gray-100" />}
</React.Fragment>
))
)}
</div>
<div
onClick={() => {
setIsExpanded(false)
navigate({
to: isFlashDelivery ? '/flash/cart' : '/cart',
})
}}
className="flex flex-row items-center justify-center rounded-2xl py-4 shadow-lg"
style={{
background: isFlashDelivery
? 'linear-gradient(90deg, #f81260, #c40e50)'
: 'linear-gradient(90deg, #1570EF, #194185)',
}}
>
<p className="font-bold text-base uppercase tracking-widest text-white">
Go to cart
</p>
<ChevronRight className="ml-1 h-5 w-5 text-white" />
</div>
{/* Footer */}
<div className="border-t border-gray-100 bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-gray-400">Subtotal</p>
<p className="font-display text-2xl font-extrabold text-gray-900">{totalCartValue}</p>
</div>
{remainingForFreeDelivery === 0 && itemCount > 0 && (
<div className="flex items-center gap-1.5 rounded-lg bg-brand-25 px-3 py-1.5">
<Package className="h-4 w-4 text-brand-600" />
<p className="text-[10px] font-black uppercase text-brand-700">Free Delivery</p>
</div>
)}
</div>
</Dialog>
<button
onClick={goToCart}
disabled={itemCount === 0}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-brand-600 py-3.5 font-bold text-white transition-colors hover:bg-brand-700 disabled:pointer-events-none disabled:opacity-40"
>
Go to Cart
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
)
return (
<>
{/* Mobile: compact bottom bar */}
<div className="fixed bottom-18 left-3 right-3 z-40 md:hidden">
<button
onClick={() => itemCount > 0 && setIsExpanded(true)}
className="flex w-full items-center justify-between rounded-xl px-4 py-3 shadow-lg"
style={{
backgroundColor: cartBarColor,
borderColor: cartBarBorderColor,
borderWidth: 1,
}}
>
<div className="flex items-center gap-2">
<ShoppingCart className="h-5 w-5 text-white" />
<p className="text-sm font-bold text-white">
{itemCount === 0
? isFlashDelivery
? 'No Flash Items'
: 'No Items In Cart'
: `${totalCartValue} · ${itemCount} ${itemCount === 1 ? 'Item' : 'Items'}`}
</p>
</div>
<div className="rounded-lg bg-white/20 px-3 py-1.5">
<p className="text-xs font-bold text-white">View</p>
</div>
</button>
</div>
{/* Desktop: slide-over */}
<div
className={`fixed inset-0 z-50 transition-opacity duration-300 ${
isExpanded ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
}`}
>
<div
className="absolute inset-0 bg-gray-900/40"
onClick={() => setIsExpanded(false)}
/>
<div
className={`absolute inset-y-0 right-0 flex transition-transform duration-300 ease-out ${
isExpanded ? 'translate-x-0' : 'translate-x-full'
}`}
>
{slideOver}
</div>
</div>
</>
)
}

View file

@ -1,5 +1,5 @@
import React from 'react'
import { p, div, Quantifier, MiniQuantifier } from 'web-components'
import { Quantifier, MiniQuantifier } from 'web-components'
import { useGetCart, useUpdateCartItem, useRemoveFromCart, useAddToCart } from '../hooks/cart-query-hooks'
import { useCartStore } from '../lib/stores/cart-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store'
@ -94,36 +94,49 @@ export function ProductCard({
}
}
const discountPercent =
item.marketPrice && Number(item.marketPrice) > Number(item.price)
? Math.round(((Number(item.marketPrice) - Number(item.price)) / Number(item.marketPrice)) * 100)
: 0
return (
<div
className="flex max-w-[300px] flex-col overflow-hidden rounded-2xl bg-white pb-2 border border-gray-300"
className="group flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-brand-300 hover:shadow-md"
onClick={onPress}
>
{/* Image Container */}
<div className="relative aspect-square w-full overflow-hidden bg-gray-100">
{/* Image */}
<div className="relative aspect-[4/3] w-full overflow-hidden bg-gray-50">
{imageUri && !imageError ? (
<img
src={imageUri}
alt={item.name}
className="h-full w-full object-cover"
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
onError={() => setImageError(true)}
onLoad={() => setImageLoading(false)}
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-gray-100">
<ImageOff className="h-8 w-8 text-gray-400" />
<div className="flex h-full w-full items-center justify-center bg-gray-50">
<ImageOff className="h-8 w-8 text-gray-300" />
</div>
)}
{imageLoading && imageUri && !imageError && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
</div>
)}
{discountPercent > 0 && (
<div className="absolute left-3 top-3 rounded bg-brand-600 px-2 py-0.5">
<p className="text-[10px] font-black uppercase tracking-wide text-white">
{discountPercent}% OFF
</p>
</div>
)}
{displayIsOutOfStock && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
<div className="rounded-full bg-red-500 px-3 py-1">
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/50">
<div className="rounded-full bg-gray-900 px-3 py-1">
<p className="text-xs font-bold text-white">Out of Stock</p>
</div>
</div>
@ -139,13 +152,13 @@ export function ProductCard({
/>
) : (
<div
className="flex h-8 w-8 items-center justify-center rounded-full bg-white shadow-md"
className="flex h-9 w-9 items-center justify-center rounded-full bg-white shadow-md transition-colors hover:bg-brand-50"
onClick={(e) => {
e.stopPropagation()
handleQuantityChange(1)
}}
>
<ShoppingCart className="h-4 w-4 text-brand-500" />
<ShoppingCart className="h-4 w-4 text-brand-600" />
</div>
)}
</div>
@ -153,33 +166,24 @@ export function ProductCard({
</div>
{/* Content */}
<div className="px-3 pt-3">
<p className="font-bold mb-1 text-sm text-gray-900">
<div className="flex flex-1 flex-col px-4 pb-4 pt-3">
<p className="mb-1.5 text-sm font-bold leading-snug text-gray-900 line-clamp-2">
{item.name}
</p>
<div className="mb-2 flex flex-row items-baseline">
<p className="font-bold text-base text-brand-500">
{item.price}
</p>
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<p className="ml-2 text-xs text-gray-400 line-through">
{item.marketPrice}
</p>
<div className="mb-1 flex items-baseline">
<p className="text-lg font-extrabold text-brand-600">{item.price}</p>
{discountPercent > 0 && (
<p className="ml-2 text-xs text-gray-400 line-through">{item.marketPrice}</p>
)}
</div>
<div className="mb-2 flex flex-row items-center">
<p className="text-xs font-medium text-gray-500">
Quantity:{' '}
<span className="font-semibold text-brand-500">
{formatQuantity(item.productQuantity || 1, item.unitNotation).display}
</span>
</p>
</div>
<p className="mb-3 text-xs font-medium text-gray-500">
{formatQuantity(item.productQuantity || 1, item.unitNotation).display}
</p>
{showDeliveryInfo && displayDeliveryDate && (
<div className="mb-2 flex flex-row items-center self-start rounded-lg border border-brand-100 bg-brand-50 px-2 py-1.5">
<div className="mb-3 flex items-center self-start rounded-md border border-brand-100 bg-brand-25 px-2 py-1">
<Truck className="h-3 w-3 text-brand-600" />
<p className="ml-1.5 text-[10px] font-bold text-brand-700">
{dayjs(displayDeliveryDate).format('ddd, DD MMM • h:mm A')}
@ -188,9 +192,9 @@ export function ProductCard({
)}
{!miniView && (
<>
<div className="mt-auto">
{displayIsOutOfStock ? (
<div className="mt-1 rounded-lg bg-gray-100 py-2 text-center">
<div className="rounded-lg bg-gray-50 py-2 text-center">
<p className="text-xs font-bold uppercase tracking-wide text-gray-400">
Unavailable
</p>
@ -204,19 +208,17 @@ export function ProductCard({
/>
) : (
<button
className="mt-1 flex w-full items-center justify-center gap-1 rounded-lg bg-brand-500 py-2 hover:bg-brand-600 active:bg-brand-700"
className="flex w-full items-center justify-center gap-1.5 rounded-lg bg-brand-600 py-2.5 text-xs font-bold uppercase tracking-wide text-white transition-colors hover:bg-brand-700"
onClick={(e) => {
e.stopPropagation()
handleQuantityChange(1)
}}
>
<ShoppingCart className="h-4 w-4 text-white" />
<span className="text-xs font-bold uppercase tracking-wide text-white">
Add to Cart
</span>
<ShoppingCart className="h-4 w-4" />
Add to Cart
</button>
)}
</>
</div>
)}
</div>
</div>

View file

@ -0,0 +1,132 @@
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, Beef } from 'lucide-react'
interface NavItem {
path: string
label: string
icon: React.ReactNode
match: (path: string) => boolean
}
const navItems: NavItem[] = [
{
path: '/home',
label: 'Home',
icon: <Home className="h-5 w-5" />,
match: (p) => p === '/home' || p.startsWith('/home/'),
},
{
path: '/stores',
label: 'Stores',
icon: <Store className="h-5 w-5" />,
match: (p) => p === '/stores' || p.startsWith('/stores/'),
},
{
path: '/flash',
label: '1 Hr Delivery',
icon: <Zap className="h-5 w-5" />,
match: (p) => p === '/flash' || p.startsWith('/flash/'),
},
{
path: '/offers',
label: 'Offers',
icon: <Tag className="h-5 w-5" />,
match: (p) => p === '/offers' || p.startsWith('/offers/'),
},
{
path: '/me',
label: 'My Account',
icon: <User className="h-5 w-5" />,
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 (
<aside className="shell-sidebar sticky top-0 hidden h-screen flex-col border-r border-gray-200 bg-white md:flex">
{/* Brand */}
<div className="flex h-16 items-center gap-2.5 border-b border-gray-100 px-6">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-brand-600">
<Beef className="h-5 w-5 text-white" />
</div>
<div>
<p className="font-display text-lg font-extrabold leading-none tracking-tight text-gray-900">
Freshyo
</p>
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-gray-400">
Butcher &amp; Grocer
</p>
</div>
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto px-3 py-6">
<ul className="space-y-1">
{navItems.map((item) => {
const active = item.match(currentPath)
return (
<li key={item.path}>
<button
onClick={() => navigate({ to: item.path as any })}
className={`relative flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-semibold transition-colors ${
active
? 'bg-brand-50 text-brand-700'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
>
{/* Flat thick underline indicator */}
{active && (
<span className="absolute left-0 top-1/2 h-6 w-1 -translate-y-1/2 rounded-r-full bg-brand-600" />
)}
<span className={active ? 'text-brand-600' : 'text-gray-400'}>{item.icon}</span>
{item.label}
</button>
</li>
)
})}
</ul>
</nav>
{/* Account footer */}
<div className="border-t border-gray-100 p-4">
{user ? (
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-brand-100 text-sm font-bold text-brand-700">
{user.profileImage ? (
<img src={user.profileImage} alt={user.name || 'User'} className="h-full w-full object-cover" />
) : (
(user.name || 'U').charAt(0).toUpperCase()
)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold text-gray-900">{user.name || 'User'}</p>
<p className="truncate text-xs text-gray-500">{user.mobile}</p>
</div>
<button
onClick={logout}
title="Logout"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-50 hover:text-brand-600"
>
<LogOut className="h-4 w-4" />
</button>
</div>
) : (
<button
onClick={() => navigate({ to: '/login' })}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
<LogIn className="h-4 w-4" />
Sign In
</button>
)}
</div>
</aside>
)
}

View file

@ -0,0 +1,89 @@
import React from 'react'
import { useNavigate } from '@tanstack/react-router'
import { SearchBar } from 'web-components'
import { useGetCart } from '../../hooks/cart-query-hooks'
import { ShoppingCart, Beef } from 'lucide-react'
interface TopbarProps {
isFlashDelivery?: boolean
onCartClick?: () => void
onSearchClick?: () => void
}
export function Topbar({ isFlashDelivery = false, onCartClick, onSearchClick }: 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 = () => {
if (onSearchClick) {
onSearchClick()
return
}
navigate({ to: '/home/search' as any })
}
return (
<header className="shell-topbar sticky top-0 z-30 border-b border-gray-200 bg-white/95 backdrop-blur-sm">
{/* Row 1 — brand + cart (mobile) / search + cart (desktop) */}
<div className="flex h-16 items-center gap-3 px-4 md:px-6">
{/* Brand — always visible, compact on mobile */}
<div className="flex shrink-0 items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-600 md:h-8 md:w-8">
<Beef className="h-5 w-5 text-white md:h-4 md:w-4" />
</div>
<span className="font-display text-lg font-extrabold tracking-tight text-gray-900">
Freshyo
</span>
</div>
{/* Search — desktop wide field */}
<div className="hidden flex-1 max-w-xl md:block">
<SearchBar
placeholder="Search chicken, mutton, fish, groceries..."
onClick={handleSearchClick}
/>
</div>
<div className="ml-auto flex items-center gap-3">
{/* Flash status */}
{isFlashDelivery && (
<span className="hidden items-center gap-1.5 rounded-md bg-flash-50 px-2.5 py-1 text-xs font-bold text-flash-600 sm:flex">
<span className="h-1.5 w-1.5 rounded-full bg-flash-500" />
1 Hr Delivery
</span>
)}
{/* Cart button */}
<button
onClick={handleCartClick}
aria-label={`Cart, ${itemCount} items`}
className="relative flex h-11 items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 text-sm font-bold text-gray-800 transition-colors hover:border-brand-300 hover:text-brand-700 md:px-4"
>
<ShoppingCart className="h-5 w-5" />
<span className="hidden sm:inline">Cart</span>
{itemCount > 0 && (
<span className="absolute -right-1.5 -top-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-brand-600 px-1 text-[10px] font-black text-white">
{itemCount}
</span>
)}
</button>
</div>
</div>
{/* Row 2 — mobile search field */}
<div className="px-4 pb-3 md:hidden">
<SearchBar placeholder="Search chicken, mutton, fish..." onClick={handleSearchClick} />
</div>
</header>
)
}

View file

@ -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) || 'http://localhost:8787'
export const trpc = createTRPCReact<AppRouter>()

View file

@ -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('/cart')({ component: CartPage })
@ -28,71 +29,109 @@ function CartPage() {
})
return (
<AppContainer>
<p className="font-bold mb-4 text-xl">
Your Cart
</p>
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 py-20">
<p className="text-gray-500">Your cart is empty</p>
<MyButton
textContent="Browse Products"
onClick={() => navigate({ to: '/home' })}
/>
<AppLayout>
<div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Review Before Delivery
</p>
<h1 className="display-2 mt-1 text-gray-900">Your Cart</h1>
</div>
) : (
<>
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.price
return (
<div
key={item.productId}
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm"
>
<img
src={product.images?.[0]}
alt={product.name}
className="h-16 w-16 rounded-lg object-cover"
/>
<div className="flex-1">
<p className="font-semibold text-sm">
{product.name}
</p>
<p className="text-brand-600 text-sm font-bold">
{price}
</p>
<Quantifier
value={item.quantity}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })}
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
<ShoppingCart className="h-7 w-7 text-gray-400" />
</div>
<p className="font-bold text-gray-800">Your cart is empty</p>
<p className="text-sm text-gray-500">Fresh cuts are waiting at the counter</p>
<button
onClick={() => navigate({ to: '/home' })}
className="mt-2 rounded-lg bg-brand-600 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
Browse Products
</button>
</div>
) : (
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
{/* Items */}
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.price
return (
<div
key={item.productId}
className="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-4"
>
<img
src={product.images?.[0]}
alt={product.name}
className="h-20 w-20 shrink-0 rounded-lg object-cover"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-gray-900">{product.name}</p>
<p className="mt-0.5 text-xs text-gray-500">
{product.productQuantity || 1}{product.unitNotation || ''} per unit
</p>
<div className="mt-2 flex items-center gap-3">
<Quantifier
value={item.quantity}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })}
/>
<p className="text-sm font-bold text-gray-900">{price}</p>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<p className="text-base font-extrabold text-brand-600">{price * item.quantity}</p>
<button
onClick={() => removeItem.mutate(item.productId)}
className="flex h-9 w-9 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
aria-label={`Remove ${product.name}`}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div onClick={() => removeItem.mutate(item.productId)}>
<Trash2 className="h-5 w-5 text-red-500" />
)
})}
</div>
{/* Summary */}
<div className="lg:sticky lg:top-24 lg:self-start">
<div className="rounded-xl border border-gray-200 bg-white p-6">
<p className="font-display text-lg font-extrabold text-gray-900">Bill Summary</p>
<div className="mt-4 space-y-2 border-b border-gray-100 pb-4">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Item Total</p>
<p className="text-sm font-bold text-gray-900">{total}</p>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Delivery Fee</p>
<p className="text-sm font-semibold text-green-600">Calculated at checkout</p>
</div>
</div>
)
})}
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 flex items-center justify-between">
<p className="font-bold">Total</p>
<p className="font-bold text-lg text-brand-600">
{total}
</p>
<div className="flex items-center justify-between py-4">
<p className="font-bold text-gray-900">Total</p>
<p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<button
onClick={() => navigate({ to: '/checkout' })}
className="w-full rounded-lg bg-brand-600 py-3.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
Proceed to Checkout
</button>
<button
onClick={() => navigate({ to: '/home' })}
className="mt-3 w-full rounded-lg border border-gray-200 py-3 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-50"
>
Continue Shopping
</button>
</div>
</div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/checkout' })}
className="bg-brand-500 text-white"
/>
</div>
</>
)}
</AppContainer>
)}
</div>
</AppLayout>
)
}

View file

@ -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 (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6">
<ShoppingCart className="mb-4 h-16 w-16 text-gray-400" />
<p className="font-bold mb-2 text-center text-xl text-gray-900">
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-white">
<ShoppingCart className="h-9 w-9 text-gray-400" />
</div>
<p className="mt-6 text-xl font-bold text-gray-900">
Your cart is empty
</p>
<p className="mb-6 text-center text-gray-500">
<p className="mb-6 mt-1 text-gray-500">
Add some delicious items to your cart before checking out
</p>
<div
<button
onClick={() => 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"
>
<p className="font-bold text-white">
Back to Shopping
</p>
</div>
Back to Shopping
</button>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 pb-8">
<div className="min-h-screen bg-gray-50">
{/* Checkout Header */}
<div className="sticky top-0 z-10 mb-4 border-b border-gray-100 bg-white px-4 py-3">
<div className="flex items-center">
<div
<div className="sticky top-0 z-20 border-b border-gray-200 bg-white">
<div className="mx-auto flex h-16 w-full max-w-7xl items-center gap-3 px-4 md:px-8">
<button
onClick={() => navigate({ to: '/cart' })}
className="-ml-2 mr-1 flex items-center p-2"
className="flex h-9 w-9 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-50"
aria-label="Back to cart"
>
<ChevronLeft className="h-7 w-7 text-gray-700" />
<ChevronLeft className="h-5 w-5" />
</button>
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-25">
<MapPin className="h-4 w-4 text-brand-600" />
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-50">
<MapPin className="h-5 w-5 text-blue-500" />
</div>
<p className="font-bold ml-3 text-lg text-gray-800">
<p className="font-display text-lg font-extrabold text-gray-900">
Checkout
</p>
</div>
</div>
<div className="px-4">
{/* Address Selection */}
<CheckoutAddressSelector
onAddressSelect={setSelectedAddressId}
onAddAddress={() => {
setEditingAddress(null)
setShowAddAddress(true)
}}
onEditAddress={(address) => {
setEditingAddress(address)
setShowAddAddress(true)
}}
/>
<div className="mx-auto grid w-full max-w-7xl gap-8 px-4 py-6 md:px-8 lg:grid-cols-[minmax(0,1fr)_420px]">
{/* Left: Address Selection + Payment */}
<div>
<CheckoutAddressSelector
onAddressSelect={setSelectedAddressId}
onAddAddress={() => {
setEditingAddress(null)
setShowAddAddress(true)
}}
onEditAddress={(address) => {
setEditingAddress(address)
setShowAddAddress(true)
}}
/>
</div>
{/* Payment and Order Summary */}
<PaymentAndOrderComponent
selectedAddress={selectedAddressId}
cartItems={cartItems}
isFlashDelivery={false}
onBack={() => navigate({ to: '/cart' })}
/>
{/* Right: Order summary + payment */}
<div className="lg:sticky lg:top-24 lg:self-start">
<PaymentAndOrderComponent
selectedAddress={selectedAddressId}
cartItems={cartItems}
isFlashDelivery={false}
onBack={() => navigate({ to: '/cart' })}
/>
</div>
</div>
{/* Add/Edit Address Dialog */}

View file

@ -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 (
<AppContainer>
<div className="mb-4 flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
<p className="font-bold text-xl">
Flash Cart
</p>
</div>
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 py-20">
<p className="text-gray-500">Your flash cart is empty</p>
<MyButton
textContent="Browse Flash Products"
onClick={() => navigate({ to: '/flash' })}
/>
<AppLayout isFlashDelivery={true}>
<div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-flash-500">
1 Hr Delivery
</p>
<h1 className="display-2 mt-1 flex items-center gap-2 text-gray-900">
<Zap className="h-6 w-6 text-flash-500" fill="currentColor" />
Flash Cart
</h1>
</div>
) : (
<>
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.discountedPrice ?? product.price
return (
<div
key={item.productId}
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm"
>
<img
src={product.images?.[0]}
alt={product.name}
className="h-16 w-16 rounded-lg object-cover"
/>
<div className="flex-1">
<p className="font-semibold text-sm">
{product.name}
</p>
<p className="text-brand-600 text-sm font-bold">{price}</p>
<Quantifier
value={item.quantity}
setValue={(q) =>
updateItem.mutate({ productId: item.productId, quantity: q })
}
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-flash-25">
<ShoppingCart className="h-7 w-7 text-flash-500" />
</div>
<p className="font-bold text-gray-800">Your flash cart is empty</p>
<p className="text-sm text-gray-500">Pick something from 1 hr delivery</p>
<button
onClick={() => navigate({ to: '/flash' })}
className="mt-2 rounded-lg bg-flash-500 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-flash-600"
>
Browse Flash Products
</button>
</div>
) : (
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
{/* Items */}
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.discountedPrice ?? product.price
return (
<div
key={item.productId}
className="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-4"
>
<img
src={product.images?.[0]?.uri || product.images?.[0] as any}
alt={product.name}
className="h-20 w-20 shrink-0 rounded-lg object-cover"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-gray-900">{product.name}</p>
<p className="mt-0.5 text-xs text-gray-500">
{product.unitValue || 1}{product.unit || ''} per unit
</p>
<div className="mt-2 flex items-center gap-3">
<Quantifier
value={item.quantity}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })}
/>
<p className="text-sm font-bold text-gray-900">{price}</p>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<p className="text-base font-extrabold text-flash-500">{price * item.quantity}</p>
<button
onClick={() => removeItem.mutate(item.productId)}
className="flex h-9 w-9 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
aria-label={`Remove ${product.name}`}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div onClick={() => removeItem.mutate(item.productId)}>
<Trash2 className="h-5 w-5 text-red-500" />
)
})}
</div>
{/* Summary */}
<div className="lg:sticky lg:top-24 lg:self-start">
<div className="rounded-xl border border-gray-200 bg-white p-6">
<p className="font-display text-lg font-extrabold text-gray-900">Bill Summary</p>
<div className="mt-4 space-y-2 border-b border-gray-100 pb-4">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Item Total</p>
<p className="text-sm font-bold text-gray-900">{total}</p>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Delivery Fee</p>
<p className="text-sm font-semibold text-green-600">Calculated at checkout</p>
</div>
</div>
)
})}
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 flex items-center justify-between">
<p className="font-bold">Total</p>
<p className="font-bold text-lg text-brand-600">
{total}
</p>
<div className="flex items-center justify-between py-4">
<p className="font-bold text-gray-900">Total</p>
<p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<button
onClick={() => navigate({ to: '/flash/checkout' })}
className="w-full rounded-lg bg-flash-500 py-3.5 text-sm font-bold text-white transition-colors hover:bg-flash-600"
>
Proceed to Checkout
</button>
<button
onClick={() => navigate({ to: '/flash' })}
className="mt-3 w-full rounded-lg border border-gray-200 py-3 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-50"
>
Continue Shopping
</button>
</div>
</div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/flash/checkout' })}
className="bg-brand-500 text-white"
/>
</div>
</>
)}
</AppContainer>
)}
</div>
</AppLayout>
)
}

View file

@ -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 (
<div className="flex min-h-screen flex-col items-center justify-center bg-yellow-50 p-6">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-yellow-100">
<Zap className="h-10 w-10 text-yellow-600" />
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-flash-25">
<Zap className="h-10 w-10 text-flash-500" fill="currentColor" />
</div>
<p className="font-bold mb-2 text-2xl text-gray-900">
1 Hr Order Placed!
<p className="mb-2 mt-6 text-2xl font-extrabold text-gray-900">
1 Hr Order Placed
</p>
<p className="mb-1 text-gray-600">Order ID: #{orderId}</p>
<p className="mb-8 text-gray-600">Total: {totalAmount}</p>
<MyButton
textContent="Continue Shopping"
<button
onClick={() => navigate({ to: '/flash' })}
className="mb-3 bg-brand-500 text-white"
/>
<MyButton
textContent="View My Orders"
className="mb-3 rounded-lg bg-flash-500 px-8 py-3 text-sm font-bold text-white transition-colors hover:bg-flash-600"
>
Continue Shopping
</button>
<button
onClick={() => navigate({ to: '/me/orders' })}
className="bg-gray-100 text-gray-700"
/>
className="rounded-lg border border-gray-200 bg-white px-8 py-3 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-100"
>
View My Orders
</button>
</div>
)
}

View file

@ -1,12 +1,12 @@
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { useState, useMemo } from 'react'
import { p, div, MiniQuantifier } from 'web-components'
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 { AppLayout } from '../components/AppLayout'
import { Store, Grid3X3, ChevronLeft, ShoppingCart, Zap } from 'lucide-react'
import { Store, Grid3X3, Zap, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/flash')({
component: FlashDeliveryPage,
@ -56,56 +56,46 @@ function FlashDeliveryPage() {
return (
<AppLayout isFlashDelivery={true}>
<div className="min-h-screen bg-gray-50">
{/* Header - Flash Delivery Style */}
<div className="sticky top-0 z-20 border-b border-gray-200 bg-white px-4 py-3">
<div className="flex items-center gap-2">
{/* Back Button */}
<button
onClick={() => navigate({ to: '/home' })}
className="p-1 hover:bg-gray-100 rounded-full"
>
<ChevronLeft className="h-6 w-6 text-gray-700" />
</button>
{/* Flash Delivery Title */}
<div className="flex items-center gap-2">
<Zap className="h-6 w-6 text-[#f81260]" fill="#f81260" />
<p className="text-lg font-bold text-[#f81260]">
Delivery within 1 hour
{/* Page header */}
<div className="border-b border-gray-200 bg-white">
<div className="mx-auto flex w-full max-w-7xl items-center gap-2 px-4 py-5 md:px-8">
<Zap className="h-6 w-6 text-flash-500" fill="currentColor" />
<div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-flash-500">
Express Counter
</p>
<h1 className="display-2 text-gray-900">Delivery within 1 hour</h1>
</div>
</div>
</div>
<div className="flex flex-row">
{/* StoreSidebar - Fixed width on left for both mobile and desktop */}
<div className="w-20 shrink-0 md:w-24">
<StoreSidebar
stores={stores}
storeId={storeId}
onStoreSelect={(newStoreId) =>
navigate({
to: '/flash',
search: { storeId: newStoreId },
})
}
onAllSelect={() =>
navigate({ to: '/flash' })
}
/>
</div>
<div className="mx-auto flex w-full max-w-7xl gap-6 px-4 py-6 md:px-8">
{/* Store sidebar — desktop: vertical list; mobile: horizontal strip */}
<StoreSidebar
stores={stores}
storeId={storeId}
onStoreSelect={(newStoreId) =>
navigate({
to: '/flash',
search: { storeId: newStoreId },
})
}
onAllSelect={() =>
navigate({ to: '/flash' })
}
/>
{/* Products Grid */}
<div className="flex-1 p-4">
<div className="min-w-0 flex-1">
{/* Info Banner */}
<div className="mb-4 rounded-xl bg-yellow-50 p-3 border border-yellow-200">
<div className="mb-4 rounded-xl border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
Get these products delivered within 1 hour! Only available for select items.
</p>
</div>
<div className="mb-4 flex items-center justify-between">
<p className="font-bold text-xl text-gray-900">
<p className="text-lg font-extrabold text-gray-900">
{storeId
? stores.find((s: any) => s.id === storeId)?.name ||
'Store Products'
@ -116,7 +106,7 @@ function FlashDeliveryPage() {
</p>
</div>
<div className="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-3 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-3 xl:grid-cols-4">
{filteredProducts.map((product: any) => (
<CompactProductCard
key={product.id}
@ -161,83 +151,95 @@ function StoreSidebar({
onStoreSelect,
onAllSelect,
}: StoreSidebarProps) {
const allActive = !storeId
return (
<div className="sticky top-[73px] z-10 h-[calc(100vh-73px)] w-full overflow-y-auto border-r border-gray-200 bg-white p-2 md:top-0 md:h-auto md:p-3">
<div className="flex flex-col gap-2 md:gap-3">
{/* All Products Item */}
<div
<>
{/* Mobile: horizontal chip strip (always visible) */}
<div className="scrollbar-hide -mx-4 mb-4 flex gap-2 overflow-x-auto px-4 md:hidden">
<button
onClick={onAllSelect}
className={`flex flex-col items-center rounded-2xl p-2 md:p-3 ${
!storeId
? 'bg-gradient-to-br from-[#f81260] to-[#d10f4f] text-white shadow-lg'
: 'border border-gray-100 bg-white text-gray-500'
className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
allActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
}`}
>
<div
className={`mb-1 flex h-8 w-8 items-center justify-center rounded-full border md:h-10 md:w-10 ${
!storeId ? 'border-white/30 bg-white/20' : 'bg-gray-50'
}`}
>
<Grid3X3
className={`h-4 w-4 md:h-5 md:w-5 ${!storeId ? 'text-white' : 'text-gray-500'}`}
/>
</div>
<p
className={`text-center text-[10px] font-bold ${!storeId ? 'text-white' : 'text-gray-500'}`}
>
ALL
</p>
</div>
<div className="h-px bg-gray-200 my-1" />
{/* Store Items */}
All
</button>
{stores.map((store: any) => {
const isActive = storeId === store.id;
const isActive = storeId === store.id
return (
<div
<button
key={store.id}
onClick={() => 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'
className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
isActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
}`}
>
<div
className={`mb-1 md:mb-2 flex h-10 w-10 items-center justify-center overflow-hidden rounded-full border-2 md:h-12 md:w-12 ${
isActive
? 'border-white bg-white'
: 'border-gray-100 bg-gray-50'
}`}
>
{store.signedImageUrl ? (
<img
src={store.signedImageUrl}
alt={store.name}
className="h-full w-full object-cover"
/>
) : (
<Store
className={`h-5 w-5 md:h-6 md:w-6 ${isActive ? 'text-[#f81260]' : 'text-gray-400'}`}
/>
)}
</div>
<p
className={`w-full text-center text-[10px] leading-tight ${
isActive
? 'font-bold text-white'
: 'font-medium text-gray-500'
}`}
>
{store.name.replace(/^The\s+/i, '')}
</p>
</div>
);
{store.name.replace(/^The\s+/i, '')}
</button>
)
})}
</div>
</div>
{/* Desktop: vertical sidebar */}
<div className="hidden w-48 shrink-0 md:block">
<div className="sticky top-24 max-h-[calc(100vh-8rem)] overflow-y-auto rounded-xl border border-gray-200 bg-white p-3">
<p className="mb-3 px-2 text-[10px] font-black uppercase tracking-[0.18em] text-gray-400">
Stores
</p>
<div className="flex flex-col gap-1.5">
{/* All Products Item */}
<button
onClick={onAllSelect}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors ${
allActive
? 'bg-flash-500 text-white'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
<Grid3X3 className={`h-4 w-4 shrink-0 ${allActive ? 'text-white' : 'text-gray-400'}`} />
<span className="text-sm font-bold">All Stores</span>
</button>
<div className="my-1 h-px bg-gray-100" />
{/* Store Items */}
{stores.map((store: any) => {
const isActive = storeId === store.id;
return (
<button
key={store.id}
onClick={() => onStoreSelect(store.id)}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors ${
isActive
? 'bg-flash-25 text-flash-600'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
<div className={`h-8 w-8 shrink-0 overflow-hidden rounded-lg border ${isActive ? 'border-flash-200' : 'border-gray-100'} bg-gray-50`}>
{store.signedImageUrl ? (
<img
src={store.signedImageUrl}
alt={store.name}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<Store className={`h-4 w-4 ${isActive ? 'text-flash-500' : 'text-gray-400'}`} />
</div>
)}
</div>
<span className="truncate text-sm font-semibold">
{store.name.replace(/^The\s+/i, '')}
</span>
</button>
);
})}
</div>
</div>
</div>
</>
);
}
@ -291,16 +293,16 @@ function CompactProductCard({
return (
<div
onClick={onPress}
className="mb-2 overflow-hidden rounded-lg border border-gray-100 bg-white shadow-sm"
className="group overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-flash-300 hover:shadow-md"
>
<div className="relative">
<img
src={item.images?.[0]}
alt={item.name}
className="aspect-square w-full object-cover"
className="aspect-square w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
/>
{isOutOfStock && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/40">
<p className="text-xs font-bold text-white">Out of Stock</p>
</div>
)}
@ -313,36 +315,35 @@ function CompactProductCard({
/>
) : (
<div
className="flex h-8 w-8 items-center justify-center rounded-full bg-white shadow-md"
className="flex h-9 w-9 items-center justify-center rounded-full bg-white shadow-md transition-colors hover:bg-flash-25"
onClick={(e) => {
e.stopPropagation();
handleQuantityChange(1);
}}
>
<ShoppingCart className="h-4 w-4 text-[#f81260]" />
<ShoppingCart className="h-4 w-4 text-flash-500" />
</div>
)}
</div>
</div>
<div className="p-2">
<p className="font-medium mb-1 text-xs text-gray-900">{item.name}</p>
<div className="p-3">
<p className="mb-1 line-clamp-2 text-xs font-bold text-gray-900">{item.name}</p>
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-baseline">
<p className="font-bold text-sm text-[#f81260]">{price}</p>
<p className="text-sm font-extrabold text-flash-500">{price}</p>
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<p className="ml-1 text-xs text-gray-400 line-through">
{item.marketPrice}
</p>
)}
<p className="ml-1 text-xs text-gray-600">
Qty:{" "}
<span className="font-semibold text-[#f81260]">
{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
</span>
</p>
</div>
<p className="text-xs text-gray-500">
<span className="font-semibold text-flash-500">
{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
</span>
</p>
</div>
</div>
</div>

View file

@ -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 (
<AppContainer>
<p className="font-bold mb-4 text-xl">
Your Cart
</p>
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 py-20">
<p className="text-gray-500">Your cart is empty</p>
<MyButton
textContent="Browse Products"
onClick={() => navigate({ to: '/home' })}
/>
<AppLayout>
<div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Review Before Delivery
</p>
<h1 className="display-2 mt-1 text-gray-900">Your Cart</h1>
</div>
) : (
<>
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.price
return (
<div
key={item.productId}
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm"
>
<img
src={product.images?.[0]}
alt={product.name}
className="h-16 w-16 rounded-lg object-cover"
/>
<div className="flex-1">
<p className="font-semibold text-sm">
{product.name}
</p>
<p className="text-brand-600 text-sm font-bold">
{price}
</p>
<Quantifier
value={item.quantity}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })}
{cartItems.length === 0 ? (
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
<ShoppingCart className="h-7 w-7 text-gray-400" />
</div>
<p className="font-bold text-gray-800">Your cart is empty</p>
<p className="text-sm text-gray-500">Fresh cuts are waiting at the counter</p>
<button
onClick={() => navigate({ to: '/home' })}
className="mt-2 rounded-lg bg-brand-600 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
Browse Products
</button>
</div>
) : (
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
{/* Items */}
<div className="flex flex-col gap-3">
{cartItems.map((item) => {
const product = productsById[item.productId]
const price = product.price
return (
<div
key={item.productId}
className="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-4"
>
<img
src={product.images?.[0]}
alt={product.name}
className="h-20 w-20 shrink-0 rounded-lg object-cover"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-gray-900">{product.name}</p>
<p className="mt-0.5 text-xs text-gray-500">
{product.productQuantity || 1}{product.unitNotation || ''} per unit
</p>
<div className="mt-2 flex items-center gap-3">
<Quantifier
value={item.quantity}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })}
/>
<p className="text-sm font-bold text-gray-900">{price}</p>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<p className="text-base font-extrabold text-brand-600">{price * item.quantity}</p>
<button
onClick={() => removeItem.mutate(item.productId)}
className="flex h-9 w-9 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
aria-label={`Remove ${product.name}`}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div onClick={() => removeItem.mutate(item.productId)}>
<Trash2 className="h-5 w-5 text-red-500" />
)
})}
</div>
{/* Summary */}
<div className="lg:sticky lg:top-24 lg:self-start">
<div className="rounded-xl border border-gray-200 bg-white p-6">
<p className="font-display text-lg font-extrabold text-gray-900">Bill Summary</p>
<div className="mt-4 space-y-2 border-b border-gray-100 pb-4">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Item Total</p>
<p className="text-sm font-bold text-gray-900">{total}</p>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">Delivery Fee</p>
<p className="text-sm font-semibold text-green-600">Calculated at checkout</p>
</div>
</div>
)
})}
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 flex items-center justify-between">
<p className="font-bold">Total</p>
<p className="font-bold text-lg text-brand-600">
{total}
</p>
<div className="flex items-center justify-between py-4">
<p className="font-bold text-gray-900">Total</p>
<p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<button
onClick={() => navigate({ to: '/home/checkout' })}
className="w-full rounded-lg bg-brand-600 py-3.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
Proceed to Checkout
</button>
<button
onClick={() => navigate({ to: '/home' })}
className="mt-3 w-full rounded-lg border border-gray-200 py-3 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-50"
>
Continue Shopping
</button>
</div>
</div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/home/checkout' })}
className="bg-brand-500 text-white"
/>
</div>
</>
)}
</AppContainer>
)}
</div>
</AppLayout>
)
}

View file

@ -1,11 +1,7 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState, useEffect, useMemo, useRef } from 'react'
import dayjs from 'dayjs'
import {
p,
div,
SearchBar,
} from 'web-components'
import { SearchBar } from 'web-components'
import {
useAllProducts,
useStores,
@ -15,21 +11,20 @@ import {
} from '../hooks/prominent-api-hooks'
import { useCartStore } from '../lib/stores/cart-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { useCentralProductStore } from '../lib/stores/central-product-store'
import { AppLayout } from '../components/AppLayout'
import { ProductCard } from '../components/ProductCard'
import AddToCartDialog from '../components/AddToCartDialog'
import { useProductSlotIdentifier } from '../hooks/useProductSlotIdentifier'
import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores'
import { Store, ImageOff, Loader2 } from 'lucide-react'
import { Store, ImageOff, Loader2, Clock, Beef, Truck } from 'lucide-react'
// Scroll Indicator Component
function ScrollIndicator({
containerRef,
itemCount,
itemWidth
itemWidth,
}: {
containerRef: React.RefObject<HTMLDivElement>
containerRef: React.RefObject<HTMLDivElement | null>
itemCount: number
itemWidth: number
}) {
@ -63,9 +58,7 @@ function ScrollIndicator({
<div
key={i}
className={`h-1.5 rounded-full transition-all duration-300 ${
i === activeIndex
? 'w-4 bg-brand-500'
: 'w-1.5 bg-gray-300'
i === activeIndex ? 'w-4 bg-brand-600' : 'w-1.5 bg-gray-300'
}`}
/>
))}
@ -80,34 +73,18 @@ function SectionSpinner({ label }: { label?: string }) {
return (
<div className="flex flex-col items-center justify-center py-10">
<Loader2 className="h-8 w-8 animate-spin text-brand-500" />
{label && (
<p className="mt-2 text-sm text-gray-500">{label}</p>
)}
{label && <p className="mt-2 text-sm text-gray-500">{label}</p>}
</div>
)
}
// Light/pastel color pairs for the Explore Products tabs.
const TAG_COLORS = [
{ text: '#BE123C', border: '#FECDD3', bg: '#FFE4E6' }, // rose
{ text: '#B45309', border: '#FDE68A', bg: '#FEF3C7' }, // amber
{ text: '#15803D', border: '#BBF7D0', bg: '#DCFCE7' }, // green
{ text: '#1D4ED8', border: '#BFDBFE', bg: '#DBEAFE' }, // blue
{ text: '#6D28D9', border: '#DDD6FE', bg: '#EDE9FE' }, // violet
{ text: '#C2410C', border: '#FFD6B0', bg: '#FFE4CC' }, // orange
{ text: '#0E7490', border: '#A5F3FC', bg: '#CFFAFE' }, // cyan
{ text: '#BE185D', border: '#FBCFE8', bg: '#FCE7F3' }, // pink
]
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length]
function HomePage() {
const navigate = useNavigate()
const { data: productsData, isLoading: isProductsLoading } = useAllProducts()
const { data: storesData, isLoading: isStoresLoading } = useStores()
const { data: bannersData } = useBanners()
const { data: slotsData, isLoading: isSlotsLoading } = useSlots()
const { data: essentialConsts, isLoading: isEssentialConstsLoading } = useGetEssentialConsts()
const { data: essentialConsts } = useGetEssentialConsts()
// Handle bootstrapping: products/stores/slots are disabled until essentialConsts provides cacheUrl
// NOTE: with persisted caches, render immediately from placeholderData; no startup spinner.
@ -133,6 +110,10 @@ function HomePage() {
const dashboardTags = productsData?.tags || []
const [selectedTagId, setSelectedTagId] = useState<number | null>(null)
const activeTagId = selectedTagId ?? dashboardTags[0]?.id ?? null
const [expandedByTagId, setExpandedByTagId] = useState<Record<number, boolean>>({})
const handleToggleExpand = (tagId: number) => {
setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }))
}
const productsByTagId = useMemo(() => {
const map: Record<number, any[]> = {}
@ -216,201 +197,221 @@ function HomePage() {
return (
<AppLayout>
<div className="min-h-screen bg-white pb-24">
{/* Search Bar */}
<div className="sticky top-0 z-10 border-b border-gray-100 bg-white/95 px-4 pb-3 pt-4 backdrop-blur-sm">
<SearchBar
placeholder="Search products here..."
onClick={() => navigate({ to: '/home/search' })}
/>
<div className="mx-auto w-full max-w-7xl px-4 pb-24 md:px-8 md:pb-12">
{/* Welcome band — desktop editorial opener */}
<div className="flex flex-col gap-1 py-6 md:py-8">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Fresh cut, daily
</p>
<h1 className="display-1 text-gray-900">
Butcher-fresh meat, delivered to your door
</h1>
<p className="mt-1 max-w-2xl text-gray-600">
Chicken, mutton, fish and more, cut to order from our own stores.
Reserve a delivery slot and we will bring it cold and clean.
</p>
</div>
<div className="px-4">
{/* Banner Carousel - Commented out */}
{/* {banners.length > 0 && (
<div className="mb-6 mt-4 overflow-hidden rounded-xl">
<BannerCarousel banners={banners} />
{/* Download App Banner */}
<div className="mb-8 overflow-hidden rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 px-6 py-4 md:flex md:items-center md:justify-between">
<div className="flex items-center gap-4">
<div className="hidden h-12 w-12 items-center justify-center rounded-xl bg-white/20 md:flex">
<Beef className="h-6 w-6 text-white" />
</div>
)} */}
{/* Download App Banner */}
<div className="mb-6 mt-4 rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 p-4 shadow-lg">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
<p className="text-sm text-white/80 mt-1">Download for exclusive offers & faster checkout</p>
</div>
<a
href={essentialConsts?.playStoreUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-bold text-brand-600 shadow-md hover:bg-gray-50 transition-colors"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 20.5V3.5C3 2.91 3.34 2.39 3.84 2.15L13.69 12L3.84 21.85C3.34 21.6 3 21.09 3 20.5ZM16.81 15.12L6.05 21.34L14.54 12.85L16.81 15.12ZM20.16 10.81C20.5 11.08 20.75 11.5 20.75 12C20.75 12.5 20.53 12.9 20.18 13.18L17.89 14.5L15.39 12L17.89 9.5L20.16 10.81ZM6.05 2.66L16.81 8.88L14.54 11.15L6.05 2.66Z"/>
</svg>
Get App
</a>
<div className="flex-1">
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
<p className="text-sm text-white/80">Download for exclusive offers & faster checkout</p>
</div>
</div>
<a
href={essentialConsts?.playStoreUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="mt-3 inline-flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-bold text-brand-600 shadow-md transition-colors hover:bg-gray-50 md:mt-0"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 20.5V3.5C3 2.91 3.34 2.39 3.84 2.15L13.69 12L3.84 21.85C3.34 21.6 3 21.09 3 20.5ZM16.81 15.12L6.05 21.34L14.54 12.85L16.81 15.12ZM20.16 10.81C20.5 11.08 20.75 11.5 20.75 12C20.75 12.5 20.53 12.9 20.18 13.18L17.89 14.5L15.39 12L17.89 9.5L20.16 10.81ZM6.05 2.66L16.81 8.88L14.54 11.15L6.05 2.66Z"/>
</svg>
Get App
</a>
</div>
{/* Stores Section */}
<div className="mb-6">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="font-bold text-xl text-gray-900">
Our Stores
</p>
<p className="mt-0.5 text-xs text-gray-500">
Fresh from our locations
</p>
</div>
{/* Stores Section */}
<section className="mb-10">
<div className="section-rule mb-5 flex items-end justify-between">
<div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Our Outlets
</p>
<h2 className="display-2 mt-1 text-gray-900">Shop by Store</h2>
</div>
{storesLoading ? (
<SectionSpinner label="Loading stores..." />
) : (
<div className="flex flex-wrap gap-4">
{stores.map((store: any) => (
<div key={store.id}>
<StoreCard
store={store}
onClick={() =>
navigate({
to: '/stores/$storeId',
params: { storeId: String(store.id) },
})
}
/>
</div>
))}
</div>
)}
<button
onClick={() => navigate({ to: '/stores' })}
className="text-sm font-bold text-brand-600 hover:text-brand-700"
>
All stores
</button>
</div>
{/* Explore Products Section */}
{dashboardTags.length > 0 && (
<div className="mb-6">
<div className="mb-4">
<p className="font-bold text-xl text-gray-900">
Explore Products
</p>
<p className="mt-0.5 text-sm text-gray-500">
Browse by category
</p>
</div>
{/* Tab strip */}
<div className="scrollbar-hide -mx-4 flex gap-3 overflow-x-auto px-4 pb-2">
{dashboardTags.map((tag: any) => {
const color = getTagColor(tag.id)
const active = activeTagId === tag.id
return (
<button
key={tag.id}
onClick={() => setSelectedTagId(tag.id)}
className="shrink-0 cursor-pointer border-b-4 bg-transparent px-3 py-2 text-left transition-colors"
style={{
borderBottomColor: active ? color.text : 'transparent',
}}
>
<span
className="text-sm font-semibold whitespace-nowrap"
style={{ color: active ? color.text : '#6B7280' }}
>
{tag.tagName} ({tag.productIds?.length || 0})
</span>
</button>
)
})}
</div>
{/* Active tag products */}
<div className="mt-4">
{activeTagProducts.length > 0 ? (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
{activeTagProducts.map((product: any) => (
<ProductCard
key={product.id}
item={product}
onPress={() => handleProductPress(product.id)}
showDeliveryInfo={false}
miniView={true}
useAddToCartDialog={true}
/>
))}
</div>
) : (
<p className="py-6 text-center text-sm text-gray-500">
No products in this category yet
</p>
)}
</div>
{storesLoading ? (
<SectionSpinner label="Loading stores..." />
) : (
<div className="flex flex-wrap gap-4">
{stores.map((store: any) => (
<div key={store.id}>
<StoreCard
store={store}
onClick={() =>
navigate({
to: '/stores/$storeId',
params: { storeId: String(store.id) },
})
}
/>
</div>
))}
</div>
)}
</section>
{/* Upcoming Delivery Slots Section */}
<div className="mb-6">
<div className="mb-4">
<p className="font-bold text-xl text-gray-900">
Upcoming Delivery Slots
</p>
<p className="mt-0.5 text-sm text-gray-500">
Plan your fresh deliveries ahead
</p>
{/* Explore Products Section */}
{dashboardTags.length > 0 && (
<section className="mb-10">
<div className="section-rule mb-5 flex items-end justify-between">
<div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Browse by Category
</p>
<h2 className="display-2 mt-1 text-gray-900">Explore Products</h2>
</div>
</div>
{/* Tab strip — flat, brand underline active (matches user-ui home) */}
<div className="scrollbar-hide -mx-4 mb-5 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0">
{dashboardTags.map((tag: any) => {
const active = activeTagId === tag.id
return (
<button
key={tag.id}
onClick={() => setSelectedTagId(tag.id)}
className="shrink-0 cursor-pointer px-3 py-2"
>
<span
className={`block text-sm font-bold ${
active ? 'text-brand-600' : 'text-gray-500'
}`}
>
{tag.tagName}
</span>
<span
className={`mt-1 block h-1 rounded-full ${
active ? 'bg-brand-500' : 'bg-transparent'
}`}
/>
</button>
)
})}
</div>
{/* Active tag products — brand-tinted container (matches user-ui) */}
<div className="mb-2 rounded-[28px] border border-brand-100 bg-brand-25 px-3 pb-4 pt-3">
{activeTagProducts.length > 0 ? (
<>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{activeTagProducts
.slice(0, expandedByTagId[activeTagId] ? undefined : 6)
.map((product: any) => (
<ProductCard
key={product.id}
item={product}
onPress={() => handleProductPress(product.id)}
showDeliveryInfo={false}
miniView={true}
useAddToCartDialog={true}
/>
))}
</div>
{activeTagProducts.length > 6 && !expandedByTagId[activeTagId] && (
<div className="mt-4 flex justify-center">
<button
onClick={() => handleToggleExpand(activeTagId)}
className="rounded-full bg-brand-500 px-6 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-brand-600"
>
Show More
</button>
</div>
)}
</>
) : (
<p className="py-6 text-center text-sm text-gray-500">
No products in this category yet
</p>
)}
</div>
</section>
)}
{/* Upcoming Delivery Slots Section */}
<section className="mb-10">
<div className="section-rule mb-5 flex items-end justify-between">
<div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Plan Ahead
</p>
<h2 className="display-2 mt-1 text-gray-900">Upcoming Delivery Slots</h2>
</div>
{slotsLoading ? (
<SectionSpinner label="Loading slots..." />
) : (
<>
<div
ref={slotsScrollRef}
className="scrollbar-hide -mx-4 flex gap-4 overflow-x-auto px-4 pb-2"
>
{sortedSlots.slice(0, 5).map((slot) => (
<SlotCard key={slot.id} slot={slot} />
))}
</div>
<ScrollIndicator
containerRef={slotsScrollRef}
itemCount={sortedSlots.slice(0, 5).length}
itemWidth={280}
/>
</>
)}
</div>
{/* All Products Section */}
<div className="rounded-t-3xl bg-white pt-4">
<div className="mb-4">
<p className="font-bold text-xl text-gray-900">
All Available Products
</p>
<p className="mt-0.5 text-sm text-gray-500">
Browse our complete selection
</p>
</div>
{productsLoading ? (
<SectionSpinner label="Loading products..." />
) : (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
{sortedProducts.map((product) => (
<ProductCard
key={product.id}
item={product}
onPress={() => handleProductPress(product.id)}
showDeliveryInfo={true}
miniView={false}
useAddToCartDialog={true}
/>
{slotsLoading ? (
<SectionSpinner label="Loading slots..." />
) : (
<>
<div
ref={slotsScrollRef}
className="scrollbar-hide -mx-4 flex gap-4 overflow-x-auto px-4 pb-2"
>
{sortedSlots.slice(0, 5).map((slot) => (
<SlotCard key={slot.id} slot={slot} />
))}
</div>
)}
</div>
</div>
<ScrollIndicator
containerRef={slotsScrollRef}
itemCount={sortedSlots.slice(0, 5).length}
itemWidth={280}
/>
</>
)}
</section>
<AddToCartDialog />
{/* All Products Section */}
<section>
<div className="section-rule mb-5">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
The Full Counter
</p>
<h2 className="display-2 mt-1 text-gray-900">All Available Products</h2>
<p className="mt-1 text-sm text-gray-500">Browse our complete selection</p>
</div>
{productsLoading ? (
<SectionSpinner label="Loading products..." />
) : (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{sortedProducts.map((product) => (
<ProductCard
key={product.id}
item={product}
onPress={() => handleProductPress(product.id)}
showDeliveryInfo={true}
miniView={false}
useAddToCartDialog={true}
/>
))}
</div>
)}
</section>
</div>
<AddToCartDialog />
</AppLayout>
)
}
@ -479,7 +480,7 @@ function StoreCard({ store, onClick }: { store: any; onClick: () => void }) {
onClick={onClick}
className="flex flex-col items-center"
>
<div className="mb-2 flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl border-2 border-white/30 bg-gray-100 shadow-lg">
<div className="mb-2 flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl border-2 border-white/30 bg-gray-100 shadow-md transition-transform hover:scale-105">
{store.signedImageUrl ? (
<img
src={store.signedImageUrl}
@ -490,7 +491,7 @@ function StoreCard({ store, onClick }: { store: any; onClick: () => void }) {
<Store className="h-7 w-7 text-gray-400" />
)}
</div>
<p className="font-bold text-center text-xs tracking-wide text-gray-800">
<p className="text-center text-xs font-bold tracking-wide text-gray-800">
{store.name.replace(/^The\s+/i, '')}
</p>
</div>
@ -519,8 +520,8 @@ function SlotCard({ slot }: { slot: any }) {
return (
<div
onClick={() => navigate({ to: '/slot-view', search: { slotId: slot.id } })}
className={`min-w-70 shrink-0 cursor-pointer rounded-3xl border border-slate-100 bg-white p-5 shadow-xl ${
isClosingSoon ? 'border-l-4 border-l-amber-400' : 'border-l-4 border-l-brand-500'
className={`min-w-70 shrink-0 cursor-pointer rounded-xl border bg-white p-5 shadow-sm transition-all hover:shadow-md ${
isClosingSoon ? 'border-l-4 border-l-amber-400' : 'border-l-4 border-l-brand-600'
}`}
>
<div className="mb-4 flex flex-row items-start justify-end">
@ -535,17 +536,15 @@ function SlotCard({ slot }: { slot: any }) {
<div className="mb-5 flex flex-row justify-between">
<div className="mr-4 flex-1">
<div className="mb-1.5 flex flex-row items-center">
<div className="mr-1.5 rounded-md bg-brand-50 p-1">
<svg className="h-3 w-3 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<div className="mr-1.5 rounded-md bg-brand-25 p-1">
<Truck className="h-3 w-3 text-brand-600" />
</div>
<p className="text-[10px] font-bold uppercase text-brand-700">Delivery At</p>
</div>
<p className="font-bold text-sm text-slate-900">
<p className="text-sm font-bold text-gray-900">
{formatTimeRange(slot.deliveryTime)}
</p>
<p className="text-[11px] font-bold text-slate-500">
<p className="text-[11px] font-bold text-gray-500">
{dayjs(slot.deliveryTime).format('ddd, MMM DD')}
</p>
</div>
@ -553,16 +552,14 @@ function SlotCard({ slot }: { slot: any }) {
<div className="flex-1">
<div className="mb-1.5 flex flex-row items-center">
<div className="mr-1.5 rounded-md bg-amber-50 p-1">
<svg className="h-3 w-3 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<Clock className="h-3 w-3 text-amber-600" />
</div>
<p className="text-[10px] font-bold uppercase text-amber-700">Order By</p>
</div>
<p className="font-bold text-sm text-slate-900">
<p className="text-sm font-bold text-gray-900">
{dayjs(slot.freezeTime).format('h:mm A')}
</p>
<p className="text-[11px] font-bold text-slate-500">
<p className="text-[11px] font-bold text-gray-500">
{dayjs(slot.freezeTime).format('ddd, MMM DD')}
</p>
</div>
@ -573,13 +570,13 @@ function SlotCard({ slot }: { slot: any }) {
{slot.products?.slice(0, 3).map((p: any, i: number) => (
<div
key={p.id}
className={`h-8 w-8 overflow-hidden rounded-full border-2 border-white bg-slate-100 ${i > 0 ? '-ml-3' : ''}`}
className={`h-8 w-8 overflow-hidden rounded-full border-2 border-white bg-gray-100 ${i > 0 ? '-ml-3' : ''}`}
>
{p.images?.[0] ? (
<img src={p.images[0]} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center">
<ImageOff className="h-3.5 w-3.5 text-slate-400" />
<ImageOff className="h-3.5 w-3.5 text-gray-400" />
</div>
)}
</div>

View file

@ -1,6 +1,5 @@
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { p, MyButton } from 'web-components'
import { Package } from 'lucide-react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { CheckCircle2 } from 'lucide-react'
export const Route = createFileRoute('/home/order-success')({
component: OrderSuccessPage,
@ -15,12 +14,12 @@ function OrderSuccessPage() {
const { orderId, totalAmount } = Route.useSearch()
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-green-50 p-6">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-green-100">
<Package className="h-10 w-10 text-green-600" />
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-brand-25">
<CheckCircle2 className="h-10 w-10 text-brand-600" />
</div>
<p className="font-bold mb-2 text-2xl text-gray-900">
Order Placed!
<p className="mb-2 mt-6 text-2xl font-extrabold text-gray-900">
Order Placed
</p>
<p className="mb-1 text-gray-600">
Order ID: #{orderId}
@ -28,17 +27,18 @@ function OrderSuccessPage() {
<p className="mb-8 text-gray-600">
Total: {totalAmount}
</p>
<MyButton
textContent="Continue Shopping"
<button
onClick={() => navigate({ to: '/home' })}
className="mb-3 bg-brand-500 text-white"
/>
<MyButton
textContent="View My Orders"
className="mb-3 rounded-lg bg-brand-600 px-8 py-3 text-sm font-bold text-white transition-colors hover:bg-brand-700"
>
Continue Shopping
</button>
<button
onClick={() => navigate({ to: '/me/orders' })}
fillColor="gray"
className="bg-gray-100 text-gray-700"
/>
className="rounded-lg border border-gray-200 bg-white px-8 py-3 text-sm font-bold text-gray-700 transition-colors hover:bg-gray-100"
>
View My Orders
</button>
</div>
)
}

View file

@ -2,7 +2,7 @@ import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import Fuse from 'fuse.js'
import { useAllProducts } from '../hooks/prominent-api-hooks'
import { p, SearchBar, div } from 'web-components'
import { SearchBar } from 'web-components'
import { ProductCard } from '../components/ProductCard'
import { SearchX, Loader2, ChevronLeft } from 'lucide-react'
import { AppLayout } from '../components/AppLayout'
@ -107,7 +107,7 @@ function SearchPage() {
<p className="mt-4 text-lg font-medium text-gray-900">Failed to load products</p>
<button
onClick={() => refetch()}
className="mt-4 rounded-lg bg-brand-500 px-4 py-2 font-medium text-white"
className="mt-4 rounded-lg bg-brand-600 px-4 py-2 font-medium text-white transition-colors hover:bg-brand-700"
>
Retry
</button>
@ -120,13 +120,14 @@ function SearchPage() {
<AppLayout>
<div className="flex min-h-full flex-1 flex-col bg-gray-50">
{/* Search Header */}
<div className="sticky top-0 z-10 border-b border-gray-100 bg-white px-4 pb-3 pt-4">
<div className="flex items-center gap-3">
<div className="sticky top-0 z-10 border-b border-gray-100 bg-white px-4 pb-4 pt-4">
<div className="mx-auto flex w-full max-w-7xl items-center gap-3">
<button
onClick={() => navigate({ to: '/home' })}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gray-50 text-gray-700 transition-colors hover:bg-gray-100"
aria-label="Back to home"
>
<ChevronLeft className="h-5 w-5 text-gray-700" />
<ChevronLeft className="h-5 w-5" />
</button>
<div className="flex-1">
<SearchBar
@ -138,7 +139,7 @@ function SearchPage() {
/>
</div>
</div>
<div className="mt-3 flex flex-row items-center justify-between">
<div className="mx-auto mt-3 flex w-full max-w-7xl flex-row items-center justify-between">
<p className="text-lg font-bold text-gray-900">
{debouncedQuery ? `Search Results for "${debouncedQuery}"` : 'All Products'}
</p>
@ -147,9 +148,9 @@ function SearchPage() {
</div>
{/* Products Grid */}
<div className="flex-1 p-4">
<div className="mx-auto w-full max-w-7xl flex-1 p-4">
{products.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center py-12 px-4">
<div className="flex flex-1 flex-col items-center justify-center px-4 py-12">
<SearchX className="h-16 w-16 text-gray-300" />
<p className="mt-4 text-center text-lg font-medium text-gray-500">
No products found
@ -161,7 +162,7 @@ function SearchPage() {
)}
</div>
) : (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{products.map((product: any) => (
<ProductCard
key={product.id}

View file

@ -4,7 +4,8 @@ import { useForm, Controller } from 'react-hook-form'
import { useAuth } from '../lib/auth-context'
import { trpc } from '../lib/trpc-client'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
import { p, MyButton, pInput as PInput, div } from 'web-components'
import { MyButton, pInput as PInput } from 'web-components'
import { Beef } from 'lucide-react'
export const Route = createFileRoute('/login')({ component: LoginPage })
@ -126,145 +127,188 @@ function LoginPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-b from-brand-400 to-brand-700 p-4">
<div className="w-full max-w-md">
<p className="font-bold mb-2 text-center text-4xl text-white">
Welcome
</p>
<p className="mb-8 text-center text-lg text-blue-100">
Sign in to continue your journey
</p>
<div className="rounded-2xl bg-white p-8 shadow-xl">
<form onSubmit={handleSubmit(onSubmit)}>
{step === 'mobile' && (
<Controller
control={control}
name="mobile"
render={({ field: { onChange, value } }) => (
<PInput
placeholder="Enter your mobile number"
value={value}
onChange={(e) => {
const clean = e.target.value.replace(/\D/g, '')
if (clean.length <= 10) onChange(clean)
}}
className="bg-gray-50"
/>
)}
/>
)}
{step === 'otp' && (
<div className="mb-6">
<p className="font-semibold mb-3 text-center text-base text-gray-800">
Enter 4-digit OTP
</p>
<div className="flex justify-center gap-2">
{[0, 1, 2, 3].map((i) => (
<input
key={i}
ref={(el) => { inputRefs.current[i] = el }}
className="h-14 w-14 rounded-xl border-2 text-center text-2xl font-bold"
style={{
borderColor: otpCells[i] ? '#E63946' : '#E5E7EB',
backgroundColor: otpCells[i] ? '#FFF5F6' : '#F9FAFB',
}}
type="text"
inputMode="numeric"
maxLength={1}
value={otpCells[i]}
onChange={(e) => handleOtpChange(i, e.target.value)}
/>
))}
</div>
<div className="mt-4 flex items-center justify-between border-t border-gray-100 pt-4">
<div
onClick={() => { setStep('choice'); setOtpCells(['', '', '', '']) }}
>
<p className="font-medium text-gray-500">Back</p>
</div>
<button
onClick={() => sendOtpMutation.mutate({ mobile: selectedMobile })}
disabled={!canResend}
>
<p
className={`font-semibold ${canResend ? 'text-brand-600' : 'text-gray-400'}`}
>
{canResend ? 'Resend OTP' : `Resend in ${resendCountdown}s`}
</p>
</button>
</div>
</div>
)}
{step === 'password' && (
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<PInput
placeholder="Enter your password"
value={value}
onChange={(e) => onChange(e.target.value)}
type="password"
className="bg-gray-50"
/>
)}
/>
)}
<div className="mt-6">
<MyButton
type="submit"
fullWidth
className="h-12 rounded-xl bg-brand-600 text-white shadow-lg"
disabled={
sendOtpMutation.isPending ||
verifyOtpMutation.isPending ||
loginMutation.isPending
}
textContent={
step === 'otp'
? 'Verify & Login'
: step === 'password'
? 'Login'
: 'Continue'
}
/>
</div>
</form>
{step === 'otp' && (
<div
onClick={() => { setStep('password'); setOtpCells(['', '', '', '']) }}
className="mt-4 block text-center"
>
<p className="font-semibold text-brand-600">
Or login with Password
</p>
</div>
)}
<div className="flex min-h-screen bg-gray-50">
{/* Brand panel — desktop only */}
<div className="hidden w-[42%] flex-col justify-between bg-gradient-to-b from-brand-500 to-brand-700 p-12 lg:flex">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/20">
<Beef className="h-6 w-6 text-white" />
</div>
<div>
<p className="font-display text-xl font-extrabold tracking-tight text-white">
Freshyo
</p>
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-white/60">
Butcher &amp; Grocer
</p>
</div>
</div>
{/* Download App Banner */}
<div className="mt-6 rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 p-4 shadow-lg">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
<p className="text-sm text-white/80 mt-1">Download for exclusive offers & faster checkout</p>
<div>
<h1 className="display-1 max-w-md text-white">
Fresh cuts, planned deliveries, zero queues
</h1>
<p className="mt-4 max-w-md text-white/80">
Reserve a delivery slot, pick your cuts, and we will bring it cold and clean to your door.
</p>
</div>
<p className="text-xs text-white/50">
Freshyo &copy; {new Date().getFullYear()}
</p>
</div>
{/* Form panel */}
<div className="flex flex-1 items-center justify-center p-6">
<div className="w-full max-w-md">
<div className="mb-8 flex items-center gap-3 lg:hidden">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-brand-600">
<Beef className="h-5 w-5 text-white" />
</div>
<p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
Freshyo
</p>
</div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Welcome Back
</p>
<h1 className="display-2 mt-1 text-gray-900">Sign in to continue</h1>
<p className="mb-8 mt-1 text-gray-500">
Enter your mobile number to receive a one-time password
</p>
<div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
<form onSubmit={handleSubmit(onSubmit)}>
{step === 'mobile' && (
<Controller
control={control}
name="mobile"
render={({ field: { onChange, value } }) => (
<PInput
placeholder="Enter your mobile number"
value={value}
onChange={(e) => {
const clean = e.target.value.replace(/\D/g, '')
if (clean.length <= 10) onChange(clean)
}}
className="bg-gray-50"
/>
)}
/>
)}
{step === 'otp' && (
<div className="mb-6">
<p className="mb-3 text-center text-base font-semibold text-gray-800">
Enter 4-digit OTP
</p>
<div className="flex justify-center gap-2">
{[0, 1, 2, 3].map((i) => (
<input
key={i}
ref={(el) => { inputRefs.current[i] = el }}
className="h-14 w-14 rounded-xl border-2 text-center text-2xl font-bold"
style={{
borderColor: otpCells[i] ? '#1570EF' : '#E5E7EB',
backgroundColor: otpCells[i] ? '#EFF8FF' : '#F9FAFB',
}}
type="text"
inputMode="numeric"
maxLength={1}
value={otpCells[i]}
onChange={(e) => handleOtpChange(i, e.target.value)}
/>
))}
</div>
<div className="mt-4 flex items-center justify-between border-t border-gray-100 pt-4">
<div
onClick={() => { setStep('choice'); setOtpCells(['', '', '', '']) }}
>
<p className="font-medium text-gray-500">Back</p>
</div>
<button
onClick={() => sendOtpMutation.mutate({ mobile: selectedMobile })}
disabled={!canResend}
>
<p
className={`font-semibold ${canResend ? 'text-brand-600' : 'text-gray-400'}`}
>
{canResend ? 'Resend OTP' : `Resend in ${resendCountdown}s`}
</p>
</button>
</div>
</div>
)}
{step === 'password' && (
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<PInput
placeholder="Enter your password"
value={value}
onChange={(e) => onChange(e.target.value)}
type="password"
className="bg-gray-50"
/>
)}
/>
)}
<div className="mt-6">
<MyButton
type="submit"
fullWidth
className="h-12 rounded-lg bg-brand-600 text-white shadow-md transition-colors hover:bg-brand-700"
disabled={
sendOtpMutation.isPending ||
verifyOtpMutation.isPending ||
loginMutation.isPending
}
textContent={
step === 'otp'
? 'Verify & Login'
: step === 'password'
? 'Login'
: 'Continue'
}
/>
</div>
</form>
{step === 'otp' && (
<div
onClick={() => { setStep('password'); setOtpCells(['', '', '', '']) }}
className="mt-4 block text-center"
>
<p className="font-semibold text-brand-600">
Or login with Password
</p>
</div>
)}
</div>
{/* Download App Banner */}
<div className="mt-6 rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
<p className="mt-1 text-sm text-white/80">Download for exclusive offers & faster checkout</p>
</div>
<a
href={constsData?.playStoreUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-bold text-brand-600 shadow-md transition-colors hover:bg-gray-50"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 20.5V3.5C3 2.91 3.34 2.39 3.84 2.15L13.69 12L3.84 21.85C3.34 21.6 3 21.09 3 20.5ZM16.81 15.12L6.05 21.34L14.54 12.85L16.81 15.12ZM20.16 10.81C20.5 11.08 20.75 11.5 20.75 12C20.75 12.5 20.53 12.9 20.18 13.18L17.89 14.5L15.39 12L17.89 9.5L20.16 10.81ZM6.05 2.66L16.81 8.88L14.54 11.15L6.05 2.66Z"/>
</svg>
Get App
</a>
</div>
<a
href={constsData?.playStoreUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-bold text-brand-600 shadow-md hover:bg-gray-50 transition-colors"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 20.5V3.5C3 2.91 3.34 2.39 3.84 2.15L13.69 12L3.84 21.85C3.34 21.6 3 21.09 3 20.5ZM16.81 15.12L6.05 21.34L14.54 12.85L16.81 15.12ZM20.16 10.81C20.5 11.08 20.75 11.5 20.75 12C20.75 12.5 20.53 12.9 20.18 13.18L17.89 14.5L15.39 12L17.89 9.5L20.16 10.81ZM6.05 2.66L16.81 8.88L14.54 11.15L6.05 2.66Z"/>
</svg>
Get App
</a>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router'
import { useAuth } from '../lib/auth-context'
import { p, MyButton, ProfileImage } from 'web-components'
import { MyButton, ProfileImage } from 'web-components'
import { AppLayout } from '../components/AppLayout'
import {
Package,
@ -12,6 +12,7 @@ import {
FileText,
LogOut,
ShoppingCart,
ChevronRight,
} from 'lucide-react'
export const Route = createFileRoute('/me')({ component: MePage })
@ -27,13 +28,14 @@ function MePage() {
if (!user) {
return (
<AppLayout>
<div className="flex flex-col items-center gap-4 py-20">
<p>Please sign in</p>
<MyButton
textContent="Sign In"
onClick={() => navigate({ to: '/login' })}
/>
</div>
<div className="flex flex-col items-center gap-4 py-20">
<p className="text-gray-600">Please sign in</p>
<MyButton
textContent="Sign In"
onClick={() => navigate({ to: '/login' })}
className="bg-brand-600 text-white"
/>
</div>
</AppLayout>
)
}
@ -72,12 +74,19 @@ function MePage() {
return (
<AppLayout>
{isExactMePath ? (
<div className="p-4">
<div className="mx-auto w-full max-w-5xl px-4 py-6 pb-24 md:px-8 md:pb-12">
{/* Profile Header */}
<div className="mb-6 flex items-center gap-4 rounded-xl bg-brand-50 p-4">
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Your Account
</p>
<h1 className="display-2 mt-1 text-gray-900">My Freshyo</h1>
</div>
<div className="mb-8 flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-6">
<ProfileImage uri={user.profileImage} size={64} />
<div>
<p className="text-lg font-bold">
<p className="text-lg font-extrabold text-gray-900">
{user.name || 'User'}
</p>
<p className="text-sm text-gray-500">{user.mobile}</p>
@ -85,38 +94,41 @@ function MePage() {
</div>
{/* Menu */}
{menuItems.map((section) => (
<div key={section.section} className="mb-6">
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-gray-500">
{section.section}
</p>
<div className="rounded-xl border border-gray-100 bg-white shadow-sm">
{section.items.map((item) => (
<div
key={item.label}
onClick={() => navigate({ to: item.to as any })}
className="flex w-full items-center gap-3 border-b border-gray-50 px-4 py-3.5 last:border-b-0"
>
<item.icon className="h-5 w-5 text-gray-400" />
<p className="flex-1 text-left text-sm">{item.label}</p>
</div>
))}
<div className="grid gap-6 md:grid-cols-2">
{menuItems.map((section) => (
<div key={section.section}>
<p className="mb-2 text-[11px] font-black uppercase tracking-[0.18em] text-gray-400">
{section.section}
</p>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white">
{section.items.map((item) => (
<button
key={item.label}
onClick={() => navigate({ to: item.to as any })}
className="flex w-full items-center gap-3 border-b border-gray-100 px-5 py-4 text-left transition-colors last:border-b-0 hover:bg-gray-50"
>
<item.icon className="h-5 w-5 text-brand-600" />
<p className="flex-1 text-sm font-semibold text-gray-800">{item.label}</p>
<ChevronRight className="h-4 w-4 text-gray-300" />
</button>
))}
</div>
</div>
</div>
))}
))}
</div>
{/* Logout */}
<MyButton
fullWidth
onClick={logout}
variant="red"
className="mb-8"
textContent="Logout"
/>
<p className="mb-8 text-center text-xs text-gray-400">
Version 1.0.0
</p>
<div className="mt-8 flex flex-col items-center gap-4">
<MyButton
onClick={logout}
variant="red"
className="bg-red-600 text-white hover:bg-red-700"
textContent="Logout"
/>
<p className="text-xs text-gray-400">
Version 1.0.0
</p>
</div>
</div>
) : (
/* Render child routes when not on exact /me path */

View file

@ -1,7 +1,6 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { trpc } from '../lib/trpc-client'
import { AppContainer } from 'web-components'
import { Tag, Loader2, AlertCircle } from 'lucide-react'
import { ProductCard } from '../components/ProductCard'
import { AppLayout } from '../components/AppLayout'
@ -24,10 +23,10 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
const hasMore = products.length > SHOW_MORE_STEP && !expanded
return (
<div className="pt-4">
<div className="mb-4 px-4">
<p className="font-bold text-xl text-gray-900">{title}</p>
<p className="mt-0.5 text-sm text-gray-500">{subtitle}</p>
<section className="mb-10">
<div className="section-rule mb-5">
<h2 className="display-2 text-gray-900">{title}</h2>
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
</div>
{products.length === 0 ? (
@ -37,7 +36,7 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
</div>
) : (
<>
<div className="grid grid-cols-2 gap-3 px-4 md:grid-cols-3 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{visible.map((item: any) => (
<ProductCard
key={item.id}
@ -50,10 +49,10 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
))}
</div>
{hasMore && (
<div className="mt-2 mb-1 flex justify-center">
<div className="mt-6 flex justify-center">
<button
onClick={onExpand}
className="rounded-full bg-brand-500 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-600"
className="rounded-lg border border-gray-200 bg-white px-6 py-2.5 text-sm font-bold text-gray-800 shadow-sm transition-colors hover:border-brand-300 hover:text-brand-700"
>
Show More
</button>
@ -61,7 +60,7 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
)}
</>
)}
</div>
</section>
)
}
@ -103,7 +102,15 @@ function OffersPage() {
return (
<AppLayout>
<div className="min-h-screen bg-white pb-24">
<div className="mx-auto min-h-screen w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Deals & Bundles
</p>
<h1 className="display-2 mt-1 text-gray-900">Offers</h1>
<p className="mt-1 text-gray-500">Great prices on select items</p>
</div>
<OffersSection
title="Offers"
subtitle="Great prices on select items"

View file

@ -1,7 +1,6 @@
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useStoreWithProducts, useAllProducts } from '../hooks/prominent-api-hooks'
import { useState, useMemo } from 'react'
import { p } from 'web-components'
import { AppLayout } from '../components/AppLayout'
import AddToCartDialog from '../components/AddToCartDialog'
import { ProductCard } from '../components/ProductCard'
@ -71,10 +70,10 @@ function StoreDetailPage() {
return (
<AppLayout>
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<svg className="mb-4 h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="mb-4 h-12 w-12 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="font-bold mb-2 text-lg text-gray-900">
<p className="mb-2 text-lg font-bold text-gray-900">
Oops!
</p>
<p className="text-gray-500">Store not found or error loading</p>
@ -86,38 +85,52 @@ function StoreDetailPage() {
return (
<AppLayout>
<div className="min-h-screen bg-gray-50 pb-24">
{/* Back Button */}
<div className="sticky top-0 z-10 border-b border-gray-200 bg-white px-4 py-3">
<div className="flex items-center gap-3">
<div onClick={() => navigate({ to: '/stores' })} className="p-2">
<ArrowLeft className="h-5 w-5 text-gray-700" />
{/* Header band */}
<div className="border-b border-gray-200 bg-white">
<div className="mx-auto w-full max-w-7xl px-4 py-5 md:px-8">
<button
onClick={() => navigate({ to: '/stores' })}
className="mb-4 flex items-center gap-2 text-sm font-bold text-gray-600 transition-colors hover:text-brand-700"
>
<ArrowLeft className="h-4 w-4" />
All Stores
</button>
<div className="flex items-center gap-4">
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-xl bg-brand-25">
{storeData?.store?.signedImageUrl ? (
<img
src={storeData?.store?.signedImageUrl}
alt={storeData?.store?.name}
className="h-full w-full rounded-xl object-cover"
/>
) : (
<Store className="h-7 w-7 text-brand-600" />
)}
</div>
<div>
<h1 className="display-2 text-gray-900">
{storeData?.store?.name}
</h1>
{storeData?.store?.description && (
<p className="mt-1 max-w-2xl text-gray-500">
{storeData?.store?.description}
</p>
)}
</div>
</div>
<p className="font-bold text-lg text-gray-900">
{storeData?.store?.name || 'Store'}
</p>
</div>
</div>
<div className="px-4 pt-4">
{/* Store Info Card */}
<div className="flex items-center gap-2 mb-6 rounded-2xl border border-gray-100 bg-white p-6 text-center shadow-sm">
<div className="mb-4 flex h-16 w-16 items-center justify-center self-center rounded-full bg-pink-50">
<Store className="h-7 w-7 text-brand-500" />
</div>
<p className="font-bold mb-2 text-center text-2xl text-gray-900">
{storeData?.store?.name}
</p>
{storeData?.store?.description && (
<p className="px-4 text-center leading-5 text-gray-500">
{storeData?.store?.description}
</p>
)}
</div>
<div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8">
{/* Tags Section */}
{storeData?.tags && storeData.tags.length > 0 && (
<div className="mb-6 flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
<div className="mb-6 flex flex-wrap gap-2">
<TagChip
tag={{ id: 0, tagName: 'All', productIds: storeProducts.map((p: any) => p.id) }}
isSelected={selectedTagId === null}
onPress={() => setSelectedTagId(null)}
/>
{storeData.tags.map((tag: Tag) => (
<TagChip
key={tag.id}
@ -133,7 +146,7 @@ function StoreDetailPage() {
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center">
<Grid3X3 className="mr-2 h-5 w-5 text-gray-700" />
<p className="font-bold text-lg text-gray-900">
<p className="text-lg font-bold text-gray-900">
{selectedTagId
? `${storeData?.tags.find((t: Tag) => t.id === selectedTagId)?.tagName} items`
: `${filteredProducts.length} products`}
@ -144,14 +157,14 @@ function StoreDetailPage() {
onClick={() => setSelectedTagId(null)}
className="flex items-center"
>
<p className="mr-1 text-sm font-medium text-brand-500">Clear</p>
<X className="h-4 w-4 text-brand-500" />
<p className="mr-1 text-sm font-medium text-brand-600">Clear</p>
<X className="h-4 w-4 text-brand-600" />
</div>
)}
</div>
{/* Products Grid */}
<div className="grid gap-4 sm:grid-cols-2" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }}>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{filteredProducts.map((product: any) => (
<ProductCard
key={product.id}
@ -196,13 +209,13 @@ function TagChip({ tag, isSelected, onPress }: TagChipProps) {
return (
<button
onClick={onPress}
className={`whitespace-nowrap rounded-lg border px-4 py-2 ${
className={`whitespace-nowrap rounded-lg border px-4 py-2 transition-colors ${
isSelected
? 'border-brand-500 bg-brand-500 text-white'
: 'border-brand-500 bg-white text-brand-500'
? 'border-brand-600 bg-brand-600 text-white'
: 'border-gray-200 bg-white text-gray-700 hover:border-brand-300'
}`}
>
<span className={`text-sm font-medium ${isSelected ? 'text-white' : 'text-brand-500'}`}>
<span className={`text-sm font-medium ${isSelected ? 'text-white' : ''}`}>
{tag.tagName} ({productCount})
</span>
</button>

View file

@ -1,6 +1,5 @@
import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router'
import { useStores } from '../hooks/prominent-api-hooks'
import { p, div } from 'web-components'
import { AppLayout } from '../components/AppLayout'
import { Store, ArrowRight, Building2 } from 'lucide-react'
@ -20,11 +19,11 @@ function StoresPage() {
if (isLoading) {
return (
<AppLayout>
<div className="flex min-h-screen flex-col items-center justify-center bg-slate-50">
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<div className="flex h-20 w-20 items-center justify-center">
<Building2 className="h-12 w-12 text-brand-200" />
<Building2 className="h-12 w-12 text-gray-300" />
</div>
<p className="mt-4 text-[10px] font-black uppercase tracking-widest text-slate-400">
<p className="mt-4 text-[10px] font-black uppercase tracking-widest text-gray-400">
Opening Marketplace...
</p>
</div>
@ -35,21 +34,21 @@ function StoresPage() {
if (error) {
return (
<AppLayout>
<div className="flex min-h-screen flex-col items-center justify-center bg-slate-50 p-10">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-rose-50">
<svg className="h-8 w-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-10">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-brand-25">
<svg className="h-8 w-8 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="font-bold mb-2 text-center text-xl text-slate-900">
<p className="mb-2 text-center text-xl font-bold text-gray-900">
Store Fetch Failed
</p>
<p className="mb-8 text-center font-medium leading-5 text-slate-500">
<p className="mb-8 text-center font-medium leading-5 text-gray-500">
We couldn't reach our vendor network.
</p>
<button
onClick={() => window.location.reload()}
className="rounded-2xl bg-brand-600 px-8 py-3 text-xs font-black uppercase tracking-widest text-white shadow-lg shadow-brand-200"
className="rounded-lg bg-brand-600 px-8 py-3 text-xs font-black uppercase tracking-widest text-white transition-colors hover:bg-brand-700"
>
Retry
</button>
@ -60,29 +59,22 @@ function StoresPage() {
return (
<AppLayout>
<div className="min-h-screen bg-slate-50 pb-32">
<div className="min-h-screen bg-gray-50 pb-32">
{isExactStoresPath ? (
<>
<div className="px-3 pt-6">
{/* Header */}
<div className="mb-4 flex items-center">
<div className="mr-3 h-6 w-1 rounded-full bg-gradient-to-b from-brand-500 to-brand-700" />
<div>
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">
Our Outlets
</p>
<p className="font-bold text-3xl tracking-tight text-slate-900">
Our Stores
</p>
</div>
</div>
<p className="pr-4 text-sm font-medium leading-5 text-slate-500">
<div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8">
{/* Header */}
<div className="mb-6">
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Our Outlets
</p>
<h1 className="display-2 mt-1 text-gray-900">Our Stores</h1>
<p className="mt-1 max-w-2xl text-gray-600">
Experience the finest selection of premium meat, poultry, fresh fruits, vegetables, and dairy directly from our own stores.
</p>
</div>
{/* Store Cards */}
<div className="px-3 pt-4">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{stores.map((store: any) => (
<StoreCard key={store.id} store={store} />
))}
@ -91,14 +83,14 @@ function StoresPage() {
{stores.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center py-20">
<div className="mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-white shadow-sm">
<Building2 className="h-12 w-12 text-slate-400" />
<Building2 className="h-12 w-12 text-gray-400" />
</div>
<p className="font-bold text-center text-xl tracking-tight text-slate-900">
<p className="text-center text-xl font-bold tracking-tight text-gray-900">
No Stores Available
</p>
</div>
)}
</>
</div>
) : (
/* Render child routes (e.g., store detail) when not on exact /stores path */
<Outlet />
@ -114,116 +106,106 @@ interface StoreCardProps {
function StoreCard({ store }: StoreCardProps) {
const navigate = useNavigate()
const sampleProducts = store.sampleProducts || []
const remainingCount = store.productCount - sampleProducts.length
const isMeatStore = store.name?.toLowerCase().includes('meat')
const sampleProducts = store.sampleProducts || []
const remainingCount = store.productCount - sampleProducts.length
const navigateToStore = () => {
navigate({
to: '/stores/$storeId',
params: { storeId: String(store.id) },
})
}
const navigateToStore = () => {
navigate({
to: '/stores/$storeId',
params: { storeId: String(store.id) },
})
}
return (
<div className="mb-4 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-lg shadow-slate-200">
{/* Meat Store Images - Show at top if store name contains 'meat' */}
{/* Top Header Section */}
<div onClick={navigateToStore} className="cursor-pointer p-4 pb-0">
<div className="mb-4 flex items-center">
<div className="h-12 w-12 rounded-xl border border-slate-200 bg-slate-50 p-0.5 shadow-sm">
<div className="relative h-full w-full overflow-hidden rounded-[10px]">
{store.signedImageUrl || store.imageUrl ? (
<img
src={store.signedImageUrl || store.imageUrl}
alt={store.name}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<Store className="h-5 w-5 text-slate-400" />
</div>
)}
</div>
</div>
<div className="ml-3 flex-1">
<p className="font-bold text-lg text-slate-900">
{store.name}
</p>
</div>
<div className="flex items-center justify-center rounded-xl border border-brand-100 bg-brand-50 px-2.5 py-1.5">
<p className="font-bold text-sm text-brand-700">
{store.productCount}
</p>
<p className="ml-1 text-[8px] font-black uppercase tracking-tighter text-brand-600">
Items
</p>
</div>
</div>
</div>
{/* Horizontal Scrollable Product Collection */}
{sampleProducts.length > 0 && (
<div className="mb-5">
<div className="flex gap-3 overflow-x-auto px-4 pb-2 scrollbar-hide">
{sampleProducts.map((product: any) => (
<div
key={product.id}
onClick={navigateToStore}
className="w-24 shrink-0 cursor-pointer items-center"
>
<div className="mb-2 h-24 w-24 rounded-2xl border border-slate-200 bg-slate-50 p-1 shadow-sm">
<img
src={product.signedImageUrl || product.images?.[0]}
alt={product.name}
className="h-full w-full rounded-xl object-cover"
/>
</div>
<p
className="font-bold text-center text-[10px] leading-tight text-slate-900"
>
{product.name}
</p>
</div>
))}
{remainingCount > 0 && (
<div className="flex shrink-0 flex-col items-center justify-center">
<div
onClick={navigateToStore}
className="flex h-24 w-24 cursor-pointer flex-col items-center justify-center rounded-2xl bg-slate-900 shadow-md"
>
<p className="font-bold text-base text-white">
+{remainingCount}
</p>
<p className="text-[8px] font-black uppercase tracking-widest text-white/60">
Discover
</p>
<ArrowRight className="mt-1 h-4 w-4 text-white" />
</div>
<div className="h-8" />
return (
<div className="flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-brand-300 hover:shadow-md">
{/* Header */}
<div onClick={navigateToStore} className="cursor-pointer p-5 pb-0">
<div className="mb-4 flex items-center">
<div className="h-12 w-12 rounded-xl border border-gray-100 bg-gray-50 p-0.5">
<div className="relative h-full w-full overflow-hidden rounded-lg">
{store.signedImageUrl || store.imageUrl ? (
<img
src={store.signedImageUrl || store.imageUrl}
alt={store.name}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<Store className="h-5 w-5 text-gray-400" />
</div>
)}
</div>
</div>
)}
{/* Explore Store Button */}
<div className="px-4 pb-4">
<div
onClick={navigateToStore}
className="flex flex-row items-center justify-center rounded-[18px] bg-brand-600 py-3 shadow-lg shadow-brand-200"
>
<p className="font-bold mr-2 text-sm uppercase tracking-wider text-white">
Explore Store
<div className="ml-3 flex-1">
<p className="text-lg font-bold text-gray-900">
{store.name}
</p>
</div>
<div className="flex items-center rounded-lg border border-brand-100 bg-brand-25 px-2.5 py-1.5">
<p className="text-sm font-bold text-brand-700">
{store.productCount}
</p>
<p className="ml-1 text-[8px] font-black uppercase tracking-tighter text-brand-600">
Items
</p>
<ArrowRight className="h-4 w-4 text-white" />
</div>
</div>
</div>
)
}
{/* Product preview strip */}
{sampleProducts.length > 0 && (
<div className="mb-5">
<div className="flex gap-3 overflow-x-auto px-5 pb-2 scrollbar-hide">
{sampleProducts.map((product: any) => (
<div
key={product.id}
onClick={navigateToStore}
className="w-20 shrink-0 cursor-pointer items-center"
>
<div className="mb-1.5 h-20 w-20 rounded-lg border border-gray-100 bg-gray-50 p-1">
<img
src={product.signedImageUrl || product.images?.[0]}
alt={product.name}
className="h-full w-full rounded-md object-cover"
/>
</div>
<p className="line-clamp-2 text-center text-[10px] font-bold leading-tight text-gray-800">
{product.name}
</p>
</div>
))}
{remainingCount > 0 && (
<button
onClick={navigateToStore}
className="flex shrink-0 flex-col items-center justify-center"
>
<div className="flex h-20 w-20 flex-col items-center justify-center rounded-lg bg-gray-900">
<p className="text-base font-bold text-white">
+{remainingCount}
</p>
<p className="text-[8px] font-black uppercase tracking-widest text-white/60">
Discover
</p>
</div>
</button>
)}
</div>
</div>
)}
{/* Explore Store Button */}
<div className="mt-auto px-5 pb-5">
<button
onClick={navigateToStore}
className="flex w-full flex-row items-center justify-center gap-2 rounded-lg bg-brand-600 py-3 text-sm font-bold uppercase tracking-wider text-white transition-colors hover:bg-brand-700"
>
Explore Store
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
)
}

View file

@ -13,21 +13,20 @@
--color-brand-700: #175CD3;
--color-brand-800: #1849A9;
--color-brand-900: #194185;
}
/* @theme {
--color-brand-25: #FFF5F6;
--color-brand-50: #FFE8EA;
--color-brand-100: #FFD1D6;
--color-brand-200: #FFA3AE;
--color-brand-300: #FF7585;
--color-brand-400: #FF475D;
--color-brand-500: #E63946;
--color-brand-600: #C5303C;
--color-brand-700: #9E2630;
--color-brand-800: #771D24;
--color-brand-900: #501318;
} */
/* Flash delivery accent — consistent with user-ui */
--color-flash-25: #FFF5F6;
--color-flash-50: #FFE8EA;
--color-flash-100: #FFD1D6;
--color-flash-200: #FFA3AE;
--color-flash-300: #FF7585;
--color-flash-400: #F81260;
--color-flash-500: #E11D48;
--color-flash-600: #C40E50;
--color-flash-700: #9E0A3F;
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
* {
box-sizing: border-box;
@ -41,6 +40,11 @@ body,
body {
margin: 0;
background-color: #f9fafb;
color: #111827;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* Safe area padding for mobile devices */
@ -66,8 +70,70 @@ body {
overflow: hidden;
}
/* Safe area padding for mobile devices */
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0px);
/* Focus rings — architecture, not decoration */
:focus-visible {
outline: 2px solid var(--color-brand-600);
outline-offset: 2px;
border-radius: 4px;
}
/* App shell — single column on mobile, sidebar + topbar grid at md+ */
.shell-grid {
min-height: 100vh;
display: grid;
grid-template-areas: "topbar" "content";
grid-template-columns: minmax(0, 1fr);
grid-template-rows: 64px minmax(0, 1fr);
}
@media (min-width: 768px) {
.shell-grid {
grid-template-areas: "sidebar topbar" "sidebar content";
grid-template-columns: 264px minmax(0, 1fr);
grid-template-rows: 64px minmax(0, 1fr);
}
}
.shell-sidebar {
grid-area: sidebar;
}
.shell-topbar {
grid-area: topbar;
}
.shell-content {
grid-area: content;
min-width: 0;
}
/* Typography scale — display */
.display-1 {
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.1;
letter-spacing: -0.02em;
font-weight: 800;
}
.display-2 {
font-size: clamp(1.5rem, 2.5vw, 2rem);
line-height: 1.2;
letter-spacing: -0.015em;
font-weight: 800;
}
/* Rule under section titles — flat, editorial */
.section-rule {
border-bottom: 1px solid #e5e7eb;
padding-bottom: 1rem;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View file

@ -23,7 +23,7 @@ export function pInput({
...props
}: pInputProps) {
const inputClasses = cn(
'flex w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'flex w-full rounded-md border border-input bg-background px-3 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm',
shrunkPadding ? 'py-1.5' : 'py-2',
error && 'border-destructive',
className

View file

@ -78,7 +78,7 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
value={value}
onChange={handleChange}
placeholder={placeholder}
className="min-w-0 flex-1 bg-transparent text-sm text-gray-800 placeholder:text-gray-400 !outline-none focus:!outline-none focus-visible:!outline-none"
className="min-w-0 flex-1 bg-transparent text-base text-gray-800 placeholder:text-gray-400 !outline-none focus:!outline-none focus-visible:!outline-none sm:text-sm"
/>
{value && (
<button