Job search tracker: CRUD, AI parse/tailor/cover via OmniRoute, legacy import

This commit is contained in:
Jonathan Sykes
2026-08-16 19:05:19 +08:00
parent 8aebc279db
commit 9ba9ae675a
31 changed files with 2294 additions and 85 deletions

View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"];
export default function JobActions({ job }: { job: { id: string; status: string } }) {
const router = useRouter();
const [busy, setBusy] = useState("");
const [msg, setMsg] = useState("");
async function setStatus(status: string) {
await fetch(`/api/jobs/${job.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
router.refresh();
}
async function run(kind: "tailor" | "cover") {
setBusy(kind); setMsg("");
try {
const res = await fetch(`/api/ai/${kind}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jobId: job.id }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "failed");
setMsg(kind === "tailor" ? "Tailored resume created ✅" : "Cover letter created ✅");
router.refresh();
} catch (e) { setMsg(String(e)); } finally { setBusy(""); }
}
async function markApplied() {
await fetch("/api/applications", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jobId: job.id, method: "manual", submittedAt: new Date().toISOString() }),
});
router.refresh();
}
async function del() {
if (!confirm("Delete this job?")) return;
await fetch(`/api/jobs/${job.id}`, { method: "DELETE" });
router.push("/jobs");
}
return (
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4 space-y-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-zinc-500">Status:</span>
<select value={job.status} onChange={(e) => setStatus(e.target.value)}
className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-1">
{STATUSES.map((s) => <option key={s}>{s}</option>)}
</select>
</div>
<div className="flex flex-wrap gap-2">
<button onClick={() => run("tailor")} disabled={!!busy}
className="rounded bg-purple-600 text-white px-3 py-1.5 text-sm disabled:opacity-50 hover:bg-purple-700">
{busy === "tailor" ? "Generating…" : "🪄 AI Tailor Resume"}
</button>
<button onClick={() => run("cover")} disabled={!!busy}
className="rounded bg-indigo-600 text-white px-3 py-1.5 text-sm disabled:opacity-50 hover:bg-indigo-700">
{busy === "cover" ? "Generating…" : "✉️ AI Cover Letter"}
</button>
<button onClick={markApplied} className="rounded bg-green-600 text-white px-3 py-1.5 text-sm hover:bg-green-700">
✅ Mark Applied
</button>
<button onClick={del} className="rounded border border-red-300 text-red-600 px-3 py-1.5 text-sm hover:bg-red-50 dark:hover:bg-red-950">
Delete
</button>
</div>
{msg && <div className="text-sm">{msg}</div>}
</div>
);
}