feat(products-ui): admin nav + Products page with import history (§5.2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:29:20 -07:00
parent b5dac8886f
commit 26f84cb916
7 changed files with 395 additions and 22 deletions
@@ -0,0 +1,112 @@
// 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 { getProductsSummary, listRuns, type ProductsSummary, type RunSummary } from "../../productsApi";
import { Banner } from "../../ui/kit";
const STATUS_LABELS: Record<string, string> = {
applying: "Importing…",
fetching_images: "Fetching images…",
complete: "Complete",
complete_with_problems: "Complete with problems",
};
export default function ProductsPage() {
const [summary, setSummary] = useState<ProductsSummary | null>(null);
const [runs, setRuns] = useState<RunSummary[] | null>(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 (
<Banner tone="attn" title="Couldn't load your products">
Something went wrong on our side.{" "}
<button type="button" className="linklike" onClick={() => void load()}>
Retry
</button>
</Banner>
);
}
if (!summary || !runs) return <p className="note">Loading</p>;
const empty = summary.product_count === 0;
return (
<div className="products">
<header className="products__header">
<h1>
Products
{!empty && <span className="products__count"> · {summary.product_count.toLocaleString()}</span>}
</h1>
<div className="products__actions">
<button type="button" className="btn-secondary" disabled title="Export arrives in a coming release">
Export
</button>
<a className="btn-primary" href="#/products/import">
Import products
</a>
</div>
</header>
{empty ? (
<div className="empty">
<p className="empty__copy">No products yet. Bulk import is how product data gets in.</p>
<a className="btn-primary" href="#/products/import">
Import products
</a>
<p className="note">
<a href="/api/products/sample.csv" download>
Download sample CSV
</a>
</p>
</div>
) : (
<p className="note">Your catalog is loaded. The browsable product list arrives with an upcoming release.</p>
)}
<section className="products__history">
<h2>Import history</h2>
{runs.length === 0 ? (
<p className="note">No imports yet.</p>
) : (
<table className="datatable">
<thead>
<tr>
<th>Date</th><th>File</th><th>Dialect</th><th>Added</th><th>Updated</th><th>Errors</th><th>Status</th>
</tr>
</thead>
<tbody>
{runs.map((r) => (
<tr
key={r.id}
className="datatable__rowlink"
onClick={() => {
window.location.hash = `#/products/imports/runs/${r.id}`;
}}
>
<td>{new Date(r.created_at).toLocaleString()}</td>
<td>{r.file_name}</td>
<td>{r.dialect}</td>
<td>{r.products_added}</td>
<td>{r.products_updated}</td>
<td>{r.rows_errored}</td>
<td>{STATUS_LABELS[r.status] ?? r.status}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</div>
);
}