SPB Git
3.1 KB · 19 lines tsx
1"use client";2import { useState } from "react";3import { useRouter } from "next/navigation";4import { api } from "./api";5type Col = { slug: string; title: string; description: string; published: boolean; items: { tool: string; blurb?: string | null }[] };6export function CollectionsEditor({ initial }: { initial: Col[] }) {7  const r = useRouter(); const [sel, setSel] = useState<Col | null>(null); const [msg, setMsg] = useState("");8  const blank: Col = { slug: "", title: "", description: "", published: false, items: [] };9  return <div className="mt-4 grid gap-6 lg:grid-cols-[18rem_1fr]">10    <ul className="space-y-1">{initial.map((c) => <li key={c.slug}><button onClick={() => setSel(c)} className={`w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-bg-3 ${sel?.slug === c.slug ? "bg-bg-3 font-medium" : ""}`}>{c.title} <span className="text-xs text-fg-3">· {c.items.length}{c.published ? "" : " · draft"}</span></button></li>)}<li><button onClick={() => setSel(blank)} className="btn-ghost mt-2 w-full">New collection</button></li></ul>11    {sel && <form className="card space-y-3 p-5" onSubmit={async (e) => { e.preventDefault(); try { await api("/api/admin/collections", { method: "POST", body: JSON.stringify(sel) }); setMsg("Saved"); r.refresh(); } catch (e) { setMsg((e as Error).message); } }}>12      <div className="grid gap-3 sm:grid-cols-2"><label className="block"><span className="label">Slug</span><input value={sel.slug} onChange={(e) => setSel({ ...sel, slug: e.target.value })} className="input" required pattern="[a-z0-9-]+" /></label><label className="block"><span className="label">Title</span><input value={sel.title} onChange={(e) => setSel({ ...sel, title: e.target.value })} className="input" required /></label></div>13      <label className="block"><span className="label">Description</span><textarea value={sel.description} onChange={(e) => setSel({ ...sel, description: e.target.value })} rows={4} className="input" required /></label>14      <label className="block"><span className="label">Items (tool-slug | blurb, one per line, order matters)</span><textarea value={sel.items.map((i) => i.tool + (i.blurb ? ` | ${i.blurb}` : "")).join("\n")} onChange={(e) => setSel({ ...sel, items: e.target.value.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => { const [tool, ...rest] = l.split("|"); return { tool: tool.trim(), blurb: rest.join("|").trim() || undefined }; }) })} rows={10} className="input font-mono text-sm" /></label>15      <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={sel.published} onChange={(e) => setSel({ ...sel, published: e.target.checked })} /> Published</label>16      <div className="flex gap-2"><button className="btn-primary">Save</button>{initial.some((c) => c.slug === sel.slug) && <button type="button" onClick={async () => { if (confirm("Delete collection?")) { await api("/api/admin/collections", { method: "DELETE", body: JSON.stringify({ slug: sel.slug }) }); setSel(null); r.refresh(); } }} className="btn-ghost">Delete</button>}{msg && <span className="self-center text-sm text-fg-2">{msg}</span>}</div></form>}17  </div>;18}19
Files · main
Menu

Theme