diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx
index c16b727..9c15cad 100644
--- a/src/app/applications/page.tsx
+++ b/src/app/applications/page.tsx
@@ -3,25 +3,212 @@ import Link from "next/link";
export const dynamic = "force-dynamic";
+const STATUS_CONFIG: Record<
+ string,
+ { label: string; bg: string; text: string; border: string; dot: string }
+> = {
+ prospect: {
+ label: "Prospect",
+ bg: "bg-zinc-100 dark:bg-zinc-800/80",
+ text: "text-zinc-700 dark:text-zinc-300",
+ border: "border-zinc-200 dark:border-zinc-700",
+ dot: "bg-zinc-400 dark:bg-zinc-500",
+ },
+ reviewing: {
+ label: "Reviewing",
+ bg: "bg-amber-50 dark:bg-amber-950/40",
+ text: "text-amber-700 dark:text-amber-300",
+ border: "border-amber-200 dark:border-amber-800/60",
+ dot: "bg-amber-500",
+ },
+ tailoring: {
+ label: "Tailoring",
+ bg: "bg-purple-50 dark:bg-purple-950/40",
+ text: "text-purple-700 dark:text-purple-300",
+ border: "border-purple-200 dark:border-purple-800/60",
+ dot: "bg-purple-500",
+ },
+ ready: {
+ label: "Ready",
+ bg: "bg-blue-50 dark:bg-blue-950/40",
+ text: "text-blue-700 dark:text-blue-300",
+ border: "border-blue-200 dark:border-blue-800/60",
+ dot: "bg-blue-500",
+ },
+ applied: {
+ label: "Applied",
+ bg: "bg-indigo-50 dark:bg-indigo-950/40",
+ text: "text-indigo-700 dark:text-indigo-300",
+ border: "border-indigo-200 dark:border-indigo-800/60",
+ dot: "bg-indigo-500",
+ },
+ interviewing: {
+ label: "Interviewing",
+ bg: "bg-teal-50 dark:bg-teal-950/40",
+ text: "text-teal-700 dark:text-teal-300",
+ border: "border-teal-200 dark:border-teal-800/60",
+ dot: "bg-teal-500",
+ },
+ offer: {
+ label: "Offer",
+ bg: "bg-emerald-50 dark:bg-emerald-950/40",
+ text: "text-emerald-700 dark:text-emerald-300",
+ border: "border-emerald-200 dark:border-emerald-800/60",
+ dot: "bg-emerald-500",
+ },
+ rejected: {
+ label: "Rejected",
+ bg: "bg-rose-50 dark:bg-rose-950/40",
+ text: "text-rose-700 dark:text-rose-300",
+ border: "border-rose-200 dark:border-rose-800/60",
+ dot: "bg-rose-500",
+ },
+ dead: {
+ label: "Dead",
+ bg: "bg-zinc-100 dark:bg-zinc-800/60",
+ text: "text-zinc-500 dark:text-zinc-400",
+ border: "border-zinc-200 dark:border-zinc-700/60",
+ dot: "bg-zinc-400",
+ },
+};
+
+function getCompanyColor(name: string) {
+ const colors = [
+ "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950/80 dark:text-indigo-300 dark:border-indigo-800",
+ "bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-950/80 dark:text-violet-300 dark:border-violet-800",
+ "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950/80 dark:text-blue-300 dark:border-blue-800",
+ "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/80 dark:text-emerald-300 dark:border-emerald-800",
+ "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-950/80 dark:text-amber-300 dark:border-amber-800",
+ "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-950/80 dark:text-rose-300 dark:border-rose-800",
+ "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-950/80 dark:text-teal-300 dark:border-teal-800",
+ "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-950/80 dark:text-cyan-300 dark:border-cyan-800",
+ ];
+ let hash = 0;
+ for (let i = 0; i < (name || "").length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return colors[Math.abs(hash) % colors.length];
+}
+
+function getInitials(name: string) {
+ if (!name) return "?";
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return name.slice(0, 2).toUpperCase();
+}
+
export default async function ApplicationsPage() {
const apps = await prisma.application.findMany({
orderBy: { updatedAt: "desc" },
include: { job: { select: { id: true, title: true, company: true, status: true } } },
});
+
return (
-
-
Applications ({apps.length})
- {apps.length === 0 &&
No applications yet. Open a job and click “Mark Applied”.
}
-
+
+ {/* Header */}
+
+
+
+
Applications
+
+ {apps.length} {apps.length === 1 ? "application" : "applications"}
+
+
+
+ Track submitted job applications and their current recruitment pipeline status.
+
+
+
+
+ {/* Applications List */}
+ {apps.length === 0 ? (
+
+
+
No applications recorded yet
+
+ Open any job in your list and click “Mark Applied” to log an application submission here.
+
+
+
+ Browse Jobs
+
+
+
+ ) : (
+
+ {apps.map((a) => {
+ const statusInfo = STATUS_CONFIG[a.job.status] || STATUS_CONFIG.applied;
+ const companyColor = getCompanyColor(a.job.company);
+ const initials = getInitials(a.job.company);
+
+ return (
+
+
+
+ {initials}
+
+
+
+ {a.job.title}
+
+
+ {a.job.company}
+ ·
+ {a.method} submission
+ ·
+
+ {a.submittedAt
+ ? `Submitted on ${new Date(a.submittedAt).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })}`
+ : "Not submitted"}
+
+
+
+
+
+
+
+
+ {a.job.status}
+
+
+
+
View Job
+
+
+
+
+ );
+ })}
+
+ )}
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..79f8eb3 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,147 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --background: #f8fafc;
+ --foreground: #0f172a;
+ --card-bg: #ffffff;
+ --card-border: #e2e8f0;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --background: #09090b;
+ --foreground: #f4f4f5;
+ --card-bg: #121215;
+ --card-border: #27272a;
}
}
body {
- background: var(--background);
+ background-color: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+::selection {
+ background-color: rgba(99, 102, 241, 0.25);
+ color: inherit;
+}
+
+/* Custom Scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(148, 163, 184, 0.4);
+ border-radius: 9999px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(148, 163, 184, 0.6);
+}
+
+/* Resume & Markdown Preview Styling */
+.resume-preview {
+ font-family: "Charter", "Bitstream Vera Serif", "Cambria", "Times New Roman", serif;
+ color: #1e293b;
+ line-height: 1.6;
+}
+
+.dark .resume-preview {
+ color: #e2e8f0;
+}
+
+.resume-preview h1 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ margin-bottom: 0.25rem;
+ color: #0f172a;
+ border-bottom: 2px solid #e2e8f0;
+ padding-bottom: 0.5rem;
+}
+
+.dark .resume-preview h1 {
+ color: #f8fafc;
+ border-bottom-color: #334155;
+}
+
+.resume-preview h2 {
+ font-size: 1.2rem;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ margin-top: 1.5rem;
+ margin-bottom: 0.5rem;
+ color: #1e293b;
+ border-bottom: 1px solid #e2e8f0;
+ padding-bottom: 0.25rem;
+ text-transform: uppercase;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ font-size: 0.85rem;
+ letter-spacing: 0.08em;
+}
+
+.dark .resume-preview h2 {
+ color: #94a3b8;
+ border-bottom-color: #334155;
+}
+
+.resume-preview h3 {
+ font-size: 1.05rem;
+ font-weight: 600;
+ margin-top: 0.75rem;
+ margin-bottom: 0.25rem;
+ color: #0f172a;
+}
+
+.dark .resume-preview h3 {
+ color: #f1f5f9;
+}
+
+.resume-preview p {
+ margin-bottom: 0.5rem;
+ font-size: 0.95rem;
+}
+
+.resume-preview ul {
+ list-style-type: disc;
+ padding-left: 1.25rem;
+ margin-top: 0.25rem;
+ margin-bottom: 0.75rem;
+}
+
+.resume-preview li {
+ margin-bottom: 0.25rem;
+ font-size: 0.925rem;
+}
+
+.resume-preview strong {
+ font-weight: 600;
+ color: #0f172a;
+}
+
+.dark .resume-preview strong {
+ color: #f8fafc;
+}
+
+.resume-preview a {
+ color: #4f46e5;
+ text-decoration: underline;
+}
+
+.dark .resume-preview a {
+ color: #818cf8;
}
diff --git a/src/app/jobs/[id]/job-actions.tsx b/src/app/jobs/[id]/job-actions.tsx
index 9d65600..9afd43f 100644
--- a/src/app/jobs/[id]/job-actions.tsx
+++ b/src/app/jobs/[id]/job-actions.tsx
@@ -4,22 +4,39 @@ import { useRouter } from "next/navigation";
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"];
+const STATUS_CONFIG: Record
= {
+ prospect: { label: "Prospect", dot: "bg-zinc-400" },
+ reviewing: { label: "Reviewing", dot: "bg-amber-500" },
+ tailoring: { label: "Tailoring", dot: "bg-purple-500" },
+ ready: { label: "Ready", dot: "bg-blue-500" },
+ applied: { label: "Applied", dot: "bg-indigo-500" },
+ interviewing: { label: "Interviewing", dot: "bg-teal-500" },
+ offer: { label: "Offer", dot: "bg-emerald-500" },
+ rejected: { label: "Rejected", dot: "bg-rose-500" },
+ dead: { label: "Dead", dot: "bg-zinc-400" },
+};
+
export default function JobActions({ job }: { job: { id: string; status: string } }) {
const router = useRouter();
const [busy, setBusy] = useState("");
- const [msg, setMsg] = useState("");
+ const [msg, setMsg] = useState<{ type: "success" | "error"; text: string } | null>(null);
async function setStatus(status: string) {
- await fetch(`/api/jobs/${job.id}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ status }),
- });
- router.refresh();
+ try {
+ await fetch(`/api/jobs/${job.id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ status }),
+ });
+ router.refresh();
+ } catch {
+ setMsg({ type: "error", text: "Failed to update status." });
+ }
}
async function run(kind: "tailor" | "cover") {
- setBusy(kind); setMsg("");
+ setBusy(kind);
+ setMsg(null);
try {
const res = await fetch(`/api/ai/${kind}`, {
method: "POST",
@@ -27,53 +44,163 @@ export default function JobActions({ job }: { job: { id: string; status: string
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 ✅");
+ if (!res.ok) throw new Error(data.error ?? "Generation failed");
+ setMsg({
+ type: "success",
+ text: kind === "tailor" ? "Tailored resume created successfully! ✨" : "Cover letter created successfully! 📄",
+ });
router.refresh();
- } catch (e) { setMsg(String(e)); } finally { setBusy(""); }
+ } catch (e) {
+ setMsg({ type: "error", text: 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();
+ setBusy("applied");
+ setMsg(null);
+ try {
+ await fetch("/api/applications", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ jobId: job.id, method: "manual", submittedAt: new Date().toISOString() }),
+ });
+ setMsg({ type: "success", text: "Marked as applied! Application logged. ✅" });
+ router.refresh();
+ } catch (e) {
+ setMsg({ type: "error", text: String(e) });
+ } finally {
+ setBusy("");
+ }
}
async function del() {
- if (!confirm("Delete this job?")) return;
- await fetch(`/api/jobs/${job.id}`, { method: "DELETE" });
- router.push("/jobs");
+ if (!confirm("Are you sure you want to delete this job and its associated data?")) return;
+ try {
+ await fetch(`/api/jobs/${job.id}`, { method: "DELETE" });
+ router.push("/jobs");
+ } catch (e) {
+ setMsg({ type: "error", text: String(e) });
+ }
}
return (
-
-
-
Status:
-
+
+
+ {/* Status Dropdown */}
+
+
+ Status:
+
+
+
+
+
+
+
+ {/* Action Buttons */}
+
+ {/* AI Tailor Resume */}
+
+
+ {/* AI Cover Letter */}
+
+
+ {/* Mark Applied */}
+
+
+ {/* Delete Button */}
+
+
-
-
-
-
-
-
- {msg &&
{msg}
}
+
+ {/* Alert / Toast Message */}
+ {msg && (
+
+ {msg.text}
+
+ )}
);
}
diff --git a/src/app/jobs/[id]/page.tsx b/src/app/jobs/[id]/page.tsx
index 0ea01bd..54ea618 100644
--- a/src/app/jobs/[id]/page.tsx
+++ b/src/app/jobs/[id]/page.tsx
@@ -5,65 +5,308 @@ import JobActions from "./job-actions";
export const dynamic = "force-dynamic";
+function getCompanyColor(name: string) {
+ const colors = [
+ "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950/80 dark:text-indigo-300 dark:border-indigo-800",
+ "bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-950/80 dark:text-violet-300 dark:border-violet-800",
+ "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950/80 dark:text-blue-300 dark:border-blue-800",
+ "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/80 dark:text-emerald-300 dark:border-emerald-800",
+ "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-950/80 dark:text-amber-300 dark:border-amber-800",
+ "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-950/80 dark:text-rose-300 dark:border-rose-800",
+ "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-950/80 dark:text-teal-300 dark:border-teal-800",
+ "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-950/80 dark:text-cyan-300 dark:border-cyan-800",
+ ];
+ let hash = 0;
+ for (let i = 0; i < (name || "").length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return colors[Math.abs(hash) % colors.length];
+}
+
+function getInitials(name: string) {
+ if (!name) return "?";
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return name.slice(0, 2).toUpperCase();
+}
+
+function formatCompensation(j: { payMin: number | null; payMax: number | null; payCurrency: string | null; payPeriod: string | null }) {
+ if (!j.payMin && !j.payMax) return null;
+ const cur = j.payCurrency === "PHP" ? "₱" : "$";
+ const per = j.payPeriod === "hour" ? "/hr" : j.payPeriod === "month" ? "/mo" : j.payPeriod === "year" ? "/yr" : "";
+ const fmt = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`);
+ return `${cur}${fmt(j.payMin ?? 0)}${j.payMax ? ` – ${fmt(j.payMax)}` : ""}${per}`;
+}
+
export default async function JobDetail({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const job = await prisma.job.findUnique({
where: { id },
- include: { resumes: true, coverLetters: true, applications: true },
+ include: {
+ resumes: { orderBy: { updatedAt: "desc" } },
+ coverLetters: { orderBy: { updatedAt: "desc" } },
+ applications: { orderBy: { updatedAt: "desc" } },
+ },
});
if (!job) notFound();
+ const comp = formatCompensation(job);
+ const companyColor = getCompanyColor(job.company);
+ const initials = getInitials(job.company);
+
return (
-
+
+ {/* Breadcrumb & Top Bar */}
-
← Jobs
-
{job.title}
-
{job.company} · {job.location ?? "Remote"} · PH: {job.phEligibility}
-
- {job.url &&
Listing ↗}
- {job.applyUrl &&
Apply page ↗}
+
+
+
Back to Jobs
+
+
+ {/* Main Header Card */}
+
+
+
+ {initials}
+
+
+
+ {job.title}
+
+
+
{job.company}
+
·
+
+
+ {job.location ?? "Remote"}
+
+ {job.remoteType && (
+ <>
+
·
+
{job.remoteType}
+ >
+ )}
+
+
+ {/* Badges & Meta */}
+
+ {job.phEligibility === "yes" ? (
+
+
+ PH Eligible
+
+ ) : job.phEligibility === "no" ? (
+
+
+ Not Eligible
+
+ ) : (
+
+
+ PH: Unclear
+
+ )}
+
+ {comp && (
+
+ 💰 {comp}
+
+ )}
+
+ {job.tags && (
+
+ 🏷️ {job.tags}
+
+ )}
+
+
+
+ {/* External Links */}
+
+
+ {/* Action Toolbar */}
- {job.description && (
-
- Description
- {job.description}
-
- )}
- {job.requirements && (
-
- Requirements
- {job.requirements}
-
- )}
- {job.notes && (
-
- Notes
- {job.notes}
+ {/* Tailored Resumes Section */}
+ {job.resumes.length > 0 && (
+
+
+
+
+ Tailored Resumes ({job.resumes.length})
+
+
+
+ {job.resumes.map((r) => (
+
+
+
+ {r.name}
+
+
+ {r.kind} · v{r.version}
+
+
+
+ Edit / Print →
+
+
+ ))}
+
)}
- {job.resumes.length > 0 && (
-
- Tailored resumes
-
- {job.resumes.map((r) => (
- - {r.name}
+ {/* Cover Letters Section */}
+ {job.coverLetters.length > 0 && (
+
+
+
+ Cover Letters ({job.coverLetters.length})
+
+
+ {job.coverLetters.map((c) => (
+
+ {c.markdown}
+
))}
-
+
)}
- {job.coverLetters.length > 0 && (
-
- Cover letters
-
- {job.coverLetters.map((c) => (
- - {c.markdown}
+
+ {/* Description */}
+ {job.description && (
+
+
+
+ Job Description
+
+
+ {job.description}
+
+
+ )}
+
+ {/* Requirements */}
+ {job.requirements && (
+
+
+
+ Requirements
+
+
+ {job.requirements}
+
+
+ )}
+
+ {/* Notes */}
+ {job.notes && (
+
+
+ 📝 Private Notes
+
+
+ {job.notes}
+
+
+ )}
+
+ {/* Applications Log */}
+ {job.applications.length > 0 && (
+
+
+
+ Application Submissions
+
+
+ {job.applications.map((a) => (
+
+
+
+ Method: {a.method}
+
+ ·
+
+ {a.submittedAt
+ ? `Submitted on ${new Date(a.submittedAt).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })}`
+ : "Pending submission"}
+
+
+ {a.confirmation && (
+
Ref: {a.confirmation}
+ )}
+
))}
-
+
)}
diff --git a/src/app/jobs/new/page.tsx b/src/app/jobs/new/page.tsx
index 518d983..c8b9391 100644
--- a/src/app/jobs/new/page.tsx
+++ b/src/app/jobs/new/page.tsx
@@ -1,9 +1,25 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
+import Link from "next/link";
type Parsed = Record
;
+const FIELD_LABELS: Record = {
+ title: "Job Title",
+ company: "Company Name",
+ url: "Listing URL",
+ applyUrl: "Application / Portal URL",
+ location: "Location",
+ remoteType: "Remote Type (e.g. remote, hybrid, onsite)",
+ phEligibility: "PH Eligibility (yes / no / unclear)",
+ payMin: "Minimum Pay",
+ payMax: "Maximum Pay",
+ payCurrency: "Currency (USD, PHP, etc.)",
+ payPeriod: "Pay Period (hour, month, year)",
+ tags: "Tags (comma-separated)",
+};
+
export default function NewJobPage() {
const router = useRouter();
const [url, setUrl] = useState("");
@@ -13,7 +29,8 @@ export default function NewJobPage() {
const [err, setErr] = useState("");
async function parse() {
- setBusy(true); setErr("");
+ setBusy(true);
+ setErr("");
try {
const res = await fetch("/api/ai/parse", {
method: "POST",
@@ -21,62 +38,222 @@ export default function NewJobPage() {
body: JSON.stringify({ url: url || undefined, text: text || undefined }),
});
const data = await res.json();
- if (!res.ok) throw new Error(data.error ?? "parse failed");
+ if (!res.ok) throw new Error(data.error ?? "Parse failed");
setParsed(data);
- } catch (e) { setErr(String(e)); } finally { setBusy(false); }
+ } catch (e) {
+ setErr(String(e));
+ } finally {
+ setBusy(false);
+ }
}
async function save() {
if (!parsed) return;
setBusy(true);
- const res = await fetch("/api/jobs", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(parsed),
- });
- const job = await res.json();
- router.push(`/jobs/${job.id}`);
+ try {
+ const res = await fetch("/api/jobs", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(parsed),
+ });
+ const job = await res.json();
+ router.push(`/jobs/${job.id}`);
+ } catch (e) {
+ setErr(String(e));
+ setBusy(false);
+ }
}
- function set(k: string, v: string) { setParsed((p) => ({ ...p!, [k]: v || null })); }
+ function set(k: string, v: string) {
+ setParsed((p) => ({ ...p!, [k]: v || null }));
+ }
return (
-
-
Add Job
- {!parsed && (
- <>
-
setUrl(e.target.value)} placeholder="Job URL (may fail on Cloudflare sites)"
- className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-2 text-sm" />
-
— or paste job text (more reliable) —
-