redesign UI with left sidebar, card grids, and two-column dashboard layout
This commit is contained in:
@@ -16,51 +16,51 @@ const STATUS_CONFIG: Record<
|
||||
},
|
||||
reviewing: {
|
||||
label: "Reviewing",
|
||||
bg: "bg-amber-50 dark:bg-amber-950/40",
|
||||
bg: "bg-amber-50 dark:bg-amber-950/50",
|
||||
text: "text-amber-700 dark:text-amber-300",
|
||||
border: "border-amber-200 dark:border-amber-800/60",
|
||||
border: "border-amber-200/80 dark:border-amber-900/60",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
tailoring: {
|
||||
label: "Tailoring",
|
||||
bg: "bg-purple-50 dark:bg-purple-950/40",
|
||||
bg: "bg-purple-50 dark:bg-purple-950/50",
|
||||
text: "text-purple-700 dark:text-purple-300",
|
||||
border: "border-purple-200 dark:border-purple-800/60",
|
||||
border: "border-purple-200/80 dark:border-purple-900/60",
|
||||
dot: "bg-purple-500",
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
bg: "bg-blue-50 dark:bg-blue-950/40",
|
||||
bg: "bg-blue-50 dark:bg-blue-950/50",
|
||||
text: "text-blue-700 dark:text-blue-300",
|
||||
border: "border-blue-200 dark:border-blue-800/60",
|
||||
border: "border-blue-200/80 dark:border-blue-900/60",
|
||||
dot: "bg-blue-500",
|
||||
},
|
||||
applied: {
|
||||
label: "Applied",
|
||||
bg: "bg-indigo-50 dark:bg-indigo-950/40",
|
||||
bg: "bg-indigo-50 dark:bg-indigo-950/50",
|
||||
text: "text-indigo-700 dark:text-indigo-300",
|
||||
border: "border-indigo-200 dark:border-indigo-800/60",
|
||||
border: "border-indigo-200/80 dark:border-indigo-900/60",
|
||||
dot: "bg-indigo-500",
|
||||
},
|
||||
interviewing: {
|
||||
label: "Interviewing",
|
||||
bg: "bg-teal-50 dark:bg-teal-950/40",
|
||||
bg: "bg-teal-50 dark:bg-teal-950/50",
|
||||
text: "text-teal-700 dark:text-teal-300",
|
||||
border: "border-teal-200 dark:border-teal-800/60",
|
||||
border: "border-teal-200/80 dark:border-teal-900/60",
|
||||
dot: "bg-teal-500",
|
||||
},
|
||||
offer: {
|
||||
label: "Offer",
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/40",
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/50",
|
||||
text: "text-emerald-700 dark:text-emerald-300",
|
||||
border: "border-emerald-200 dark:border-emerald-800/60",
|
||||
border: "border-emerald-200/80 dark:border-emerald-900/60",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
label: "Rejected",
|
||||
bg: "bg-rose-50 dark:bg-rose-950/40",
|
||||
bg: "bg-rose-50 dark:bg-rose-950/50",
|
||||
text: "text-rose-700 dark:text-rose-300",
|
||||
border: "border-rose-200 dark:border-rose-800/60",
|
||||
border: "border-rose-200/80 dark:border-rose-900/60",
|
||||
dot: "bg-rose-500",
|
||||
},
|
||||
dead: {
|
||||
@@ -108,36 +108,48 @@ export default async function ApplicationsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-2 border-b border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Applications</h1>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
Applications
|
||||
</h1>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-indigo-50 text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800/60">
|
||||
{apps.length} {apps.length === 1 ? "application" : "applications"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Track submitted job applications and their current recruitment pipeline status.
|
||||
Track submitted job applications and monitor feedback throughout your hiring process.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200/90 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/70 shadow-2xs transition-all shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
<span>Browse Jobs Pipeline</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Applications List */}
|
||||
{apps.length === 0 ? (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-xs">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-2xs">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400 mb-3">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">No applications recorded yet</h3>
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-zinc-100">No applications recorded yet</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
|
||||
Open any job in your list and click <strong>“Mark Applied”</strong> to log an application submission here.
|
||||
Open any job in your list and click <strong>“Mark Applied”</strong> or complete the <strong>“Apply Wizard”</strong> to record a submission.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Browse Jobs
|
||||
</Link>
|
||||
@@ -153,7 +165,7 @@ export default async function ApplicationsPage() {
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all group"
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-2xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group"
|
||||
>
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div
|
||||
@@ -169,7 +181,7 @@ export default async function ApplicationsPage() {
|
||||
{a.job.title}
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span className="font-medium text-zinc-700 dark:text-zinc-300">{a.job.company}</span>
|
||||
<span className="font-semibold text-zinc-700 dark:text-zinc-300">{a.job.company}</span>
|
||||
<span>·</span>
|
||||
<span className="capitalize">{a.method} submission</span>
|
||||
<span>·</span>
|
||||
@@ -196,10 +208,10 @@ export default async function ApplicationsPage() {
|
||||
|
||||
<Link
|
||||
href={`/jobs/${a.job.id}`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800/80 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors group-hover:border-zinc-300"
|
||||
>
|
||||
<span>View Job</span>
|
||||
<svg className="w-3.5 h-3.5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-3.5 h-3.5 text-zinc-400 group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
:root {
|
||||
--background: #f8fafc;
|
||||
--foreground: #0f172a;
|
||||
--foreground: #09090b;
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #e2e8f0;
|
||||
}
|
||||
@@ -21,20 +21,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: var(--card-border);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
letter-spacing: -0.011em;
|
||||
}
|
||||
|
||||
/* Headings tracking & typography tweaks */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgba(99, 102, 241, 0.25);
|
||||
background-color: rgba(99, 102, 241, 0.22);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
/* Polished Minimalist Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -45,7 +56,7 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.4);
|
||||
background: rgba(148, 163, 184, 0.35);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
@@ -53,6 +64,21 @@ body {
|
||||
background: rgba(148, 163, 184, 0.6);
|
||||
}
|
||||
|
||||
/* Focus styles for keyboard accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid rgba(99, 102, 241, 0.6);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Smooth Interactive Card Accents */
|
||||
.card-hover-effect {
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.card-hover-effect:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Resume & Markdown Preview Styling */
|
||||
.resume-preview {
|
||||
font-family: "Charter", "Bitstream Vera Serif", "Cambria", "Times New Roman", serif;
|
||||
@@ -80,18 +106,16 @@ body {
|
||||
}
|
||||
|
||||
.resume-preview h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #1e293b;
|
||||
color: #334155;
|
||||
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 {
|
||||
|
||||
@@ -13,13 +13,22 @@ function CopyField({ label, value }: { label: string; value: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
if (!value) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-zinc-500 w-28 shrink-0">{label}</span>
|
||||
<span className="truncate flex-1">{value}</span>
|
||||
<div className="flex items-center justify-between gap-3 p-2.5 rounded-xl bg-zinc-50/70 dark:bg-zinc-800/40 border border-zinc-200/60 dark:border-zinc-800 text-sm group hover:border-zinc-300 dark:hover:border-zinc-700 transition-colors">
|
||||
<span className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 w-32 shrink-0">{label}</span>
|
||||
<span className="truncate flex-1 text-zinc-800 dark:text-zinc-200 font-medium text-xs sm:text-sm">{value}</span>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); }}
|
||||
className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-0.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
{copied ? "✓" : "Copy"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-semibold transition-all shrink-0 ${
|
||||
copied
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800"
|
||||
: "bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 shadow-2xs"
|
||||
}`}
|
||||
>
|
||||
{copied ? "✓ Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -35,7 +44,8 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
async function genAnswers() {
|
||||
setBusy(true); setErr("");
|
||||
setBusy(true);
|
||||
setErr("");
|
||||
try {
|
||||
const qs = questions.split("\n").map((q) => q.trim()).filter(Boolean);
|
||||
const res = await fetch("/api/ai/answers", {
|
||||
@@ -46,7 +56,11 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error ?? "failed");
|
||||
setAnswers(data.answers);
|
||||
} catch (e) { setErr(String(e)); } finally { setBusy(false); }
|
||||
} catch (e) {
|
||||
setErr(String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function markSubmitted() {
|
||||
@@ -67,95 +81,213 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<Link href={`/jobs/${job.id}`} className="text-sm text-zinc-500 hover:text-indigo-600">← Back to job</Link>
|
||||
<h1 className="text-2xl font-bold mt-1">Apply: {job.title}</h1>
|
||||
<div className="text-zinc-500">{job.company}</div>
|
||||
<Link
|
||||
href={`/jobs/${job.id}`}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-2 group"
|
||||
>
|
||||
<svg className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
<span>Back to Job</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
Apply Wizard: {job.title}
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
{job.company} · Follow these 5 steps to efficiently submit and record your application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step 1: open the application page */}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
|
||||
<h2 className="font-semibold">1 · Open the application page</h2>
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-indigo-50 dark:bg-indigo-950/80 text-indigo-600 dark:text-indigo-400 text-xs font-bold border border-indigo-200/60 dark:border-indigo-800/60">
|
||||
1
|
||||
</span>
|
||||
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Open the Employer Application Page</h2>
|
||||
</div>
|
||||
{job.applyUrl || job.url ? (
|
||||
<a href={job.applyUrl ?? job.url ?? "#"} target="_blank"
|
||||
className="inline-block rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 text-white px-4 py-2 text-sm font-semibold hover:from-indigo-500 hover:to-violet-500">
|
||||
Open application page ↗
|
||||
<a
|
||||
href={job.applyUrl ?? job.url ?? "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 text-white px-4 py-2.5 text-xs font-semibold hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
<span>Open Application Page</span>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
</a>
|
||||
) : <p className="text-sm text-zinc-500">No apply URL saved — add one on the job page.</p>}
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">No application URL saved — add one on the job page.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Step 2: copy-ready profile fields */}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-2">
|
||||
<h2 className="font-semibold mb-2">2 · Copy your details into the form</h2>
|
||||
<CopyField label="Full name" value={profile.fullName ?? ""} />
|
||||
<CopyField label="Email" value={profile.email ?? ""} />
|
||||
<CopyField label="Phone" value={profile.phone ?? ""} />
|
||||
<CopyField label="Location" value={profile.location ?? ""} />
|
||||
<CopyField label="LinkedIn" value={profile.linkedin ?? ""} />
|
||||
<CopyField label="GitHub" value={profile.github ?? ""} />
|
||||
<CopyField label="Portfolio" value={profile.portfolio ?? ""} />
|
||||
<CopyField label="Headline" value={profile.headline ?? ""} />
|
||||
<CopyField label="Summary" value={profile.summary ?? ""} />
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-violet-50 dark:bg-violet-950/80 text-violet-600 dark:text-violet-400 text-xs font-bold border border-violet-200/60 dark:border-violet-800/60">
|
||||
2
|
||||
</span>
|
||||
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Copy Profile Fields into Form</h2>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Click copy on any field to easily paste into the employer's application portal.
|
||||
</p>
|
||||
<div className="space-y-2 pt-1">
|
||||
<CopyField label="Full Name" value={profile.fullName ?? ""} />
|
||||
<CopyField label="Email" value={profile.email ?? ""} />
|
||||
<CopyField label="Phone" value={profile.phone ?? ""} />
|
||||
<CopyField label="Location" value={profile.location ?? ""} />
|
||||
<CopyField label="LinkedIn" value={profile.linkedin ?? ""} />
|
||||
<CopyField label="GitHub" value={profile.github ?? ""} />
|
||||
<CopyField label="Portfolio" value={profile.portfolio ?? ""} />
|
||||
<CopyField label="Headline" value={profile.headline ?? ""} />
|
||||
<CopyField label="Summary" value={profile.summary ?? ""} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Step 3: resume + cover letter */}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
|
||||
<h2 className="font-semibold">3 · Attach documents</h2>
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-purple-50 dark:bg-purple-950/80 text-purple-600 dark:text-purple-400 text-xs font-bold border border-purple-200/60 dark:border-purple-800/60">
|
||||
3
|
||||
</span>
|
||||
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Attach Tailored Documents</h2>
|
||||
</div>
|
||||
{job.resumes.length ? (
|
||||
<div className="text-sm space-y-2">
|
||||
<label className="block">
|
||||
<span className="text-zinc-500">Resume to use</span>
|
||||
<select value={resumeId} onChange={(e) => setResumeId(e.target.value)}
|
||||
className="mt-1 w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-2">
|
||||
{job.resumes.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||
</select>
|
||||
<div className="space-y-2 text-sm">
|
||||
<label className="block text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
||||
Select Tailored Resume
|
||||
</label>
|
||||
{resumeId && <Link href={`/resumes/${resumeId}`} className="text-indigo-600 hover:underline text-sm">Open resume → Print/PDF ↗</Link>}
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={resumeId}
|
||||
onChange={(e) => setResumeId(e.target.value)}
|
||||
className="flex-1 rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-2.5 text-xs sm:text-sm text-zinc-900 dark:text-zinc-100 focus:border-indigo-500 focus:outline-hidden focus:ring-2 focus:ring-indigo-500/20"
|
||||
>
|
||||
{job.resumes.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{resumeId && (
|
||||
<Link
|
||||
href={`/resumes/${resumeId}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-1 px-3 py-2 rounded-xl text-xs font-semibold text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/60 border border-purple-200/80 dark:border-purple-900/60 hover:bg-purple-100 transition-colors shrink-0"
|
||||
>
|
||||
<span>Open PDF ↗</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">No tailored resume yet — generate one from the job page (“AI Tailor Resume”).</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
No tailored resume yet — generate one on the job page with “AI Tailor Resume”.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{job.coverLetters.length > 0 && (
|
||||
<details className="text-sm">
|
||||
<summary className="cursor-pointer text-zinc-600 dark:text-zinc-300">Cover letter v{job.coverLetters[job.coverLetters.length - 1].version} (click to view/copy)</summary>
|
||||
<div className="mt-2 rounded-xl border border-zinc-200 dark:border-zinc-800 p-3 whitespace-pre-wrap">{job.coverLetters[job.coverLetters.length - 1].markdown}</div>
|
||||
<button onClick={() => navigator.clipboard.writeText(job.coverLetters[job.coverLetters.length - 1].markdown)}
|
||||
className="mt-2 rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">Copy letter</button>
|
||||
<details className="text-sm rounded-xl border border-zinc-200 dark:border-zinc-800 p-3.5 bg-zinc-50/50 dark:bg-zinc-950/40">
|
||||
<summary className="cursor-pointer font-semibold text-xs text-zinc-800 dark:text-zinc-200 select-none">
|
||||
Cover Letter v{job.coverLetters[job.coverLetters.length - 1].version} (Click to view / copy)
|
||||
</summary>
|
||||
<div className="mt-3 p-3 rounded-lg bg-white dark:bg-zinc-900 border border-zinc-200/80 dark:border-zinc-800 text-xs text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">
|
||||
{job.coverLetters[job.coverLetters.length - 1].markdown}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(job.coverLetters[job.coverLetters.length - 1].markdown)}
|
||||
className="mt-2.5 px-3 py-1 rounded-lg text-xs font-semibold bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors shadow-2xs"
|
||||
>
|
||||
Copy Cover Letter
|
||||
</button>
|
||||
</details>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Step 4: AI answers for form questions */}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
|
||||
<h2 className="font-semibold">4 · Application questions (optional, AI-drafted)</h2>
|
||||
<textarea value={questions} onChange={(e) => setQuestions(e.target.value)} rows={4}
|
||||
placeholder={"Paste the form's questions, one per line:\nWhy do you want to work here?\nDescribe your experience with LLMs."}
|
||||
className="w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-3 text-sm" />
|
||||
<button onClick={genAnswers} disabled={busy || !questions.trim()}
|
||||
className="rounded-xl bg-indigo-600 text-white px-4 py-2 text-sm font-semibold disabled:opacity-50 hover:bg-indigo-500">
|
||||
{busy ? "Drafting…" : "🪄 Draft answers with AI"}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-50 dark:bg-blue-950/80 text-blue-600 dark:text-blue-400 text-xs font-bold border border-blue-200/60 dark:border-blue-800/60">
|
||||
4
|
||||
</span>
|
||||
<h2 className="font-bold text-base text-zinc-900 dark:text-white">AI Application Answers (Optional)</h2>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Paste specific form questions (one per line) and AI will draft customized responses based on your profile and the job requirements.
|
||||
</p>
|
||||
<textarea
|
||||
value={questions}
|
||||
onChange={(e) => setQuestions(e.target.value)}
|
||||
rows={4}
|
||||
placeholder={"Why do you want to work at this company?\nDescribe your experience with TypeScript and Next.js."}
|
||||
className="w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-3.5 text-xs sm:text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:border-indigo-500 focus:outline-hidden focus:ring-2 focus:ring-indigo-500/20 leading-relaxed"
|
||||
/>
|
||||
<button
|
||||
onClick={genAnswers}
|
||||
disabled={busy || !questions.trim()}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 disabled:opacity-50 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
{busy ? (
|
||||
<>
|
||||
<svg className="animate-spin w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
<span>Drafting with AI…</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>🪄 Draft Answers with AI</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{err && <div className="text-sm text-rose-600">{err}</div>}
|
||||
|
||||
{err && <div className="text-xs font-medium text-rose-600 dark:text-rose-400">{err}</div>}
|
||||
|
||||
{answers.map((qa, i) => (
|
||||
<div key={i} className="rounded-xl border border-zinc-200 dark:border-zinc-800 p-3 text-sm space-y-1">
|
||||
<div className="font-medium">{qa.question}</div>
|
||||
<div className="text-zinc-600 dark:text-zinc-300 whitespace-pre-wrap">{qa.answer}</div>
|
||||
<button onClick={() => navigator.clipboard.writeText(qa.answer)}
|
||||
className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-0.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">Copy</button>
|
||||
<div key={i} className="rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-950/40 p-4 space-y-2">
|
||||
<div className="font-bold text-xs text-zinc-900 dark:text-zinc-100">{qa.question}</div>
|
||||
<div className="text-xs text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed bg-white dark:bg-zinc-900 p-3 rounded-lg border border-zinc-200/80 dark:border-zinc-800">
|
||||
{qa.answer}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(qa.answer)}
|
||||
className="px-2.5 py-1 rounded-lg text-xs font-semibold bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors shadow-2xs"
|
||||
>
|
||||
Copy Answer
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Step 5: confirm */}
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
|
||||
<h2 className="font-semibold">5 · Done?</h2>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={confirmSubmit} onChange={(e) => setConfirmSubmit(e.target.checked)} />
|
||||
I submitted the application on the employer's site
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-950/80 text-emerald-600 dark:text-emerald-400 text-xs font-bold border border-emerald-200/60 dark:border-emerald-800/60">
|
||||
5
|
||||
</span>
|
||||
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Record Submission</h2>
|
||||
</div>
|
||||
<label className="flex items-center gap-2.5 text-xs sm:text-sm font-medium text-zinc-800 dark:text-zinc-200 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmSubmit}
|
||||
onChange={(e) => setConfirmSubmit(e.target.checked)}
|
||||
className="w-4 h-4 rounded-md text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||||
/>
|
||||
<span>I have submitted the application on the employer's site</span>
|
||||
</label>
|
||||
<button onClick={markSubmitted} disabled={!confirmSubmit || busy}
|
||||
className="rounded-xl bg-emerald-600 text-white px-5 py-2 text-sm font-semibold disabled:opacity-50 hover:bg-emerald-500">
|
||||
✅ Record application
|
||||
<button
|
||||
onClick={markSubmitted}
|
||||
disabled={!confirmSubmit || busy}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-emerald-600 via-emerald-600 to-teal-600 text-white px-5 py-2.5 text-xs font-semibold disabled:opacity-50 hover:from-emerald-500 hover:to-teal-500 shadow-sm shadow-emerald-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
<span>✅ Record Application in Pipeline</span>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"];
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"] as const;
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; dot: string }> = {
|
||||
prospect: { label: "Prospect", dot: "bg-zinc-400" },
|
||||
@@ -86,12 +86,12 @@ export default function JobActions({ job }: { job: { id: string; status: string
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-xs space-y-4">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-2xs space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
{/* Status Dropdown */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
|
||||
Status:
|
||||
Pipeline Stage:
|
||||
</span>
|
||||
<div className="relative inline-flex items-center">
|
||||
<select
|
||||
@@ -113,13 +113,13 @@ export default function JobActions({ job }: { job: { id: string; status: string
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{/* Action Buttons Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* AI Tailor Resume */}
|
||||
<button
|
||||
onClick={() => run("tailor")}
|
||||
disabled={!!busy}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 shadow-sm shadow-purple-500/20 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95 transition-all"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-purple-600 via-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 shadow-sm shadow-purple-500/20 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95 transition-all"
|
||||
>
|
||||
{busy === "tailor" ? (
|
||||
<>
|
||||
@@ -143,7 +143,7 @@ export default function JobActions({ job }: { job: { id: string; status: string
|
||||
<button
|
||||
onClick={() => run("cover")}
|
||||
disabled={!!busy}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-500 hover:to-blue-500 shadow-sm shadow-indigo-500/20 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95 transition-all"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-blue-600 hover:from-indigo-500 hover:to-blue-500 shadow-sm shadow-indigo-500/20 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95 transition-all"
|
||||
>
|
||||
{busy === "cover" ? (
|
||||
<>
|
||||
@@ -166,7 +166,7 @@ export default function JobActions({ job }: { job: { id: string; status: string
|
||||
{/* Apply Wizard */}
|
||||
<a
|
||||
href={`/jobs/${job.id}/apply`}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 shadow-sm shadow-blue-500/20 active:scale-95 transition-all"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-blue-600 via-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 shadow-sm shadow-blue-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
@@ -178,7 +178,7 @@ export default function JobActions({ job }: { job: { id: string; status: string
|
||||
<button
|
||||
onClick={markApplied}
|
||||
disabled={!!busy}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 shadow-sm shadow-emerald-500/20 disabled:opacity-50 active:scale-95 transition-all"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-emerald-600 via-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 shadow-sm shadow-emerald-500/20 disabled:opacity-50 active:scale-95 transition-all"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
|
||||
@@ -62,22 +62,22 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
<div>
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-3"
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-3 group"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
<span>Back to Jobs</span>
|
||||
</Link>
|
||||
|
||||
{/* Main Header Card */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-4">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl font-bold text-base border shadow-xs ${companyColor}`}>
|
||||
<div className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl font-bold text-base border shadow-2xs ${companyColor}`}>
|
||||
{initials}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">
|
||||
<h1 className="text-2xl font-extrabold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">
|
||||
{job.title}
|
||||
</h1>
|
||||
<div className="flex flex-wrap items-center gap-2.5 mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@@ -101,17 +101,17 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
{/* Badges & Meta */}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-3">
|
||||
{job.phEligibility === "yes" ? (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
PH Eligible
|
||||
</span>
|
||||
) : job.phEligibility === "no" ? (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" />
|
||||
Not Eligible
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 text-zinc-600 border border-zinc-200 dark:bg-zinc-800/60 dark:text-zinc-400 dark:border-zinc-700/60">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 text-zinc-600 border border-zinc-200 dark:bg-zinc-800/60 dark:text-zinc-400 dark:border-zinc-700/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
|
||||
PH: Unclear
|
||||
</span>
|
||||
@@ -138,7 +138,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
href={job.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
<span>Listing</span>
|
||||
<svg className="w-3.5 h-3.5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -151,7 +151,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
href={job.applyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/60 border border-indigo-200 dark:border-indigo-900/60 hover:bg-indigo-100 dark:hover:bg-indigo-900/80 transition-colors"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/60 border border-indigo-200 dark:border-indigo-900/60 hover:bg-indigo-100 dark:hover:bg-indigo-900/80 transition-colors"
|
||||
>
|
||||
<span>Apply Page</span>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -169,7 +169,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Tailored Resumes Section */}
|
||||
{job.resumes.length > 0 && (
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -178,7 +178,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
<span>Tailored Resumes ({job.resumes.length})</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="grid gap-2.5 sm:grid-cols-2">
|
||||
{job.resumes.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
@@ -197,7 +197,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
</div>
|
||||
<Link
|
||||
href={`/resumes/${r.id}`}
|
||||
className="px-3 py-1 rounded-lg text-xs font-medium text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/60 border border-purple-200/80 dark:border-purple-900/60 hover:bg-purple-100 transition-colors"
|
||||
className="px-3 py-1 rounded-lg text-xs font-semibold text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/60 border border-purple-200/80 dark:border-purple-900/60 hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
Edit / Print →
|
||||
</Link>
|
||||
@@ -209,7 +209,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Cover Letters Section */}
|
||||
{job.coverLetters.length > 0 && (
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
@@ -231,7 +231,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Description */}
|
||||
{job.description && (
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
|
||||
@@ -246,7 +246,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Requirements */}
|
||||
{job.requirements && (
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
@@ -261,7 +261,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Notes */}
|
||||
{job.notes && (
|
||||
<section className="rounded-2xl border border-amber-200/80 dark:border-amber-900/60 bg-amber-50/30 dark:bg-amber-950/20 p-6 shadow-xs space-y-2">
|
||||
<section className="rounded-2xl border border-amber-200/80 dark:border-amber-900/60 bg-amber-50/30 dark:bg-amber-950/20 p-6 shadow-2xs space-y-2">
|
||||
<h2 className="text-sm font-bold text-amber-800 dark:text-amber-300 flex items-center gap-2">
|
||||
<span>📝 Private Notes</span>
|
||||
</h2>
|
||||
@@ -273,7 +273,7 @@ export default async function JobDetail({ params }: { params: Promise<{ id: stri
|
||||
|
||||
{/* Applications Log */}
|
||||
{job.applications.length > 0 && (
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
|
||||
@@ -74,21 +74,21 @@ export default function NewJobPage() {
|
||||
<div>
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-2"
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-2 group"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
<span>Back to Jobs</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Add New Job</h1>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">Add New Job</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Provide a job URL or paste the job posting description. AI will automatically extract key details and structured fields.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{err && (
|
||||
<div className="rounded-xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-800/60 p-4 text-sm text-rose-800 dark:text-rose-200 flex items-start gap-3">
|
||||
<div className="rounded-2xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-800/60 p-4 text-sm text-rose-800 dark:text-rose-200 flex items-start gap-3 shadow-2xs">
|
||||
<svg className="w-5 h-5 text-rose-500 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
@@ -97,14 +97,14 @@ export default function NewJobPage() {
|
||||
)}
|
||||
|
||||
{!parsed ? (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-6">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-6">
|
||||
{/* URL Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-semibold text-zinc-800 dark:text-zinc-200">
|
||||
<label className="block text-xs font-bold uppercase tracking-wider text-zinc-700 dark:text-zinc-300">
|
||||
Job Listing URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-zinc-400">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-zinc-400">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
@@ -118,21 +118,21 @@ export default function NewJobPage() {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
Note: Some sites with anti-bot or Cloudflare protection may block scraping; pasting raw text below is recommended.
|
||||
Note: If the site blocks web scraping (Cloudflare, auth walls), paste raw text below for guaranteed extraction.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative flex items-center justify-center">
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-800 w-full" />
|
||||
<span className="bg-white dark:bg-zinc-900 px-3 text-xs font-medium text-zinc-400 uppercase tracking-wider absolute">
|
||||
<span className="bg-white dark:bg-zinc-900 px-3 text-xs font-semibold text-zinc-400 uppercase tracking-wider absolute">
|
||||
or paste job text
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Textarea */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-semibold text-zinc-800 dark:text-zinc-200">
|
||||
<label className="block text-xs font-bold uppercase tracking-wider text-zinc-700 dark:text-zinc-300">
|
||||
Job Description / Posting Text
|
||||
</label>
|
||||
<textarea
|
||||
@@ -149,7 +149,7 @@ export default function NewJobPage() {
|
||||
<button
|
||||
onClick={parse}
|
||||
disabled={busy || (!url && !text)}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 disabled:opacity-50 disabled:cursor-not-allowed active:scale-98 transition-all"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed active:scale-98 transition-all"
|
||||
>
|
||||
{busy ? (
|
||||
<>
|
||||
@@ -171,11 +171,11 @@ export default function NewJobPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-6">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-6">
|
||||
<div className="flex items-center justify-between pb-4 border-b border-zinc-200/80 dark:border-zinc-800/80">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/80 text-emerald-600 dark:text-emerald-400 text-xs">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/80 text-emerald-600 dark:text-emerald-400 text-xs font-bold">
|
||||
✓
|
||||
</span>
|
||||
Review Extracted Fields
|
||||
@@ -187,7 +187,7 @@ export default function NewJobPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setParsed(null)}
|
||||
className="text-xs font-medium text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||
className="text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
||||
>
|
||||
← Edit Raw Text
|
||||
</button>
|
||||
@@ -196,7 +196,7 @@ export default function NewJobPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{["title", "company", "url", "applyUrl", "location", "remoteType", "phEligibility", "payMin", "payMax", "payCurrency", "payPeriod", "tags"].map((k) => (
|
||||
<div key={k} className="space-y-1">
|
||||
<label className="block text-xs font-semibold text-zinc-600 dark:text-zinc-300">
|
||||
<label className="block text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
||||
{FIELD_LABELS[k] || k}
|
||||
</label>
|
||||
<input
|
||||
@@ -211,7 +211,7 @@ export default function NewJobPage() {
|
||||
|
||||
{["description", "requirements"].map((k) => (
|
||||
<div key={k} className="space-y-1">
|
||||
<label className="block text-xs font-semibold text-zinc-600 dark:text-zinc-300 capitalize">
|
||||
<label className="block text-xs font-semibold text-zinc-700 dark:text-zinc-300 capitalize">
|
||||
{k}
|
||||
</label>
|
||||
<textarea
|
||||
|
||||
@@ -3,11 +3,11 @@ import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"];
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"] as const;
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
string,
|
||||
{ label: string; bg: string; text: string; border: string; dot: string }
|
||||
{ label: string; bg: string; text: string; border: string; dot: string; accentBorder: string }
|
||||
> = {
|
||||
prospect: {
|
||||
label: "Prospect",
|
||||
@@ -15,55 +15,63 @@ const STATUS_CONFIG: Record<
|
||||
text: "text-zinc-700 dark:text-zinc-300",
|
||||
border: "border-zinc-200 dark:border-zinc-700",
|
||||
dot: "bg-zinc-400 dark:bg-zinc-500",
|
||||
accentBorder: "border-l-zinc-400 dark:border-l-zinc-500",
|
||||
},
|
||||
reviewing: {
|
||||
label: "Reviewing",
|
||||
bg: "bg-amber-50 dark:bg-amber-950/40",
|
||||
bg: "bg-amber-50 dark:bg-amber-950/50",
|
||||
text: "text-amber-700 dark:text-amber-300",
|
||||
border: "border-amber-200 dark:border-amber-800/60",
|
||||
border: "border-amber-200/80 dark:border-amber-900/60",
|
||||
dot: "bg-amber-500",
|
||||
accentBorder: "border-l-amber-500",
|
||||
},
|
||||
tailoring: {
|
||||
label: "Tailoring",
|
||||
bg: "bg-purple-50 dark:bg-purple-950/40",
|
||||
bg: "bg-purple-50 dark:bg-purple-950/50",
|
||||
text: "text-purple-700 dark:text-purple-300",
|
||||
border: "border-purple-200 dark:border-purple-800/60",
|
||||
border: "border-purple-200/80 dark:border-purple-900/60",
|
||||
dot: "bg-purple-500",
|
||||
accentBorder: "border-l-purple-500",
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
bg: "bg-blue-50 dark:bg-blue-950/40",
|
||||
bg: "bg-blue-50 dark:bg-blue-950/50",
|
||||
text: "text-blue-700 dark:text-blue-300",
|
||||
border: "border-blue-200 dark:border-blue-800/60",
|
||||
border: "border-blue-200/80 dark:border-blue-900/60",
|
||||
dot: "bg-blue-500",
|
||||
accentBorder: "border-l-blue-500",
|
||||
},
|
||||
applied: {
|
||||
label: "Applied",
|
||||
bg: "bg-indigo-50 dark:bg-indigo-950/40",
|
||||
bg: "bg-indigo-50 dark:bg-indigo-950/50",
|
||||
text: "text-indigo-700 dark:text-indigo-300",
|
||||
border: "border-indigo-200 dark:border-indigo-800/60",
|
||||
border: "border-indigo-200/80 dark:border-indigo-900/60",
|
||||
dot: "bg-indigo-500",
|
||||
accentBorder: "border-l-indigo-500",
|
||||
},
|
||||
interviewing: {
|
||||
label: "Interviewing",
|
||||
bg: "bg-teal-50 dark:bg-teal-950/40",
|
||||
bg: "bg-teal-50 dark:bg-teal-950/50",
|
||||
text: "text-teal-700 dark:text-teal-300",
|
||||
border: "border-teal-200 dark:border-teal-800/60",
|
||||
border: "border-teal-200/80 dark:border-teal-900/60",
|
||||
dot: "bg-teal-500",
|
||||
accentBorder: "border-l-teal-500",
|
||||
},
|
||||
offer: {
|
||||
label: "Offer",
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/40",
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/50",
|
||||
text: "text-emerald-700 dark:text-emerald-300",
|
||||
border: "border-emerald-200 dark:border-emerald-800/60",
|
||||
border: "border-emerald-200/80 dark:border-emerald-900/60",
|
||||
dot: "bg-emerald-500",
|
||||
accentBorder: "border-l-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
label: "Rejected",
|
||||
bg: "bg-rose-50 dark:bg-rose-950/40",
|
||||
bg: "bg-rose-50 dark:bg-rose-950/50",
|
||||
text: "text-rose-700 dark:text-rose-300",
|
||||
border: "border-rose-200 dark:border-rose-800/60",
|
||||
border: "border-rose-200/80 dark:border-rose-900/60",
|
||||
dot: "bg-rose-500",
|
||||
accentBorder: "border-l-rose-500",
|
||||
},
|
||||
dead: {
|
||||
label: "Dead",
|
||||
@@ -71,6 +79,7 @@ const STATUS_CONFIG: Record<
|
||||
text: "text-zinc-500 dark:text-zinc-400",
|
||||
border: "border-zinc-200 dark:border-zinc-700/60",
|
||||
dot: "bg-zinc-400",
|
||||
accentBorder: "border-l-zinc-400",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,12 +110,12 @@ function getInitials(name: string) {
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function pay(j: { payMin: number | null; payMax: number | null; payCurrency: string | null; payPeriod: string | null }) {
|
||||
if (!j.payMin && !j.payMax) return "—";
|
||||
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}`;
|
||||
return `${cur}${fmt(j.payMin ?? 0)}${j.payMax && j.payMax !== j.payMin ? `–${fmt(j.payMax)}` : ""}${per}`;
|
||||
}
|
||||
|
||||
export default async function JobsPage({ searchParams }: { searchParams: Promise<{ status?: string }> }) {
|
||||
@@ -125,11 +134,11 @@ export default async function JobsPage({ searchParams }: { searchParams: Promise
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
{/* Header & Page Action */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-2 border-b border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
Jobs
|
||||
</h1>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
@@ -138,17 +147,25 @@ export default async function JobsPage({ searchParams }: { searchParams: Promise
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
{status ? (
|
||||
<span>
|
||||
Filtering by status: <strong className="text-zinc-800 dark:text-zinc-200 capitalize">{status}</strong>
|
||||
<span className="flex items-center gap-1.5 flex-wrap">
|
||||
Filtering by status:
|
||||
<strong className="text-zinc-800 dark:text-zinc-200 capitalize font-bold">{status}</strong>
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 text-xs ml-2 underline underline-offset-2"
|
||||
>
|
||||
Clear filter
|
||||
</Link>
|
||||
</span>
|
||||
) : (
|
||||
"All tracked job opportunities across your application pipeline."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-98 transition-all"
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/25 active:scale-98 transition-all shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@@ -157,182 +174,174 @@ export default async function JobsPage({ searchParams }: { searchParams: Promise
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
{/* Filter Tabs with Active State */}
|
||||
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 text-xs">
|
||||
<Link
|
||||
href="/jobs"
|
||||
className={`px-3 py-1.5 rounded-lg font-medium whitespace-nowrap transition-all ${
|
||||
!status
|
||||
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900 shadow-xs"
|
||||
: "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900 shadow-xs font-semibold"
|
||||
: "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/80"
|
||||
}`}
|
||||
>
|
||||
All ({allJobs.length})
|
||||
All <span className="opacity-75">({allJobs.length})</span>
|
||||
</Link>
|
||||
{STATUSES.map((s) => {
|
||||
const count = countByStatus[s] || 0;
|
||||
const isActive = status === s;
|
||||
const config = STATUS_CONFIG[s];
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={s}
|
||||
href={`/jobs?status=${s}`}
|
||||
className={`px-2.5 py-1.5 rounded-lg font-medium whitespace-nowrap capitalize transition-all ${
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg font-medium whitespace-nowrap capitalize transition-all ${
|
||||
isActive
|
||||
? "bg-indigo-600 text-white shadow-xs font-semibold"
|
||||
: "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
: "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/80"
|
||||
}`}
|
||||
>
|
||||
{s} <span className={`ml-1 text-[11px] ${isActive ? "text-indigo-100" : "text-zinc-400"}`}>({count})</span>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isActive ? "bg-white" : config?.dot ?? "bg-zinc-400"}`} />
|
||||
<span>{s}</span>
|
||||
<span className={`text-[11px] ${isActive ? "text-indigo-100" : "text-zinc-400 dark:text-zinc-500"}`}>
|
||||
({count})
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Jobs Table Card */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-xs overflow-hidden">
|
||||
{jobs.length === 0 ? (
|
||||
<div className="py-16 px-4 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800 text-zinc-400 mb-3">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">No jobs found</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
|
||||
{status
|
||||
? `There are currently no job listings with status "${status}".`
|
||||
: "Get started by adding your first job opportunity."}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-3">
|
||||
{status && (
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="px-3.5 py-1.5 rounded-lg text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||
>
|
||||
Clear filter
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-500 shadow-xs"
|
||||
>
|
||||
+ Add Job
|
||||
</Link>
|
||||
</div>
|
||||
{/* Job Cards in 2-Column Grid (Desktop) */}
|
||||
{jobs.length === 0 ? (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-2xs">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800 text-zinc-400 mb-3">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-zinc-200/80 dark:border-zinc-800/80 bg-zinc-50/70 dark:bg-zinc-900/50 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-5 pr-3">Job & Company</th>
|
||||
<th scope="col" className="py-3.5 px-3">Compensation</th>
|
||||
<th scope="col" className="py-3.5 px-3">PH Eligibility</th>
|
||||
<th scope="col" className="py-3.5 px-3">Status</th>
|
||||
<th scope="col" className="py-3.5 pl-3 pr-5 text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800/60 font-normal">
|
||||
{jobs.map((j) => {
|
||||
const statusInfo = STATUS_CONFIG[j.status] || STATUS_CONFIG.prospect;
|
||||
const companyColor = getCompanyColor(j.company);
|
||||
const initials = getInitials(j.company);
|
||||
<h3 className="text-base font-bold text-zinc-900 dark:text-zinc-100">No jobs found</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
|
||||
{status
|
||||
? `There are currently no job listings with status "${status}".`
|
||||
: "Get started by adding your first job opportunity."}
|
||||
</p>
|
||||
<div className="mt-5 flex items-center justify-center gap-3">
|
||||
{status && (
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="px-4 py-2 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
Clear filter
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
+ Add Job
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{jobs.map((j) => {
|
||||
const statusInfo = STATUS_CONFIG[j.status] || STATUS_CONFIG.prospect;
|
||||
const companyColor = getCompanyColor(j.company);
|
||||
const initials = getInitials(j.company);
|
||||
const compStr = formatCompensation(j);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={j.id}
|
||||
className="hover:bg-zinc-50/80 dark:hover:bg-zinc-800/40 transition-colors group"
|
||||
>
|
||||
{/* Job & Company with Avatar */}
|
||||
<td className="py-4 pl-5 pr-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl font-bold text-xs border shadow-2xs ${companyColor}`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
href={`/jobs/${j.id}`}
|
||||
className="font-semibold text-zinc-900 dark:text-zinc-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors line-clamp-1"
|
||||
>
|
||||
{j.title}
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
<span className="font-medium text-zinc-700 dark:text-zinc-300">{j.company}</span>
|
||||
{j.location && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="truncate">{j.location}</span>
|
||||
</>
|
||||
)}
|
||||
{j.remoteType && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.2 rounded text-[10px] font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300">
|
||||
{j.remoteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
return (
|
||||
<div
|
||||
key={j.id}
|
||||
className={`relative flex flex-col justify-between rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 border-l-[3.5px] ${statusInfo.accentBorder} bg-white dark:bg-zinc-900 p-5 shadow-2xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group`}
|
||||
>
|
||||
<div>
|
||||
{/* Top Row: Avatar, Title + Company, and Status Badge */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3.5 min-w-0 flex-1">
|
||||
{/* Company Initials Avatar (Colored Circle) */}
|
||||
<div
|
||||
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-full font-bold text-xs border shadow-2xs ${companyColor}`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
{/* Pay */}
|
||||
<td className="py-4 px-3 whitespace-nowrap">
|
||||
<span className="font-medium text-zinc-700 dark:text-zinc-300">
|
||||
{pay(j)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* PH Eligibility */}
|
||||
<td className="py-4 px-3 whitespace-nowrap">
|
||||
{j.phEligibility === "yes" ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
PH Eligible
|
||||
</span>
|
||||
) : j.phEligibility === "no" ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" />
|
||||
Not Eligible
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 text-zinc-600 border border-zinc-200 dark:bg-zinc-800/60 dark:text-zinc-400 dark:border-zinc-700/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
|
||||
Unverified
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Status Badge */}
|
||||
<td className="py-4 px-3 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border capitalize ${statusInfo.bg} ${statusInfo.text} ${statusInfo.border}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
|
||||
{j.status}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* View Action Link */}
|
||||
<td className="py-4 pl-3 pr-5 text-right whitespace-nowrap">
|
||||
{/* Title & Company/Location */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
href={`/jobs/${j.id}`}
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 opacity-80 group-hover:opacity-100 transition-opacity"
|
||||
className="font-bold text-base text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors line-clamp-1 block"
|
||||
>
|
||||
<span>View</span>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
{j.title}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 mt-0.5 truncate">
|
||||
<span className="font-semibold text-zinc-700 dark:text-zinc-300 truncate">{j.company}</span>
|
||||
{j.location && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="truncate">{j.location}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge in Top-Right Corner */}
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold border capitalize shrink-0 ${statusInfo.bg} ${statusInfo.text} ${statusInfo.border}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
|
||||
{j.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Badges & Meta Row (PH Eligibility, Pay, Remote Type) */}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-4">
|
||||
{j.phEligibility === "yes" && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
PH Eligible
|
||||
</span>
|
||||
)}
|
||||
|
||||
{compStr && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200/60 dark:border-zinc-700/60">
|
||||
{compStr}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{j.remoteType && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium bg-zinc-50 dark:bg-zinc-800/60 text-zinc-600 dark:text-zinc-400 capitalize border border-zinc-200/50 dark:border-zinc-700/40">
|
||||
{j.remoteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Row: Timestamp + "View Details →" */}
|
||||
<div className="mt-4 pt-3.5 border-t border-zinc-100 dark:border-zinc-800/80 flex items-center justify-between text-xs">
|
||||
<span className="text-zinc-400 dark:text-zinc-500">
|
||||
Updated{" "}
|
||||
{new Date(j.updatedAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
<Link
|
||||
href={`/jobs/${j.id}`}
|
||||
className="inline-flex items-center gap-1 font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors group-hover:translate-x-0.5 transition-transform"
|
||||
>
|
||||
<span>View Details</span>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Navbar from "./navbar";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI Job Search — Smart Career Tracker & Resume Tailor",
|
||||
title: "JobFlow AI — Smart Career Tracker & Resume Tailor",
|
||||
description: "Track job applications, tailor resumes, and optimize your job hunt with AI.",
|
||||
};
|
||||
|
||||
const nav = [
|
||||
{ href: "/", label: "Dashboard" },
|
||||
{ href: "/jobs", label: "Jobs" },
|
||||
{ href: "/resumes", label: "Resumes" },
|
||||
{ href: "/applications", label: "Applications" },
|
||||
{ href: "/profile", label: "Profile" },
|
||||
{ href: "/report", label: "Report" },
|
||||
];
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<body className="min-h-full flex flex-col bg-zinc-50/70 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100 antialiased selection:bg-indigo-500/20 selection:text-indigo-900 dark:selection:text-indigo-200">
|
||||
<header className="sticky top-0 z-40 border-b border-zinc-200/80 dark:border-zinc-800/80 bg-white/85 dark:bg-zinc-950/85 backdrop-blur-md transition-colors">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between gap-4">
|
||||
{/* Logo / Brand */}
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/" className="flex items-center gap-2.5 group">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-tr from-indigo-600 to-violet-500 text-white shadow-md shadow-indigo-500/20 group-hover:scale-105 transition-transform">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-bold tracking-tight text-zinc-900 dark:text-white text-base">JobFlow</span>
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold bg-indigo-50 text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800/60">
|
||||
AI
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-zinc-400 dark:text-zinc-500 -mt-0.5 font-medium hidden sm:inline-block">Job Search Tracker</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Main Navigation */}
|
||||
<nav className="flex items-center gap-1">
|
||||
{nav.map((n) => (
|
||||
<Link
|
||||
key={n.href}
|
||||
href={n.href}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100/80 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:bg-zinc-800/60 transition-all"
|
||||
>
|
||||
{n.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Right CTA */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>Add Job</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Navbar />
|
||||
<main className="flex-1 w-full md:pl-[56px] lg:pl-[220px] transition-[padding] duration-200">
|
||||
<div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6 md:py-8 pb-24 md:pb-12">
|
||||
{children}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
179
src/app/navbar.tsx
Normal file
179
src/app/navbar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
icon: (active: boolean) => React.ReactNode;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
label: "Dashboard",
|
||||
shortLabel: "Home",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/jobs",
|
||||
label: "Jobs",
|
||||
shortLabel: "Jobs",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/resumes",
|
||||
label: "Resumes",
|
||||
shortLabel: "Resumes",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/applications",
|
||||
label: "Applications",
|
||||
shortLabel: "Apps",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/profile",
|
||||
label: "Profile",
|
||||
shortLabel: "Profile",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/report",
|
||||
label: "Report",
|
||||
shortLabel: "Report",
|
||||
icon: () => (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.9}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === "/") {
|
||||
return pathname === "/";
|
||||
}
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Fixed Left Sidebar (Desktop 220px, Tablet 56px icon-only, Hidden on Mobile) */}
|
||||
<aside className="hidden md:flex flex-col fixed inset-y-0 left-0 z-40 md:w-[56px] lg:w-[220px] bg-white dark:bg-zinc-950 border-r border-zinc-200/80 dark:border-zinc-800/80 transition-[width] duration-200 select-none">
|
||||
{/* Brand Header */}
|
||||
<div className="h-16 flex items-center px-3 lg:px-4 border-b border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2.5 group w-full justify-center lg:justify-start"
|
||||
title="JobFlow AI"
|
||||
>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-tr from-indigo-600 via-indigo-600 to-violet-500 text-white shadow-md shadow-indigo-500/20 group-hover:scale-105 transition-all">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="hidden lg:flex flex-col min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-bold tracking-tight text-zinc-900 dark:text-white text-base">JobFlow</span>
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded-md text-[10px] font-semibold bg-indigo-50 text-indigo-700 dark:bg-indigo-950/80 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800/60">
|
||||
AI
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-zinc-400 dark:text-zinc-500 -mt-0.5 font-medium truncate">
|
||||
Job Search Copilot
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<nav className="flex-1 py-4 px-2 space-y-1 overflow-y-auto">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
title={item.label}
|
||||
className={`group flex items-center gap-3 px-2.5 py-2.5 rounded-xl text-xs sm:text-sm transition-all duration-150 justify-center lg:justify-start ${
|
||||
active
|
||||
? "border-l-[3px] border-indigo-600 bg-indigo-50/80 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-400 font-semibold shadow-2xs"
|
||||
: "border-l-[3px] border-transparent text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100/70 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:bg-zinc-800/50 font-medium"
|
||||
}`}
|
||||
>
|
||||
<div className={`${active ? "text-indigo-600 dark:text-indigo-400" : "text-zinc-500 dark:text-zinc-400 group-hover:text-zinc-900 dark:group-hover:text-zinc-100"} transition-colors`}>
|
||||
{item.icon(active)}
|
||||
</div>
|
||||
<span className="hidden lg:inline truncate">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Bottom Prominent "+ Add Job" Button */}
|
||||
<div className="p-2 lg:p-3 border-t border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
title="Add Job"
|
||||
className="flex items-center justify-center gap-2 w-full py-2.5 px-3 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/25 active:scale-95 transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline whitespace-nowrap">Add Job</span>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Bottom Tab Bar (Mobile Only, Fixed Bottom) */}
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-white/95 dark:bg-zinc-950/95 backdrop-blur-lg border-t border-zinc-200/80 dark:border-zinc-800/80 px-2 py-1.5 flex items-center justify-around select-none shadow-lg">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex flex-col items-center justify-center py-1 px-2 rounded-xl text-[10px] font-medium transition-all ${
|
||||
active
|
||||
? "text-indigo-600 dark:text-indigo-400 font-semibold"
|
||||
: "text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||
}`}
|
||||
>
|
||||
<div className={`p-1 rounded-lg transition-colors ${active ? "bg-indigo-50 dark:bg-indigo-950/80 text-indigo-600 dark:text-indigo-400" : ""}`}>
|
||||
{item.icon(active)}
|
||||
</div>
|
||||
<span className="mt-0.5 tracking-tight">{item.shortLabel}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
500
src/app/page.tsx
500
src/app/page.tsx
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"];
|
||||
const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"] as const;
|
||||
|
||||
const STAGE_CONFIG: Record<
|
||||
string,
|
||||
@@ -11,82 +11,109 @@ const STAGE_CONFIG: Record<
|
||||
> = {
|
||||
prospect: {
|
||||
label: "Prospect",
|
||||
description: "Saved positions",
|
||||
description: "Saved roles",
|
||||
color: "bg-zinc-400 dark:bg-zinc-500",
|
||||
badge: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300",
|
||||
border: "border-zinc-200 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600",
|
||||
bgActive: "bg-zinc-50 dark:bg-zinc-800/40",
|
||||
border: "border-zinc-200/90 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600",
|
||||
bgActive: "bg-zinc-50/90 dark:bg-zinc-800/50",
|
||||
},
|
||||
reviewing: {
|
||||
label: "Reviewing",
|
||||
description: "Evaluating fit",
|
||||
color: "bg-amber-500",
|
||||
badge: "bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300",
|
||||
border: "border-amber-200/80 dark:border-amber-900/60 hover:border-amber-400 dark:hover:border-amber-600",
|
||||
bgActive: "bg-amber-50/50 dark:bg-amber-950/20",
|
||||
badge: "bg-amber-50 text-amber-700 dark:bg-amber-950/70 dark:text-amber-300 border border-amber-200/60 dark:border-amber-900/60",
|
||||
border: "border-amber-200/80 dark:border-amber-900/50 hover:border-amber-400 dark:hover:border-amber-600",
|
||||
bgActive: "bg-amber-50/60 dark:bg-amber-950/30",
|
||||
},
|
||||
tailoring: {
|
||||
label: "Tailoring",
|
||||
description: "Customizing resume",
|
||||
description: "Customizing CV",
|
||||
color: "bg-purple-500",
|
||||
badge: "bg-purple-100 text-purple-800 dark:bg-purple-950/80 dark:text-purple-300",
|
||||
border: "border-purple-200/80 dark:border-purple-900/60 hover:border-purple-400 dark:hover:border-purple-600",
|
||||
bgActive: "bg-purple-50/50 dark:bg-purple-950/20",
|
||||
badge: "bg-purple-50 text-purple-700 dark:bg-purple-950/70 dark:text-purple-300 border border-purple-200/60 dark:border-purple-900/60",
|
||||
border: "border-purple-200/80 dark:border-purple-900/50 hover:border-purple-400 dark:hover:border-purple-600",
|
||||
bgActive: "bg-purple-50/60 dark:bg-purple-950/30",
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
description: "Ready to submit",
|
||||
color: "bg-blue-500",
|
||||
badge: "bg-blue-100 text-blue-800 dark:bg-blue-950/80 dark:text-blue-300",
|
||||
border: "border-blue-200/80 dark:border-blue-900/60 hover:border-blue-400 dark:hover:border-blue-600",
|
||||
bgActive: "bg-blue-50/50 dark:bg-blue-950/20",
|
||||
badge: "bg-blue-50 text-blue-700 dark:bg-blue-950/70 dark:text-blue-300 border border-blue-200/60 dark:border-blue-900/60",
|
||||
border: "border-blue-200/80 dark:border-blue-900/50 hover:border-blue-400 dark:hover:border-blue-600",
|
||||
bgActive: "bg-blue-50/60 dark:bg-blue-950/30",
|
||||
},
|
||||
applied: {
|
||||
label: "Applied",
|
||||
description: "Application sent",
|
||||
description: "Submitted",
|
||||
color: "bg-indigo-500",
|
||||
badge: "bg-indigo-100 text-indigo-800 dark:bg-indigo-950/80 dark:text-indigo-300",
|
||||
border: "border-indigo-200/80 dark:border-indigo-900/60 hover:border-indigo-400 dark:hover:border-indigo-600",
|
||||
bgActive: "bg-indigo-50/50 dark:bg-indigo-950/20",
|
||||
badge: "bg-indigo-50 text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-900/60",
|
||||
border: "border-indigo-200/80 dark:border-indigo-900/50 hover:border-indigo-400 dark:hover:border-indigo-600",
|
||||
bgActive: "bg-indigo-50/60 dark:bg-indigo-950/30",
|
||||
},
|
||||
interviewing: {
|
||||
label: "Interviewing",
|
||||
description: "In discussions",
|
||||
color: "bg-teal-500",
|
||||
badge: "bg-teal-100 text-teal-800 dark:bg-teal-950/80 dark:text-teal-300",
|
||||
border: "border-teal-200/80 dark:border-teal-900/60 hover:border-teal-400 dark:hover:border-teal-600",
|
||||
bgActive: "bg-teal-50/50 dark:bg-teal-950/20",
|
||||
badge: "bg-teal-50 text-teal-700 dark:bg-teal-950/70 dark:text-teal-300 border border-teal-200/60 dark:border-teal-900/60",
|
||||
border: "border-teal-200/80 dark:border-teal-900/50 hover:border-teal-400 dark:hover:border-teal-600",
|
||||
bgActive: "bg-teal-50/60 dark:bg-teal-950/30",
|
||||
},
|
||||
offer: {
|
||||
label: "Offer",
|
||||
description: "Offer received",
|
||||
color: "bg-emerald-500",
|
||||
badge: "bg-emerald-100 text-emerald-800 dark:bg-emerald-950/80 dark:text-emerald-300",
|
||||
border: "border-emerald-200/80 dark:border-emerald-900/60 hover:border-emerald-400 dark:hover:border-emerald-600",
|
||||
bgActive: "bg-emerald-50/50 dark:bg-emerald-950/20",
|
||||
badge: "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/70 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-900/60",
|
||||
border: "border-emerald-200/80 dark:border-emerald-900/50 hover:border-emerald-400 dark:hover:border-emerald-600",
|
||||
bgActive: "bg-emerald-50/60 dark:bg-emerald-950/30",
|
||||
},
|
||||
rejected: {
|
||||
label: "Rejected",
|
||||
description: "Not selected",
|
||||
color: "bg-rose-500",
|
||||
badge: "bg-rose-100 text-rose-800 dark:bg-rose-950/80 dark:text-rose-300",
|
||||
border: "border-rose-200/80 dark:border-rose-900/60 hover:border-rose-400 dark:hover:border-rose-600",
|
||||
bgActive: "bg-rose-50/50 dark:bg-rose-950/20",
|
||||
badge: "bg-rose-50 text-rose-700 dark:bg-rose-950/70 dark:text-rose-300 border border-rose-200/60 dark:border-rose-900/60",
|
||||
border: "border-rose-200/80 dark:border-rose-900/50 hover:border-rose-400 dark:hover:border-rose-600",
|
||||
bgActive: "bg-rose-50/60 dark:bg-rose-950/30",
|
||||
},
|
||||
dead: {
|
||||
label: "Dead",
|
||||
description: "Closed / Inactive",
|
||||
description: "Inactive / Stale",
|
||||
color: "bg-zinc-400",
|
||||
badge: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
|
||||
badge: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 border border-zinc-200/60 dark:border-zinc-700/60",
|
||||
border: "border-zinc-200/80 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600",
|
||||
bgActive: "bg-zinc-50 dark:bg-zinc-800/30",
|
||||
bgActive: "bg-zinc-50/70 dark:bg-zinc-800/40",
|
||||
},
|
||||
};
|
||||
|
||||
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 Dashboard() {
|
||||
const jobs = await prisma.job.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 5,
|
||||
take: 6,
|
||||
});
|
||||
const totalJobsCount = await prisma.job.count();
|
||||
const allJobs = await prisma.job.findMany({ select: { status: true, phEligibility: true } });
|
||||
@@ -95,219 +122,328 @@ export default async function Dashboard() {
|
||||
const byStatus = Object.fromEntries(STATUSES.map((s) => [s, allJobs.filter((j) => j.status === s).length]));
|
||||
const phYes = allJobs.filter((j) => j.phEligibility === "yes").length;
|
||||
|
||||
const activeCount =
|
||||
(byStatus.reviewing ?? 0) +
|
||||
(byStatus.tailoring ?? 0) +
|
||||
(byStatus.ready ?? 0) +
|
||||
(byStatus.applied ?? 0) +
|
||||
(byStatus.interviewing ?? 0) +
|
||||
(byStatus.offer ?? 0);
|
||||
const activePercent = totalJobsCount > 0 ? Math.round((activeCount / totalJobsCount) * 100) : 0;
|
||||
|
||||
const statCards = [
|
||||
{
|
||||
label: "Total Jobs Tracked",
|
||||
value: totalJobsCount,
|
||||
sub: "All pipeline opportunities",
|
||||
sub: "Pipeline opportunities",
|
||||
gradient: "from-indigo-500/[0.08] via-indigo-500/[0.02] to-transparent dark:from-indigo-500/[0.14] dark:to-transparent",
|
||||
iconBorder: "border-indigo-200/80 dark:border-indigo-800/60 bg-indigo-50/80 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400",
|
||||
icon: (
|
||||
<svg className="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
),
|
||||
iconBg: "bg-indigo-50 dark:bg-indigo-950/60 border-indigo-100 dark:border-indigo-900/50",
|
||||
},
|
||||
{
|
||||
label: "PH-Eligible Roles",
|
||||
value: phYes,
|
||||
sub: `${totalJobsCount > 0 ? Math.round((phYes / totalJobsCount) * 100) : 0}% of all positions`,
|
||||
sub: `${totalJobsCount > 0 ? Math.round((phYes / totalJobsCount) * 100) : 0}% of all tracked positions`,
|
||||
gradient: "from-emerald-500/[0.08] via-emerald-500/[0.02] to-transparent dark:from-emerald-500/[0.14] dark:to-transparent",
|
||||
iconBorder: "border-emerald-200/80 dark:border-emerald-800/60 bg-emerald-50/80 dark:bg-emerald-950/60 text-emerald-600 dark:text-emerald-400",
|
||||
icon: (
|
||||
<svg className="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
iconBg: "bg-emerald-50 dark:bg-emerald-950/60 border-emerald-100 dark:border-emerald-900/50",
|
||||
},
|
||||
{
|
||||
label: "Tailored Resumes",
|
||||
value: resumes,
|
||||
sub: "AI-customized versions",
|
||||
sub: "AI-customized CV documents",
|
||||
gradient: "from-purple-500/[0.08] via-purple-500/[0.02] to-transparent dark:from-purple-500/[0.14] dark:to-transparent",
|
||||
iconBorder: "border-purple-200/80 dark:border-purple-800/60 bg-purple-50/80 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400",
|
||||
icon: (
|
||||
<svg className="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
),
|
||||
iconBg: "bg-purple-50 dark:bg-purple-950/60 border-purple-100 dark:border-purple-900/50",
|
||||
},
|
||||
{
|
||||
label: "Applications Sent",
|
||||
value: apps,
|
||||
sub: "Active submissions",
|
||||
sub: "Logged submissions",
|
||||
gradient: "from-blue-500/[0.08] via-blue-500/[0.02] to-transparent dark:from-blue-500/[0.14] dark:to-transparent",
|
||||
iconBorder: "border-blue-200/80 dark:border-blue-800/60 bg-blue-50/80 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400",
|
||||
icon: (
|
||||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
</svg>
|
||||
),
|
||||
iconBg: "bg-blue-50 dark:bg-blue-950/60 border-blue-100 dark:border-blue-900/50",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header & Quick Action */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-2 border-b border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
|
||||
{/* LEFT COLUMN (2/3 width) */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Large Bold Greeting Heading */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Dashboard</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Track applications, monitor your hiring pipeline, and tailor resumes with AI.
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 dark:bg-emerald-950/70 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Pipeline Live
|
||||
</span>
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500">·</span>
|
||||
<span className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{activeCount} active opportunities
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
Good morning
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1 max-w-xl leading-relaxed">
|
||||
Monitor active hiring stages, manage candidate positioning, and tailor resumes with AI across your job search pipeline.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-sm font-medium text-zinc-700 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/60 shadow-xs transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
<span>View all jobs</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-98 transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>Add Job (AI parse)</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{statCards.map((card) => (
|
||||
<div
|
||||
key={card.label}
|
||||
className="relative overflow-hidden rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-xs transition-all hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
|
||||
{card.label}
|
||||
</span>
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-xl border ${card.iconBg}`}>
|
||||
{card.icon}
|
||||
{/* 2x2 Stat Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{statCards.map((card) => (
|
||||
<div
|
||||
key={card.label}
|
||||
className={`relative overflow-hidden rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 bg-gradient-to-b ${card.gradient} p-5 shadow-2xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
|
||||
{card.label}
|
||||
</span>
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-xl border ${card.iconBorder} group-hover:scale-105 transition-transform`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
{card.value}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{card.sub}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Horizontal Pipeline Section with Stepper-Style Funnel Bar */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-bold tracking-tight text-zinc-900 dark:text-white">Application Pipeline</h2>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-semibold bg-indigo-50 text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800/60">
|
||||
{activePercent}% In Funnel
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
Horizontal funnel flow across active stages
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
|
||||
<span>{totalJobsCount} Total</span>
|
||||
<span>·</span>
|
||||
<span className="text-indigo-600 dark:text-indigo-400 font-semibold">{activeCount} Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stepper-Style Horizontal Funnel Segment Bar */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-1.5 w-full bg-zinc-100 dark:bg-zinc-800/80 p-1.5 rounded-2xl">
|
||||
{STATUSES.map((s) => {
|
||||
const count = byStatus[s] ?? 0;
|
||||
const config = STAGE_CONFIG[s];
|
||||
const widthPercent = totalJobsCount > 0 ? (count / totalJobsCount) * 100 : 100 / STATUSES.length;
|
||||
const minWidth = count > 0 ? Math.max(widthPercent, 7) : 4;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={s}
|
||||
href={`/jobs?status=${s}`}
|
||||
title={`${config.label}: ${count} jobs`}
|
||||
style={{ flexGrow: count > 0 ? minWidth : 1 }}
|
||||
className={`group/bar relative h-7 rounded-lg transition-all duration-200 flex items-center justify-center text-[11px] font-bold ${
|
||||
count > 0
|
||||
? `${config.color} text-white shadow-xs hover:brightness-110`
|
||||
: "bg-zinc-200/70 dark:bg-zinc-700/50 text-zinc-400 dark:text-zinc-500 hover:bg-zinc-300 dark:hover:bg-zinc-700"
|
||||
}`}
|
||||
>
|
||||
{count > 0 && (
|
||||
<span className="drop-shadow-xs truncate px-1">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Funnel Phase Labels */}
|
||||
<div className="flex items-center justify-between text-[11px] text-zinc-400 dark:text-zinc-500 px-1 font-medium">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||||
<span>Sourcing & Prep ({ (byStatus.prospect ?? 0) + (byStatus.reviewing ?? 0) + (byStatus.tailoring ?? 0) })</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
|
||||
<span>Active Funnel ({ (byStatus.ready ?? 0) + (byStatus.applied ?? 0) + (byStatus.interviewing ?? 0) })</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
<span>Outcomes ({ (byStatus.offer ?? 0) + (byStatus.rejected ?? 0) + (byStatus.dead ?? 0) })</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
|
||||
{card.value}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{card.sub}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Visual Pipeline Section */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-zinc-900 dark:text-white">Application Pipeline</h2>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">Click any stage to filter jobs by status</p>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{totalJobsCount} total opportunities
|
||||
</div>
|
||||
</div>
|
||||
{/* Interactive Stage Overview Grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2.5 pt-2">
|
||||
{STATUSES.map((s) => {
|
||||
const count = byStatus[s] ?? 0;
|
||||
const config = STAGE_CONFIG[s];
|
||||
const hasItems = count > 0;
|
||||
|
||||
{/* Pipeline Stage Cards */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-9 gap-2.5">
|
||||
{STATUSES.map((s) => {
|
||||
const count = byStatus[s] ?? 0;
|
||||
const config = STAGE_CONFIG[s] || {
|
||||
label: s,
|
||||
description: "",
|
||||
color: "bg-zinc-400",
|
||||
badge: "bg-zinc-100 text-zinc-700",
|
||||
border: "border-zinc-200",
|
||||
bgActive: "bg-zinc-50",
|
||||
};
|
||||
const hasItems = count > 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={s}
|
||||
href={`/jobs?status=${s}`}
|
||||
className={`group flex flex-col justify-between p-3 rounded-xl border transition-all ${
|
||||
config.border
|
||||
} ${
|
||||
hasItems
|
||||
? `${config.bgActive} shadow-xs hover:shadow-sm hover:-translate-y-0.5`
|
||||
: "bg-zinc-50/50 dark:bg-zinc-900/40 opacity-75 hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`w-2 h-2 rounded-full ${config.color}`} />
|
||||
return (
|
||||
<Link
|
||||
key={s}
|
||||
href={`/jobs?status=${s}`}
|
||||
className={`group relative flex items-center justify-between p-3 rounded-xl border transition-all duration-150 ${
|
||||
config.border
|
||||
} ${
|
||||
hasItems
|
||||
? `${config.bgActive} shadow-2xs hover:shadow-sm hover:-translate-y-0.5`
|
||||
: "bg-zinc-50/50 dark:bg-zinc-900/40 opacity-70 hover:opacity-100 hover:bg-zinc-50 dark:hover:bg-zinc-800/40"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${config.color}`} />
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-bold text-zinc-900 dark:text-zinc-100 capitalize group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors truncate">
|
||||
{config.label}
|
||||
</div>
|
||||
<div className="text-[10px] text-zinc-400 dark:text-zinc-500 truncate">
|
||||
{config.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-bold ${
|
||||
className={`inline-flex items-center justify-center min-w-[22px] h-5 px-1.5 rounded-full text-xs font-bold shrink-0 ${
|
||||
hasItems ? config.badge : "text-zinc-400 dark:text-zinc-600 bg-zinc-100 dark:bg-zinc-800"
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-zinc-800 dark:text-zinc-200 capitalize group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
|
||||
{config.label}
|
||||
</div>
|
||||
<div className="text-[10px] text-zinc-400 dark:text-zinc-500 truncate mt-0.5">
|
||||
{config.description}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Jobs Preview */}
|
||||
{jobs.length > 0 && (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold tracking-tight text-zinc-900 dark:text-white">Recently Updated Jobs</h2>
|
||||
<Link href="/jobs" className="text-xs font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300">
|
||||
{/* RIGHT COLUMN (1/3 width, Sticky Sidebar) */}
|
||||
<div className="lg:col-span-1 space-y-6 lg:sticky lg:top-8 self-start">
|
||||
{/* "Quick Add" Card */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-indigo-200/80 dark:border-indigo-900/60 bg-gradient-to-b from-indigo-50/90 via-white to-white dark:from-indigo-950/40 dark:via-zinc-900 dark:to-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600 text-white shadow-md shadow-indigo-500/25 shrink-0">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-zinc-900 dark:text-white">Quick Add Job</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">AI-powered parser</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-zinc-600 dark:text-zinc-300 leading-relaxed">
|
||||
Paste a listing URL or raw job description. JobFlow AI extracts titles, compensation, PH eligibility, and requirements automatically.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 px-4 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-md shadow-indigo-500/25 active:scale-98 transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>+ Add New Position</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Compact Activity Feed */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-2xs space-y-4">
|
||||
<div className="flex items-center justify-between pb-3 border-b border-zinc-100 dark:border-zinc-800/80">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white">Recent Activity</h3>
|
||||
<p className="text-[11px] text-zinc-400 dark:text-zinc-500">Latest pipeline updates</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="text-xs font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 transition-colors"
|
||||
>
|
||||
View all ({totalJobsCount}) →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-zinc-100 dark:divide-zinc-800/80">
|
||||
{jobs.map((j) => (
|
||||
<div key={j.id} className="py-3 flex items-center justify-between gap-4 first:pt-0 last:pb-0 group">
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
{jobs.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">No jobs tracked yet.</p>
|
||||
<Link
|
||||
href="/jobs/new"
|
||||
className="text-xs font-semibold text-indigo-600 dark:text-indigo-400 hover:underline mt-1 inline-block"
|
||||
>
|
||||
Add your first job →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-zinc-100 dark:divide-zinc-800/80">
|
||||
{jobs.map((j) => {
|
||||
const companyColor = getCompanyColor(j.company);
|
||||
const initials = getInitials(j.company);
|
||||
const stage = STAGE_CONFIG[j.status] || STAGE_CONFIG.prospect;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={j.id}
|
||||
href={`/jobs/${j.id}`}
|
||||
className="font-medium text-sm text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors truncate block"
|
||||
className="py-3 first:pt-1 last:pb-1 flex items-center justify-between gap-3 group hover:bg-zinc-50/80 dark:hover:bg-zinc-800/40 px-2 -mx-2 rounded-xl transition-colors"
|
||||
>
|
||||
{j.title}
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div
|
||||
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl font-bold text-[11px] border shadow-2xs ${companyColor}`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-semibold text-xs text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors truncate">
|
||||
{j.title}
|
||||
</div>
|
||||
<div className="text-[11px] text-zinc-500 dark:text-zinc-400 truncate mt-0.5">
|
||||
{j.company} {j.location ? `· ${j.location}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold capitalize shrink-0 ${stage.badge}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${stage.color}`} />
|
||||
{j.status}
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
{j.company} · {j.location ?? "Remote"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
|
||||
STAGE_CONFIG[j.status]?.badge ?? "bg-zinc-100 text-zinc-700"
|
||||
}`}
|
||||
>
|
||||
{j.status}
|
||||
</span>
|
||||
<Link
|
||||
href={`/jobs/${j.id}`}
|
||||
className="p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-4 border-b border-zinc-200/80 dark:border-zinc-800/80">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Profile & AI Knowledge Base</h1>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">Profile & AI Knowledge Base</h1>
|
||||
{saved ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
@@ -71,10 +71,10 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={saved || saving}
|
||||
className={`inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold text-white transition-all shadow-sm ${
|
||||
className={`inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-semibold text-white transition-all shadow-sm shrink-0 ${
|
||||
saved
|
||||
? "bg-zinc-400 dark:bg-zinc-700 opacity-60 cursor-default"
|
||||
: "bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-indigo-500/20 active:scale-95 cursor-pointer"
|
||||
: "bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-indigo-500/20 active:scale-95 cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{saving ? (
|
||||
@@ -96,8 +96,8 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Section 1: Contact Information */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-4">
|
||||
{/* Section 1: Personal Information */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
@@ -123,7 +123,7 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
</div>
|
||||
|
||||
{/* Section 2: Online Presence & Links */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-4">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-violet-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
@@ -149,7 +149,7 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
</div>
|
||||
|
||||
{/* Section 3: Summary */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
@@ -169,7 +169,7 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
</div>
|
||||
|
||||
{/* Section 4: Skills (JSON) */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-teal-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
|
||||
@@ -189,7 +189,7 @@ export default function ProfileEditor({ profile }: { profile: P }) {
|
||||
</div>
|
||||
|
||||
{/* Section 5: Work History (JSON) */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-xs space-y-3">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
|
||||
|
||||
@@ -5,38 +5,58 @@ export const dynamic = "force-dynamic";
|
||||
export default async function ReportPage() {
|
||||
const jobs = await prisma.job.findMany({ orderBy: [{ status: "asc" }, { company: "asc" }] });
|
||||
const apps = await prisma.application.findMany({ include: { job: true }, orderBy: { createdAt: "desc" } });
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
const now = new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
||||
const applied = jobs.filter((j) => ["applied", "interviewing", "offer"].includes(j.status)).length;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6 print:text-black">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Report Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-4 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Job Search Report</h1>
|
||||
<p className="text-sm text-zinc-500">Generated {now} · {jobs.length} tracked · {applied} in application pipeline</p>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">Job Search Pipeline Report</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Generated on {now} · {jobs.length} total tracked positions · {applied} active in application funnel
|
||||
</p>
|
||||
</div>
|
||||
<button className="print:hidden rounded-xl border border-zinc-300 dark:border-zinc-700 px-4 py-2 text-sm">
|
||||
Use browser Print (Ctrl+P) to save as PDF
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="print:hidden inline-flex items-center gap-1.5 rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-4 py-2 text-xs font-semibold text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-800 shadow-2xs transition-colors shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" />
|
||||
</svg>
|
||||
<span>Print / Save as PDF (Ctrl+P)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="font-semibold mb-2">All jobs</h2>
|
||||
<div className="overflow-x-auto rounded-xl border border-zinc-200 dark:border-zinc-800">
|
||||
<table className="w-full text-sm bg-white dark:bg-zinc-900">
|
||||
<thead className="text-left text-zinc-500 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<tr><th className="p-2">Company</th><th className="p-2">Title</th><th className="p-2">Pay</th><th className="p-2">PH?</th><th className="p-2">Status</th></tr>
|
||||
{/* Jobs Overview Table */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white">All Tracked Positions ({jobs.length})</h2>
|
||||
<div className="overflow-x-auto rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-2xs">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-zinc-200/80 dark:border-zinc-800/80 bg-zinc-50/70 dark:bg-zinc-900/60 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
<tr>
|
||||
<th scope="col" className="py-3 px-4">Company</th>
|
||||
<th scope="col" className="py-3 px-4">Role Title</th>
|
||||
<th scope="col" className="py-3 px-4">Compensation</th>
|
||||
<th scope="col" className="py-3 px-4">PH Status</th>
|
||||
<th scope="col" className="py-3 px-4">Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800/60">
|
||||
{jobs.map((j) => (
|
||||
<tr key={j.id} className="border-b border-zinc-100 dark:border-zinc-800">
|
||||
<td className="p-2">{j.company}</td>
|
||||
<td className="p-2">{j.title}</td>
|
||||
<td className="p-2 whitespace-nowrap">
|
||||
<tr key={j.id} className="hover:bg-zinc-50/60 dark:hover:bg-zinc-800/30 transition-colors">
|
||||
<td className="py-3 px-4 font-semibold text-zinc-900 dark:text-zinc-100">{j.company}</td>
|
||||
<td className="py-3 px-4 text-zinc-800 dark:text-zinc-200">{j.title}</td>
|
||||
<td className="py-3 px-4 whitespace-nowrap text-zinc-600 dark:text-zinc-400">
|
||||
{j.payMin ? `${j.payCurrency === "PHP" ? "₱" : "$"}${j.payMin}${j.payMax && j.payMax !== j.payMin ? `–${j.payMax}` : ""}/${j.payPeriod === "hour" ? "hr" : j.payPeriod === "month" ? "mo" : "yr"}` : "—"}
|
||||
</td>
|
||||
<td className="p-2">{j.phEligibility}</td>
|
||||
<td className="p-2">{j.status}</td>
|
||||
<td className="py-3 px-4 capitalize text-zinc-600 dark:text-zinc-400">{j.phEligibility}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 capitalize">
|
||||
{j.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -44,16 +64,26 @@ export default async function ReportPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Applications Submissions */}
|
||||
{apps.length > 0 && (
|
||||
<section>
|
||||
<h2 className="font-semibold mb-2">Applications</h2>
|
||||
<ul className="text-sm space-y-1">
|
||||
<section className="space-y-3 pt-2">
|
||||
<h2 className="text-base font-bold text-zinc-900 dark:text-white">Logged Applications ({apps.length})</h2>
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-2xs divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{apps.map((a) => (
|
||||
<li key={a.id}>
|
||||
{a.job.company} — {a.job.title} · {a.method} · {a.submittedAt ? a.submittedAt.toISOString().slice(0, 10) : "not submitted"}
|
||||
</li>
|
||||
<div key={a.id} className="py-2.5 first:pt-0 last:pb-0 flex items-center justify-between text-sm">
|
||||
<div>
|
||||
<span className="font-semibold text-zinc-900 dark:text-zinc-100">{a.job.company}</span>
|
||||
<span className="text-zinc-400 mx-1.5">·</span>
|
||||
<span className="text-zinc-700 dark:text-zinc-300">{a.job.title}</span>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 flex items-center gap-2">
|
||||
<span className="capitalize">{a.method}</span>
|
||||
<span>·</span>
|
||||
<span>{a.submittedAt ? new Date(a.submittedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "Pending"}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,20 +52,20 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Top Navigation & Toolbar */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-4 shadow-xs">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-4 shadow-2xs">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<Link
|
||||
href="/resumes"
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-1"
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-1 group"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="w-3.5 h-3.5 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
<span>Back to Resumes</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold tracking-tight text-zinc-900 dark:text-white">
|
||||
<h1 className="text-xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
{resume.name}
|
||||
</h1>
|
||||
{saved ? (
|
||||
@@ -83,10 +83,10 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
</div>
|
||||
|
||||
{/* Action Toolbar Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
@@ -97,7 +97,7 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
|
||||
<button
|
||||
onClick={printResume}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" />
|
||||
@@ -111,7 +111,7 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white transition-all shadow-sm ${
|
||||
saved
|
||||
? "bg-zinc-400 dark:bg-zinc-700 opacity-60 cursor-default"
|
||||
: "bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-indigo-500/20 active:scale-95 cursor-pointer"
|
||||
: "bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-indigo-500/20 active:scale-95 cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{saving ? (
|
||||
@@ -138,8 +138,8 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
{/* Editor & Preview Split Grid */}
|
||||
<div className={`grid gap-6 ${preview ? "lg:grid-cols-2" : "grid-cols-1"}`}>
|
||||
{/* Monospace Markdown Editor */}
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-xs overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-medium">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-2xs overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-semibold">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
|
||||
@@ -155,26 +155,26 @@ export default function ResumeEditor({ resume }: { resume: { id: string; name: s
|
||||
setSaved(false);
|
||||
}}
|
||||
rows={30}
|
||||
className="w-full flex-1 p-4 font-mono text-sm leading-relaxed bg-white dark:bg-zinc-950/60 text-zinc-900 dark:text-zinc-100 focus:outline-hidden resize-y min-h-[500px]"
|
||||
className="w-full flex-1 p-4 font-mono text-xs sm:text-sm leading-relaxed bg-white dark:bg-zinc-950/60 text-zinc-900 dark:text-zinc-100 focus:outline-hidden resize-y min-h-[500px]"
|
||||
placeholder="# Your Name Email | Phone | Location ## Summary ..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Live Resume Document Preview */}
|
||||
{preview && (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-xs overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-medium">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-2xs overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-semibold">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
Document Live Preview
|
||||
</span>
|
||||
<span>Formatted View</span>
|
||||
<span>Formatted Output</span>
|
||||
</div>
|
||||
<div className="p-6 md:p-8 bg-zinc-50/40 dark:bg-zinc-950/40 flex-1 overflow-auto max-h-[850px]">
|
||||
<div
|
||||
className="resume-preview max-w-2xl mx-auto bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-xs border border-zinc-200/60 dark:border-zinc-800/80"
|
||||
className="resume-preview max-w-2xl mx-auto bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-2xs border border-zinc-200/60 dark:border-zinc-800/80"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,12 @@ export default async function ResumesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-2 border-b border-zinc-200/70 dark:border-zinc-800/70">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Resumes</h1>
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
||||
Resumes
|
||||
</h1>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-purple-50 text-purple-700 dark:bg-purple-950/70 dark:text-purple-300 border border-purple-200/60 dark:border-purple-800/60">
|
||||
{resumes.length} {resumes.length === 1 ? "version" : "versions"}
|
||||
</span>
|
||||
@@ -24,74 +26,105 @@ export default async function ResumesPage() {
|
||||
Tailored resumes customized by AI for specific positions and target roles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200/90 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/70 shadow-2xs transition-all shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
<span>Browse Jobs Pipeline</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Resumes List */}
|
||||
{/* Resumes Grid / Empty State */}
|
||||
{resumes.length === 0 ? (
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-xs">
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-2xs">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-purple-50 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400 mb-3">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">No resumes yet</h3>
|
||||
<h3 className="text-base font-bold text-zinc-900 dark:text-zinc-100">No tailored resumes yet</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
|
||||
Open any job in your tracker and click <strong>“AI Tailor Resume”</strong> to generate a custom CV.
|
||||
Open any job in your tracker and click <strong>“AI Tailor Resume”</strong> to generate a custom CV version optimized for that role.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<div className="mt-5">
|
||||
<Link
|
||||
href="/jobs"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Go to Jobs Pipeline
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{resumes.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
className="relative flex flex-col justify-between rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all group"
|
||||
className="relative flex flex-col justify-between rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 shadow-2xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-purple-50 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400 border border-purple-100 dark:border-purple-900/50">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
href={`/resumes/${r.id}`}
|
||||
className="font-semibold text-sm text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors truncate block"
|
||||
>
|
||||
{r.name}
|
||||
</Link>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 capitalize mt-0.5">
|
||||
{r.kind} version
|
||||
</span>
|
||||
</div>
|
||||
{/* Stylized Document Thumbnail Placeholder */}
|
||||
<div className="mb-4 rounded-xl border border-zinc-200/70 dark:border-zinc-800 bg-zinc-50/80 dark:bg-zinc-950/60 p-3.5 space-y-2 group-hover:border-indigo-300/80 dark:group-hover:border-indigo-800/60 transition-colors">
|
||||
{/* Header bar simulations */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-2 w-2/5 rounded-xs bg-zinc-400 dark:bg-zinc-600" />
|
||||
<div className="h-1.5 w-1/5 rounded-xs bg-zinc-300 dark:bg-zinc-700" />
|
||||
</div>
|
||||
<div className="h-1.5 w-3/5 rounded-xs bg-zinc-300 dark:bg-zinc-700" />
|
||||
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-1.5 space-y-1.5">
|
||||
{/* Section 1 */}
|
||||
<div className="h-1.5 w-1/4 rounded-xs bg-indigo-400/80 dark:bg-indigo-500/80" />
|
||||
<div className="h-1.5 w-full rounded-xs bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="h-1.5 w-4/5 rounded-xs bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
|
||||
<div className="pt-0.5 space-y-1.5">
|
||||
{/* Section 2 */}
|
||||
<div className="h-1.5 w-1/4 rounded-xs bg-purple-400/80 dark:bg-purple-500/80" />
|
||||
<div className="h-1.5 w-full rounded-xs bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="h-1.5 w-3/5 rounded-xs bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{r.job && (
|
||||
<div className="mt-3.5 pt-3 border-t border-zinc-100 dark:border-zinc-800/80 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<span className="text-zinc-400 dark:text-zinc-500">Target Role: </span>
|
||||
<Link
|
||||
href={`/jobs/${r.job.id}`}
|
||||
className="font-medium text-zinc-800 dark:text-zinc-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
{r.job.company} — {r.job.title}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{/* Resume Label & Version Tag */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<Link
|
||||
href={`/resumes/${r.id}`}
|
||||
className="font-bold text-sm text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors line-clamp-1 block flex-1"
|
||||
>
|
||||
{r.name}
|
||||
</Link>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-semibold bg-purple-50 text-purple-700 dark:bg-purple-950/70 dark:text-purple-300 border border-purple-200/60 dark:border-purple-900/60 capitalize shrink-0">
|
||||
{r.kind} · v{r.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Linked Job Title + Company */}
|
||||
<div className="mt-2.5 text-xs text-zinc-600 dark:text-zinc-400 min-h-[36px]">
|
||||
{r.job ? (
|
||||
<div className="line-clamp-2">
|
||||
<span className="text-zinc-400 dark:text-zinc-500">Target Role: </span>
|
||||
<Link
|
||||
href={`/jobs/${r.job.id}`}
|
||||
className="font-semibold text-zinc-800 dark:text-zinc-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
{r.job.company} — {r.job.title}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-zinc-400 dark:text-zinc-500 italic">General master resume</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-zinc-100 dark:border-zinc-800/80 text-xs text-zinc-400">
|
||||
<span>
|
||||
Updated{" "}
|
||||
{/* Bottom Row: Timestamp + Open Button */}
|
||||
<div className="mt-4 pt-3.5 border-t border-zinc-100 dark:border-zinc-800/80 flex items-center justify-between text-xs">
|
||||
<span className="text-zinc-400 dark:text-zinc-500 text-[11px]">
|
||||
{new Date(r.updatedAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
@@ -100,10 +133,10 @@ export default async function ResumesPage() {
|
||||
</span>
|
||||
<Link
|
||||
href={`/resumes/${r.id}`}
|
||||
className="inline-flex items-center gap-1 font-semibold text-indigo-600 dark:text-indigo-400 group-hover:translate-x-0.5 transition-transform"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl font-semibold text-xs text-white bg-indigo-600 hover:bg-indigo-500 shadow-2xs active:scale-95 transition-all group-hover:shadow-indigo-500/20 group-hover:shadow-sm"
|
||||
>
|
||||
<span>Open Editor</span>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<span>Open</span>
|
||||
<svg className="w-3 h-3 group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user