feat(products-ui): import upload screen (§5.3, PUC-2/5a)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,58 @@
|
|||||||
// Import upload (SD-0002 §5.3) — stub; Task 12 replaces this with the dropzone.
|
// Import — upload (SD-0002 §5.3). Selecting a file starts upload + validation
|
||||||
|
// immediately (PUC-2); file-level rejections (PUC-5a) render in place with the
|
||||||
|
// picker live for retry. No notifications — errors render here.
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { uploadImport } from "../../productsApi";
|
||||||
|
import { Banner } from "../../ui/kit";
|
||||||
|
|
||||||
export default function ImportUpload() {
|
export default function ImportUpload() {
|
||||||
return null;
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
async function onPick(files: FileList | null) {
|
||||||
|
const file = files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
const resp = await uploadImport(file);
|
||||||
|
setBusy(false);
|
||||||
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
|
if (!resp.ok) {
|
||||||
|
setError(resp.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.hash = `#/products/imports/drafts/${resp.value.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="products products--narrow">
|
||||||
|
<p className="note">
|
||||||
|
<a href="#/products">← Products</a>
|
||||||
|
</p>
|
||||||
|
<h1>Import products</h1>
|
||||||
|
{error && (
|
||||||
|
<Banner tone="attn" title="That file can't be imported">
|
||||||
|
{error}
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
<label className={`dropzone${busy ? " dropzone--busy" : ""}`}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv,text/csv"
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => void onPick(e.target.files)}
|
||||||
|
/>
|
||||||
|
<span className="dropzone__title">{busy ? "Validating…" : "Choose a CSV file"}</span>
|
||||||
|
<span className="note">CSV, up to 5,000 rows</span>
|
||||||
|
</label>
|
||||||
|
<p className="note">
|
||||||
|
Works with the canonical format.{" "}
|
||||||
|
<a href="/api/products/sample.csv" download>
|
||||||
|
Download sample CSV
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,14 @@
|
|||||||
clip: rect(0 0 0 0);
|
clip: rect(0 0 0 0);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.dropzone__title {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--wv-font-display);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--text-on-dark-soft);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── small screens ──────────────────────────────────────────────────────────── */
|
/* ── small screens ──────────────────────────────────────────────────────────── */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user