111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { api } from "@/lib/client";
|
|
import type { GitStatus } from "@/lib/git";
|
|
import type { PublishStep } from "@/lib/publish";
|
|
|
|
type Config = { push: boolean; pushAuth: string; deploy: boolean; deployUrl: string | null };
|
|
|
|
function defaultMessage(status: GitStatus): string {
|
|
const n = status.changes.length;
|
|
return n === 1 && status.changes[0].path.endsWith("blog-posts.json") ? "blog: update posts" : `blog: update posts (${n} files)`;
|
|
}
|
|
|
|
export default function PublishPanel({ status: initial, config }: { status: GitStatus; config: Config }) {
|
|
const router = useRouter();
|
|
const [status, setStatus] = useState(initial);
|
|
const [message, setMessage] = useState(() => defaultMessage(initial));
|
|
const [running, setRunning] = useState(false);
|
|
const [result, setResult] = useState<{ ok: boolean; steps: PublishStep[] } | null>(null);
|
|
|
|
if (!status.available) {
|
|
return (
|
|
<div className="notice error">
|
|
git isn't usable on the portfolio checkout: <code>{status.error}</code>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const ahead = status.ahead ?? 0;
|
|
const nothingToDo = status.changes.length === 0 && ahead === 0;
|
|
|
|
async function run() {
|
|
setRunning(true);
|
|
setResult(null);
|
|
try {
|
|
const res = await api<{ ok: boolean; steps: PublishStep[]; status: GitStatus }>("/api/publish", {
|
|
method: "POST",
|
|
json: { message },
|
|
});
|
|
setResult(res);
|
|
setStatus(res.status);
|
|
} catch (err) {
|
|
setResult({ ok: false, steps: [{ step: "stage", status: "failed", output: (err as Error).message }] });
|
|
} finally {
|
|
setRunning(false);
|
|
router.refresh();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<p className="muted">
|
|
Branch <code>{status.branch}</code>
|
|
{status.ahead !== undefined && (
|
|
<>
|
|
{" "}· {status.ahead} ahead / {status.behind} behind upstream (as of last fetch)
|
|
</>
|
|
)}
|
|
. Only <code>data/blog-posts.json</code> and <code>public/img/blog/</code> are committed.
|
|
</p>
|
|
|
|
<h2>Pending changes</h2>
|
|
{status.changes.length === 0 ? (
|
|
<p className="muted">No uncommitted blog changes.{ahead > 0 && ` ${ahead} commit(s) waiting to be pushed.`}</p>
|
|
) : (
|
|
<table className="list" style={{ marginBottom: 12 }}>
|
|
<tbody>
|
|
{status.changes.map((c) => (
|
|
<tr key={c.path}>
|
|
<td className="mono small" style={{ width: 40 }}>
|
|
{c.code}
|
|
</td>
|
|
<td className="mono small">{c.path}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
{status.diffStat && <pre className="log">{status.diffStat}</pre>}
|
|
|
|
<h2 style={{ marginTop: 24 }}>Publish</h2>
|
|
<p className="muted small">
|
|
commit → {config.push ? `push (${config.pushAuth})` : "push disabled (GIT_PUSH=false)"} →{" "}
|
|
{config.deploy ? `trigger redeploy at ${config.deployUrl}` : "redeploy trigger not configured"}
|
|
</p>
|
|
<div className="row" style={{ marginBottom: 16 }}>
|
|
<input
|
|
style={{ flex: 1, minWidth: 260 }}
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
placeholder="commit message"
|
|
/>
|
|
<button className="primary" disabled={running || nothingToDo || !message.trim()} onClick={run}>
|
|
{running ? "Publishing…" : "Publish"}
|
|
</button>
|
|
</div>
|
|
|
|
{result && (
|
|
<>
|
|
<div className={`notice ${result.ok ? "ok" : "error"}`}>{result.ok ? "Published." : "Publish failed. See log."}</div>
|
|
<pre className="log">
|
|
{result.steps.map((s) => `[${s.status}] ${s.step}${s.output ? `\n${s.output}` : ""}`).join("\n\n")}
|
|
</pre>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|