Compare commits
No commits in common. "6c4a75497c17400b576c37b822171d5c9cc809a9" and "496faffe0a585a87b0643a7171e5ca9e592a7876" have entirely different histories.
6c4a75497c
...
496faffe0a
30 changed files with 384 additions and 564 deletions
|
|
@ -12,7 +12,6 @@
|
||||||
- Prefers embedding related data into existing cache files over creating or calling separate API endpoints. Confidence: 0.9
|
- Prefers embedding related data into existing cache files over creating or calling separate API endpoints. Confidence: 0.9
|
||||||
- Prefers using icons from Hicons (rather than custom/hand-authored SVGs or other icon libraries). Confidence: 0.9
|
- Prefers using icons from Hicons (rather than custom/hand-authored SVGs or other icon libraries). Confidence: 0.9
|
||||||
- When a feature or page is repurposed, wants file names, route names, and code references renamed to match the new purpose (not just UI content/icons). Confidence: 0.8
|
- When a feature or page is repurposed, wants file names, route names, and code references renamed to match the new purpose (not just UI content/icons). Confidence: 0.8
|
||||||
- When replicating a UI pattern (e.g., home page tabs) onto a different page/context, adapt the copied colors to that page's own brand accent (e.g., the flash page's tabs use the flash pink `text-flash-600`/`bg-flash-500` instead of the home page's brand blue) while keeping the layout and styling identical. Confidence: 0.6
|
|
||||||
- Wants analysis findings and plans persisted in a markdown file before any implementation begins, with explicit confirmation that implementation is paused until instructed. Confidence: 0.8
|
- Wants analysis findings and plans persisted in a markdown file before any implementation begins, with explicit confirmation that implementation is paused until instructed. 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
|
- 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
|
||||||
|
|
@ -24,8 +23,6 @@
|
||||||
- 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", "go ahead and implement") before implementation proceeds, expecting the agent to autonomously execute the already-presented plan. Confidence: 0.85
|
- After reviewing a presented plan, prefers brief, action-oriented approval (e.g., "nice. go ahead", "go ahead and implement") before implementation proceeds, expecting the agent to autonomously execute the already-presented plan. Confidence: 0.85
|
||||||
- Wants feature parity maintained between the web-ui and user-ui apps (port logic/data patterns, rebuild UI per platform). Confidence: 0.95
|
- Wants feature parity maintained between the web-ui and user-ui apps (port logic/data patterns, rebuild UI per platform). Confidence: 0.95
|
||||||
- Wants the web-ui home page section/component order to match the reference user-ui home page exactly (e.g., "on the home page have the order same as that of @apps/user-ui/.../home/index.tsx"), including when deciding placement of sections like Explore, Stores, Slots, and All Products. Confidence: 0.7
|
|
||||||
- Wants brand/logo spots to use the single real brand logo asset (shared across apps, e.g., the Freshyo splash logo) displayed as an image, not a generic/decorative icon (e.g., a "meat piece" Meat/Beef icon) — web-ui should be on par with user-ui and show the same logo; applies to favicon, PWA icons, and every in-UI branding spot (sidebar, topbar, login, home banner). Confidence: 0.85
|
|
||||||
- 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
|
||||||
|
|
@ -35,13 +32,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
|
- Wants true responsive web design for desktop, not just color/theme changes or mobile layouts stretched to desktop. Confidence: 0.95
|
||||||
- Specifies layout proportions in relative terms rather than fixed pixel sizes — e.g., the left sidebar/menu should be 20% of the viewport width at most ("the left menu should be 20% of width at max"), implemented as a viewport-relative track (20vw) rather than a hard-coded px column. Confidence: 0.7
|
|
||||||
- On the web-ui, wants the topbar (logo/search/cart) hidden on mobile screens and only rendered from the tablet breakpoint (`md`, 768px) upward, since mobile gets its own bottom navigation; when hiding it, the surrounding grid layout must be cleaned up so no empty reserved space remains. Confidence: 0.8
|
|
||||||
- The search bar must be independent of the topbar and always visible on every screen size; decoupling it from the topbar's tablet-only visibility — hiding the topbar must not hide search ("the search bar... should be independent of the top bar. I want it visible always"). When a page already has its own search UI (e.g., the search route's sticky header), the global always-visible search row should be suppressed there to avoid stacked duplicate bars. Confidence: 0.8
|
|
||||||
- Wants the search bar (and other persistent page-level UI like the topbar) to be sticky, pinning to the top of the viewport while the page scrolls ("I see it's not sticky. I want it sticky"); to make sticky actually work, the header group should live in a full-height flex column rather than a bounded grid row that scrolls away. Confidence: 0.9
|
|
||||||
- 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
|
- 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
|
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.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
|
- 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
|
- 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
|
- 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
|
||||||
|
|
@ -50,7 +43,6 @@ er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the
|
||||||
- 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
|
- 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
|
- 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
|
- 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
|
||||||
- Wants product grid sections (e.g., Explore Products, All Available Products on the home page) to render at least 3 columns even on the smallest screens, scaling up on larger breakpoints — prefers the minimum column count on mobile to not be reduced to 2. 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
|
- 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
|
- 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
|
- 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
|
||||||
|
|
@ -58,22 +50,6 @@ er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the
|
||||||
- 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
|
- 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
|
- 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
|
- 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
|
||||||
- On mobile screens, prefers abbreviated button labels to conserve horizontal space — e.g., the add-to-cart button shows just the cart icon + "Add" on mobile and the full "Add to Cart" label from the `md` breakpoint up ("if the screen is mobile one just show cart symbol and Add instead of Add to cart"), implemented with responsive `md:hidden` / `hidden md:inline` spans. Confidence: 0.8
|
|
||||||
- Expects the UI to reflect state changes immediately after an action, without a manual page refresh — e.g., items added to the cart must show up in the cart/badge right away; reports staleness bugs as "added items are not visible unless page is refreshed. They are added but not shown in cart," distinguishing that data was persisted (localStorage) but the UI didn't update. Confidence: 0.7
|
|
||||||
- Wants the same UI component variant used consistently across every section/page in the app: when the user designates one section as having the "correct" version of a component, that exact section is the reference and its variant must be replicated everywhere else. Confirmed by an explicit correction: the Explore tabs section's mini floating cart icon (`miniView={true}`) was the correct one — "what was first present in tabs section, should've been replicated elsewhere" — i.e., replicated to All Products, search results, offers, and store detail, not the other way around. He expects the agent to follow the stated direction precisely and will correct a swapped interpretation. Confidence: 0.9
|
|
||||||
- When the user asks for "similar tabs" on a page (e.g., the home page's category tabs on the 1-hr flash page), they mean the tabs model should REPLACE the page's existing filter/navigation mechanism — e.g., the store selector sidebar — entirely, not be layered on top of it ("I meant, replace the store selector with tabs model"). The page should use a single, consistent navigation/filter model like the home page, not two stacked filter controls; expects the agent to remove the superseded component and its now-unused imports/state. Confidence: 0.85
|
|
||||||
- Dislikes transparent backgrounds on UI controls that overlay content — e.g., the quantity stepper floating over product images must have a solid background (white with a subtle shadow) so it reads clearly over any image, not a transparent one ("the background of the quantifier is transparent. Don't do that."). Confidence: 0.7
|
|
||||||
- Modal dialogs (e.g., the add-to-cart dialog) must always layer above persistent fixed UI — the floating cart bar and the bottom navigation — never behind them; states the rule explicitly: the bar "shouldn't have more z index than the dialog," on both mobile and desktop. On mobile, interactive dialog content (the Add/Update button) must also clear the bottom nav (higher z-index than the nav's z-50 plus mobile-only bottom padding) while desktop behavior stays unchanged ("Move it a little top in case of mobile view"). Confidence: 0.75
|
|
||||||
- In any cart surface, decrementing a quantity stepper down to 0 must remove the item from the cart entirely (make it disappear), never leave it persisted at quantity 0 — expected both on the cart page and in the cart slide-over opened from the floating cart bar, and the user will explicitly correct which surface he meant if the fix targets the wrong one ("I'm unable to click - to make it 0 and disappear" / "I clicked the cart bar to open cart view. There I'm not able to make '0' and remove"). Confidence: 0.7
|
|
||||||
- In Tailwind v4 apps, arbitrary-value classes (e.g., `z-[60]`) authored in external/shared packages (like `packages/web-components`) can be silently dropped by the content scanner even when sibling classes from the same file compile — so for critical layering/positioning, prefer built-in scale values or an inline `style={{ zIndex: ... }}` and verify the compiled output (CSS/JS bundle) actually contains it rather than trusting the class source. Confidence: 0.7
|
|
||||||
- In modal dialogs that contain a list plus a primary action (e.g., the add-to-cart dialog with delivery slots and an Add to Cart button), only the list region should scroll — the header and footer (quantity + action buttons) stay pinned and always visible, so reaching the primary action never requires scrolling ("only the slots should be scrollable... I need to scroll down to see the add to cart button"); implemented as a flex column with a fixed header, a `flex-1 min-h-0 overflow-y-auto` scrollable middle, and a `shrink-0` footer. Confidence: 0.75
|
|
||||||
- On narrow/small screens, prefers text to scale DOWN to avoid crowding — prices/amounts and general card text alike: e.g., cart item prices `text-sm` on mobile bumping to `text-base` at the `sm` breakpoint, product card prices `text-base` → `text-lg`, and tightened vertical gaps near the quantity row ("if the width is small, reduce the price size and also the gap below the quantity"). Explicitly generalized to all card text (name, price, unit/quantity) with the direct request "when the screen width is small, reduce the font size too" — implemented as smaller arbitrary sizes (e.g., `text-[13px]`, `text-[15px]`, `text-[11px]`) below the `sm` breakpoint returning to normal sizes at `sm` and up. Explicitly requested and then repeatedly reinforced ("I still see large blue text in price"). Confidence: 0.8
|
|
||||||
- When he reports a visual issue persisting ("I still see the same"), he is referring to the same visual pattern (e.g., "large blue text in price" = any `text-brand-600` price across cart pages, product cards) wherever it appears — expects the agent to grep the whole codebase for every occurrence of the matching styling and fix them all, not just the one surface initially identified. Confidence: 0.7
|
|
||||||
- When a spacing/visual complaint persists ("I still see the gap below the quantity"), the same issue may live in a different rendering mode of the component than the one already fixed — e.g., product cards in `miniView` where the quantity control overlays the image and the gap comes from the unit-line bottom margin plus the card's bottom padding, not the cart slide-over that was edited first. Expects the agent to re-inspect the actual rendered layout of each variant/mode and tighten spacing everywhere the pattern appears. Confidence: 0.6
|
|
||||||
- Prefers vertical tab/sidebar lists for store/category selection on ALL screen sizes — a vertical sidebar layout must not be swapped to a horizontal scrolling chip strip on mobile ("on the flash screen, the store tabs are horizontal, they should have been vertical"); instead keep the vertical list, stacked full-width above the content on small screens and as a sticky rail on desktop. Confidence: 0.7
|
|
||||||
- When adopting the tabs model to replace a store filter, the tabs must be driven by the same dimension as the replaced filter — one tab per store, labeled by the store name, with each tab showing only that store's items (per-tab scoped content), not category/product tags. Explicitly corrected after a misinterpretation ("the tab labels should be store names. In every tab we need to show the items of that store"). Confidence: 0.8
|
|
||||||
- When replicating a horizontal scrollable tab strip (store/category tabs) from user-ui to web-ui, expects identical interactive behavior, not just styling — the active tab must auto-scroll into view on selection so clicking a right-most tab brings it fully into view (matching user-ui's scroll-to-tab-position behavior, e.g., scroll to `offsetLeft - 16`), and the same auto-scroll must be applied consistently to every page using that tab strip pattern ("the tabs aren't behaving same as user-ui. They aren't auto scrolling when I try to click a right most tab"). Confidence: 0.8
|
|
||||||
- For compact product cards (price, strike-through market price, quantity), dictates a specific vertical stacking layout rather than a side-by-side row: show the price first, then the struck market price on the same line, and the quantity on a new line below ("first show the price, then the striked price, in new line show the quantity") — this supersedes earlier attempts to pin the quantity to the right of the same row. Confidence: 0.7
|
|
||||||
e changes. Confidence: 0.95
|
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
|
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
|
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
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ interface Product {
|
||||||
productTags: string[]
|
productTags: string[]
|
||||||
productType: string
|
productType: string
|
||||||
isComboOnly: boolean
|
isComboOnly: boolean
|
||||||
isOffer: boolean
|
|
||||||
comboItems: Array<{
|
comboItems: Array<{
|
||||||
skuId: number
|
skuId: number
|
||||||
skuName: string | null
|
skuName: string | null
|
||||||
|
|
@ -288,7 +287,6 @@ export async function getAllProducts(): Promise<Product[]> {
|
||||||
productTags: productTags,
|
productTags: productTags,
|
||||||
productType: product.productType || 'item',
|
productType: product.productType || 'item',
|
||||||
isComboOnly: product.isComboOnly,
|
isComboOnly: product.isComboOnly,
|
||||||
isOffer: product.isOffer,
|
|
||||||
comboItems: comboItems,
|
comboItems: comboItems,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ export async function scaffoldProducts() {
|
||||||
images: product.images,
|
images: product.images,
|
||||||
productType: product.productType || 'item',
|
productType: product.productType || 'item',
|
||||||
isComboOnly: product.isComboOnly,
|
isComboOnly: product.isComboOnly,
|
||||||
isOffer: product.isOffer,
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "common-ui";
|
} from "common-ui";
|
||||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
import ProductCard from "@/components/ProductCard";
|
import ProductCard from "@/components/ProductCard";
|
||||||
import { useAllProducts } from "@/src/hooks/prominent-api-hooks";
|
import { trpc } from "@/src/trpc-client";
|
||||||
import FloatingCartBar from "@/components/floating-cart-bar";
|
import FloatingCartBar from "@/components/floating-cart-bar";
|
||||||
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
|
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
|
||||||
|
|
||||||
|
|
@ -97,11 +97,11 @@ export default function Offers() {
|
||||||
const [expandedOffers, setExpandedOffers] = useState(false);
|
const [expandedOffers, setExpandedOffers] = useState(false);
|
||||||
const [expandedCombos, setExpandedCombos] = useState(false);
|
const [expandedCombos, setExpandedCombos] = useState(false);
|
||||||
|
|
||||||
const { data, isLoading, error, refetch } = useAllProducts();
|
const { data, isLoading, error, refetch } =
|
||||||
|
trpc.user.product.getOffersPage.useQuery();
|
||||||
|
|
||||||
const allProducts = data?.products || [];
|
const combos = data?.combos || [];
|
||||||
const combos = allProducts.filter((p) => p.productType === 'combo');
|
const offers = data?.offers || [];
|
||||||
const offers = allProducts.filter((p) => p.isOffer);
|
|
||||||
|
|
||||||
useManualRefresh(() => {
|
useManualRefresh(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,11 @@ export default function Layout() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="(tabs)/order-again"
|
name="(tabs)/stores"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Offers',
|
tabBarLabel: 'Stores',
|
||||||
tabBarIcon: ({ color, size, focused }) => (
|
tabBarIcon: ({ color, size, focused }) => (
|
||||||
<OffersIcon focused={focused} size={size} color={color} />
|
<StoresIcon focused={focused} size={size} color={color} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -95,11 +95,11 @@ export default function Layout() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="(tabs)/stores"
|
name="(tabs)/order-again"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Stores',
|
tabBarLabel: 'Offers',
|
||||||
tabBarIcon: ({ color, size, focused }) => (
|
tabBarIcon: ({ color, size, focused }) => (
|
||||||
<StoresIcon focused={focused} size={size} color={color} />
|
<OffersIcon focused={focused} size={size} color={color} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -460,40 +460,17 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
<View key={item.id}>
|
<View key={item.id}>
|
||||||
<View style={tw`p-4`}>
|
<View style={tw`p-4`}>
|
||||||
<View style={tw`flex-row items-center mb-2`}>
|
<View style={tw`flex-row items-center mb-2`}>
|
||||||
<MyTouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
isFlashDelivery
|
|
||||||
? `/(drawer)/(tabs)/flash-delivery/product-detail/${item.skuId}` as any
|
|
||||||
: `/(drawer)/(tabs)/home/product-detail/${item.skuId}` as any
|
|
||||||
)
|
|
||||||
}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: product?.images?.[0] }}
|
source={{ uri: product?.images?.[0] }}
|
||||||
style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`}
|
style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`}
|
||||||
/>
|
/>
|
||||||
</MyTouchableOpacity>
|
|
||||||
|
|
||||||
<MyTouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
isFlashDelivery
|
|
||||||
? `/(drawer)/(tabs)/flash-delivery/product-detail/${item.skuId}` as any
|
|
||||||
: `/(drawer)/(tabs)/home/product-detail/${item.skuId}` as any
|
|
||||||
)
|
|
||||||
}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
style={tw`flex-1 mr-3`}
|
|
||||||
>
|
|
||||||
<MyText
|
<MyText
|
||||||
style={tw`text-sm text-gray-900`}
|
style={tw`text-sm text-gray-900 flex-1 mr-3`}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{product?.name}
|
{product?.name}
|
||||||
</MyText>
|
</MyText>
|
||||||
</MyTouchableOpacity>
|
|
||||||
<MyText style={tw`text-xs text-gray-500 mr-2`}>
|
<MyText style={tw`text-xs text-gray-500 mr-2`}>
|
||||||
{product?.productType !== 'combo' ? (product?.unitNotation || '') : ''}
|
{product?.productType !== 'combo' ? (product?.unitNotation || '') : ''}
|
||||||
</MyText>
|
</MyText>
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 9.4 KiB |
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"short_name": "Freshyo",
|
"short_name": "TanStack App",
|
||||||
"name": "Freshyo",
|
"name": "Create TanStack App Sample",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.png",
|
"src": "favicon.ico",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/png"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo192.png",
|
"src": "logo192.png",
|
||||||
|
|
@ -20,6 +20,6 @@
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#0033CC",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export default function AddToCartDialog() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-[30vh] space-y-3 overflow-y-auto mb-4">
|
<div className="max-h-[40vh] space-y-3 overflow-y-auto mb-4">
|
||||||
{availableSlots.map((slot: any) => (
|
{availableSlots.map((slot: any) => (
|
||||||
<div
|
<div
|
||||||
key={slot.id}
|
key={slot.id}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@ interface AppLayoutProps {
|
||||||
// Routes where the full shell (sidebar/topbar/bottom nav) should be hidden
|
// Routes where the full shell (sidebar/topbar/bottom nav) should be hidden
|
||||||
const hideShellRoutes = ['/login', '/register', '/checkout', '/home/checkout', '/flash/checkout']
|
const hideShellRoutes = ['/login', '/register', '/checkout', '/home/checkout', '/flash/checkout']
|
||||||
|
|
||||||
// Routes that render their own top bar UI — skip the global Topbar there
|
|
||||||
const selfTopbarRoutes = ['/home/search']
|
|
||||||
|
|
||||||
export function AppLayout({
|
export function AppLayout({
|
||||||
children,
|
children,
|
||||||
showCartBar = true,
|
showCartBar = true,
|
||||||
|
|
@ -31,10 +28,6 @@ export function AppLayout({
|
||||||
hideShell ||
|
hideShell ||
|
||||||
hideShellRoutes.some((route) => currentPath === route || currentPath.startsWith(`${route}/`))
|
hideShellRoutes.some((route) => currentPath === route || currentPath.startsWith(`${route}/`))
|
||||||
|
|
||||||
const hasOwnTopbar = selfTopbarRoutes.some(
|
|
||||||
(route) => currentPath === route || currentPath.startsWith(`${route}/`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// On mobile the cart bar replaces the old floating pill
|
// On mobile the cart bar replaces the old floating pill
|
||||||
const shouldShowCartBar = showCartBar && !shouldHideShell
|
const shouldShowCartBar = showCartBar && !shouldHideShell
|
||||||
|
|
||||||
|
|
@ -47,13 +40,11 @@ export function AppLayout({
|
||||||
{/* Desktop rail */}
|
{/* Desktop rail */}
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<div className="shell-main">
|
{/* Top bar with search + cart */}
|
||||||
{/* Single top bar — logo + search + cart (desktop) / search only (mobile) */}
|
<Topbar isFlashDelivery={isFlashDelivery} />
|
||||||
{!hasOwnTopbar && <Topbar isFlashDelivery={isFlashDelivery} />}
|
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main className="shell-content min-w-0">{children}</main>
|
<main className="shell-content min-w-0">{children}</main>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cart slide-over (desktop) / bottom bar (mobile) */}
|
{/* Cart slide-over (desktop) / bottom bar (mobile) */}
|
||||||
{shouldShowCartBar && <FloatingCartBar isFlashDelivery={isFlashDelivery} />}
|
{shouldShowCartBar && <FloatingCartBar isFlashDelivery={isFlashDelivery} />}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function Dialog({ open, onClose, children, title }: DialogProps) {
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
||||||
|
|
|
||||||
|
|
@ -165,20 +165,20 @@ export function FloatingCartBar({ isFlashDelivery = false }: FloatingCartBarProp
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-1 flex items-center justify-between gap-2">
|
<div className="mt-1.5 flex items-center justify-between">
|
||||||
<MiniQuantifier
|
<MiniQuantifier
|
||||||
value={quantities[item.id] || item.quantity}
|
value={quantities[item.id] || item.quantity}
|
||||||
setValue={(value) => {
|
setValue={(value) => {
|
||||||
if (value === 0) {
|
if (value === 0) {
|
||||||
removeFromCart.mutate(item.productId)
|
removeFromCart.mutate(item.id)
|
||||||
} else {
|
} else {
|
||||||
setQuantities((prev) => ({ ...prev, [item.id]: value }))
|
setQuantities((prev) => ({ ...prev, [item.id]: value }))
|
||||||
updateCartItem.mutate({ productId: item.productId, quantity: value })
|
updateCartItem.mutate({ productId: item.id, quantity: value })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
step={productsById[item.productId]?.incrementStep || 1}
|
step={productsById[item.productId]?.incrementStep || 1}
|
||||||
/>
|
/>
|
||||||
<p className="text-[13px] font-bold text-gray-900 sm:text-sm">
|
<p className="text-sm font-bold text-gray-900">
|
||||||
₹{(() => {
|
₹{(() => {
|
||||||
const product = productsById[item.productId]
|
const product = productsById[item.productId]
|
||||||
const basePrice = product?.price ?? 0
|
const basePrice = product?.price ?? 0
|
||||||
|
|
|
||||||
|
|
@ -104,12 +104,8 @@ export function ProductCard({
|
||||||
className="group flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-brand-300 hover:shadow-md"
|
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 — grows to fill card height in miniView so content area stays tight */}
|
{/* Image */}
|
||||||
<div
|
<div className="relative aspect-[4/3] w-full overflow-hidden bg-gray-50">
|
||||||
className={`relative w-full overflow-hidden bg-gray-50 ${
|
|
||||||
miniView ? 'min-h-[110px] flex-1' : 'aspect-[4/3] shrink-0'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{imageUri && !imageError ? (
|
{imageUri && !imageError ? (
|
||||||
<img
|
<img
|
||||||
src={imageUri}
|
src={imageUri}
|
||||||
|
|
@ -170,19 +166,19 @@ export function ProductCard({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-1 flex-col px-4 pb-3 pt-3">
|
<div className="flex flex-1 flex-col px-4 pb-4 pt-3">
|
||||||
<p className="mb-1.5 text-[13px] font-bold leading-snug text-gray-900 line-clamp-2 sm:text-sm">
|
<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-1 flex items-baseline">
|
<div className="mb-1 flex items-baseline">
|
||||||
<p className="text-[15px] font-extrabold text-brand-600 sm:text-lg">₹{item.price}</p>
|
<p className="text-lg font-extrabold text-brand-600">₹{item.price}</p>
|
||||||
{discountPercent > 0 && (
|
{discountPercent > 0 && (
|
||||||
<p className="ml-2 text-[11px] text-gray-400 line-through sm:text-xs">₹{item.marketPrice}</p>
|
<p className="ml-2 text-xs text-gray-400 line-through">₹{item.marketPrice}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-1 text-[11px] font-medium text-gray-500 sm:text-xs">
|
<p className="mb-3 text-xs font-medium text-gray-500">
|
||||||
{formatQuantity(item.productQuantity || 1, item.unitNotation).display}
|
{formatQuantity(item.productQuantity || 1, item.unitNotation).display}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -219,8 +215,7 @@ export function ProductCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShoppingCart className="h-4 w-4" />
|
<ShoppingCart className="h-4 w-4" />
|
||||||
<span className="md:hidden">Add</span>
|
Add to Cart
|
||||||
<span className="hidden md:inline">Add to Cart</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation, useNavigate } from '@tanstack/react-router'
|
import { useLocation, useNavigate } from '@tanstack/react-router'
|
||||||
import { useAuth } from '../../lib/auth-context'
|
import { useAuth } from '../../lib/auth-context'
|
||||||
import { Home, Store, Zap, Tag, User, LogOut, LogIn } from 'lucide-react'
|
import { Home, Store, Zap, Tag, User, LogOut, LogIn, Beef } from 'lucide-react'
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
path: string
|
path: string
|
||||||
|
|
@ -53,19 +53,15 @@ export function Sidebar() {
|
||||||
<aside className="shell-sidebar sticky top-0 hidden h-screen flex-col border-r border-gray-200 bg-white md:flex">
|
<aside className="shell-sidebar sticky top-0 hidden h-screen flex-col border-r border-gray-200 bg-white md:flex">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="flex h-16 items-center gap-2.5 border-b border-gray-100 px-6">
|
<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 overflow-hidden rounded-xl">
|
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-brand-600">
|
||||||
<img
|
<Beef className="h-5 w-5 text-white" />
|
||||||
src="/freshyo-logo.png"
|
|
||||||
alt="Freshyo logo"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-display text-lg font-extrabold leading-none tracking-tight text-gray-900">
|
<p className="font-display text-lg font-extrabold leading-none tracking-tight text-gray-900">
|
||||||
Freshyo
|
Freshyo
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-gray-400">
|
<p className="text-[10px] font-bold uppercase tracking-[0.18em] text-gray-400">
|
||||||
Delivering Freshness
|
Butcher & Grocer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ import React from 'react'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { SearchBar } from 'web-components'
|
import { SearchBar } from 'web-components'
|
||||||
import { useGetCart } from '../../hooks/cart-query-hooks'
|
import { useGetCart } from '../../hooks/cart-query-hooks'
|
||||||
import { ShoppingCart } from 'lucide-react'
|
import { ShoppingCart, Beef } from 'lucide-react'
|
||||||
|
|
||||||
interface TopbarProps {
|
interface TopbarProps {
|
||||||
isFlashDelivery?: boolean
|
isFlashDelivery?: boolean
|
||||||
onCartClick?: () => void
|
onCartClick?: () => void
|
||||||
|
onSearchClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Topbar({ isFlashDelivery = false, onCartClick }: TopbarProps) {
|
export function Topbar({ isFlashDelivery = false, onCartClick, onSearchClick }: TopbarProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const cartType = isFlashDelivery ? 'flash' : 'regular'
|
const cartType = isFlashDelivery ? 'flash' : 'regular'
|
||||||
const { data: cartData } = useGetCart(cartType)
|
const { data: cartData } = useGetCart(cartType)
|
||||||
|
|
@ -24,35 +25,36 @@ export function Topbar({ isFlashDelivery = false, onCartClick }: TopbarProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchClick = () => {
|
const handleSearchClick = () => {
|
||||||
|
if (onSearchClick) {
|
||||||
|
onSearchClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
navigate({ to: '/home/search' as any })
|
navigate({ to: '/home/search' as any })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="shell-topbar sticky top-0 z-30 border-b border-gray-200 bg-white/95 backdrop-blur-sm">
|
<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">
|
<div className="flex h-16 items-center gap-3 px-4 md:px-6">
|
||||||
{/* Brand — tablet+ only */}
|
{/* Brand — always visible, compact on mobile */}
|
||||||
<div className="hidden shrink-0 items-center gap-2 md:flex">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg md:h-8 md:w-8">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-600 md:h-8 md:w-8">
|
||||||
<img
|
<Beef className="h-5 w-5 text-white md:h-4 md:w-4" />
|
||||||
src="/freshyo-logo.png"
|
|
||||||
alt="Freshyo logo"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="font-display text-lg font-extrabold tracking-tight text-gray-900">
|
<span className="font-display text-lg font-extrabold tracking-tight text-gray-900">
|
||||||
Freshyo
|
Freshyo
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search — always visible */}
|
{/* Search — desktop wide field */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="hidden flex-1 max-w-xl md:block">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="Search chicken, mutton, fish"
|
placeholder="Search chicken, mutton, fish, groceries..."
|
||||||
onClick={handleSearchClick}
|
onClick={handleSearchClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-3">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
{/* Flash status */}
|
{/* Flash status */}
|
||||||
{isFlashDelivery && (
|
{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="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">
|
||||||
|
|
@ -61,11 +63,11 @@ export function Topbar({ isFlashDelivery = false, onCartClick }: TopbarProps) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cart button — tablet+ only */}
|
{/* Cart button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleCartClick}
|
onClick={handleCartClick}
|
||||||
aria-label={`Cart, ${itemCount} items`}
|
aria-label={`Cart, ${itemCount} items`}
|
||||||
className="relative hidden 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:flex md:px-4"
|
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" />
|
<ShoppingCart className="h-5 w-5" />
|
||||||
<span className="hidden sm:inline">Cart</span>
|
<span className="hidden sm:inline">Cart</span>
|
||||||
|
|
@ -77,6 +79,11 @@ export function Topbar({ isFlashDelivery = false, onCartClick }: TopbarProps) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,6 @@ interface CartItem {
|
||||||
deliveryDate?: string | null
|
deliveryDate?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CartData {
|
|
||||||
items: CartItem[]
|
|
||||||
totalItems: number
|
|
||||||
totalAmount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCartKey(cartType: CartType): string {
|
function getCartKey(cartType: CartType): string {
|
||||||
return `local-cart-${cartType}`
|
return `local-cart-${cartType}`
|
||||||
}
|
}
|
||||||
|
|
@ -35,16 +29,15 @@ function writeCart(cartType: CartType, items: CartItem[]) {
|
||||||
localStorage.setItem(getCartKey(cartType), JSON.stringify(items))
|
localStorage.setItem(getCartKey(cartType), JSON.stringify(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the exact shape useGetCart returns, so setQueryData matches the cache.
|
|
||||||
function toCartData(items: CartItem[]): CartData {
|
|
||||||
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
||||||
return { items, totalItems, totalAmount: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGetCart(cartType: CartType = 'regular') {
|
export function useGetCart(cartType: CartType = 'regular') {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [getCartKey(cartType)],
|
queryKey: [getCartKey(cartType)],
|
||||||
queryFn: () => toCartData(readCart(cartType)),
|
queryFn: () => {
|
||||||
|
const items = readCart(cartType)
|
||||||
|
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
|
||||||
|
const totalAmount = 0 // Amount calculated in components with product data
|
||||||
|
return { items, totalItems, totalAmount }
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,9 +76,6 @@ export function useAddToCart(cartType: CartType = 'regular') {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeCart(cartType, items)
|
writeCart(cartType, items)
|
||||||
// Optimistically update the cache so the UI reflects the cart instantly,
|
|
||||||
// before any refetch completes.
|
|
||||||
queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
||||||
|
|
@ -108,19 +98,14 @@ export function useUpdateCartItem(cartType: CartType = 'regular') {
|
||||||
slotId?: number | null
|
slotId?: number | null
|
||||||
deliveryDate?: string | null
|
deliveryDate?: string | null
|
||||||
}) => {
|
}) => {
|
||||||
let items = readCart(cartType)
|
const items = readCart(cartType)
|
||||||
const existing = items.find((i) => i.productId === productId)
|
const existing = items.find((i) => i.productId === productId)
|
||||||
if (!existing) return
|
if (existing) {
|
||||||
if (quantity <= 0) {
|
|
||||||
// Quantity hit 0 — remove the item from the cart entirely
|
|
||||||
items = items.filter((i) => i.productId !== productId)
|
|
||||||
} else {
|
|
||||||
existing.quantity = quantity
|
existing.quantity = quantity
|
||||||
if (slotId !== undefined) existing.slotId = slotId
|
if (slotId !== undefined) existing.slotId = slotId
|
||||||
if (deliveryDate !== undefined) existing.deliveryDate = deliveryDate
|
if (deliveryDate !== undefined) existing.deliveryDate = deliveryDate
|
||||||
}
|
}
|
||||||
writeCart(cartType, items)
|
writeCart(cartType, items)
|
||||||
queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
||||||
|
|
@ -136,7 +121,6 @@ export function useRemoveFromCart(cartType: CartType = 'regular') {
|
||||||
let items = readCart(cartType)
|
let items = readCart(cartType)
|
||||||
items = items.filter((i) => i.productId !== productId)
|
items = items.filter((i) => i.productId !== productId)
|
||||||
writeCart(cartType, items)
|
writeCart(cartType, items)
|
||||||
queryClient.setQueryData([getCartKey(cartType)], toCartData(items))
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
queryClient.invalidateQueries({ queryKey: [getCartKey(cartType)] })
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
import { QueryClient } from '@tanstack/react-query'
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
// Module-level singleton — shared across SSR hydration, tRPC, and all
|
|
||||||
// components so mutations/invalidations hit the same cache.
|
|
||||||
let queryClient: QueryClient | undefined
|
|
||||||
|
|
||||||
export function getQueryClient() {
|
export function getQueryClient() {
|
||||||
if (!queryClient) {
|
return new QueryClient({
|
||||||
queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 30 * 1000,
|
staleTime: 30 * 1000,
|
||||||
|
|
@ -16,5 +11,3 @@ export function getQueryClient() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return queryClient
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ 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 =
|
export const BASE_API_URL =
|
||||||
(import.meta.env?.VITE_API_URL as string | undefined) || 'https://worker.freshyo.in'
|
(import.meta.env?.VITE_API_URL as string | undefined) || 'http://localhost:8787'
|
||||||
|
|
||||||
export const trpc = createTRPCReact<AppRouter>()
|
export const trpc = createTRPCReact<AppRouter>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ function CartPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<p className="text-sm font-extrabold text-brand-600 sm:text-base">₹{price * item.quantity}</p>
|
<p className="text-base font-extrabold text-brand-600">₹{price * item.quantity}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeItem.mutate(item.productId)}
|
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"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
|
||||||
import { useState, useMemo, useRef, useEffect } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { 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 { usePopulateCentralStores } from '../hooks/usePopulateCentralStores'
|
|
||||||
import { usePopulateCentralProductStore } from '../hooks/usePopulateCentralProductStore'
|
|
||||||
import { AppLayout } from '../components/AppLayout'
|
import { AppLayout } from '../components/AppLayout'
|
||||||
import { Zap, ShoppingCart } 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,
|
||||||
|
|
@ -16,10 +14,8 @@ export const Route = createFileRoute('/flash')({
|
||||||
|
|
||||||
function FlashDeliveryPage() {
|
function FlashDeliveryPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const search = useSearch({ from: '/flash' }) as { storeId?: string }
|
||||||
// Populate central stores so flash-eligible products resolve on direct load
|
const storeId = search.storeId ? Number(search.storeId) : undefined
|
||||||
usePopulateCentralStores()
|
|
||||||
usePopulateCentralProductStore()
|
|
||||||
|
|
||||||
const { data: storesData } = useStores()
|
const { data: storesData } = useStores()
|
||||||
const { productsById } = useCentralProductStore()
|
const { productsById } = useCentralProductStore()
|
||||||
|
|
@ -30,36 +26,6 @@ function FlashDeliveryPage() {
|
||||||
const addToCart = useAddToCart('flash')
|
const addToCart = useAddToCart('flash')
|
||||||
const { data: cartData } = useGetCart('flash')
|
const { data: cartData } = useGetCart('flash')
|
||||||
|
|
||||||
// Store tabs — one tab per store (same model as home's category tabs)
|
|
||||||
const storesWithFlash = useMemo(() => {
|
|
||||||
return stores.filter((store: any) =>
|
|
||||||
Object.values(productsById).some(
|
|
||||||
(p: any) =>
|
|
||||||
p?.storeId === store.id &&
|
|
||||||
productSlotsMap[p.id]?.isFlashAvailable &&
|
|
||||||
!productSlotsMap[p.id]?.isOutOfStock
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, [stores, productsById, productSlotsMap])
|
|
||||||
|
|
||||||
const [selectedStoreId, setSelectedStoreId] = useState<number | null>(null)
|
|
||||||
const activeStoreId = selectedStoreId ?? storesWithFlash[0]?.id ?? null
|
|
||||||
|
|
||||||
// Auto-scroll the active store tab into view (matches user-ui tab behavior)
|
|
||||||
const tabsScrollRef = useRef<HTMLDivElement>(null)
|
|
||||||
const tabPositions = useRef<number[]>([])
|
|
||||||
const activeTabIndex = Math.max(
|
|
||||||
0,
|
|
||||||
storesWithFlash.findIndex((s: any) => s.id === activeStoreId)
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const x = tabPositions.current[activeTabIndex]
|
|
||||||
if (x != null) {
|
|
||||||
tabsScrollRef.current?.scrollTo({ left: Math.max(0, x - 16), behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
}, [activeTabIndex])
|
|
||||||
|
|
||||||
// Get flash products from central store
|
// Get flash products from central store
|
||||||
const allFlashProducts = useMemo(() => {
|
const allFlashProducts = useMemo(() => {
|
||||||
return Object.values(productsById).filter(
|
return Object.values(productsById).filter(
|
||||||
|
|
@ -70,18 +36,15 @@ function FlashDeliveryPage() {
|
||||||
)
|
)
|
||||||
}, [productsById, productSlotsMap])
|
}, [productsById, productSlotsMap])
|
||||||
|
|
||||||
// Products to show: active store's flash items, or all flash items when no stores
|
// Filter by store if selected
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = storeId
|
||||||
if (activeStoreId != null) {
|
? allFlashProducts.filter((p: any) => p.storeId === storeId)
|
||||||
return allFlashProducts.filter((p: any) => p.storeId === activeStoreId)
|
: allFlashProducts
|
||||||
}
|
|
||||||
return allFlashProducts
|
|
||||||
}, [activeStoreId, allFlashProducts])
|
|
||||||
|
|
||||||
const handleAddToCart = (productId: number) => {
|
const handleAddToCart = (productId: number) => {
|
||||||
const item = filteredProducts.find((p: any) => p.id === productId)
|
const item = filteredProducts.find((p: any) => p.id === productId)
|
||||||
addToCart.mutate(
|
addToCart.mutate(
|
||||||
{ productId, quantity: 1, storeId: item?.storeId ?? 0 },
|
{ productId, quantity: 1, storeId: item?.storeId },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
alert(`Added ${item?.name || 'item'} for 1 Hr Delivery`)
|
alert(`Added ${item?.name || 'item'} for 1 Hr Delivery`)
|
||||||
|
|
@ -106,7 +69,24 @@ function FlashDeliveryPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8">
|
<div className="mx-auto flex w-full max-w-7xl gap-6 px-4 py-6 md:px-8">
|
||||||
|
{/* Store sidebar — desktop: vertical list; mobile: horizontal strip */}
|
||||||
|
<StoreSidebar
|
||||||
|
stores={stores}
|
||||||
|
storeId={storeId}
|
||||||
|
onStoreSelect={(newStoreId) =>
|
||||||
|
navigate({
|
||||||
|
to: '/flash',
|
||||||
|
search: { storeId: newStoreId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onAllSelect={() =>
|
||||||
|
navigate({ to: '/flash' })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Products Grid */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
{/* Info Banner */}
|
{/* Info Banner */}
|
||||||
<div className="mb-4 rounded-xl border border-yellow-200 bg-yellow-50 p-3">
|
<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">
|
||||||
|
|
@ -114,45 +94,10 @@ function FlashDeliveryPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Store tabs — same model as home page tabs */}
|
|
||||||
{storesWithFlash.length > 0 && (
|
|
||||||
<div
|
|
||||||
ref={tabsScrollRef}
|
|
||||||
className="scrollbar-hide -mx-4 mb-5 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0"
|
|
||||||
>
|
|
||||||
{storesWithFlash.map((store: any, i: number) => {
|
|
||||||
const active = activeStoreId === store.id
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={store.id}
|
|
||||||
ref={(el) => {
|
|
||||||
tabPositions.current[i] = el?.offsetLeft ?? 0
|
|
||||||
}}
|
|
||||||
onClick={() => setSelectedStoreId(store.id)}
|
|
||||||
className="shrink-0 cursor-pointer px-3 py-2"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`block text-sm font-bold ${
|
|
||||||
active ? 'text-flash-600' : 'text-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{store.name.replace(/^The\s+/i, '')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`mt-1 block h-1 rounded-full ${
|
|
||||||
active ? 'bg-flash-500' : 'bg-transparent'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-lg font-extrabold text-gray-900">
|
<p className="text-lg font-extrabold text-gray-900">
|
||||||
{activeStoreId != null
|
{storeId
|
||||||
? stores.find((s: any) => s.id === activeStoreId)?.name.replace(/^The\s+/i, '') ||
|
? stores.find((s: any) => s.id === storeId)?.name ||
|
||||||
'Store Products'
|
'Store Products'
|
||||||
: 'All Products'}
|
: 'All Products'}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -161,7 +106,7 @@ function FlashDeliveryPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3 sm:gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
<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}
|
||||||
|
|
@ -180,16 +125,124 @@ function FlashDeliveryPage() {
|
||||||
{filteredProducts.length === 0 && (
|
{filteredProducts.length === 0 && (
|
||||||
<div className="py-10 text-center">
|
<div className="py-10 text-center">
|
||||||
<p className="font-medium text-gray-400">
|
<p className="font-medium text-gray-400">
|
||||||
No flash delivery products available.
|
{storeId
|
||||||
|
? 'No flash delivery products from this store.'
|
||||||
|
: 'No flash delivery products available.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StoreSidebarProps {
|
||||||
|
stores: any[];
|
||||||
|
storeId?: number;
|
||||||
|
onStoreSelect: (storeId: number) => void;
|
||||||
|
onAllSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StoreSidebar({
|
||||||
|
stores,
|
||||||
|
storeId,
|
||||||
|
onStoreSelect,
|
||||||
|
onAllSelect,
|
||||||
|
}: StoreSidebarProps) {
|
||||||
|
const allActive = !storeId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile: horizontal chip strip (always visible) */}
|
||||||
|
<div className="scrollbar-hide -mx-4 mb-4 flex gap-2 overflow-x-auto px-4 md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={onAllSelect}
|
||||||
|
className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
|
||||||
|
allActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{stores.map((store: any) => {
|
||||||
|
const isActive = storeId === store.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={store.id}
|
||||||
|
onClick={() => onStoreSelect(store.id)}
|
||||||
|
className={`shrink-0 rounded-full px-4 py-2 text-sm font-bold transition-colors ${
|
||||||
|
isActive ? 'bg-flash-500 text-white' : 'border border-gray-200 bg-white text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{store.name.replace(/^The\s+/i, '')}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const formatQuantity = (
|
const formatQuantity = (
|
||||||
quantity: number,
|
quantity: number,
|
||||||
unit: string,
|
unit: string,
|
||||||
|
|
@ -277,20 +330,22 @@ function CompactProductCard({
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<p className="mb-1 line-clamp-2 text-xs font-bold 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 flex-wrap items-baseline gap-x-1.5">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-wrap items-baseline">
|
||||||
<p className="text-sm font-extrabold text-flash-500">₹{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="text-[11px] text-gray-400 line-through">
|
<p className="ml-1 text-xs text-gray-400 line-through">
|
||||||
₹{item.marketPrice}
|
₹{item.marketPrice}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-[11px] text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
<span className="font-semibold text-flash-500">
|
<span className="font-semibold text-flash-500">
|
||||||
{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
|
{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ function CartPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<p className="text-sm font-extrabold text-brand-600 sm:text-base">₹{price * item.quantity}</p>
|
<p className="text-base font-extrabold text-brand-600">₹{price * item.quantity}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeItem.mutate(item.productId)}
|
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"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +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 { SearchBar } from 'web-components'
|
||||||
import {
|
import {
|
||||||
useAllProducts,
|
useAllProducts,
|
||||||
useStores,
|
useStores,
|
||||||
|
|
@ -15,7 +16,7 @@ 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, Clock, Truck } from 'lucide-react'
|
import { Store, ImageOff, Loader2, Clock, Beef, Truck } from 'lucide-react'
|
||||||
|
|
||||||
// Scroll Indicator Component
|
// Scroll Indicator Component
|
||||||
function ScrollIndicator({
|
function ScrollIndicator({
|
||||||
|
|
@ -114,21 +115,6 @@ function HomePage() {
|
||||||
setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }))
|
setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scroll the active tab into view (matches user-ui tab behavior)
|
|
||||||
const tabsScrollRef = useRef<HTMLDivElement>(null)
|
|
||||||
const tabPositions = useRef<number[]>([])
|
|
||||||
const activeTabIndex = Math.max(
|
|
||||||
0,
|
|
||||||
dashboardTags.findIndex((t: any) => t.id === activeTagId)
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const x = tabPositions.current[activeTabIndex]
|
|
||||||
if (x != null) {
|
|
||||||
tabsScrollRef.current?.scrollTo({ left: Math.max(0, x - 16), behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
}, [activeTabIndex])
|
|
||||||
|
|
||||||
const productsByTagId = useMemo(() => {
|
const productsByTagId = useMemo(() => {
|
||||||
const map: Record<number, any[]> = {}
|
const map: Record<number, any[]> = {}
|
||||||
for (const tag of dashboardTags) {
|
for (const tag of dashboardTags) {
|
||||||
|
|
@ -215,10 +201,10 @@ function HomePage() {
|
||||||
{/* Welcome band — desktop editorial opener */}
|
{/* Welcome band — desktop editorial opener */}
|
||||||
<div className="flex flex-col gap-1 py-6 md:py-8">
|
<div className="flex flex-col gap-1 py-6 md:py-8">
|
||||||
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
|
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
|
||||||
Smiles delivered with
|
Fresh cut, daily
|
||||||
</p>
|
</p>
|
||||||
<h1 className="display-1 text-gray-900">
|
<h1 className="display-1 text-gray-900">
|
||||||
The best harvest, delivered to your door
|
Butcher-fresh meat, delivered to your door
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 max-w-2xl text-gray-600">
|
<p className="mt-1 max-w-2xl text-gray-600">
|
||||||
Chicken, mutton, fish and more, cut to order from our own stores.
|
Chicken, mutton, fish and more, cut to order from our own stores.
|
||||||
|
|
@ -229,12 +215,8 @@ function HomePage() {
|
||||||
{/* Download App Banner */}
|
{/* Download App Banner */}
|
||||||
<div className="mb-8 overflow-hidden rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 px-6 py-4 md:flex md:items-center md:justify-between">
|
<div className="mb-8 overflow-hidden rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 px-6 py-4 md:flex md:items-center md:justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="hidden h-12 w-12 items-center justify-center overflow-hidden rounded-xl bg-white/20 md:flex">
|
<div className="hidden h-12 w-12 items-center justify-center rounded-xl bg-white/20 md:flex">
|
||||||
<img
|
<Beef className="h-6 w-6 text-white" />
|
||||||
src="/freshyo-logo.png"
|
|
||||||
alt="Freshyo logo"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
|
<p className="text-lg font-bold text-white">Get the FreshYo App</p>
|
||||||
|
|
@ -254,89 +236,6 @@ function HomePage() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Explore Products Section — first, matching user-ui home order */}
|
|
||||||
{dashboardTags.length > 0 && (
|
|
||||||
<section className="mb-10">
|
|
||||||
<div className="section-rule mb-5 flex items-end justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
|
|
||||||
Browse by Category
|
|
||||||
</p>
|
|
||||||
<h2 className="display-2 mt-1 text-gray-900">Explore Products</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab strip — flat, brand underline active (matches user-ui home) */}
|
|
||||||
<div
|
|
||||||
ref={tabsScrollRef}
|
|
||||||
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, i: number) => {
|
|
||||||
const active = activeTagId === tag.id
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tag.id}
|
|
||||||
ref={(el) => {
|
|
||||||
tabPositions.current[i] = el?.offsetLeft ?? 0
|
|
||||||
}}
|
|
||||||
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-3 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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stores Section */}
|
{/* Stores Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<div className="section-rule mb-5 flex items-end justify-between">
|
<div className="section-rule mb-5 flex items-end justify-between">
|
||||||
|
|
@ -375,6 +274,83 @@ function HomePage() {
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Explore Products Section */}
|
||||||
|
{dashboardTags.length > 0 && (
|
||||||
|
<section className="mb-10">
|
||||||
|
<div className="section-rule mb-5 flex items-end justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-brand-600">
|
||||||
|
Browse by Category
|
||||||
|
</p>
|
||||||
|
<h2 className="display-2 mt-1 text-gray-900">Explore Products</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab strip — flat, brand underline active (matches user-ui home) */}
|
||||||
|
<div className="scrollbar-hide -mx-4 mb-5 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0">
|
||||||
|
{dashboardTags.map((tag: any) => {
|
||||||
|
const active = activeTagId === tag.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tag.id}
|
||||||
|
onClick={() => setSelectedTagId(tag.id)}
|
||||||
|
className="shrink-0 cursor-pointer px-3 py-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`block text-sm font-bold ${
|
||||||
|
active ? 'text-brand-600' : 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag.tagName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`mt-1 block h-1 rounded-full ${
|
||||||
|
active ? 'bg-brand-500' : 'bg-transparent'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active tag products — brand-tinted container (matches user-ui) */}
|
||||||
|
<div className="mb-2 rounded-[28px] border border-brand-100 bg-brand-25 px-3 pb-4 pt-3">
|
||||||
|
{activeTagProducts.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||||
|
{activeTagProducts
|
||||||
|
.slice(0, expandedByTagId[activeTagId] ? undefined : 6)
|
||||||
|
.map((product: any) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
item={product}
|
||||||
|
onPress={() => handleProductPress(product.id)}
|
||||||
|
showDeliveryInfo={false}
|
||||||
|
miniView={true}
|
||||||
|
useAddToCartDialog={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{activeTagProducts.length > 6 && !expandedByTagId[activeTagId] && (
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleExpand(activeTagId)}
|
||||||
|
className="rounded-full bg-brand-500 px-6 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
Show More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="py-6 text-center text-sm text-gray-500">
|
||||||
|
No products in this category yet
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Upcoming Delivery Slots Section */}
|
{/* Upcoming Delivery Slots Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<div className="section-rule mb-5 flex items-end justify-between">
|
<div className="section-rule mb-5 flex items-end justify-between">
|
||||||
|
|
@ -419,14 +395,14 @@ function HomePage() {
|
||||||
{productsLoading ? (
|
{productsLoading ? (
|
||||||
<SectionSpinner label="Loading products..." />
|
<SectionSpinner label="Loading products..." />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-3 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||||
{sortedProducts.map((product) => (
|
{sortedProducts.map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={product.id}
|
key={product.id}
|
||||||
item={product}
|
item={product}
|
||||||
onPress={() => handleProductPress(product.id)}
|
onPress={() => handleProductPress(product.id)}
|
||||||
showDeliveryInfo={true}
|
showDeliveryInfo={true}
|
||||||
miniView={true}
|
miniView={false}
|
||||||
useAddToCartDialog={true}
|
useAddToCartDialog={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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 { MyButton, pInput as PInput } 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 })
|
||||||
|
|
||||||
|
|
@ -130,12 +131,8 @@ function LoginPage() {
|
||||||
{/* Brand panel — desktop only */}
|
{/* Brand panel — desktop only */}
|
||||||
<div className="hidden w-[42%] flex-col justify-between bg-gradient-to-b from-brand-500 to-brand-700 p-12 lg:flex">
|
<div className="hidden w-[42%] flex-col justify-between bg-gradient-to-b from-brand-500 to-brand-700 p-12 lg:flex">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-11 w-11 items-center justify-center overflow-hidden rounded-xl">
|
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/20">
|
||||||
<img
|
<Beef className="h-6 w-6 text-white" />
|
||||||
src="/freshyo-logo.png"
|
|
||||||
alt="Freshyo logo"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-display text-xl font-extrabold tracking-tight text-white">
|
<p className="font-display text-xl font-extrabold tracking-tight text-white">
|
||||||
|
|
@ -165,12 +162,8 @@ function LoginPage() {
|
||||||
<div className="flex flex-1 items-center justify-center p-6">
|
<div className="flex flex-1 items-center justify-center p-6">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="mb-8 flex items-center gap-3 lg:hidden">
|
<div className="mb-8 flex items-center gap-3 lg:hidden">
|
||||||
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-brand-600">
|
||||||
<img
|
<Beef className="h-5 w-5 text-white" />
|
||||||
src="/freshyo-logo.png"
|
|
||||||
alt="Freshyo logo"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
|
<p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
|
||||||
Freshyo
|
Freshyo
|
||||||
|
|
|
||||||
|
|
@ -77,22 +77,20 @@ body {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* App shell — full-height flex column for content; sidebar rail at md+.
|
/* App shell — single column on mobile, sidebar + topbar grid at md+ */
|
||||||
Sticky topbar/search live inside .shell-main so they can stick against
|
|
||||||
the viewport while scrolling. */
|
|
||||||
.shell-grid {
|
.shell-grid {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "main";
|
grid-template-areas: "topbar" "content";
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
grid-template-rows: minmax(0, 1fr);
|
grid-template-rows: 64px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.shell-grid {
|
.shell-grid {
|
||||||
grid-template-areas: "sidebar main";
|
grid-template-areas: "sidebar topbar" "sidebar content";
|
||||||
grid-template-columns: 20vw minmax(0, 1fr);
|
grid-template-columns: 264px minmax(0, 1fr);
|
||||||
grid-template-rows: minmax(0, 1fr);
|
grid-template-rows: 64px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,21 +98,12 @@ body {
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flex column so sticky header elements are bounded by the full column,
|
|
||||||
not a fixed grid row. */
|
|
||||||
.shell-main {
|
|
||||||
grid-area: main;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell-topbar {
|
.shell-topbar {
|
||||||
flex-shrink: 0;
|
grid-area: topbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-content {
|
.shell-content {
|
||||||
flex: 1;
|
grid-area: content;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ export interface ProductBasicData {
|
||||||
flashPrice: string | null
|
flashPrice: string | null
|
||||||
productType: string
|
productType: string
|
||||||
isComboOnly: boolean
|
isComboOnly: boolean
|
||||||
isOffer: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailabilityCacheData {
|
export interface AvailabilityCacheData {
|
||||||
|
|
@ -151,7 +150,6 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
||||||
flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null,
|
flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null,
|
||||||
productType: sku.product?.productType ?? 'item',
|
productType: sku.product?.productType ?? 'item',
|
||||||
isComboOnly: sku.isComboOnly ?? false,
|
isComboOnly: sku.isComboOnly ?? false,
|
||||||
isOffer: sku.isOffer ?? false,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function BottomDialog({
|
||||||
if (!visible && !open) return null
|
if (!visible && !open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[100] flex items-end justify-center md:items-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-black/30 transition-opacity duration-200 ${
|
className={`absolute inset-0 bg-black/30 transition-opacity duration-200 ${
|
||||||
open ? 'opacity-100' : 'opacity-0'
|
open ? 'opacity-100' : 'opacity-0'
|
||||||
|
|
@ -38,7 +38,7 @@ export function BottomDialog({
|
||||||
onClick={enableDismiss ? onClose : undefined}
|
onClick={enableDismiss ? onClose : undefined}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`relative w-full max-w-lg rounded-t-2xl bg-white p-6 pb-24 shadow-lg transition-transform duration-200 md:pb-6 ${
|
className={`relative w-full max-w-lg rounded-t-2xl bg-white p-6 shadow-lg transition-transform duration-200 ${
|
||||||
open ? 'translate-y-0' : 'translate-y-full'
|
open ? 'translate-y-0' : 'translate-y-full'
|
||||||
}`}
|
}`}
|
||||||
style={{ maxHeight: '85vh' }}
|
style={{ maxHeight: '85vh' }}
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,7 @@ export function Quantifier({
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center rounded-lg border border-gray-200">
|
<div className="inline-flex items-center rounded-lg border border-gray-200">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={decrease}
|
||||||
e.stopPropagation()
|
|
||||||
decrease()
|
|
||||||
}}
|
|
||||||
disabled={value <= min}
|
disabled={value <= min}
|
||||||
className="flex h-8 w-8 items-center justify-center text-gray-500 hover:text-gray-700 disabled:opacity-30"
|
className="flex h-8 w-8 items-center justify-center text-gray-500 hover:text-gray-700 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
|
|
@ -43,10 +40,7 @@ export function Quantifier({
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={increase}
|
||||||
e.stopPropagation()
|
|
||||||
increase()
|
|
||||||
}}
|
|
||||||
disabled={value >= max}
|
disabled={value >= max}
|
||||||
className="flex h-8 w-8 items-center justify-center text-gray-500 hover:text-gray-700 disabled:opacity-30"
|
className="flex h-8 w-8 items-center justify-center text-gray-500 hover:text-gray-700 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
|
|
@ -64,12 +58,9 @@ export function MiniQuantifier({
|
||||||
max = 99,
|
max = 99,
|
||||||
}: QuantifierProps) {
|
}: QuantifierProps) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center rounded-md border border-gray-200 bg-white shadow-sm">
|
<div className="inline-flex items-center rounded-md border border-gray-200">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={() => value > min && setValue(value - step)}
|
||||||
e.stopPropagation()
|
|
||||||
if (value > min) setValue(value - step)
|
|
||||||
}}
|
|
||||||
disabled={value <= min}
|
disabled={value <= min}
|
||||||
className="flex h-6 w-6 items-center justify-center text-gray-500 disabled:opacity-30"
|
className="flex h-6 w-6 items-center justify-center text-gray-500 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
|
|
@ -79,10 +70,7 @@ export function MiniQuantifier({
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={() => value < max && setValue(value + step)}
|
||||||
e.stopPropagation()
|
|
||||||
if (value < max) setValue(value + step)
|
|
||||||
}}
|
|
||||||
disabled={value >= max}
|
disabled={value >= max}
|
||||||
className="flex h-6 w-6 items-center justify-center text-gray-500 disabled:opacity-30"
|
className="flex h-6 w-6 items-center justify-center text-gray-500 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
-- ============================================================
|
|
||||||
-- clear-db-old.sql — DROP ALL TABLES of the PRE-SKU-SPLIT database
|
|
||||||
--
|
|
||||||
-- This targets a database migrated only up to:
|
|
||||||
-- 0000_nifty_sauron.sql
|
|
||||||
-- 0001_migrate_product_slots.sql
|
|
||||||
-- (i.e. 0002_sku_split.sql has NOT been applied)
|
|
||||||
--
|
|
||||||
-- In this state there are NO product_skus / sku_features /
|
|
||||||
-- product_market_stats / product_combos tables, and product_slots
|
|
||||||
-- has already been dropped by 0001 (hence its IF EXISTS below).
|
|
||||||
--
|
|
||||||
-- Usage:
|
|
||||||
-- sqlite3 <your.db> < scripts/clear-db-old.sql
|
|
||||||
-- wrangler d1 execute <db-name> --local --file scripts/clear-db-old.sql
|
|
||||||
-- wrangler d1 execute <db-name> --remote --file scripts/clear-db-old.sql
|
|
||||||
--
|
|
||||||
-- WARNING: This DROPS every table in the database. There is no undo.
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Don't enforce FK constraints while dropping (ordering below is
|
|
||||||
-- child-first anyway, so this is belt-and-braces).
|
|
||||||
PRAGMA defer_foreign_keys = on;
|
|
||||||
PRAGMA foreign_keys = OFF;
|
|
||||||
|
|
||||||
-- ---- leaf tables (no inbound references) ----
|
|
||||||
DROP TABLE IF EXISTS order_items;
|
|
||||||
DROP TABLE IF EXISTS cart_items;
|
|
||||||
DROP TABLE IF EXISTS special_deals;
|
|
||||||
DROP TABLE IF EXISTS coupon_applicable_products;
|
|
||||||
DROP TABLE IF EXISTS product_reviews;
|
|
||||||
DROP TABLE IF EXISTS product_tags;
|
|
||||||
DROP TABLE IF EXISTS product_group_membership;
|
|
||||||
|
|
||||||
-- ---- product hierarchy (old flat product_info) ----
|
|
||||||
DROP TABLE IF EXISTS product_slots;
|
|
||||||
DROP TABLE IF EXISTS product_info;
|
|
||||||
DROP TABLE IF EXISTS store_info;
|
|
||||||
|
|
||||||
-- ---- orders & dependents ----
|
|
||||||
DROP TABLE IF EXISTS coupon_usage;
|
|
||||||
DROP TABLE IF EXISTS order_status;
|
|
||||||
DROP TABLE IF EXISTS payments;
|
|
||||||
DROP TABLE IF EXISTS refunds;
|
|
||||||
DROP TABLE IF EXISTS complaints;
|
|
||||||
DROP TABLE IF EXISTS user_incidents;
|
|
||||||
DROP TABLE IF EXISTS orders;
|
|
||||||
DROP TABLE IF EXISTS payment_info;
|
|
||||||
DROP TABLE IF EXISTS coupon_applicable_users;
|
|
||||||
|
|
||||||
-- ---- coupons & snippets ----
|
|
||||||
DROP TABLE IF EXISTS coupons;
|
|
||||||
DROP TABLE IF EXISTS reserved_coupons;
|
|
||||||
DROP TABLE IF EXISTS vendor_snippets;
|
|
||||||
DROP TABLE IF EXISTS delivery_slot_info;
|
|
||||||
|
|
||||||
-- ---- content/catalog ----
|
|
||||||
DROP TABLE IF EXISTS home_banners;
|
|
||||||
DROP TABLE IF EXISTS product_group_info;
|
|
||||||
DROP TABLE IF EXISTS product_tag_info;
|
|
||||||
DROP TABLE IF EXISTS product_categories;
|
|
||||||
DROP TABLE IF EXISTS units;
|
|
||||||
DROP TABLE IF EXISTS upload_url_status;
|
|
||||||
|
|
||||||
-- ---- notifications / misc ----
|
|
||||||
DROP TABLE IF EXISTS user_notifications;
|
|
||||||
DROP TABLE IF EXISTS notifications;
|
|
||||||
DROP TABLE IF EXISTS notif_creds;
|
|
||||||
DROP TABLE IF EXISTS key_val_store;
|
|
||||||
|
|
||||||
-- ---- addresses ----
|
|
||||||
DROP TABLE IF EXISTS addresses;
|
|
||||||
DROP TABLE IF EXISTS address_areas;
|
|
||||||
DROP TABLE IF EXISTS address_zones;
|
|
||||||
|
|
||||||
-- ---- users ----
|
|
||||||
DROP TABLE IF EXISTS unlogged_user_tokens;
|
|
||||||
DROP TABLE IF EXISTS user_creds;
|
|
||||||
DROP TABLE IF EXISTS user_details;
|
|
||||||
DROP TABLE IF EXISTS users;
|
|
||||||
|
|
||||||
-- ---- staff ----
|
|
||||||
DROP TABLE IF EXISTS staff_role_permissions;
|
|
||||||
DROP TABLE IF EXISTS staff_users;
|
|
||||||
DROP TABLE IF EXISTS staff_roles;
|
|
||||||
DROP TABLE IF EXISTS staff_permissions;
|
|
||||||
|
|
||||||
-- (sqlite_sequence is internal; SQLite drops it automatically once the last
|
|
||||||
-- AUTOINCREMENT table is gone)
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
PRAGMA defer_foreign_keys = off;
|
|
||||||
|
|
||||||
-- Optionally reclaim disk space (sqlite3 CLI only; not supported in D1):
|
|
||||||
-- VACUUM;
|
|
||||||
Loading…
Add table
Reference in a new issue