initial commit

This commit is contained in:
Hunter
2026-11-01 13:23:44 -05:00
commit e03ea4f770
53 changed files with 10802 additions and 0 deletions

110
components/PublishPanel.tsx Normal file
View File

@@ -0,0 +1,110 @@
"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&apos;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>
</>
)}
</>
);
}