Job search tracker: CRUD, AI parse/tailor/cover via OmniRoute, legacy import
This commit is contained in:
79
src/app/jobs/[id]/job-actions.tsx
Normal file
79
src/app/jobs/[id]/job-actions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user