update backend apis
This commit is contained in:
@@ -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 }); }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
@@ -147,36 +172,70 @@ export function HomePage() {
|
||||
setSelectedCategory(null);
|
||||
setQuery('');
|
||||
setMode('search');
|
||||
setView('brands');
|
||||
}}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
{mode === 'search' ? (
|
||||
<div className="p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="font-display text-sm font-semibold text-ink-900">
|
||||
{hasSearched
|
||||
? `Results for "${query}"`
|
||||
: selectedBrand
|
||||
? `Browsing ${selectedBrand}${selectedCategory ? ` · ${selectedCategory}` : ''}`
|
||||
: 'Browsing All Brands'}
|
||||
</p>
|
||||
{products.length > 0 && <p className="text-xs text-slate-500">{products.length} shown</p>}
|
||||
</div>
|
||||
{view === 'brands' ? (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="font-display text-sm font-semibold text-ink-900">Browse Brands</p>
|
||||
{brandCards.length > 0 && (
|
||||
<p className="text-xs text-slate-500">
|
||||
{brandCards.length} brands ·{' '}
|
||||
{brandCards.reduce((n, b) => n + (b.product_count || 0), 0).toLocaleString()} products
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProductGrid
|
||||
products={products}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
showSimilarity={hasSearched}
|
||||
emptyTitle={brands.length === 0 ? 'No catalog data yet' : 'No products found'}
|
||||
emptySubtitle={
|
||||
brands.length === 0
|
||||
? "Run `python scripts/seed_sample_data.py` in the backend to load sample brands, or use the Admin panel to ingest a real one."
|
||||
: 'Try a different brand, category, or search term.'
|
||||
}
|
||||
onSelect={setSelectedProduct}
|
||||
/>
|
||||
<BrandGrid
|
||||
brands={brandCards}
|
||||
loading={brandCardsLoading}
|
||||
error={brandCardsError}
|
||||
emptyTitle="No catalog data yet"
|
||||
emptySubtitle="Run `python scripts/seed_sample_data.py` in the backend to load sample brands, or use the Admin panel to ingest a real one."
|
||||
onSelect={(b) => { setSelectedBrand(b.name); setQuery(''); setView('products'); }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setView('brands')}
|
||||
className="text-xs text-slate-500 transition hover:text-amber-600"
|
||||
>
|
||||
← All brands
|
||||
</button>
|
||||
<p className="font-display text-sm font-semibold text-ink-900">
|
||||
{hasSearched
|
||||
? `Results for "${query}"`
|
||||
: selectedBrand
|
||||
? `Browsing ${selectedBrand}${selectedCategory ? ` · ${selectedCategory}` : ''}`
|
||||
: 'Browsing All Brands'}
|
||||
</p>
|
||||
</div>
|
||||
{products.length > 0 && <p className="text-xs text-slate-500">{products.length} shown</p>}
|
||||
</div>
|
||||
|
||||
<ProductGrid
|
||||
products={products}
|
||||
loading={productsLoading}
|
||||
error={productsError}
|
||||
showSimilarity={hasSearched}
|
||||
emptyTitle={brands.length === 0 ? 'No catalog data yet' : 'No products found'}
|
||||
emptySubtitle={
|
||||
brands.length === 0
|
||||
? "Run `python scripts/seed_sample_data.py` in the backend to load sample brands, or use the Admin panel to ingest a real one."
|
||||
: 'Try a different brand, category, or search term.'
|
||||
}
|
||||
onSelect={setSelectedProduct}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ChatPanel brand={selectedBrand} onSelectSource={setSelectedProduct} />
|
||||
|
||||
Reference in New Issue
Block a user