From 586eeecbf5ea5a7e0efd39887ec3106e322ea4e4 Mon Sep 17 00:00:00 2001 From: sriram Date: Thu, 13 Aug 2026 16:03:51 +0530 Subject: [PATCH] update backend apis --- src/api/client.js | 5 ++ src/components/BrandCard.jsx | 79 +++++++++++++++++++++++ src/components/BrandGrid.jsx | 42 +++++++++++++ src/pages/HomePage.jsx | 117 ++++++++++++++++++++++++++--------- 4 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 src/components/BrandCard.jsx create mode 100644 src/components/BrandGrid.jsx diff --git a/src/api/client.js b/src/api/client.js index 3249230..89d6a65 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -163,6 +163,11 @@ export const api = { getBrands: () => request('/api/brands'), + // Richer per-brand summaries for the home page card grid. `getBrands` above + // still returns plain names and is what the sidebar and admin pages use. + getBrandCards: ({ refresh = false } = {}) => + request(`/api/brands/overview${qs({ refresh: refresh || undefined })}`), + getBrandCategories: (brand) => request(`/api/brands/${encodeURIComponent(brand)}/categories`), getBrandProducts: (brand, { category, limit = SHOW_ALL_PRODUCTS_LIMIT, offset = 0 } = {}) => diff --git a/src/components/BrandCard.jsx b/src/components/BrandCard.jsx new file mode 100644 index 0000000..9579216 --- /dev/null +++ b/src/components/BrandCard.jsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { Package, Tag } from 'lucide-react'; + +export function BrandCard({ brand, onClick }) { + // Hooks run before the null guard, matching ProductCard: React identifies + // hooks by call order, so bailing out above them would change how many run + // between renders if `brand` ever went from set to null. + const [imgError, setImgError] = useState(false); + + if (!brand) return null; + + const { + name = 'Brand', + product_count = 0, + category_count = 0, + categories = [], + image_url, + initials = '?', + } = brand; + + const showImage = Boolean(image_url) && !imgError; + + return ( + + ); +} diff --git a/src/components/BrandGrid.jsx b/src/components/BrandGrid.jsx new file mode 100644 index 0000000..224e63e --- /dev/null +++ b/src/components/BrandGrid.jsx @@ -0,0 +1,42 @@ +import { Boxes } from 'lucide-react'; +import { BrandCard } from './BrandCard'; +import { Spinner, EmptyState } from './Atoms'; + +export function BrandGrid({ brands, loading, error, emptyTitle, emptySubtitle, onSelect }) { + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + + ); + } + + if (!brands || brands.length === 0) { + return ( + + ); + } + + // Denser than the product grid - a brand tile carries far less content. + return ( +
+ {brands.map((b) => ( + onSelect?.(b)} /> + ))} +
+ ); +} diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index e860cf9..391591e 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react'; import { Sidebar } from '../components/Sidebar'; import { TopBar } from '../components/TopBar'; import { ProductGrid } from '../components/ProductGrid'; +import { BrandGrid } from '../components/BrandGrid'; import { ProductModal } from '../components/ProductModal'; import { ChatPanel } from '../components/ChatPanel'; import { api } from '../api/client'; @@ -12,6 +13,14 @@ export function HomePage() { const [brands, setBrands] = useState([]); const [brandsLoading, setBrandsLoading] = useState(true); + // `selectedBrand === null` already means "All brands - show every product", + // so it cannot also mean "show the brand cards". This is the explicit + // landing-vs-listing switch. + const [view, setView] = useState('brands'); // 'brands' | 'products' + const [brandCards, setBrandCards] = useState([]); + const [brandCardsLoading, setBrandCardsLoading] = useState(true); + const [brandCardsError, setBrandCardsError] = useState(null); + const [categories, setCategories] = useState([]); const [categoriesLoading, setCategoriesLoading] = useState(false); @@ -37,14 +46,27 @@ export function HomePage() { .finally(() => setBrandsLoading(false)); }, []); + const loadBrandCards = useCallback(({ refresh = false } = {}) => { + setBrandCardsLoading(true); + setBrandCardsError(null); + api.getBrandCards({ refresh }) + .then((res) => setBrandCards(res.brands || [])) + .catch((err) => { + setBrandCards([]); + setBrandCardsError(err.message); + }) + .finally(() => setBrandCardsLoading(false)); + }, []); + // Initial load: health + brands, then poll health every 30s useEffect(() => { loadBrands(); + loadBrandCards(); const checkHealth = () => api.getHealth().then(setHealth).catch(() => setHealth(null)); checkHealth(); const interval = setInterval(checkHealth, 30000); return () => clearInterval(interval); - }, [loadBrands]); + }, [loadBrands, loadBrandCards]); // Load categories + default browse listing whenever the brand changes useEffect(() => { @@ -87,13 +109,16 @@ export function HomePage() { }); }, [selectedBrand, selectedCategory]); - // Re-browse whenever brand/category changes (only relevant once a search hasn't overridden it) + // Re-browse whenever brand/category changes (only relevant once a search + // hasn't overridden it). Gating on `view` also stops the landing page firing + // an unbounded /api/products request before the user has asked for one. useEffect(() => { - if (mode === 'search' && !query) browse(); + if (mode === 'search' && !query && view === 'products') browse(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedBrand, selectedCategory]); + }, [selectedBrand, selectedCategory, view]); function runSearch() { + setView('products'); if (!query.trim()) { browse(); return; @@ -129,9 +154,9 @@ export function HomePage() { categoriesLoading={categoriesLoading} selectedBrand={selectedBrand} selectedCategory={selectedCategory} - onSelectBrand={(b) => { setSelectedBrand(b); setQuery(''); }} + onSelectBrand={(b) => { setSelectedBrand(b); setQuery(''); setView('products'); }} onSelectCategory={setSelectedCategory} - onRefreshBrands={loadBrands} + onRefreshBrands={() => { loadBrands(); loadBrandCards({ refresh: true }); }} />
@@ -147,36 +172,70 @@ export function HomePage() { setSelectedCategory(null); setQuery(''); setMode('search'); + setView('brands'); }} />
{mode === 'search' ? (
-
-

- {hasSearched - ? `Results for "${query}"` - : selectedBrand - ? `Browsing ${selectedBrand}${selectedCategory ? ` · ${selectedCategory}` : ''}` - : 'Browsing All Brands'} -

- {products.length > 0 &&

{products.length} shown

} -
+ {view === 'brands' ? ( + <> +
+

Browse Brands

+ {brandCards.length > 0 && ( +

+ {brandCards.length} brands ·{' '} + {brandCards.reduce((n, b) => n + (b.product_count || 0), 0).toLocaleString()} products +

+ )} +
- + { setSelectedBrand(b.name); setQuery(''); setView('products'); }} + /> + + ) : ( + <> +
+
+ +

+ {hasSearched + ? `Results for "${query}"` + : selectedBrand + ? `Browsing ${selectedBrand}${selectedCategory ? ` · ${selectedCategory}` : ''}` + : 'Browsing All Brands'} +

+
+ {products.length > 0 &&

{products.length} shown

} +
+ + + + )}
) : (