SPB Git

spb/autoscholar

Public
6.6 KB · 228 lines tsx
1"use client";2import { useRouter } from "next/navigation";3import { useState } from "react";4import { useToast } from "@/components/Toast";5import { type ApiError, api } from "@/lib/client/api";67export function PublishButton({8  runId,9  papers,10}: {11  runId: string;12  papers: Array<{ id: string; publicId: string; title: string }>;13}) {14  const router = useRouter();15  const toast = useToast();16  const [open, setOpen] = useState(false);17  const [busy, setBusy] = useState(false);18  const [corrects, setCorrects] = useState("");19  const [changelog, setChangelog] = useState("");20  const go = async () => {21    setBusy(true);22    try {23      const r = await api<{ publicId: string; slug: string }>("/api/admin/publications", {24        json: { runId, correctsPaperId: corrects || undefined, changelog: changelog || undefined },25      });26      toast.push("success", `Published ${r.publicId}.`);27      router.refresh();28    } catch (e) {29      toast.push("error", (e as ApiError).message);30    } finally {31      setBusy(false);32      setOpen(false);33    }34  };35  return open ? (36    <div className="space-y-2 text-xs">37      <label className="block">38        Publish as39        <select className="input mt-1" value={corrects} onChange={(e) => setCorrects(e.target.value)}>40          <option value="">New paper</option>41          {papers.map((p) => (42            <option key={p.id} value={p.id}>43              Correction of {p.publicId} — {p.title}44            </option>45          ))}46        </select>47      </label>48      {corrects && (49        <label className="block">50          Changelog51          <input className="input mt-1" value={changelog} onChange={(e) => setChangelog(e.target.value)} />52        </label>53      )}54      <div className="flex gap-1">55        <button type="button" className="btn btn-sm btn-primary" disabled={busy} onClick={go}>56          Confirm publish57        </button>58        <button type="button" className="btn btn-sm" onClick={() => setOpen(false)}>59          Cancel60        </button>61      </div>62    </div>63  ) : (64    <button type="button" className="btn btn-sm btn-primary" onClick={() => setOpen(true)}>65      Publish…66    </button>67  );68}6970export function PaperActions({71  id,72  status,73  title,74  abstract,75  keywords,76}: {77  id: string;78  status: string;79  title: string;80  abstract: string;81  keywords: string[];82}) {83  const router = useRouter();84  const toast = useToast();85  const [mode, setMode] = useState<null | "retract" | "withdraw" | "meta">(null);86  const [f, setF] = useState({87    reason: "",88    notice: "",89    title,90    abstract,91    keywords: keywords.join(", "),92    note: "",93  });94  const send = async (path: string, json: unknown, msg: string) => {95    try {96      await api(path, { json });97      toast.push("success", msg);98      setMode(null);99      router.refresh();100    } catch (e) {101      toast.push("error", (e as ApiError).message);102    }103  };104  if (!mode)105    return (106      <div className="flex gap-1">107        <button type="button" className="btn btn-sm" onClick={() => setMode("meta")}>108          Edit metadata109        </button>110        {status !== "retracted" && (111          <button type="button" className="btn btn-sm btn-danger" onClick={() => setMode("retract")}>112            Retract113          </button>114        )}115        <button type="button" className="btn btn-sm btn-danger" onClick={() => setMode("withdraw")}>116          Withdraw files117        </button>118      </div>119    );120  return (121    <div className="space-y-1 text-xs" style={{ minWidth: "18rem" }}>122      {mode === "retract" && (123        <>124          <input125            className="input"126            placeholder="Reason (internal)"127            value={f.reason}128            onChange={(e) => setF({ ...f, reason: e.target.value })}129          />130          <textarea131            className="input"132            placeholder="Public retraction notice"133            value={f.notice}134            onChange={(e) => setF({ ...f, notice: e.target.value })}135          />136          <button137            type="button"138            className="btn btn-sm btn-danger"139            onClick={() =>140              confirm("Retract permanently? A public tombstone remains.") &&141              send(142                `/api/admin/publications/${id}/retract`,143                { reason: f.reason, notice: f.notice },144                "Retracted.",145              )146            }147          >148            Confirm retraction149          </button>150        </>151      )}152      {mode === "withdraw" && (153        <>154          <input155            className="input"156            placeholder="Reason (recorded)"157            value={f.reason}158            onChange={(e) => setF({ ...f, reason: e.target.value })}159          />160          <button161            type="button"162            className="btn btn-sm btn-danger"163            onClick={() =>164              confirm("Withdraw artifact delivery? The page remains with a notice.") &&165              send(`/api/admin/publications/${id}/withdraw`, { reason: f.reason }, "Artifacts withdrawn.")166            }167          >168            Confirm withdrawal169          </button>170        </>171      )}172      {mode === "meta" && (173        <>174          <input175            className="input"176            value={f.title}177            onChange={(e) => setF({ ...f, title: e.target.value })}178            aria-label="Title"179          />180          <textarea181            className="input"182            value={f.abstract}183            onChange={(e) => setF({ ...f, abstract: e.target.value })}184            aria-label="Abstract"185            rows={3}186          />187          <input188            className="input"189            value={f.keywords}190            onChange={(e) => setF({ ...f, keywords: e.target.value })}191            aria-label="Keywords, comma separated"192          />193          <input194            className="input"195            placeholder="Change note (required)"196            value={f.note}197            onChange={(e) => setF({ ...f, note: e.target.value })}198          />199          <button200            type="button"201            className="btn btn-sm btn-primary"202            onClick={() =>203              send(204                `/api/admin/publications/${id}/metadata`,205                {206                  title: f.title,207                  abstract: f.abstract,208                  keywords: f.keywords209                    .split(",")210                    .map((k) => k.trim())211                    .filter(Boolean),212                  note: f.note,213                },214                "Metadata updated as a versioned change.",215              )216            }217          >218            Save219          </button>220        </>221      )}222      <button type="button" className="btn btn-sm" onClick={() => setMode(null)}>223        Cancel224      </button>225    </div>226  );227}228
Files · main
Menu

Theme