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,59 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { marked } from "marked";
export default function ResumeEditor({ resume }: { resume: { id: string; name: string; markdown: string } }) {
const [md, setMd] = useState(resume.markdown);
const [saved, setSaved] = useState(true);
const [preview, setPreview] = useState(true);
async function save() {
await fetch(`/api/resumes/${resume.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ markdown: md }),
});
setSaved(true);
}
function printResume() {
const w = window.open("", "_blank");
if (!w) return;
w.document.write(`<!DOCTYPE html><html><head><title>${resume.name}</title><style>
body{font-family:Georgia,serif;max-width:800px;margin:2rem auto;padding:0 1rem;line-height:1.5;color:#222}
h1{font-size:1.6rem;margin-bottom:0} h2{font-size:1.1rem;border-bottom:1px solid #ccc;padding-bottom:2px;margin-top:1.2rem}
h3{font-size:1rem;margin-bottom:0} ul{margin-top:.3rem} li{margin:.15rem 0} @media print{body{margin:0}}
</style></head><body>${marked.parse(md)}</body></html>`);
w.document.close();
setTimeout(() => w.print(), 300);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<Link href="/resumes" className="text-sm text-zinc-500 hover:text-blue-600">← Resumes</Link>
<h1 className="text-xl font-bold">{resume.name}</h1>
</div>
<div className="flex gap-2">
<button onClick={() => setPreview(!preview)} className="rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm">
{preview ? "Hide" : "Show"} preview
</button>
<button onClick={printResume} className="rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm">🖨 Print / PDF</button>
<button onClick={save} disabled={saved} className="rounded bg-blue-600 text-white px-4 py-1.5 text-sm disabled:opacity-50 hover:bg-blue-700">
{saved ? "Saved" : "Save"}
</button>
</div>
</div>
<div className={`grid gap-4 ${preview ? "md:grid-cols-2" : ""}`}>
<textarea value={md} onChange={(e) => { setMd(e.target.value); setSaved(false); }} rows={30}
className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-3 font-mono text-sm" />
{preview && (
<div className="rounded border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4 prose prose-sm dark:prose-invert max-w-none overflow-auto"
dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }} />
)}
</div>
</div>
);
}