frontend changes on UI
This commit is contained in:
@@ -33,6 +33,12 @@ import { api } from '../api/client';
|
||||
* the brand and states a pack size. That search takes minutes, so it runs as a
|
||||
* job: Discover starts it, this panel polls it, then asks for the preview.
|
||||
*
|
||||
* A PRODUCT NAME WORKS TOO. "Lizol" resolves to Reckitt Benckiser and searches
|
||||
* only Lizol listings; "Lizol Floor Cleaner" narrows to that line. The result
|
||||
* is shown grouped - product line, then each variant (Citrus, Floral...), then
|
||||
* the pack sizes real listings name - above the row-by-row table, and a size
|
||||
* chip ticks or unticks the rows behind it.
|
||||
*
|
||||
* There is no progress UI here on purpose. Ingest returns a BatchOut, and
|
||||
* AdminPage hands it to BatchCatalogPanel through the `adoptBatch` prop it
|
||||
* already has - which draws the eleven stages, and owns Resume and Cancel.
|
||||
@@ -72,6 +78,66 @@ function evidenceBadge(product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// "Lizol" typed, "reckitt benckiser" resolved: say so, and say where rows go.
|
||||
function resolvedNote(result) {
|
||||
const typed = (result.brand || '').trim().toLowerCase();
|
||||
const parent = (result.parent_brand || '').trim().toLowerCase();
|
||||
if (!parent || typed === parent) return null;
|
||||
const maker = result.parent_brand.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
return `"${result.brand}" is a product of ${maker} - only its own listings were searched.`;
|
||||
}
|
||||
|
||||
// Product line -> variant -> pack sizes. A size chip stands for the preview
|
||||
// rows behind it (`rows`), and clicking it ticks or unticks all of them.
|
||||
function Families({ families, selected, onToggleRows }) {
|
||||
if (!families?.length) return null;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{families.map((family) => (
|
||||
<div
|
||||
key={family.product_line}
|
||||
className="rounded-2xl border border-ink-900/10 bg-white px-4 py-3"
|
||||
>
|
||||
<div className="text-sm font-bold text-ink-950">{family.product_line}</div>
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{family.variants.map((variant) => (
|
||||
<div key={variant.variant || '-'} className="flex flex-wrap items-center gap-1.5 text-xs">
|
||||
<span className="min-w-24 font-semibold text-slate-700">
|
||||
{variant.variant || 'Regular'}
|
||||
</span>
|
||||
{variant.sizes.map((size) => {
|
||||
const rows = size.rows || [];
|
||||
const on = rows.length > 0 && rows.every((i) => selected.has(i));
|
||||
const shops = size.retailer_count
|
||||
? `${size.retailer_count} shop${size.retailer_count === 1 ? '' : 's'}`
|
||||
: '';
|
||||
return (
|
||||
<button
|
||||
key={size.size}
|
||||
type="button"
|
||||
onClick={() => onToggleRows(rows, !on)}
|
||||
title={[shops, size.price_range, (size.providers || []).join(', ')]
|
||||
.filter(Boolean).join(' - ')}
|
||||
className={`rounded-full border px-2 py-0.5 text-[11px] font-semibold cursor-pointer ${
|
||||
on
|
||||
? 'border-amber-500 bg-amber-500/15 text-ink-950'
|
||||
: 'border-ink-900/10 bg-paper-50 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{size.size}
|
||||
{size.price_range ? ` · ${size.price_range}` : ''}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange, label, hint }) {
|
||||
return (
|
||||
<label className="flex items-start gap-2 text-xs text-slate-600 cursor-pointer">
|
||||
@@ -194,6 +260,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
fssai_license: p.fssai_license,
|
||||
barcode: p.barcode,
|
||||
image_url: p.image_url,
|
||||
product_line: p.product_line,
|
||||
variant: p.variant,
|
||||
// Web-found rows only; recorded as provenance after the batch.
|
||||
...(p.listings?.length
|
||||
? { listings: p.listings, price_range: p.price_range, retailer_count: p.retailer_count }
|
||||
@@ -222,6 +290,14 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleRows = (rows, on) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
rows.forEach((i) => (on ? next.add(i) : next.delete(i)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const allSelected = products.length > 0 && selected.size === products.length;
|
||||
|
||||
return (
|
||||
@@ -233,7 +309,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
<h2 className="text-sm font-bold text-ink-950">Brand Discovery</h2>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 max-w-3xl">
|
||||
Enter any brand - one already in the catalog or a new one. Products
|
||||
Enter any brand - one already in the catalog or a new one - or a
|
||||
product such as Lizol, which is searched under its maker. Products
|
||||
come from Open Food Facts, which supplies real names, pack sizes and
|
||||
barcodes, and from the brand's own online shop where one is
|
||||
registered. Nothing is written until you review the list and press
|
||||
@@ -247,7 +324,7 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
value={brand}
|
||||
onChange={(e) => setBrand(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && discover()}
|
||||
placeholder="Britannia"
|
||||
placeholder="Britannia, or a product like Lizol"
|
||||
className="w-full rounded-xl border border-ink-900/10 bg-white py-2 pl-9 pr-3 text-sm text-ink-950 outline-none focus:border-amber-500"
|
||||
/>
|
||||
</div>
|
||||
@@ -317,6 +394,13 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{resolvedNote(result) && (
|
||||
<div className="flex items-start gap-2 rounded-xl border border-sky-500/30 bg-sky-500/10 px-4 py-3 text-xs text-sky-800">
|
||||
<Search className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{resolvedNote(result)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(result.warnings || []).map((warning) => (
|
||||
<div
|
||||
key={warning}
|
||||
@@ -358,6 +442,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Families families={result.families} selected={selected} onToggleRows={toggleRows} />
|
||||
|
||||
<div className="overflow-x-auto rounded-2xl border border-ink-900/10 bg-white">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="border-b border-ink-900/10 bg-paper-50 text-left text-slate-500">
|
||||
|
||||
Reference in New Issue
Block a user