spb/autoscholar
Public
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