redesign UI with left sidebar, card grids, and two-column dashboard layout

This commit is contained in:
2026-08-18 03:55:07 +00:00
parent 0cdfc4806c
commit 3ca554c367
14 changed files with 1151 additions and 654 deletions

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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,27 +81,63 @@ 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 ?? ""} />
<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&apos;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 ?? ""} />
@@ -96,66 +146,148 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
<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&apos;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&apos;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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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

View File

@@ -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">
{/* Job Cards in 2-Column Grid (Desktop) */}
{jobs.length === 0 ? (
<div className="py-16 px-4 text-center">
<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>
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">No jobs found</h3>
<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-4 flex items-center justify-center gap-3">
<div className="mt-5 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"
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="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-500 shadow-xs"
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="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">
<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}`}
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>
{/* Title & Company/Location */}
<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"
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"
>
{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>
<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-1.5 py-0.2 rounded text-[10px] font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300">
<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>
</div>
</td>
{/* Pay */}
<td className="py-4 px-3 whitespace-nowrap">
<span className="font-medium text-zinc-700 dark:text-zinc-300">
{pay(j)}
{/* 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>
</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">
<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="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</span>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
</td>
</tr>
</div>
</div>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

View File

@@ -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>
</div>
</header>
<main className="flex-1 mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8 py-8">
<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>
</main>
</body>
</html>

179
src/app/navbar.tsx Normal file
View 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>
</>
);
}

View File

@@ -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,102 +122,107 @@ 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">
{/* 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 p-5 shadow-xs transition-all hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700"
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.iconBg}`}>
<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-bold tracking-tight text-zinc-900 dark:text-white">
<span className="text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
{card.value}
</span>
</div>
@@ -201,113 +233,217 @@ export default async function Dashboard() {
))}
</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">
{/* 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>
<p className="text-xs text-zinc-500 dark:text-zinc-400">Click any stage to filter jobs by status</p>
<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>
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
{totalJobsCount} total opportunities
<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>
{/* Pipeline Stage Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-9 gap-2.5">
{/* 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] || {
label: s,
description: "",
color: "bg-zinc-400",
badge: "bg-zinc-100 text-zinc-700",
border: "border-zinc-200",
bgActive: "bg-zinc-50",
};
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>
{/* 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;
return (
<Link
key={s}
href={`/jobs?status=${s}`}
className={`group flex flex-col justify-between p-3 rounded-xl border transition-all ${
className={`group relative flex items-center justify-between p-3 rounded-xl border transition-all duration-150 ${
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"
? `${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 justify-between mb-2">
<span className={`w-2 h-2 rounded-full ${config.color}`} />
<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>
);
})}
</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/${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"
href="/jobs/new"
className="text-xs font-semibold text-indigo-600 dark:text-indigo-400 hover:underline mt-1 inline-block"
>
{j.title}
Add your first job →
</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"
}`}
) : (
<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="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"
>
<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
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>
);
}

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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&#10;Email | Phone | Location&#10;&#10;## Summary&#10;..."
/>
</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>

View File

@@ -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>
{/* 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="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 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">
{/* 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-medium text-zinc-800 dark:text-zinc-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
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>