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 && 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(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(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": [], "deny": [],
"defaultMode": "default" "defaultMode": "default"

View file

@ -1,5 +1,6 @@
# Taste # 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 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 - 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 - 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 - 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 - 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.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 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 - 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 - 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 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 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 - 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> <MyText style={tw`text-gray-700 font-medium`}>Offer SKU</MyText>
</View> </View>
<View style={tw`flex-row items-center mb-3`}> {!(values.productType === 'combo' && (variant.comboItems?.length || 0) > 0) && (
<Checkbox <View style={tw`flex-row items-center mb-3`}>
checked={variant.isComboOnly} <Checkbox
onPress={() => setFieldValue(`variants.${vIndex}.isComboOnly`, !variant.isComboOnly)} checked={variant.isComboOnly}
style={tw`mr-3`} onPress={() => setFieldValue(`variants.${vIndex}.isComboOnly`, !variant.isComboOnly)}
/> style={tw`mr-3`}
<MyText style={tw`text-gray-700 font-medium`}>Combo Only SKU</MyText> />
</View> <MyText style={tw`text-gray-700 font-medium`}>Combo Only SKU</MyText>
</View>
)}
{mode === 'edit' && ( {mode === 'edit' && (
<View style={tw`flex-row items-center mb-3`}> <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 // Respect the admin-curated order — tag.productIds is already ordered by
// the tag's sortOrder (backend reorders it when building the cache). // 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)) .map((id: number) => productById.get(id))
.filter(Boolean) as any[]; .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; return map;
}, [dashboardTags, products]); }, [dashboardTags, products, productSlotsMap, getQuickestSlot]);
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setIsRefreshing(true); setIsRefreshing(true);

View file

@ -36,7 +36,7 @@ const OrderMenu: React.FC<OrderMenuProps> = ({ orderId, postActionHandler }) =>
const handleEditNotes = async () => { const handleEditNotes = async () => {
try { try {
await updateNotesMutation.mutateAsync({ await updateNotesMutation.mutateAsync({
id: orderId.toString(), id: orderId,
userNotes: editNotes.trim() userNotes: editNotes.trim()
}); });
} catch (error) { } catch (error) {
@ -106,8 +106,11 @@ const OrderMenu: React.FC<OrderMenuProps> = ({ orderId, postActionHandler }) =>
<MyTouchableOpacity <MyTouchableOpacity
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl shadow-sm`} style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl shadow-sm`}
onPress={() => { onPress={() => {
setEditNotes(''); setOpen(false);
setEditNotesDialogOpen(true); setTimeout(() => {
setEditNotes('');
setEditNotesDialogOpen(true);
}, 300);
}} }}
> >
<View style={tw`w-10 h-10 rounded-full bg-blue-50 items-center justify-center mr-4`}> <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; selectedCouponId: number | null;
cartItems: any[]; cartItems: any[];
totalPrice: number; totalPrice: number;
totalSavings?: number;
discountAmount: number; discountAmount: number;
finalTotal: number; finalTotal: number;
finalTotalWithDelivery: number; finalTotalWithDelivery: number;
@ -32,6 +33,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
selectedCouponId, selectedCouponId,
cartItems, cartItems,
totalPrice, totalPrice,
totalSavings = 0,
discountAmount, discountAmount,
finalTotal, finalTotal,
finalTotalWithDelivery, finalTotalWithDelivery,
@ -294,14 +296,22 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
{/* Item Total */} {/* Item Total */}
<View style={tw`flex-row justify-between items-center mb-2`}> <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-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> </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 */} {/* Discount */}
{discountAmount > 0 && ( {discountAmount > 0 && (
<View style={tw`flex-row justify-between items-center mb-2`}> <View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`text-gray-500`}>Product Discount</MyText> <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> </View>
)} )}
@ -356,15 +366,15 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
</View> </View>
{/* Savings Banner */} {/* 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`}> <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`} /> <MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
<MyText style={tw`text-green-700 text-xs font-bold`}> <MyText style={tw`text-green-700 text-xs font-bold`}>
You saved {discountAmount + (deliveryCharge === 0 ? ( You saved {Math.trunc(discountAmount + totalSavings + (deliveryCharge === 0 ? (
isFlashDelivery isFlashDelivery
? constsData?.flashDeliveryCharge ? constsData?.flashDeliveryCharge
: constsData?.deliveryCharge : constsData?.deliveryCharge
) : 0)} on this order ) : 0))} on this order
</MyText> </MyText>
</View> </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); const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
return sum + Number(price) * quantity; return sum + Number(price) * quantity;
}, 0); }, 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( const dropdownData = useMemo(
() => () =>
eligibleCoupons?.map((coupon) => { eligibleCoupons?.map((coupon) => {
@ -829,14 +841,22 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{/* Item Total */} {/* Item Total */}
<View style={tw`flex-row justify-between items-center mb-3`}> <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-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> </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 */} {/* Discount */}
{discountAmount > 0 && ( {discountAmount > 0 && (
<View style={tw`flex-row justify-between items-center mb-3`}> <View style={tw`flex-row justify-between items-center mb-3`}>
<MyText style={tw`text-gray-500`}>Product Discount</MyText> <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> </View>
)} )}
@ -884,15 +904,15 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{/* Grand Total */} {/* Grand Total */}
<View style={tw`flex-row justify-between items-center mt-2`}> <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-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> </View>
{/* Savings Banner */} {/* 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`}> <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`} /> <MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
<MyText style={tw`text-green-700 text-xs font-bold`}> <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> </MyText>
</View> </View>
)} )}

View file

@ -142,6 +142,20 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
0 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( const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery(
undefined, undefined,
{ enabled: isAuthenticated } { enabled: isAuthenticated }
@ -257,6 +271,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
selectedCouponId={selectedCouponId} selectedCouponId={selectedCouponId}
cartItems={selectedItems} cartItems={selectedItems}
totalPrice={totalPrice} totalPrice={totalPrice}
totalSavings={totalSavings}
discountAmount={discountAmount} discountAmount={discountAmount}
finalTotal={finalTotal} finalTotal={finalTotal}
finalTotalWithDelivery={finalTotalWithDelivery} finalTotalWithDelivery={finalTotalWithDelivery}

View file

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

View file

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import { useLocation, useNavigate } from '@tanstack/react-router' import { useLocation, useNavigate } from '@tanstack/react-router'
import { p, div } from 'web-components'
import { Home, Store, Zap, Tag, User } from 'lucide-react' import { Home, Store, Zap, Tag, User } from 'lucide-react'
interface TabItem { interface TabItem {
@ -9,7 +8,6 @@ interface TabItem {
label: string label: string
icon: React.ReactNode icon: React.ReactNode
iconActive: React.ReactNode iconActive: React.ReactNode
isCenter?: boolean
} }
export function BottomNavigation() { export function BottomNavigation() {
@ -35,10 +33,9 @@ export function BottomNavigation() {
{ {
name: 'flash', name: 'flash',
path: '/flash', path: '/flash',
label: '1 Hr Delivery', label: '1 Hr',
icon: <Zap className="h-6 w-6" />, icon: <Zap className="h-5 w-5" />,
iconActive: <Zap className="h-6 w-6 fill-current" />, iconActive: <Zap className="h-5 w-5 fill-current" />,
isCenter: true,
}, },
{ {
name: 'offers', name: 'offers',
@ -60,56 +57,34 @@ export function BottomNavigation() {
if (path === '/home') return currentPath === '/home' || currentPath.startsWith('/home/') if (path === '/home') return currentPath === '/home' || currentPath.startsWith('/home/')
if (path === '/stores') return currentPath === '/stores' || currentPath.startsWith('/stores/') if (path === '/stores') return currentPath === '/stores' || currentPath.startsWith('/stores/')
if (path === '/flash') return currentPath === '/flash' || currentPath.startsWith('/flash/') 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/')
if (path === '/me') return currentPath === '/me' || (currentPath.startsWith('/me/') && !currentPath.startsWith('/me/orders/'))
return currentPath === path || currentPath.startsWith(`${path}/`) return currentPath === path || currentPath.startsWith(`${path}/`)
} }
return ( return (
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white pb-safe"> <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-center justify-around px-2"> <div className="flex h-16 items-stretch">
{tabs.map((tab) => { {tabs.map((tab) => {
const active = isActive(tab.path) 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 ( return (
<div <button
key={tab.name} key={tab.name}
onClick={() => navigate({ to: tab.path })} onClick={() => navigate({ to: tab.path as any })}
className="flex flex-1 flex-col items-center justify-center py-2" 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} {active ? tab.iconActive : tab.icon}
</span> </span>
<p <span
className={`mt-1 text-xs font-medium ${active ? 'text-brand-600' : 'text-gray-500'}`} className={`text-[11px] font-semibold ${
active ? 'text-brand-700' : 'text-gray-500'
}`}
> >
{tab.label} {tab.label}
</p> </span>
</div> </button>
) )
})} })}
</div> </div>

View file

@ -1,11 +1,10 @@
import React, { useState, useEffect, useMemo } from 'react' import React, { useState, useEffect } from 'react'
import { useNavigate, useLocation } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart, useAddToCart } from '../hooks/cart-query-hooks' import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks' import { useAllProducts } from '../hooks/prominent-api-hooks'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks' import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
import { p, div, MiniQuantifier } from 'web-components' import { MiniQuantifier } from 'web-components'
import { Dialog } from './Dialog' import { ShoppingCart, ChevronRight, Clock, X, Package } from 'lucide-react'
import { ShoppingCart, ChevronRight, Package, X, Clock, MapPin, Home, Store, Zap, RotateCcw, User } from 'lucide-react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
interface FloatingCartBarProps { interface FloatingCartBarProps {
@ -35,12 +34,11 @@ export function FloatingCartBar({ isFlashDelivery = false }: FloatingCartBarProp
const [quantities, setQuantities] = useState<Record<number, number>>({}) const [quantities, setQuantities] = useState<Record<number, number>>({})
const cartType = isFlashDelivery ? 'flash' : 'regular' const cartType = isFlashDelivery ? 'flash' : 'regular'
const { data: cartData, refetch: refetchCart } = useGetCart(cartType) const { data: cartData } = useGetCart(cartType)
const { data: constsData } = useGetEssentialConsts() const { data: constsData } = useGetEssentialConsts()
const { data: productsData } = useAllProducts() const { data: productsData } = useAllProducts()
const updateCartItem = useUpdateCartItem(cartType) const updateCartItem = useUpdateCartItem(cartType)
const removeFromCart = useRemoveFromCart(cartType) const removeFromCart = useRemoveFromCart(cartType)
const addToCartHook = useAddToCart(cartType)
const products = productsData?.products || [] const products = productsData?.products || []
const productsById: Record<number, any> = {} const productsById: Record<number, any> = {}
@ -76,220 +74,201 @@ export function FloatingCartBar({ isFlashDelivery = false }: FloatingCartBarProp
const cartBarColor = isFlashDelivery ? '#f81260' : 'var(--brand-600, #2563eb)' const cartBarColor = isFlashDelivery ? '#f81260' : 'var(--brand-600, #2563eb)'
const cartBarBorderColor = isFlashDelivery ? '#e11d48' : 'var(--brand-500, #3b82f6)' const cartBarBorderColor = isFlashDelivery ? '#e11d48' : 'var(--brand-500, #3b82f6)'
return ( const goToCart = () => {
<> setIsExpanded(false)
{/* Collapsed Bar */} navigate({ to: isFlashDelivery ? '/flash/cart' : '/cart' })
<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>
{remainingForFreeDelivery > 0 ? ( /* ---------- Desktop: slide-over panel ---------- */
<p className="mt-1 text-[10px] font-bold text-white/80"> const slideOver = (
{remainingForFreeDelivery} more for <span className="text-emerald-300">FREE Delivery</span> <div className="pointer-events-auto flex h-full w-full max-w-md flex-col border-l border-gray-200 bg-white">
</p> {/* Header */}
) : itemCount > 0 ? ( <div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
<div className="mt-0.5 flex flex-row items-center"> <div>
<span className="text-emerald-400"></span> <p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
<p className="ml-1 text-[10px] font-black uppercase tracking-tighter text-emerald-300"> Your Cart
Free Delivery Unlocked </p>
</p> <p className="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-400">
</div> {itemCount} {itemCount === 1 ? 'Item' : 'Items'}
) : ( </p>
<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>
</div> </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> </div>
{/* Expanded Dialog */} {/* Progress */}
<Dialog open={isExpanded} onClose={() => setIsExpanded(false)} title=""> {remainingForFreeDelivery > 0 && (
<div className="flex max-h-[80vh] flex-col"> <div className="border-b border-gray-100 bg-brand-25 px-6 py-3">
{/* Header */} <div className="mb-1.5 flex items-center justify-between">
<div className="flex flex-row items-center justify-between border-b border-slate-100 px-6 py-5"> <p className="text-[10px] font-black uppercase tracking-widest text-brand-700">
<div> Free Delivery Progress
<p className="font-bold text-xl tracking-tight text-slate-900"> </p>
Your Cart <p className="text-[10px] font-black text-brand-700">
</p> {Math.round((totalCartValue / freeDeliveryThreshold) * 100)}%
<p className="text-xs font-bold uppercase tracking-widest text-slate-400"> </p>
{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>
</div> </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 */} {/* Items */}
{remainingForFreeDelivery > 0 && ( <div className="flex-1 overflow-y-auto px-6 py-2">
<div className="flex flex-row items-center justify-between bg-emerald-50/50 px-6 py-3"> {cartItems.length === 0 ? (
<div className="mr-4 flex-1"> <div className="flex h-full flex-col items-center justify-center py-16 text-center">
<div className="mb-1.5 flex flex-row items-center justify-between"> <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
<p className="text-[10px] font-black uppercase text-emerald-700">Free Delivery Progress</p> <Package className="h-7 w-7 text-gray-400" />
<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>
</div> </div>
)} <p className="mt-4 font-bold text-gray-800">
{isFlashDelivery ? 'No flash items yet' : 'Your cart is empty'}
{/* Items List */} </p>
<div className="flex-1 overflow-y-auto px-6 py-4"> <p className="mt-1 text-sm text-gray-500">
{cartItems.map((item, index) => ( {isFlashDelivery
<React.Fragment key={item.id}> ? 'Pick something from 1 hr delivery'
<div className="py-4"> : 'Add fresh products to get started'}
<div className="flex flex-row items-center"> </p>
<img </div>
src={productsById[item.productId]?.images?.[0]} ) : (
alt="" cartItems.map((item, index) => (
className="h-8 w-8 rounded-lg border border-slate-100 bg-slate-50 object-cover" <React.Fragment key={item.id}>
/> <div className="flex items-center gap-4 py-4">
<img
<div className="ml-4 flex-1"> src={productsById[item.productId]?.images?.[0]}
<div className="mb-1 flex flex-row items-center justify-between"> alt=""
<p className="font-bold flex-1 text-sm text-slate-900"> className="h-14 w-14 shrink-0 rounded-lg border border-gray-100 bg-gray-50 object-cover"
{productsById[item.productId]?.name || ''}{' '} />
<span className="text-xs font-medium text-slate-500"> <div className="min-w-0 flex-1">
({productsById[item.productId]?.productQuantity || 0} <p className="truncate text-sm font-bold text-gray-900">
{productsById[item.productId]?.unitNotation || ''}) {productsById[item.productId]?.name || ''}
</span> </p>
</p> <p className="text-xs font-medium text-gray-500">
<MiniQuantifier {productsById[item.productId]?.productQuantity || 0}
value={quantities[item.id] || item.quantity} {productsById[item.productId]?.unitNotation || ''}
setValue={(value) => { </p>
if (value === 0) { {item.slotId && (
removeFromCart.mutate(item.id) <div className="mt-1 flex items-center gap-1 rounded border border-brand-100 bg-brand-25 px-1.5 py-0.5">
} else { <Clock className="h-3 w-3 text-brand-600" />
setQuantities((prev) => ({ ...prev, [item.id]: value })) <p className="text-[10px] font-bold text-brand-700">
updateCartItem.mutate({ productId: item.id, quantity: value }) {formatTimeRange(item.deliveryDate || new Date())}
} </p>
}}
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>
</div> </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>
</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> </div>
{remainingForFreeDelivery === 0 && ( {index < cartItems.length - 1 && <div className="h-px w-full bg-gray-100" />}
<div className="flex flex-row items-center rounded-xl border border-emerald-100 bg-emerald-50 px-3 py-1.5"> </React.Fragment>
<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>
)}
</div>
<div {/* Footer */}
onClick={() => { <div className="border-t border-gray-100 bg-white p-6">
setIsExpanded(false) <div className="mb-4 flex items-center justify-between">
navigate({ <div>
to: isFlashDelivery ? '/flash/cart' : '/cart', <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>
}}
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>
</div> </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> </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 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 { useGetCart, useUpdateCartItem, useRemoveFromCart, useAddToCart } from '../hooks/cart-query-hooks'
import { useCartStore } from '../lib/stores/cart-store' import { useCartStore } from '../lib/stores/cart-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-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 ( return (
<div <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} onClick={onPress}
> >
{/* Image Container */} {/* Image */}
<div className="relative aspect-square w-full overflow-hidden bg-gray-100"> <div className="relative aspect-[4/3] w-full overflow-hidden bg-gray-50">
{imageUri && !imageError ? ( {imageUri && !imageError ? (
<img <img
src={imageUri} src={imageUri}
alt={item.name} 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)} onError={() => setImageError(true)}
onLoad={() => setImageLoading(false)} onLoad={() => setImageLoading(false)}
/> />
) : ( ) : (
<div className="flex h-full w-full items-center justify-center bg-gray-100"> <div className="flex h-full w-full items-center justify-center bg-gray-50">
<ImageOff className="h-8 w-8 text-gray-400" /> <ImageOff className="h-8 w-8 text-gray-300" />
</div> </div>
)} )}
{imageLoading && imageUri && !imageError && ( {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 className="h-6 w-6 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
</div> </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 && ( {displayIsOutOfStock && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900/50">
<div className="rounded-full bg-red-500 px-3 py-1"> <div className="rounded-full bg-gray-900 px-3 py-1">
<p className="text-xs font-bold text-white">Out of Stock</p> <p className="text-xs font-bold text-white">Out of Stock</p>
</div> </div>
</div> </div>
@ -139,13 +152,13 @@ export function ProductCard({
/> />
) : ( ) : (
<div <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) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleQuantityChange(1) handleQuantityChange(1)
}} }}
> >
<ShoppingCart className="h-4 w-4 text-brand-500" /> <ShoppingCart className="h-4 w-4 text-brand-600" />
</div> </div>
)} )}
</div> </div>
@ -153,33 +166,24 @@ export function ProductCard({
</div> </div>
{/* Content */} {/* Content */}
<div className="px-3 pt-3"> <div className="flex flex-1 flex-col px-4 pb-4 pt-3">
<p className="font-bold mb-1 text-sm text-gray-900"> <p className="mb-1.5 text-sm font-bold leading-snug text-gray-900 line-clamp-2">
{item.name} {item.name}
</p> </p>
<div className="mb-2 flex flex-row items-baseline"> <div className="mb-1 flex items-baseline">
<p className="font-bold text-base text-brand-500"> <p className="text-lg font-extrabold text-brand-600">{item.price}</p>
{item.price} {discountPercent > 0 && (
</p> <p className="ml-2 text-xs text-gray-400 line-through">{item.marketPrice}</p>
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<p className="ml-2 text-xs text-gray-400 line-through">
{item.marketPrice}
</p>
)} )}
</div> </div>
<div className="mb-2 flex flex-row items-center"> <p className="mb-3 text-xs font-medium text-gray-500">
<p className="text-xs font-medium text-gray-500"> {formatQuantity(item.productQuantity || 1, item.unitNotation).display}
Quantity:{' '} </p>
<span className="font-semibold text-brand-500">
{formatQuantity(item.productQuantity || 1, item.unitNotation).display}
</span>
</p>
</div>
{showDeliveryInfo && displayDeliveryDate && ( {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" /> <Truck className="h-3 w-3 text-brand-600" />
<p className="ml-1.5 text-[10px] font-bold text-brand-700"> <p className="ml-1.5 text-[10px] font-bold text-brand-700">
{dayjs(displayDeliveryDate).format('ddd, DD MMM • h:mm A')} {dayjs(displayDeliveryDate).format('ddd, DD MMM • h:mm A')}
@ -188,9 +192,9 @@ export function ProductCard({
)} )}
{!miniView && ( {!miniView && (
<> <div className="mt-auto">
{displayIsOutOfStock ? ( {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"> <p className="text-xs font-bold uppercase tracking-wide text-gray-400">
Unavailable Unavailable
</p> </p>
@ -204,19 +208,17 @@ export function ProductCard({
/> />
) : ( ) : (
<button <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) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleQuantityChange(1) handleQuantityChange(1)
}} }}
> >
<ShoppingCart className="h-4 w-4 text-white" /> <ShoppingCart className="h-4 w-4" />
<span className="text-xs font-bold uppercase tracking-wide text-white"> Add to Cart
Add to Cart
</span>
</button> </button>
)} )}
</> </div>
)} )}
</div> </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' import type { AppRouter } from '@backend/trpc/router'
// const BASE_API_URL = 'http://192.168.100.111:8787' // 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>() export const trpc = createTRPCReact<AppRouter>()

View file

@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks' import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks' import { useAllProducts } from '../hooks/prominent-api-hooks'
import { p, MyButton, Quantifier, AppContainer, div } from 'web-components' import { Quantifier } from 'web-components'
import { Trash2 } from 'lucide-react' import { AppLayout } from '../components/AppLayout'
import { Trash2, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/cart')({ component: CartPage }) export const Route = createFileRoute('/cart')({ component: CartPage })
@ -28,71 +29,109 @@ function CartPage() {
}) })
return ( return (
<AppContainer> <AppLayout>
<p className="font-bold mb-4 text-xl"> <div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
Your Cart <div className="mb-6">
</p> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Review Before Delivery
{cartItems.length === 0 ? ( </p>
<div className="flex flex-col items-center gap-4 py-20"> <h1 className="display-2 mt-1 text-gray-900">Your Cart</h1>
<p className="text-gray-500">Your cart is empty</p>
<MyButton
textContent="Browse Products"
onClick={() => navigate({ to: '/home' })}
/>
</div> </div>
) : (
<> {cartItems.length === 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
{cartItems.map((item) => { <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
const product = productsById[item.productId] <ShoppingCart className="h-7 w-7 text-gray-400" />
const price = product.price </div>
return ( <p className="font-bold text-gray-800">Your cart is empty</p>
<div <p className="text-sm text-gray-500">Fresh cuts are waiting at the counter</p>
key={item.productId} <button
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm" 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"
<img >
src={product.images?.[0]} Browse Products
alt={product.name} </button>
className="h-16 w-16 rounded-lg object-cover" </div>
/> ) : (
<div className="flex-1"> <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<p className="font-semibold text-sm"> {/* Items */}
{product.name} <div className="flex flex-col gap-3">
</p> {cartItems.map((item) => {
<p className="text-brand-600 text-sm font-bold"> const product = productsById[item.productId]
{price} const price = product.price
</p> return (
<Quantifier <div
value={item.quantity} key={item.productId}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })} 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>
<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>
) <div className="flex items-center justify-between py-4">
})} <p className="font-bold text-gray-900">Total</p>
</div> <p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg"> <button
<div className="mb-3 flex items-center justify-between"> onClick={() => navigate({ to: '/checkout' })}
<p className="font-bold">Total</p> className="w-full rounded-lg bg-brand-600 py-3.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
<p className="font-bold text-lg text-brand-600"> >
{total} Proceed to Checkout
</p> </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> </div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/checkout' })}
className="bg-brand-500 text-white"
/>
</div> </div>
</> )}
)} </div>
</AppContainer> </AppLayout>
) )
} }

View file

@ -10,8 +10,7 @@ import { AddressForm } from '../components/AddressForm'
import { Dialog } from '../components/Dialog' import { Dialog } from '../components/Dialog'
import { useAddressStore } from '../lib/stores/address-store' import { useAddressStore } from '../lib/stores/address-store'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { p, div } from 'web-components' import { ShoppingCart, ChevronLeft, MapPin } from 'lucide-react'
import { MapPin, ShoppingCart, ChevronLeft } from 'lucide-react'
export const Route = createFileRoute('/checkout')({ component: CheckoutPage }) export const Route = createFileRoute('/checkout')({ component: CheckoutPage })
@ -45,66 +44,71 @@ function CheckoutContent() {
if (cartItems.length === 0) { if (cartItems.length === 0) {
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6"> <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" /> <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-white">
<p className="font-bold mb-2 text-center text-xl text-gray-900"> <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 Your cart is empty
</p> </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 Add some delicious items to your cart before checking out
</p> </p>
<div <button
onClick={() => navigate({ to: '/home' })} 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
Back to Shopping </button>
</p>
</div>
</div> </div>
) )
} }
return ( return (
<div className="min-h-screen bg-gray-50 pb-8"> <div className="min-h-screen bg-gray-50">
{/* Checkout Header */} {/* Checkout Header */}
<div className="sticky top-0 z-10 mb-4 border-b border-gray-100 bg-white px-4 py-3"> <div className="sticky top-0 z-20 border-b border-gray-200 bg-white">
<div className="flex items-center"> <div className="mx-auto flex h-16 w-full max-w-7xl items-center gap-3 px-4 md:px-8">
<div <button
onClick={() => navigate({ to: '/cart' })} 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>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-50"> <p className="font-display text-lg font-extrabold text-gray-900">
<MapPin className="h-5 w-5 text-blue-500" />
</div>
<p className="font-bold ml-3 text-lg text-gray-800">
Checkout Checkout
</p> </p>
</div> </div>
</div> </div>
<div className="px-4"> <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]">
{/* Address Selection */} {/* Left: Address Selection + Payment */}
<CheckoutAddressSelector <div>
onAddressSelect={setSelectedAddressId} <CheckoutAddressSelector
onAddAddress={() => { onAddressSelect={setSelectedAddressId}
setEditingAddress(null) onAddAddress={() => {
setShowAddAddress(true) setEditingAddress(null)
}} setShowAddAddress(true)
onEditAddress={(address) => { }}
setEditingAddress(address) onEditAddress={(address) => {
setShowAddAddress(true) setEditingAddress(address)
}} setShowAddAddress(true)
/> }}
/>
</div>
{/* Payment and Order Summary */} {/* Right: Order summary + payment */}
<PaymentAndOrderComponent <div className="lg:sticky lg:top-24 lg:self-start">
selectedAddress={selectedAddressId} <PaymentAndOrderComponent
cartItems={cartItems} selectedAddress={selectedAddressId}
isFlashDelivery={false} cartItems={cartItems}
onBack={() => navigate({ to: '/cart' })} isFlashDelivery={false}
/> onBack={() => navigate({ to: '/cart' })}
/>
</div>
</div> </div>
{/* Add/Edit Address Dialog */} {/* Add/Edit Address Dialog */}

View file

@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks' import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useCentralProductStore } from '../lib/stores/central-product-store' import { useCentralProductStore } from '../lib/stores/central-product-store'
import { p, MyButton, Quantifier, AppContainer, div } from 'web-components' import { Quantifier } from 'web-components'
import { Trash2, Zap } from 'lucide-react' import { AppLayout } from '../components/AppLayout'
import { Trash2, Zap, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/flash/cart')({ component: FlashCartPage }) export const Route = createFileRoute('/flash/cart')({ component: FlashCartPage })
@ -24,74 +25,112 @@ function FlashCartPage() {
}) })
return ( return (
<AppContainer> <AppLayout isFlashDelivery={true}>
<div className="mb-4 flex items-center gap-2"> <div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
<Zap className="h-5 w-5 text-yellow-500" /> <div className="mb-6">
<p className="font-bold text-xl"> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-flash-500">
Flash Cart 1 Hr Delivery
</p> </p>
</div> <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" />
{cartItems.length === 0 ? ( Flash Cart
<div className="flex flex-col items-center gap-4 py-20"> </h1>
<p className="text-gray-500">Your flash cart is empty</p>
<MyButton
textContent="Browse Flash Products"
onClick={() => navigate({ to: '/flash' })}
/>
</div> </div>
) : (
<> {cartItems.length === 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
{cartItems.map((item) => { <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-flash-25">
const product = productsById[item.productId] <ShoppingCart className="h-7 w-7 text-flash-500" />
const price = product.discountedPrice ?? product.price </div>
return ( <p className="font-bold text-gray-800">Your flash cart is empty</p>
<div <p className="text-sm text-gray-500">Pick something from 1 hr delivery</p>
key={item.productId} <button
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm" 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"
<img >
src={product.images?.[0]} Browse Flash Products
alt={product.name} </button>
className="h-16 w-16 rounded-lg object-cover" </div>
/> ) : (
<div className="flex-1"> <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<p className="font-semibold text-sm"> {/* Items */}
{product.name} <div className="flex flex-col gap-3">
</p> {cartItems.map((item) => {
<p className="text-brand-600 text-sm font-bold">{price}</p> const product = productsById[item.productId]
<Quantifier const price = product.discountedPrice ?? product.price
value={item.quantity} return (
setValue={(q) => <div
updateItem.mutate({ productId: item.productId, quantity: q }) 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>
<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>
) <div className="flex items-center justify-between py-4">
})} <p className="font-bold text-gray-900">Total</p>
</div> <p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg"> <button
<div className="mb-3 flex items-center justify-between"> onClick={() => navigate({ to: '/flash/checkout' })}
<p className="font-bold">Total</p> className="w-full rounded-lg bg-flash-500 py-3.5 text-sm font-bold text-white transition-colors hover:bg-flash-600"
<p className="font-bold text-lg text-brand-600"> >
{total} Proceed to Checkout
</p> </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> </div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/flash/checkout' })}
className="bg-brand-500 text-white"
/>
</div> </div>
</> )}
)} </div>
</AppContainer> </AppLayout>
) )
} }

View file

@ -1,5 +1,4 @@
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { p, MyButton } from 'web-components'
import { Zap } from 'lucide-react' import { Zap } from 'lucide-react'
export const Route = createFileRoute('/flash/order-success')({ export const Route = createFileRoute('/flash/order-success')({
@ -15,25 +14,27 @@ function FlashOrderSuccessPage() {
const { orderId, totalAmount } = Route.useSearch() const { orderId, totalAmount } = Route.useSearch()
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-yellow-50 p-6"> <div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-6">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-yellow-100"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-flash-25">
<Zap className="h-10 w-10 text-yellow-600" /> <Zap className="h-10 w-10 text-flash-500" fill="currentColor" />
</div> </div>
<p className="font-bold mb-2 text-2xl text-gray-900"> <p className="mb-2 mt-6 text-2xl font-extrabold text-gray-900">
1 Hr Order Placed! 1 Hr Order Placed
</p> </p>
<p className="mb-1 text-gray-600">Order ID: #{orderId}</p> <p className="mb-1 text-gray-600">Order ID: #{orderId}</p>
<p className="mb-8 text-gray-600">Total: {totalAmount}</p> <p className="mb-8 text-gray-600">Total: {totalAmount}</p>
<MyButton <button
textContent="Continue Shopping"
onClick={() => navigate({ to: '/flash' })} onClick={() => navigate({ to: '/flash' })}
className="mb-3 bg-brand-500 text-white" className="mb-3 rounded-lg bg-flash-500 px-8 py-3 text-sm font-bold text-white transition-colors hover:bg-flash-600"
/> >
<MyButton Continue Shopping
textContent="View My Orders" </button>
<button
onClick={() => navigate({ to: '/me/orders' })} 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> </div>
) )
} }

View file

@ -1,12 +1,12 @@
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { useState, useMemo } from 'react' 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 { useAllProducts, useStores } from '../hooks/prominent-api-hooks'
import { useCentralProductStore } from '../lib/stores/central-product-store' import { useCentralProductStore } from '../lib/stores/central-product-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store' import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks' import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { AppLayout } from '../components/AppLayout' 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')({ export const Route = createFileRoute('/flash')({
component: FlashDeliveryPage, component: FlashDeliveryPage,
@ -56,56 +56,46 @@ function FlashDeliveryPage() {
return ( return (
<AppLayout isFlashDelivery={true}> <AppLayout isFlashDelivery={true}>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Header - Flash Delivery Style */} {/* Page header */}
<div className="sticky top-0 z-20 border-b border-gray-200 bg-white px-4 py-3"> <div className="border-b border-gray-200 bg-white">
<div className="flex items-center gap-2"> <div className="mx-auto flex w-full max-w-7xl items-center gap-2 px-4 py-5 md:px-8">
{/* Back Button */} <Zap className="h-6 w-6 text-flash-500" fill="currentColor" />
<button <div>
onClick={() => navigate({ to: '/home' })} <p className="text-[11px] font-black uppercase tracking-[0.22em] text-flash-500">
className="p-1 hover:bg-gray-100 rounded-full" Express Counter
>
<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
</p> </p>
<h1 className="display-2 text-gray-900">Delivery within 1 hour</h1>
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-row"> <div className="mx-auto flex w-full max-w-7xl gap-6 px-4 py-6 md:px-8">
{/* StoreSidebar - Fixed width on left for both mobile and desktop */} {/* Store sidebar — desktop: vertical list; mobile: horizontal strip */}
<div className="w-20 shrink-0 md:w-24"> <StoreSidebar
<StoreSidebar stores={stores}
stores={stores} storeId={storeId}
storeId={storeId} onStoreSelect={(newStoreId) =>
onStoreSelect={(newStoreId) => navigate({
navigate({ to: '/flash',
to: '/flash', search: { storeId: newStoreId },
search: { storeId: newStoreId }, })
}) }
} onAllSelect={() =>
onAllSelect={() => navigate({ to: '/flash' })
navigate({ to: '/flash' }) }
} />
/>
</div>
{/* Products Grid */} {/* Products Grid */}
<div className="flex-1 p-4"> <div className="min-w-0 flex-1">
{/* Info Banner */} {/* 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"> <p className="text-sm text-yellow-800">
Get these products delivered within 1 hour! Only available for select items. Get these products delivered within 1 hour! Only available for select items.
</p> </p>
</div> </div>
<div className="mb-4 flex items-center justify-between"> <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 {storeId
? stores.find((s: any) => s.id === storeId)?.name || ? stores.find((s: any) => s.id === storeId)?.name ||
'Store Products' 'Store Products'
@ -116,7 +106,7 @@ function FlashDeliveryPage() {
</p> </p>
</div> </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) => ( {filteredProducts.map((product: any) => (
<CompactProductCard <CompactProductCard
key={product.id} key={product.id}
@ -161,83 +151,95 @@ function StoreSidebar({
onStoreSelect, onStoreSelect,
onAllSelect, onAllSelect,
}: StoreSidebarProps) { }: StoreSidebarProps) {
const allActive = !storeId
return ( 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"> {/* Mobile: horizontal chip strip (always visible) */}
{/* All Products Item */} <div className="scrollbar-hide -mx-4 mb-4 flex gap-2 overflow-x-auto px-4 md:hidden">
<div <button
onClick={onAllSelect} onClick={onAllSelect}
className={`flex flex-col items-center rounded-2xl p-2 md:p-3 ${ className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
!storeId allActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
? 'bg-gradient-to-br from-[#f81260] to-[#d10f4f] text-white shadow-lg'
: 'border border-gray-100 bg-white text-gray-500'
}`} }`}
> >
<div All
className={`mb-1 flex h-8 w-8 items-center justify-center rounded-full border md:h-10 md:w-10 ${ </button>
!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 */}
{stores.map((store: any) => { {stores.map((store: any) => {
const isActive = storeId === store.id; const isActive = storeId === store.id
return ( return (
<div <button
key={store.id} key={store.id}
onClick={() => onStoreSelect(store.id)} onClick={() => onStoreSelect(store.id)}
className={`flex flex-col items-center rounded-2xl p-2 ${ className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
isActive isActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
? 'bg-gradient-to-br from-[#f81260] to-[#d10f4f] text-white shadow-lg'
: 'border border-gray-100 bg-white text-gray-500'
}`} }`}
> >
<div {store.name.replace(/^The\s+/i, '')}
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 ${ </button>
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>
);
})} })}
</div> </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 ( return (
<div <div
onClick={onPress} 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"> <div className="relative">
<img <img
src={item.images?.[0]} src={item.images?.[0]}
alt={item.name} 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 && ( {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> <p className="text-xs font-bold text-white">Out of Stock</p>
</div> </div>
)} )}
@ -313,36 +315,35 @@ function CompactProductCard({
/> />
) : ( ) : (
<div <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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleQuantityChange(1); handleQuantityChange(1);
}} }}
> >
<ShoppingCart className="h-4 w-4 text-[#f81260]" /> <ShoppingCart className="h-4 w-4 text-flash-500" />
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="p-2"> <div className="p-3">
<p className="font-medium mb-1 text-xs text-gray-900">{item.name}</p> <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 items-center justify-between">
<div className="flex flex-wrap items-baseline"> <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) && ( {item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<p className="ml-1 text-xs text-gray-400 line-through"> <p className="ml-1 text-xs text-gray-400 line-through">
{item.marketPrice} {item.marketPrice}
</p> </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> </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> </div>
</div> </div>

View file

@ -1,8 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks' import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { useAllProducts } from '../hooks/prominent-api-hooks' import { useAllProducts } from '../hooks/prominent-api-hooks'
import { p, MyButton, Quantifier, AppContainer, div } from 'web-components' import { Quantifier } from 'web-components'
import { Trash2 } from 'lucide-react' import { AppLayout } from '../components/AppLayout'
import { Trash2, ShoppingCart } from 'lucide-react'
export const Route = createFileRoute('/home/cart')({ component: CartPage }) export const Route = createFileRoute('/home/cart')({ component: CartPage })
@ -28,71 +29,109 @@ function CartPage() {
}) })
return ( return (
<AppContainer> <AppLayout>
<p className="font-bold mb-4 text-xl"> <div className="mx-auto w-full max-w-7xl px-4 py-6 pb-24 md:px-8 md:pb-12">
Your Cart <div className="mb-6">
</p> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Review Before Delivery
{cartItems.length === 0 ? ( </p>
<div className="flex flex-col items-center gap-4 py-20"> <h1 className="display-2 mt-1 text-gray-900">Your Cart</h1>
<p className="text-gray-500">Your cart is empty</p>
<MyButton
textContent="Browse Products"
onClick={() => navigate({ to: '/home' })}
/>
</div> </div>
) : (
<> {cartItems.length === 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-gray-300 bg-white py-24">
{cartItems.map((item) => { <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
const product = productsById[item.productId] <ShoppingCart className="h-7 w-7 text-gray-400" />
const price = product.price </div>
return ( <p className="font-bold text-gray-800">Your cart is empty</p>
<div <p className="text-sm text-gray-500">Fresh cuts are waiting at the counter</p>
key={item.productId} <button
className="flex items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 shadow-sm" 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"
<img >
src={product.images?.[0]} Browse Products
alt={product.name} </button>
className="h-16 w-16 rounded-lg object-cover" </div>
/> ) : (
<div className="flex-1"> <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<p className="font-semibold text-sm"> {/* Items */}
{product.name} <div className="flex flex-col gap-3">
</p> {cartItems.map((item) => {
<p className="text-brand-600 text-sm font-bold"> const product = productsById[item.productId]
{price} const price = product.price
</p> return (
<Quantifier <div
value={item.quantity} key={item.productId}
setValue={(q) => updateItem.mutate({ productId: item.productId, quantity: q })} 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>
<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>
) <div className="flex items-center justify-between py-4">
})} <p className="font-bold text-gray-900">Total</p>
</div> <p className="font-display text-xl font-extrabold text-gray-900">{total}</p>
</div>
<div className="fixed bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4 shadow-lg"> <button
<div className="mb-3 flex items-center justify-between"> onClick={() => navigate({ to: '/home/checkout' })}
<p className="font-bold">Total</p> className="w-full rounded-lg bg-brand-600 py-3.5 text-sm font-bold text-white transition-colors hover:bg-brand-700"
<p className="font-bold text-lg text-brand-600"> >
{total} Proceed to Checkout
</p> </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> </div>
<MyButton
fullWidth
textContent="Proceed to Checkout"
onClick={() => navigate({ to: '/home/checkout' })}
className="bg-brand-500 text-white"
/>
</div> </div>
</> )}
)} </div>
</AppContainer> </AppLayout>
) )
} }

View file

@ -1,11 +1,7 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState, useEffect, useMemo, useRef } from 'react' import { useState, useEffect, useMemo, useRef } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import { SearchBar } from 'web-components'
p,
div,
SearchBar,
} from 'web-components'
import { import {
useAllProducts, useAllProducts,
useStores, useStores,
@ -15,21 +11,20 @@ import {
} from '../hooks/prominent-api-hooks' } from '../hooks/prominent-api-hooks'
import { useCartStore } from '../lib/stores/cart-store' import { useCartStore } from '../lib/stores/cart-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store' import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { useCentralProductStore } from '../lib/stores/central-product-store'
import { AppLayout } from '../components/AppLayout' import { AppLayout } from '../components/AppLayout'
import { ProductCard } from '../components/ProductCard' import { ProductCard } from '../components/ProductCard'
import AddToCartDialog from '../components/AddToCartDialog' import AddToCartDialog from '../components/AddToCartDialog'
import { useProductSlotIdentifier } from '../hooks/useProductSlotIdentifier' import { useProductSlotIdentifier } from '../hooks/useProductSlotIdentifier'
import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores' 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 // Scroll Indicator Component
function ScrollIndicator({ function ScrollIndicator({
containerRef, containerRef,
itemCount, itemCount,
itemWidth itemWidth,
}: { }: {
containerRef: React.RefObject<HTMLDivElement> containerRef: React.RefObject<HTMLDivElement | null>
itemCount: number itemCount: number
itemWidth: number itemWidth: number
}) { }) {
@ -63,9 +58,7 @@ function ScrollIndicator({
<div <div
key={i} key={i}
className={`h-1.5 rounded-full transition-all duration-300 ${ className={`h-1.5 rounded-full transition-all duration-300 ${
i === activeIndex i === activeIndex ? 'w-4 bg-brand-600' : 'w-1.5 bg-gray-300'
? 'w-4 bg-brand-500'
: 'w-1.5 bg-gray-300'
}`} }`}
/> />
))} ))}
@ -80,34 +73,18 @@ function SectionSpinner({ label }: { label?: string }) {
return ( return (
<div className="flex flex-col items-center justify-center py-10"> <div className="flex flex-col items-center justify-center py-10">
<Loader2 className="h-8 w-8 animate-spin text-brand-500" /> <Loader2 className="h-8 w-8 animate-spin text-brand-500" />
{label && ( {label && <p className="mt-2 text-sm text-gray-500">{label}</p>}
<p className="mt-2 text-sm text-gray-500">{label}</p>
)}
</div> </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() { function HomePage() {
const navigate = useNavigate() const navigate = useNavigate()
const { data: productsData, isLoading: isProductsLoading } = useAllProducts() const { data: productsData, isLoading: isProductsLoading } = useAllProducts()
const { data: storesData, isLoading: isStoresLoading } = useStores() const { data: storesData, isLoading: isStoresLoading } = useStores()
const { data: bannersData } = useBanners() const { data: bannersData } = useBanners()
const { data: slotsData, isLoading: isSlotsLoading } = useSlots() 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 // Handle bootstrapping: products/stores/slots are disabled until essentialConsts provides cacheUrl
// NOTE: with persisted caches, render immediately from placeholderData; no startup spinner. // NOTE: with persisted caches, render immediately from placeholderData; no startup spinner.
@ -133,6 +110,10 @@ function HomePage() {
const dashboardTags = productsData?.tags || [] const dashboardTags = productsData?.tags || []
const [selectedTagId, setSelectedTagId] = useState<number | null>(null) const [selectedTagId, setSelectedTagId] = useState<number | null>(null)
const activeTagId = selectedTagId ?? dashboardTags[0]?.id ?? 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 productsByTagId = useMemo(() => {
const map: Record<number, any[]> = {} const map: Record<number, any[]> = {}
@ -216,201 +197,221 @@ function HomePage() {
return ( return (
<AppLayout> <AppLayout>
<div className="min-h-screen bg-white pb-24"> <div className="mx-auto w-full max-w-7xl px-4 pb-24 md:px-8 md:pb-12">
{/* Search Bar */} {/* Welcome band — desktop editorial opener */}
<div className="sticky top-0 z-10 border-b border-gray-100 bg-white/95 px-4 pb-3 pt-4 backdrop-blur-sm"> <div className="flex flex-col gap-1 py-6 md:py-8">
<SearchBar <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
placeholder="Search products here..." Fresh cut, daily
onClick={() => navigate({ to: '/home/search' })} </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>
<div className="px-4"> {/* Download App Banner */}
{/* Banner Carousel - Commented out */} <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">
{/* {banners.length > 0 && ( <div className="flex items-center gap-4">
<div className="mb-6 mt-4 overflow-hidden rounded-xl"> <div className="hidden h-12 w-12 items-center justify-center rounded-xl bg-white/20 md:flex">
<BannerCarousel banners={banners} /> <Beef className="h-6 w-6 text-white" />
</div> </div>
)} */} <div className="flex-1">
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
{/* Download App Banner */} <p className="text-sm text-white/80">Download for exclusive offers & faster checkout</p>
<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> </div>
</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 */} {/* Stores Section */}
<div className="mb-6"> <section className="mb-10">
<div className="mb-4 flex items-center justify-between"> <div className="section-rule mb-5 flex items-end justify-between">
<div> <div>
<p className="font-bold text-xl text-gray-900"> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
Our Stores Our Outlets
</p> </p>
<p className="mt-0.5 text-xs text-gray-500"> <h2 className="display-2 mt-1 text-gray-900">Shop by Store</h2>
Fresh from our locations
</p>
</div>
</div> </div>
{storesLoading ? ( <button
<SectionSpinner label="Loading stores..." /> onClick={() => navigate({ to: '/stores' })}
) : ( className="text-sm font-bold text-brand-600 hover:text-brand-700"
<div className="flex flex-wrap gap-4"> >
{stores.map((store: any) => ( All stores
<div key={store.id}> </button>
<StoreCard
store={store}
onClick={() =>
navigate({
to: '/stores/$storeId',
params: { storeId: String(store.id) },
})
}
/>
</div>
))}
</div>
)}
</div> </div>
{/* Explore Products Section */} {storesLoading ? (
{dashboardTags.length > 0 && ( <SectionSpinner label="Loading stores..." />
<div className="mb-6"> ) : (
<div className="mb-4"> <div className="flex flex-wrap gap-4">
<p className="font-bold text-xl text-gray-900"> {stores.map((store: any) => (
Explore Products <div key={store.id}>
</p> <StoreCard
<p className="mt-0.5 text-sm text-gray-500"> store={store}
Browse by category onClick={() =>
</p> navigate({
</div> to: '/stores/$storeId',
params: { storeId: String(store.id) },
{/* 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) </div>
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>
</div> </div>
)} )}
</section>
{/* Upcoming Delivery Slots Section */} {/* Explore Products Section */}
<div className="mb-6"> {dashboardTags.length > 0 && (
<div className="mb-4"> <section className="mb-10">
<p className="font-bold text-xl text-gray-900"> <div className="section-rule mb-5 flex items-end justify-between">
Upcoming Delivery Slots <div>
</p> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
<p className="mt-0.5 text-sm text-gray-500"> Browse by Category
Plan your fresh deliveries ahead </p>
</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> </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> </div>
{slotsLoading ? (
{/* All Products Section */} <SectionSpinner label="Loading slots..." />
<div className="rounded-t-3xl bg-white pt-4"> ) : (
<div className="mb-4"> <>
<p className="font-bold text-xl text-gray-900"> <div
All Available Products ref={slotsScrollRef}
</p> className="scrollbar-hide -mx-4 flex gap-4 overflow-x-auto px-4 pb-2"
<p className="mt-0.5 text-sm text-gray-500"> >
Browse our complete selection {sortedSlots.slice(0, 5).map((slot) => (
</p> <SlotCard key={slot.id} slot={slot} />
</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}
/>
))} ))}
</div> </div>
)} <ScrollIndicator
</div> containerRef={slotsScrollRef}
</div> 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> </div>
<AddToCartDialog />
</AppLayout> </AppLayout>
) )
} }
@ -479,7 +480,7 @@ function StoreCard({ store, onClick }: { store: any; onClick: () => void }) {
onClick={onClick} onClick={onClick}
className="flex flex-col items-center" 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 ? ( {store.signedImageUrl ? (
<img <img
src={store.signedImageUrl} src={store.signedImageUrl}
@ -490,7 +491,7 @@ function StoreCard({ store, onClick }: { store: any; onClick: () => void }) {
<Store className="h-7 w-7 text-gray-400" /> <Store className="h-7 w-7 text-gray-400" />
)} )}
</div> </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, '')} {store.name.replace(/^The\s+/i, '')}
</p> </p>
</div> </div>
@ -519,8 +520,8 @@ function SlotCard({ slot }: { slot: any }) {
return ( return (
<div <div
onClick={() => navigate({ to: '/slot-view', search: { slotId: slot.id } })} 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 ${ 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-500' 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"> <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="mb-5 flex flex-row justify-between">
<div className="mr-4 flex-1"> <div className="mr-4 flex-1">
<div className="mb-1.5 flex flex-row items-center"> <div className="mb-1.5 flex flex-row items-center">
<div className="mr-1.5 rounded-md bg-brand-50 p-1"> <div className="mr-1.5 rounded-md bg-brand-25 p-1">
<svg className="h-3 w-3 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Truck className="h-3 w-3 text-brand-600" />
<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> </div>
<p className="text-[10px] font-bold uppercase text-brand-700">Delivery At</p> <p className="text-[10px] font-bold uppercase text-brand-700">Delivery At</p>
</div> </div>
<p className="font-bold text-sm text-slate-900"> <p className="text-sm font-bold text-gray-900">
{formatTimeRange(slot.deliveryTime)} {formatTimeRange(slot.deliveryTime)}
</p> </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')} {dayjs(slot.deliveryTime).format('ddd, MMM DD')}
</p> </p>
</div> </div>
@ -553,16 +552,14 @@ function SlotCard({ slot }: { slot: any }) {
<div className="flex-1"> <div className="flex-1">
<div className="mb-1.5 flex flex-row items-center"> <div className="mb-1.5 flex flex-row items-center">
<div className="mr-1.5 rounded-md bg-amber-50 p-1"> <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"> <Clock className="h-3 w-3 text-amber-600" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div> </div>
<p className="text-[10px] font-bold uppercase text-amber-700">Order By</p> <p className="text-[10px] font-bold uppercase text-amber-700">Order By</p>
</div> </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')} {dayjs(slot.freezeTime).format('h:mm A')}
</p> </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')} {dayjs(slot.freezeTime).format('ddd, MMM DD')}
</p> </p>
</div> </div>
@ -573,13 +570,13 @@ function SlotCard({ slot }: { slot: any }) {
{slot.products?.slice(0, 3).map((p: any, i: number) => ( {slot.products?.slice(0, 3).map((p: any, i: number) => (
<div <div
key={p.id} 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] ? ( {p.images?.[0] ? (
<img src={p.images[0]} alt="" className="h-full w-full object-cover" /> <img src={p.images[0]} alt="" className="h-full w-full object-cover" />
) : ( ) : (
<div className="flex h-full items-center justify-center"> <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>
)} )}
</div> </div>

View file

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

View file

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

View file

@ -4,7 +4,8 @@ import { useForm, Controller } from 'react-hook-form'
import { useAuth } from '../lib/auth-context' import { useAuth } from '../lib/auth-context'
import { trpc } from '../lib/trpc-client' import { trpc } from '../lib/trpc-client'
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks' 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 }) export const Route = createFileRoute('/login')({ component: LoginPage })
@ -126,145 +127,188 @@ function LoginPage() {
} }
return ( 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="flex min-h-screen bg-gray-50">
<div className="w-full max-w-md"> {/* Brand panel — desktop only */}
<p className="font-bold mb-2 text-center text-4xl text-white"> <div className="hidden w-[42%] flex-col justify-between bg-gradient-to-b from-brand-500 to-brand-700 p-12 lg:flex">
Welcome <div className="flex items-center gap-3">
</p> <div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/20">
<p className="mb-8 text-center text-lg text-blue-100"> <Beef className="h-6 w-6 text-white" />
Sign in to continue your journey </div>
</p> <div>
<p className="font-display text-xl font-extrabold tracking-tight text-white">
<div className="rounded-2xl bg-white p-8 shadow-xl"> Freshyo
<form onSubmit={handleSubmit(onSubmit)}> </p>
{step === 'mobile' && ( <p className="text-[10px] font-bold uppercase tracking-[0.18em] text-white/60">
<Controller Butcher &amp; Grocer
control={control} </p>
name="mobile" </div>
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> </div>
{/* Download App Banner */} <div>
<div className="mt-6 rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 p-4 shadow-lg"> <h1 className="display-1 max-w-md text-white">
<div className="flex items-center justify-between"> Fresh cuts, planned deliveries, zero queues
<div className="flex-1"> </h1>
<p className="text-lg font-bold text-white">Get the FreshYo App</p> <p className="mt-4 max-w-md text-white/80">
<p className="text-sm text-white/80 mt-1">Download for exclusive offers & faster checkout</p> 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> </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> </div>
</div> </div>

View file

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

View file

@ -1,7 +1,6 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react' import { useState } from 'react'
import { trpc } from '../lib/trpc-client' import { trpc } from '../lib/trpc-client'
import { AppContainer } from 'web-components'
import { Tag, Loader2, AlertCircle } from 'lucide-react' import { Tag, Loader2, AlertCircle } from 'lucide-react'
import { ProductCard } from '../components/ProductCard' import { ProductCard } from '../components/ProductCard'
import { AppLayout } from '../components/AppLayout' 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 const hasMore = products.length > SHOW_MORE_STEP && !expanded
return ( return (
<div className="pt-4"> <section className="mb-10">
<div className="mb-4 px-4"> <div className="section-rule mb-5">
<p className="font-bold text-xl text-gray-900">{title}</p> <h2 className="display-2 text-gray-900">{title}</h2>
<p className="mt-0.5 text-sm text-gray-500">{subtitle}</p> <p className="mt-1 text-sm text-gray-500">{subtitle}</p>
</div> </div>
{products.length === 0 ? ( {products.length === 0 ? (
@ -37,7 +36,7 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
</div> </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) => ( {visible.map((item: any) => (
<ProductCard <ProductCard
key={item.id} key={item.id}
@ -50,10 +49,10 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
))} ))}
</div> </div>
{hasMore && ( {hasMore && (
<div className="mt-2 mb-1 flex justify-center"> <div className="mt-6 flex justify-center">
<button <button
onClick={onExpand} 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 Show More
</button> </button>
@ -61,7 +60,7 @@ function OffersSection({ title, subtitle, products, expanded, onExpand, onProduc
)} )}
</> </>
)} )}
</div> </section>
) )
} }
@ -103,7 +102,15 @@ function OffersPage() {
return ( return (
<AppLayout> <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 <OffersSection
title="Offers" title="Offers"
subtitle="Great prices on select items" subtitle="Great prices on select items"

View file

@ -1,7 +1,6 @@
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useStoreWithProducts, useAllProducts } from '../hooks/prominent-api-hooks' import { useStoreWithProducts, useAllProducts } from '../hooks/prominent-api-hooks'
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import { p } from 'web-components'
import { AppLayout } from '../components/AppLayout' import { AppLayout } from '../components/AppLayout'
import AddToCartDialog from '../components/AddToCartDialog' import AddToCartDialog from '../components/AddToCartDialog'
import { ProductCard } from '../components/ProductCard' import { ProductCard } from '../components/ProductCard'
@ -71,10 +70,10 @@ function StoreDetailPage() {
return ( return (
<AppLayout> <AppLayout>
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<p className="font-bold mb-2 text-lg text-gray-900"> <p className="mb-2 text-lg font-bold text-gray-900">
Oops! Oops!
</p> </p>
<p className="text-gray-500">Store not found or error loading</p> <p className="text-gray-500">Store not found or error loading</p>
@ -86,38 +85,52 @@ function StoreDetailPage() {
return ( return (
<AppLayout> <AppLayout>
<div className="min-h-screen bg-gray-50 pb-24"> <div className="min-h-screen bg-gray-50 pb-24">
{/* Back Button */} {/* Header band */}
<div className="sticky top-0 z-10 border-b border-gray-200 bg-white px-4 py-3"> <div className="border-b border-gray-200 bg-white">
<div className="flex items-center gap-3"> <div className="mx-auto w-full max-w-7xl px-4 py-5 md:px-8">
<div onClick={() => navigate({ to: '/stores' })} className="p-2"> <button
<ArrowLeft className="h-5 w-5 text-gray-700" /> 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> </div>
<p className="font-bold text-lg text-gray-900">
{storeData?.store?.name || 'Store'}
</p>
</div> </div>
</div> </div>
<div className="px-4 pt-4"> <div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8">
{/* 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>
{/* Tags Section */} {/* Tags Section */}
{storeData?.tags && storeData.tags.length > 0 && ( {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) => ( {storeData.tags.map((tag: Tag) => (
<TagChip <TagChip
key={tag.id} key={tag.id}
@ -133,7 +146,7 @@ function StoreDetailPage() {
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center">
<Grid3X3 className="mr-2 h-5 w-5 text-gray-700" /> <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 {selectedTagId
? `${storeData?.tags.find((t: Tag) => t.id === selectedTagId)?.tagName} items` ? `${storeData?.tags.find((t: Tag) => t.id === selectedTagId)?.tagName} items`
: `${filteredProducts.length} products`} : `${filteredProducts.length} products`}
@ -144,14 +157,14 @@ function StoreDetailPage() {
onClick={() => setSelectedTagId(null)} onClick={() => setSelectedTagId(null)}
className="flex items-center" className="flex items-center"
> >
<p className="mr-1 text-sm font-medium text-brand-500">Clear</p> <p className="mr-1 text-sm font-medium text-brand-600">Clear</p>
<X className="h-4 w-4 text-brand-500" /> <X className="h-4 w-4 text-brand-600" />
</div> </div>
)} )}
</div> </div>
{/* Products Grid */} {/* 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) => ( {filteredProducts.map((product: any) => (
<ProductCard <ProductCard
key={product.id} key={product.id}
@ -196,13 +209,13 @@ function TagChip({ tag, isSelected, onPress }: TagChipProps) {
return ( return (
<button <button
onClick={onPress} 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 isSelected
? 'border-brand-500 bg-brand-500 text-white' ? 'border-brand-600 bg-brand-600 text-white'
: 'border-brand-500 bg-white text-brand-500' : '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}) {tag.tagName} ({productCount})
</span> </span>
</button> </button>

View file

@ -1,6 +1,5 @@
import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router' import { createFileRoute, useNavigate, Outlet, useLocation } from '@tanstack/react-router'
import { useStores } from '../hooks/prominent-api-hooks' import { useStores } from '../hooks/prominent-api-hooks'
import { p, div } from 'web-components'
import { AppLayout } from '../components/AppLayout' import { AppLayout } from '../components/AppLayout'
import { Store, ArrowRight, Building2 } from 'lucide-react' import { Store, ArrowRight, Building2 } from 'lucide-react'
@ -20,11 +19,11 @@ function StoresPage() {
if (isLoading) { if (isLoading) {
return ( return (
<AppLayout> <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"> <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> </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... Opening Marketplace...
</p> </p>
</div> </div>
@ -35,21 +34,21 @@ function StoresPage() {
if (error) { if (error) {
return ( return (
<AppLayout> <AppLayout>
<div className="flex min-h-screen flex-col items-center justify-center bg-slate-50 p-10"> <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-rose-50"> <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-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </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 Store Fetch Failed
</p> </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. We couldn't reach our vendor network.
</p> </p>
<button <button
onClick={() => window.location.reload()} 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 Retry
</button> </button>
@ -60,29 +59,22 @@ function StoresPage() {
return ( return (
<AppLayout> <AppLayout>
<div className="min-h-screen bg-slate-50 pb-32"> <div className="min-h-screen bg-gray-50 pb-32">
{isExactStoresPath ? ( {isExactStoresPath ? (
<> <div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8">
<div className="px-3 pt-6"> {/* Header */}
{/* Header */} <div className="mb-6">
<div className="mb-4 flex items-center"> <p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
<div className="mr-3 h-6 w-1 rounded-full bg-gradient-to-b from-brand-500 to-brand-700" /> Our Outlets
<div> </p>
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400"> <h1 className="display-2 mt-1 text-gray-900">Our Stores</h1>
Our Outlets <p className="mt-1 max-w-2xl text-gray-600">
</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">
Experience the finest selection of premium meat, poultry, fresh fruits, vegetables, and dairy directly from our own stores. Experience the finest selection of premium meat, poultry, fresh fruits, vegetables, and dairy directly from our own stores.
</p> </p>
</div> </div>
{/* Store Cards */} {/* 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) => ( {stores.map((store: any) => (
<StoreCard key={store.id} store={store} /> <StoreCard key={store.id} store={store} />
))} ))}
@ -91,14 +83,14 @@ function StoresPage() {
{stores.length === 0 && ( {stores.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center py-20"> <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"> <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> </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 No Stores Available
</p> </p>
</div> </div>
)} )}
</> </div>
) : ( ) : (
/* Render child routes (e.g., store detail) when not on exact /stores path */ /* Render child routes (e.g., store detail) when not on exact /stores path */
<Outlet /> <Outlet />
@ -114,116 +106,106 @@ interface StoreCardProps {
function StoreCard({ store }: StoreCardProps) { function StoreCard({ store }: StoreCardProps) {
const navigate = useNavigate() const navigate = useNavigate()
const sampleProducts = store.sampleProducts || [] const sampleProducts = store.sampleProducts || []
const remainingCount = store.productCount - sampleProducts.length const remainingCount = store.productCount - sampleProducts.length
const isMeatStore = store.name?.toLowerCase().includes('meat')
const navigateToStore = () => { const navigateToStore = () => {
navigate({ navigate({
to: '/stores/$storeId', to: '/stores/$storeId',
params: { storeId: String(store.id) }, params: { storeId: String(store.id) },
}) })
} }
return ( return (
<div className="mb-4 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-lg shadow-slate-200"> <div className="flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-brand-300 hover:shadow-md">
{/* Meat Store Images - Show at top if store name contains 'meat' */} {/* Header */}
<div onClick={navigateToStore} className="cursor-pointer p-5 pb-0">
{/* Top Header Section */} <div className="mb-4 flex items-center">
<div onClick={navigateToStore} className="cursor-pointer p-4 pb-0"> <div className="h-12 w-12 rounded-xl border border-gray-100 bg-gray-50 p-0.5">
<div className="mb-4 flex items-center"> <div className="relative h-full w-full overflow-hidden rounded-lg">
<div className="h-12 w-12 rounded-xl border border-slate-200 bg-slate-50 p-0.5 shadow-sm"> {store.signedImageUrl || store.imageUrl ? (
<div className="relative h-full w-full overflow-hidden rounded-[10px]"> <img
{store.signedImageUrl || store.imageUrl ? ( src={store.signedImageUrl || store.imageUrl}
<img alt={store.name}
src={store.signedImageUrl || store.imageUrl} className="h-full w-full object-cover"
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 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" />
</div> </div>
)} )}
</div> </div>
</div> </div>
)}
{/* Explore Store Button */} <div className="ml-3 flex-1">
<div className="px-4 pb-4"> <p className="text-lg font-bold text-gray-900">
<div {store.name}
onClick={navigateToStore} </p>
className="flex flex-row items-center justify-center rounded-[18px] bg-brand-600 py-3 shadow-lg shadow-brand-200" </div>
>
<p className="font-bold mr-2 text-sm uppercase tracking-wider text-white"> <div className="flex items-center rounded-lg border border-brand-100 bg-brand-25 px-2.5 py-1.5">
Explore Store <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> </p>
<ArrowRight className="h-4 w-4 text-white" />
</div> </div>
</div> </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-700: #175CD3;
--color-brand-800: #1849A9; --color-brand-800: #1849A9;
--color-brand-900: #194185; --color-brand-900: #194185;
} /* Flash delivery accent — consistent with user-ui */
/* @theme { --color-flash-25: #FFF5F6;
--color-brand-25: #FFF5F6; --color-flash-50: #FFE8EA;
--color-brand-50: #FFE8EA; --color-flash-100: #FFD1D6;
--color-brand-100: #FFD1D6; --color-flash-200: #FFA3AE;
--color-brand-200: #FFA3AE; --color-flash-300: #FF7585;
--color-brand-300: #FF7585; --color-flash-400: #F81260;
--color-brand-400: #FF475D; --color-flash-500: #E11D48;
--color-brand-500: #E63946; --color-flash-600: #C40E50;
--color-brand-600: #C5303C; --color-flash-700: #9E0A3F;
--color-brand-700: #9E2630;
--color-brand-800: #771D24;
--color-brand-900: #501318;
} */
--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; box-sizing: border-box;
@ -41,6 +40,11 @@ body,
body { body {
margin: 0; 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 */ /* Safe area padding for mobile devices */
@ -66,8 +70,70 @@ body {
overflow: hidden; overflow: hidden;
} }
/* Safe area padding for mobile devices */ /* Focus rings — architecture, not decoration */
.pb-safe { :focus-visible {
padding-bottom: env(safe-area-inset-bottom, 0px); 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 ...props
}: pInputProps) { }: pInputProps) {
const inputClasses = cn( 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', shrunkPadding ? 'py-1.5' : 'py-2',
error && 'border-destructive', error && 'border-destructive',
className className

View file

@ -78,7 +78,7 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
value={value} value={value}
onChange={handleChange} onChange={handleChange}
placeholder={placeholder} 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 && ( {value && (
<button <button