Validate the pending subscriber import
React · React · beginner · greenfield
Adds the pending-import check to the newsletter admin. The panel fetches the stored upload's text, validates the header line against the expected columns, and shows either the mismatch message or a ready-to-import count of the data rows, with loading and error states around the fetch. Tried it against a hand-seeded pending import and both the happy path and the wrong-file case behave as specced.
Requirements
- Add the `SubscriberImportCheck` panel for the newsletter admin: it loads the pending import from `GET /api/imports/pending` and validates the CSV text client-side before the admin can run the import.
- The endpoint responds `200` with JSON `{ "filename": string, "csv": string }`. The `csv` string is the uploaded file's bytes decoded server-side with Node's `Buffer.toString('utf8')`, which — unlike browser text-decoding APIs such as `File.text()` — PRESERVES a leading byte-order mark. Uploads include BOM-bearing spreadsheet exports (notably Excel's UTF-8 CSV), so `csv` can begin with U+FEFF, and JSON transport keeps it intact.
- The first non-blank line of a valid file is exactly the header `email,name,status` (blank lines anywhere are ignorable padding). BOM-bearing exports are VALID subscriber files and must be accepted — users cannot be asked to re-save their files. If the header doesn't match, show `This doesn't look like a subscriber export.`; otherwise show `N subscribers ready to import` where N is the number of non-blank data lines.
- Field values never contain commas, quotes, or line breaks; line endings are `\n` or `\r\n`; blank lines (including a trailing newline) don't count as data.
- Handle the fetch's async states: show `Checking import…` while loading and `Couldn't load the pending import.` if the request fails (non-2xx or network error).
Files touched
- src/components/SubscriberImportCheck.jsx
--- src/components/SubscriberImportCheck.jsx
+import { useEffect, useState } from 'react';
+
+const EXPECTED_HEADER = 'email,name,status';
+
+export function validateSubscriberCsv(text) {
+ const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
+ if (lines.length === 0 || lines[0] !== EXPECTED_HEADER) {
+ return { ok: false, count: 0 };
+ }
+ return { ok: true, count: lines.length - 1 };
+}
+
+export default function SubscriberImportCheck() {
+ const [result, setResult] = useState(null);
+ const [failed, setFailed] = useState(false);
+
+ useEffect(() => {
+ let cancelled = false;
+ async function load() {
+ try {
+ const res = await fetch('/api/imports/pending');
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ if (!cancelled) setResult(validateSubscriberCsv(data.csv));
+ } catch {
+ if (!cancelled) setFailed(true);
+ }
+ }
+ load();