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

@@ -13,13 +13,22 @@ function CopyField({ label, value }: { label: string; value: string }) {
const [copied, setCopied] = useState(false);
if (!value) return null;
return (
<div className="flex items-center gap-2 text-sm">
<span className="text-zinc-500 w-28 shrink-0">{label}</span>
<span className="truncate flex-1">{value}</span>
<div className="flex items-center justify-between gap-3 p-2.5 rounded-xl bg-zinc-50/70 dark:bg-zinc-800/40 border border-zinc-200/60 dark:border-zinc-800 text-sm group hover:border-zinc-300 dark:hover:border-zinc-700 transition-colors">
<span className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 w-32 shrink-0">{label}</span>
<span className="truncate flex-1 text-zinc-800 dark:text-zinc-200 font-medium text-xs sm:text-sm">{value}</span>
<button
onClick={() => { navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); }}
className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-0.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">
{copied ? "✓" : "Copy"}
onClick={() => {
navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className={`px-2.5 py-1 rounded-lg text-xs font-semibold transition-all shrink-0 ${
copied
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800"
: "bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 shadow-2xs"
}`}
>
{copied ? "✓ Copied" : "Copy"}
</button>
</div>
);
@@ -35,7 +44,8 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
const [err, setErr] = useState("");
async function genAnswers() {
setBusy(true); setErr("");
setBusy(true);
setErr("");
try {
const qs = questions.split("\n").map((q) => q.trim()).filter(Boolean);
const res = await fetch("/api/ai/answers", {
@@ -46,7 +56,11 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "failed");
setAnswers(data.answers);
} catch (e) { setErr(String(e)); } finally { setBusy(false); }
} catch (e) {
setErr(String(e));
} finally {
setBusy(false);
}
}
async function markSubmitted() {
@@ -67,95 +81,213 @@ export default function ApplyWizard({ job, profile }: { job: Job; profile: Profi
return (
<div className="max-w-3xl mx-auto space-y-6">
{/* Header */}
<div>
<Link href={`/jobs/${job.id}`} className="text-sm text-zinc-500 hover:text-indigo-600">← Back to job</Link>
<h1 className="text-2xl font-bold mt-1">Apply: {job.title}</h1>
<div className="text-zinc-500">{job.company}</div>
<Link
href={`/jobs/${job.id}`}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-2 group"
>
<svg className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
<span>Back to Job</span>
</Link>
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
Apply Wizard: {job.title}
</h1>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
{job.company} · Follow these 5 steps to efficiently submit and record your application.
</p>
</div>
{/* Step 1: open the application page */}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
<h2 className="font-semibold">1 · Open the application page</h2>
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
<div className="flex items-center gap-2.5">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-indigo-50 dark:bg-indigo-950/80 text-indigo-600 dark:text-indigo-400 text-xs font-bold border border-indigo-200/60 dark:border-indigo-800/60">
1
</span>
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Open the Employer Application Page</h2>
</div>
{job.applyUrl || job.url ? (
<a href={job.applyUrl ?? job.url ?? "#"} target="_blank"
className="inline-block rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 text-white px-4 py-2 text-sm font-semibold hover:from-indigo-500 hover:to-violet-500">
Open application page ↗
<a
href={job.applyUrl ?? job.url ?? "#"}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 text-white px-4 py-2.5 text-xs font-semibold hover:from-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
>
<span>Open Application Page</span>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
) : <p className="text-sm text-zinc-500">No apply URL saved — add one on the job page.</p>}
) : (
<p className="text-sm text-zinc-500">No application URL saved — add one on the job page.</p>
)}
</section>
{/* Step 2: copy-ready profile fields */}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-2">
<h2 className="font-semibold mb-2">2 · Copy your details into the form</h2>
<CopyField label="Full name" value={profile.fullName ?? ""} />
<CopyField label="Email" value={profile.email ?? ""} />
<CopyField label="Phone" value={profile.phone ?? ""} />
<CopyField label="Location" value={profile.location ?? ""} />
<CopyField label="LinkedIn" value={profile.linkedin ?? ""} />
<CopyField label="GitHub" value={profile.github ?? ""} />
<CopyField label="Portfolio" value={profile.portfolio ?? ""} />
<CopyField label="Headline" value={profile.headline ?? ""} />
<CopyField label="Summary" value={profile.summary ?? ""} />
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
<div className="flex items-center gap-2.5">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-violet-50 dark:bg-violet-950/80 text-violet-600 dark:text-violet-400 text-xs font-bold border border-violet-200/60 dark:border-violet-800/60">
2
</span>
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Copy Profile Fields into Form</h2>
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Click copy on any field to easily paste into the employer&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 ?? ""} />
<CopyField label="LinkedIn" value={profile.linkedin ?? ""} />
<CopyField label="GitHub" value={profile.github ?? ""} />
<CopyField label="Portfolio" value={profile.portfolio ?? ""} />
<CopyField label="Headline" value={profile.headline ?? ""} />
<CopyField label="Summary" value={profile.summary ?? ""} />
</div>
</section>
{/* Step 3: resume + cover letter */}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
<h2 className="font-semibold">3 · Attach documents</h2>
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-4">
<div className="flex items-center gap-2.5">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-purple-50 dark:bg-purple-950/80 text-purple-600 dark:text-purple-400 text-xs font-bold border border-purple-200/60 dark:border-purple-800/60">
3
</span>
<h2 className="font-bold text-base text-zinc-900 dark:text-white">Attach Tailored Documents</h2>
</div>
{job.resumes.length ? (
<div className="text-sm space-y-2">
<label className="block">
<span className="text-zinc-500">Resume to use</span>
<select value={resumeId} onChange={(e) => setResumeId(e.target.value)}
className="mt-1 w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-2">
{job.resumes.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
<div className="space-y-2 text-sm">
<label className="block text-xs font-semibold text-zinc-700 dark:text-zinc-300">
Select Tailored Resume
</label>
{resumeId && <Link href={`/resumes/${resumeId}`} className="text-indigo-600 hover:underline text-sm">Open resume → Print/PDF ↗</Link>}
<div className="flex items-center gap-3">
<select
value={resumeId}
onChange={(e) => setResumeId(e.target.value)}
className="flex-1 rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-2.5 text-xs sm:text-sm text-zinc-900 dark:text-zinc-100 focus:border-indigo-500 focus:outline-hidden focus:ring-2 focus:ring-indigo-500/20"
>
{job.resumes.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
{resumeId && (
<Link
href={`/resumes/${resumeId}`}
target="_blank"
className="inline-flex items-center gap-1 px-3 py-2 rounded-xl text-xs font-semibold text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/60 border border-purple-200/80 dark:border-purple-900/60 hover:bg-purple-100 transition-colors shrink-0"
>
<span>Open PDF ↗</span>
</Link>
)}
</div>
</div>
) : (
<p className="text-sm text-zinc-500">No tailored resume yet — generate one from the job page (“AI Tailor Resume”).</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
No tailored resume yet — generate one on the job page with “AI Tailor Resume”.
</p>
)}
{job.coverLetters.length > 0 && (
<details className="text-sm">
<summary className="cursor-pointer text-zinc-600 dark:text-zinc-300">Cover letter v{job.coverLetters[job.coverLetters.length - 1].version} (click to view/copy)</summary>
<div className="mt-2 rounded-xl border border-zinc-200 dark:border-zinc-800 p-3 whitespace-pre-wrap">{job.coverLetters[job.coverLetters.length - 1].markdown}</div>
<button onClick={() => navigator.clipboard.writeText(job.coverLetters[job.coverLetters.length - 1].markdown)}
className="mt-2 rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">Copy letter</button>
<details className="text-sm rounded-xl border border-zinc-200 dark:border-zinc-800 p-3.5 bg-zinc-50/50 dark:bg-zinc-950/40">
<summary className="cursor-pointer font-semibold text-xs text-zinc-800 dark:text-zinc-200 select-none">
Cover Letter v{job.coverLetters[job.coverLetters.length - 1].version} (Click to view / copy)
</summary>
<div className="mt-3 p-3 rounded-lg bg-white dark:bg-zinc-900 border border-zinc-200/80 dark:border-zinc-800 text-xs text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">
{job.coverLetters[job.coverLetters.length - 1].markdown}
</div>
<button
onClick={() => navigator.clipboard.writeText(job.coverLetters[job.coverLetters.length - 1].markdown)}
className="mt-2.5 px-3 py-1 rounded-lg text-xs font-semibold bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors shadow-2xs"
>
Copy Cover Letter
</button>
</details>
)}
</section>
{/* Step 4: AI answers for form questions */}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
<h2 className="font-semibold">4 · Application questions (optional, AI-drafted)</h2>
<textarea value={questions} onChange={(e) => setQuestions(e.target.value)} rows={4}
placeholder={"Paste the form's questions, one per line:\nWhy do you want to work here?\nDescribe your experience with LLMs."}
className="w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-3 text-sm" />
<button onClick={genAnswers} disabled={busy || !questions.trim()}
className="rounded-xl bg-indigo-600 text-white px-4 py-2 text-sm font-semibold disabled:opacity-50 hover:bg-indigo-500">
{busy ? "Drafting…" : "🪄 Draft answers with AI"}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-6 shadow-2xs space-y-3">
<div className="flex items-center gap-2.5">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-50 dark:bg-blue-950/80 text-blue-600 dark:text-blue-400 text-xs font-bold border border-blue-200/60 dark:border-blue-800/60">
4
</span>
<h2 className="font-bold text-base text-zinc-900 dark:text-white">AI Application Answers (Optional)</h2>
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Paste specific form questions (one per line) and AI will draft customized responses based on your profile and the job requirements.
</p>
<textarea
value={questions}
onChange={(e) => setQuestions(e.target.value)}
rows={4}
placeholder={"Why do you want to work at this company?\nDescribe your experience with TypeScript and Next.js."}
className="w-full rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950/60 p-3.5 text-xs sm:text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:border-indigo-500 focus:outline-hidden focus:ring-2 focus:ring-indigo-500/20 leading-relaxed"
/>
<button
onClick={genAnswers}
disabled={busy || !questions.trim()}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 disabled:opacity-50 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
>
{busy ? (
<>
<svg className="animate-spin w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<span>Drafting with AI…</span>
</>
) : (
<>
<span>🪄 Draft Answers with AI</span>
</>
)}
</button>
{err && <div className="text-sm text-rose-600">{err}</div>}
{err && <div className="text-xs font-medium text-rose-600 dark:text-rose-400">{err}</div>}
{answers.map((qa, i) => (
<div key={i} className="rounded-xl border border-zinc-200 dark:border-zinc-800 p-3 text-sm space-y-1">
<div className="font-medium">{qa.question}</div>
<div className="text-zinc-600 dark:text-zinc-300 whitespace-pre-wrap">{qa.answer}</div>
<button onClick={() => navigator.clipboard.writeText(qa.answer)}
className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-0.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800">Copy</button>
<div key={i} className="rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-950/40 p-4 space-y-2">
<div className="font-bold text-xs text-zinc-900 dark:text-zinc-100">{qa.question}</div>
<div className="text-xs text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed bg-white dark:bg-zinc-900 p-3 rounded-lg border border-zinc-200/80 dark:border-zinc-800">
{qa.answer}
</div>
<button
onClick={() => navigator.clipboard.writeText(qa.answer)}
className="px-2.5 py-1 rounded-lg text-xs font-semibold bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors shadow-2xs"
>
Copy Answer
</button>
</div>
))}
</section>
{/* Step 5: confirm */}
<section className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-5 space-y-3">
<h2 className="font-semibold">5 · Done?</h2>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={confirmSubmit} onChange={(e) => setConfirmSubmit(e.target.checked)} />
I submitted the application on the employer&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">
{jobs.length === 0 ? (
<div className="py-16 px-4 text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800 text-zinc-400 mb-3">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
</svg>
</div>
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">No jobs found</h3>
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
{status
? `There are currently no job listings with status "${status}".`
: "Get started by adding your first job opportunity."}
</p>
<div className="mt-4 flex items-center justify-center gap-3">
{status && (
<Link
href="/jobs"
className="px-3.5 py-1.5 rounded-lg text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700"
>
Clear filter
</Link>
)}
<Link
href="/jobs/new"
className="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-500 shadow-xs"
>
+ Add Job
</Link>
</div>
{/* Job Cards in 2-Column Grid (Desktop) */}
{jobs.length === 0 ? (
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-12 text-center shadow-2xs">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800 text-zinc-400 mb-3">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
</svg>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b border-zinc-200/80 dark:border-zinc-800/80 bg-zinc-50/70 dark:bg-zinc-900/50 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
<tr>
<th scope="col" className="py-3.5 pl-5 pr-3">Job & Company</th>
<th scope="col" className="py-3.5 px-3">Compensation</th>
<th scope="col" className="py-3.5 px-3">PH Eligibility</th>
<th scope="col" className="py-3.5 px-3">Status</th>
<th scope="col" className="py-3.5 pl-3 pr-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800/60 font-normal">
{jobs.map((j) => {
const statusInfo = STATUS_CONFIG[j.status] || STATUS_CONFIG.prospect;
const companyColor = getCompanyColor(j.company);
const initials = getInitials(j.company);
<h3 className="text-base font-bold text-zinc-900 dark:text-zinc-100">No jobs found</h3>
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto">
{status
? `There are currently no job listings with status "${status}".`
: "Get started by adding your first job opportunity."}
</p>
<div className="mt-5 flex items-center justify-center gap-3">
{status && (
<Link
href="/jobs"
className="px-4 py-2 rounded-xl text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
Clear filter
</Link>
)}
<Link
href="/jobs/new"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-sm shadow-indigo-500/20 active:scale-95 transition-all"
>
+ Add Job
</Link>
</div>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{jobs.map((j) => {
const statusInfo = STATUS_CONFIG[j.status] || STATUS_CONFIG.prospect;
const companyColor = getCompanyColor(j.company);
const initials = getInitials(j.company);
const compStr = formatCompensation(j);
return (
<tr
key={j.id}
className="hover:bg-zinc-50/80 dark:hover:bg-zinc-800/40 transition-colors group"
>
{/* Job & Company with Avatar */}
<td className="py-4 pl-5 pr-3">
<div className="flex items-center gap-3">
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl font-bold text-xs border shadow-2xs ${companyColor}`}
>
{initials}
</div>
<div className="min-w-0 flex-1">
<Link
href={`/jobs/${j.id}`}
className="font-semibold text-zinc-900 dark:text-zinc-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors line-clamp-1"
>
{j.title}
</Link>
<div className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
<span className="font-medium text-zinc-700 dark:text-zinc-300">{j.company}</span>
{j.location && (
<>
<span>·</span>
<span className="truncate">{j.location}</span>
</>
)}
{j.remoteType && (
<span className="inline-flex items-center px-1.5 py-0.2 rounded text-[10px] font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300">
{j.remoteType}
</span>
)}
</div>
</div>
</div>
</td>
return (
<div
key={j.id}
className={`relative flex flex-col justify-between rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 border-l-[3.5px] ${statusInfo.accentBorder} bg-white dark:bg-zinc-900 p-5 shadow-2xs hover:shadow-md hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group`}
>
<div>
{/* Top Row: Avatar, Title + Company, and Status Badge */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3.5 min-w-0 flex-1">
{/* Company Initials Avatar (Colored Circle) */}
<div
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-full font-bold text-xs border shadow-2xs ${companyColor}`}
>
{initials}
</div>
{/* Pay */}
<td className="py-4 px-3 whitespace-nowrap">
<span className="font-medium text-zinc-700 dark:text-zinc-300">
{pay(j)}
</span>
</td>
{/* PH Eligibility */}
<td className="py-4 px-3 whitespace-nowrap">
{j.phEligibility === "yes" ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
PH Eligible
</span>
) : j.phEligibility === "no" ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" />
Not Eligible
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 text-zinc-600 border border-zinc-200 dark:bg-zinc-800/60 dark:text-zinc-400 dark:border-zinc-700/60">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
Unverified
</span>
)}
</td>
{/* Status Badge */}
<td className="py-4 px-3 whitespace-nowrap">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border capitalize ${statusInfo.bg} ${statusInfo.text} ${statusInfo.border}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
{j.status}
</span>
</td>
{/* View Action Link */}
<td className="py-4 pl-3 pr-5 text-right whitespace-nowrap">
{/* Title & Company/Location */}
<div className="min-w-0 flex-1">
<Link
href={`/jobs/${j.id}`}
className="inline-flex items-center gap-1 text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 opacity-80 group-hover:opacity-100 transition-opacity"
className="font-bold text-base text-zinc-900 dark:text-zinc-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors line-clamp-1 block"
>
<span>View</span>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
{j.title}
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
<div className="flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 mt-0.5 truncate">
<span className="font-semibold text-zinc-700 dark:text-zinc-300 truncate">{j.company}</span>
{j.location && (
<>
<span>·</span>
<span className="truncate">{j.location}</span>
</>
)}
</div>
</div>
</div>
{/* Status Badge in Top-Right Corner */}
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold border capitalize shrink-0 ${statusInfo.bg} ${statusInfo.text} ${statusInfo.border}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
{j.status}
</span>
</div>
{/* Badges & Meta Row (PH Eligibility, Pay, Remote Type) */}
<div className="flex flex-wrap items-center gap-2 mt-4">
{j.phEligibility === "yes" && (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-800/60">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
PH Eligible
</span>
)}
{compStr && (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200/60 dark:border-zinc-700/60">
{compStr}
</span>
)}
{j.remoteType && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium bg-zinc-50 dark:bg-zinc-800/60 text-zinc-600 dark:text-zinc-400 capitalize border border-zinc-200/50 dark:border-zinc-700/40">
{j.remoteType}
</span>
)}
</div>
</div>
{/* Bottom Row: Timestamp + "View Details →" */}
<div className="mt-4 pt-3.5 border-t border-zinc-100 dark:border-zinc-800/80 flex items-center justify-between text-xs">
<span className="text-zinc-400 dark:text-zinc-500">
Updated{" "}
{new Date(j.updatedAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})}
</span>
<Link
href={`/jobs/${j.id}`}
className="inline-flex items-center gap-1 font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors group-hover:translate-x-0.5 transition-transform"
>
<span>View Details</span>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</Link>
</div>
</div>
);
})}
</div>
)}
</div>
);
}