SPB Git
3.3 KB · 22 lines tsx
1"use client";2import { useState } from "react";3export function ImportPanel() {4  const [text, setText] = useState(""); const [res, setRes] = useState<any>(null); const [busy, setBusy] = useState(false); const [partial, setPartial] = useState(false); const [publish, setPublish] = useState(false);5  async function run() { setBusy(true); setRes(null); const csrf = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? ""; const r = await fetch(`/api/admin/import?partial=${partial ? 1 : 0}&publish=${publish ? 1 : 0}`, { method: "POST", headers: { "content-type": text.trimStart().startsWith("[") || text.trimStart().startsWith("{") ? "application/json" : "text/csv", "x-csrf-token": csrf }, body: text }); setRes({ status: r.status, ...(await r.json()) }); setBusy(false); }6  return (7    <div className="mt-6 grid gap-6 lg:grid-cols-2">8      <section className="card p-5"><h2 className="font-semibold">Import</h2>9        <input type="file" accept=".json,.csv,text/csv,application/json" onChange={async (e) => { const f = e.target.files?.[0]; if (f) setText(await f.text()); }} className="mt-3 block text-sm" />10        <textarea value={text} onChange={(e) => setText(e.target.value)} rows={14} className="input mt-3 font-mono text-xs" placeholder='[{"slug": "...", ...}]  or  slug,name,vendor,homepage_url,...' />11        <div className="mt-3 flex flex-wrap items-center gap-4 text-sm"><label className="flex items-center gap-2"><input type="checkbox" checked={partial} onChange={(e) => setPartial(e.target.checked)} /> Write valid rows even if some fail</label><label className="flex items-center gap-2"><input type="checkbox" checked={publish} onChange={(e) => setPublish(e.target.checked)} /> Publish imported records</label><button onClick={run} disabled={busy || !text.trim()} className="btn-primary ml-auto">Import</button></div>12        {res && <div className="mt-4 rounded-xl border border-line bg-bg-3/50 p-3 text-sm"><p className="font-medium">HTTP {res.status} · written {res.written ?? 0}</p><ul className="mt-2 max-h-64 space-y-1 overflow-auto font-mono text-xs">{(res.results ?? []).map((r: any, i: number) => <li key={i} className={r.ok ? "text-ok" : "text-bad"}>{r.slug ?? `row ${r.index}`}: {r.ok ? "ok" : r.error}</li>)}</ul>{res.error && <p className="text-bad">{res.error}</p>}</div>}13      </section>14      <section className="card p-5"><h2 className="font-semibold">Export</h2><ul className="mt-3 space-y-2 text-sm">15        <li><a className="text-accent hover:underline" href="/api/admin/export?scope=public&format=json">Public records · JSON</a> — safe to share, identical to the content schema</li>16        <li><a className="text-accent hover:underline" href="/api/admin/export?scope=public&format=csv">Public records · CSV</a></li>17        <li><a className="text-accent hover:underline" href="/api/admin/export?scope=all&format=json">Everything incl. private + owner notes · JSON</a> — owner only, keep private</li></ul>18        <p className="mt-4 text-xs text-fg-3">Deterministic round trip: an export re-imported with “Import” reproduces the same records (slugs are stable keys). The repository content folder is the canonical seed; `pnpm content:export` writes the same public export to <code className="font-mono">exports/</code>.</p></section>19    </div>20  );21}22
Files · laptop-initial-20261102
Menu

Theme