mirror of
https://github.com/prdlk/cv.git
synced 2026-08-02 17:31:41 +00:00
init(app): Setup client package for web api specific logic
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useState } from "react";
|
||||
import type { JobExtract } from "../../shared/types";
|
||||
import { ApiRequestError, post } from "../api";
|
||||
|
||||
const MIN_TEXT_CHARS = 120;
|
||||
|
||||
export function Home({ onExtracted }: { onExtracted: (jd: JobExtract) => void }) {
|
||||
const [mode, setMode] = useState<"url" | "paste">("url");
|
||||
const [url, setUrl] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function submitUrl(): Promise<void> {
|
||||
if (busy || url.trim().length === 0) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
onExtracted(await post<JobExtract>("/api/extract", { url }));
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiRequestError ? err.message : String(err));
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function submitPaste(): void {
|
||||
const trimmedTitle = title.trim();
|
||||
const trimmedText = text.trim();
|
||||
if (trimmedTitle.length === 0) {
|
||||
setError("Give the job opportunity a title.");
|
||||
return;
|
||||
}
|
||||
if (trimmedText.length < MIN_TEXT_CHARS) {
|
||||
setError(`Paste the full job description (at least ${MIN_TEXT_CHARS} characters).`);
|
||||
return;
|
||||
}
|
||||
onExtracted({ url: "", title: trimmedTitle, text: trimmedText, source: "manual" });
|
||||
}
|
||||
|
||||
function switchMode(next: "url" | "paste"): void {
|
||||
setMode(next);
|
||||
setError("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<div className="home-card">
|
||||
<h1>CV Tailor</h1>
|
||||
<p className="sub">
|
||||
{mode === "url"
|
||||
? "Paste a link to a job posting. The résumé corpus is matched against it, you refine the selection with an AI agent, and a one-page LaTeX résumé is compiled to PDF."
|
||||
: "Paste the job description below. The résumé corpus is matched against it, you refine the selection with an AI agent, and a one-page LaTeX résumé is compiled to PDF."}
|
||||
</p>
|
||||
|
||||
{mode === "url" ? (
|
||||
<form
|
||||
className="home-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void submitUrl();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://boards.greenhouse.io/company/jobs/123456"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<button type="submit" className="btn primary" disabled={busy}>
|
||||
{busy ? "Extracting…" : "Tailor Résumé"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
className="paste-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitPaste();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Job opportunity title, e.g. Senior Platform Engineer at Aptos"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Paste the full job description here…"
|
||||
rows={12}
|
||||
required
|
||||
/>
|
||||
<button type="submit" className="btn primary">
|
||||
Tailor Résumé
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
{mode === "url" ? (
|
||||
<>
|
||||
<p className="hint">
|
||||
Server-rendered boards (Greenhouse, Lever, Workable, Ashby job-post pages) extract
|
||||
best.
|
||||
</p>
|
||||
<button type="button" className="link" onClick={() => switchMode("paste")}>
|
||||
Or manually paste job description
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="link" onClick={() => switchMode("url")}>
|
||||
Or extract from a job posting URL
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user