// Products page (SD-0002 §5.2) — the catalog's home: where imports start and history // lives. SLICE-5: export is disabled (SLICE-6 ships it); the browsable list is #14's. import { useEffect, useState } from "react"; import { dialectLabel, getProductsSummary, listRuns, type ProductsSummary, type RunSummary, } from "../../productsApi"; import { Banner } from "../../ui/kit"; const STATUS_LABELS: Record = { applying: "Importing…", fetching_images: "Fetching images…", complete: "Complete", complete_with_problems: "Complete with problems", }; export default function ProductsPage() { const [summary, setSummary] = useState(null); const [runs, setRuns] = useState(null); const [failed, setFailed] = useState(false); async function load() { setFailed(false); const [s, r] = await Promise.all([getProductsSummary(), listRuns()]); if (!s.ok || !r.ok) { setFailed(true); return; } setSummary(s.value); setRuns(r.value); } useEffect(() => { void load(); }, []); if (failed) { return ( Something went wrong on our side.{" "} ); } if (!summary || !runs) { return (

Loading…

); } const empty = summary.product_count === 0; return (

Products {!empty && · {summary.product_count.toLocaleString()}}

Export arrives in a coming release
Import products
{empty ? (

No products yet. Bulk import is how product data gets in.

Import products

Download sample CSV

) : (

Your catalog is loaded. The browsable product list arrives with an upcoming release.

)}

Import history

{runs.length === 0 ? (

No imports yet.

) : ( {runs.map((r) => ( { window.location.hash = `#/products/imports/runs/${r.id}`; }} > ))}
DateFileDialectAddedUpdatedErrorsStatus
{new Date(r.created_at).toLocaleString()} {/* Anchor = the keyboard/SR path (§6.6); the row onClick stays as a mouse convenience. Both set the same hash, so the double fire on an anchor click is idempotent. */} {r.file_name} {dialectLabel(r.dialect)} {r.products_added} {r.products_updated} {r.rows_errored} {STATUS_LABELS[r.status] ?? r.status}
)}
); }