SPB Git
4.5 KB · 26 lines tsx
1"use client";2import { useEffect, useState } from "react";3import { api } from "./api";4import { USE_STATES } from "@/lib/content/schema";5export function OwnerPanel({ toolId }: { toolId: string }) {6  const [n, setN] = useState<any>(null); const [projects, setProjects] = useState<string[]>([]); const [all, setAll] = useState<{ slug: string; name: string }[]>([]); const [msg, setMsg] = useState("");7  useEffect(() => { api(`/api/admin/owner/${toolId}`).then((r) => { setN(r.note ?? { use_state: "candidate", cluster_compat: "unknown", priority: "medium", deployment_urls: [], custom_tags: [] }); setProjects(r.projects); }); api("/api/admin/projects").then(setAll); }, [toolId]);8  if (!n) return null;9  const set = (k: string, v: any) => setN((x: any) => ({ ...x, [k]: v }));10  async function save() { setMsg(""); try { await api(`/api/admin/owner/${toolId}`, { method: "PUT", body: JSON.stringify({ use_state: n.useState ?? n.use_state, rating: n.rating ?? null, notes: n.notes ?? null, cluster_compat: n.clusterCompat ?? n.cluster_compat, priority: n.priority, subscription_status: n.subscriptionStatus ?? n.subscription_status ?? null, deployment_urls: n.deploymentUrls ?? n.deployment_urls ?? [], custom_tags: n.customTags ?? n.custom_tags ?? [], review_at: n.reviewAt ?? n.review_at ?? null, projects }) }); setMsg("Saved (private)."); } catch (e) { setMsg((e as Error).message); } }11  const g = (a: string, b: string) => n[a] ?? n[b];12  return (13    <section className="card space-y-3 border-accent/30 p-4"><h2 className="font-semibold">Owner notes <span className="chip chip-accent ml-1">private</span></h2>14      <label className="block"><span className="label">Use state</span><select value={g("useState", "use_state")} onChange={(e) => { set("useState", e.target.value); set("use_state", e.target.value); }} className="input">{USE_STATES.map((s) => <option key={s}>{s}</option>)}</select></label>15      <div className="grid grid-cols-2 gap-2"><label className="block"><span className="label">Rating</span><select value={n.rating ?? ""} onChange={(e) => set("rating", e.target.value ? Number(e.target.value) : null)} className="input"><option value="">—</option>{[1, 2, 3, 4, 5].map((x) => <option key={x}>{x}</option>)}</select></label><label className="block"><span className="label">Priority</span><select value={n.priority} onChange={(e) => set("priority", e.target.value)} className="input">{["high", "medium", "low"].map((x) => <option key={x}>{x}</option>)}</select></label></div>16      <label className="block"><span className="label">MacLustr compatibility</span><select value={g("clusterCompat", "cluster_compat")} onChange={(e) => { set("clusterCompat", e.target.value); set("cluster_compat", e.target.value); }} className="input">{["yes", "partial", "no", "unknown"].map((x) => <option key={x}>{x}</option>)}</select></label>17      <label className="block"><span className="label">Subscription status</span><input value={g("subscriptionStatus", "subscription_status") ?? ""} onChange={(e) => { set("subscriptionStatus", e.target.value); set("subscription_status", e.target.value); }} className="input" /></label>18      <label className="block"><span className="label">Projects</span><select multiple value={projects} onChange={(e) => setProjects(Array.from(e.target.selectedOptions).map((o) => o.value))} className="input h-24">{all.map((p) => <option key={p.slug} value={p.slug}>{p.name}</option>)}</select></label>19      <label className="block"><span className="label">Deployment URLs (label | url per line)</span><textarea value={(g("deploymentUrls", "deployment_urls") ?? []).map((d: any) => `${d.label} | ${d.url}`).join("\n")} onChange={(e) => { const v = e.target.value.split("\n").map((l) => l.split("|").map((x) => x.trim())).filter((p) => p[0] && p[1]).map(([label, url]) => ({ label, url })); set("deploymentUrls", v); set("deployment_urls", v); }} rows={2} className="input font-mono text-xs" /></label>20      <label className="block"><span className="label">Notes</span><textarea value={n.notes ?? ""} onChange={(e) => set("notes", e.target.value)} rows={4} className="input text-sm" /></label>21      <label className="block"><span className="label">Review reminder</span><input type="date" value={g("reviewAt", "review_at") ?? ""} onChange={(e) => { set("reviewAt", e.target.value || null); set("review_at", e.target.value || null); }} className="input" /></label>22      <button type="button" onClick={save} className="btn-ghost w-full">Save owner notes</button>{msg && <p className="text-xs text-fg-2">{msg}</p>}23    </section>24  );25}26
Files · laptop-initial-20261102
Menu

Theme