diff --git a/frontend/src/screens/products/ImportUpload.tsx b/frontend/src/screens/products/ImportUpload.tsx index 185e536..a5e8421 100644 --- a/frontend/src/screens/products/ImportUpload.tsx +++ b/frontend/src/screens/products/ImportUpload.tsx @@ -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() { - return null; + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(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 ( +
+

+ ← Products +

+

Import products

+ {error && ( + + {error} + + )} + +

+ Works with the canonical format.{" "} + + Download sample CSV + +

+
+ ); } diff --git a/frontend/src/styles/products.css b/frontend/src/styles/products.css index cc6dde3..1778798 100644 --- a/frontend/src/styles/products.css +++ b/frontend/src/styles/products.css @@ -208,6 +208,14 @@ clip: rect(0 0 0 0); 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 ──────────────────────────────────────────────────────────── */ @media (max-width: 720px) {