// ============== candidate dashboard ==============
const SENIORITY = ["Mid", "Senior", "Lead", "Staff", "Director"];
const OPEN_TO = [
{ v: "actively_looking", l: "Actively looking" },
{ v: "open", l: "Open to offers" },
{ v: "passive", l: "Passive" },
{ v: "closed", l: "Not looking" },
];
const STATUS_LABEL = {
submitted: "Submitted",
reviewed: "Under review",
shortlisted: "Shortlisted",
interviewing: "Interviewing",
offered: "Offer extended",
placed: "Placed",
rejected: "Closed",
withdrawn: "Withdrawn",
};
function Tabs({ tabs, value, onChange }) {
return (
{tabs.map((t) => (
))}
);
}
function CandidateDashboard({ go }) {
const { user, logout } = useAuth();
const [tab, setTab] = React.useState("profile");
const [profile, setProfile] = React.useState(null);
const [apps, setApps] = React.useState([]);
const [roles, setRoles] = React.useState({});
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
if (!user) return;
(async () => {
try {
const [p, a] = await Promise.all([
window.W3J_API.apiFetch("/candidates/me"),
window.W3J_API.apiFetch("/applications/me"),
]);
setProfile(p);
setApps(a);
const roleIds = [...new Set(a.map((x) => x.role_id))];
const fetched = await Promise.all(roleIds.map((id) => window.W3J_API.apiFetch(`/roles/${id}`).catch(() => null)));
const map = {};
fetched.forEach((r) => { if (r) map[r.id] = r; });
setRoles(map);
} finally {
setLoading(false);
}
})();
}, [user]);
if (loading) return ;
if (!profile) return null;
return (
Candidate · Signed in as {user.email}
{profile.full_name || "Your"} file.
{tab === "profile" &&
}
{tab === "resume" &&
}
{tab === "applications" &&
}
);
}
function ProfileForm({ profile, onSaved }) {
const [form, setForm] = React.useState({ ...profile, skills: profile.skills || [], chains: profile.chains || [] });
const [skillsText, setSkillsText] = React.useState((profile.skills || []).join(", "));
const [chainsText, setChainsText] = React.useState((profile.chains || []).join(", "));
const [saving, setSaving] = React.useState(false);
const [msg, setMsg] = React.useState("");
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value === "" ? null : e.target.value }));
const setNum = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value === "" ? null : Number(e.target.value) }));
const submit = async (e) => {
e.preventDefault();
setSaving(true); setMsg("");
try {
const body = {
full_name: form.full_name,
headline: form.headline,
bio: form.bio,
location: form.location,
timezone: form.timezone,
skills: skillsText.split(",").map((s) => s.trim()).filter(Boolean),
chains: chainsText.split(",").map((s) => s.trim()).filter(Boolean),
years_experience: form.years_experience,
seniority: form.seniority,
comp_min_k: form.comp_min_k,
comp_max_k: form.comp_max_k,
open_to_offers: form.open_to_offers,
github_url: form.github_url,
twitter_url: form.twitter_url,
linkedin_url: form.linkedin_url,
website_url: form.website_url,
};
const saved = await window.W3J_API.apiFetch("/candidates/me", { method: "PUT", body });
onSaved(saved);
setMsg("Saved.");
} catch (ex) {
setMsg(ex.message || "Save failed");
} finally {
setSaving(false);
}
};
return (
);
}
function ResumePanel({ profile, onUploaded }) {
const [uploading, setUploading] = React.useState(false);
const [parsing, setParsing] = React.useState(false);
const [err, setErr] = React.useState("");
const [parsed, setParsed] = React.useState(null);
const inputRef = React.useRef(null);
const upload = async (file) => {
if (!file) return;
setErr(""); setUploading(true); setParsed(null);
try {
const fd = new FormData();
fd.append("file", file);
const res = await window.W3J_API.apiFetch("/uploads/resume", { method: "POST", body: fd, isForm: true });
onUploaded({ ...profile, resume_key: res.resume_key });
} catch (ex) {
setErr(ex.message || "Upload failed");
} finally {
setUploading(false);
}
};
const viewResume = async () => {
const { download_url } = await window.W3J_API.apiFetch(`/uploads/resume/${profile.user_id}/url`);
window.open(download_url, "_blank", "noopener");
};
const parseResume = async () => {
setParsing(true); setErr(""); setParsed(null);
try {
const res = await window.W3J_API.apiFetch("/uploads/resume/parse", { method: "POST" });
setParsed(res);
} catch (ex) {
setErr(ex.status === 503
? "Resume parsing isn't configured on this server. Set LLM_PROVIDER + LLM_API_KEY."
: (ex.message || "Parsing failed"));
} finally {
setParsing(false);
}
};
const applyParsed = async () => {
const body = {
full_name: parsed.full_name ?? profile.full_name,
headline: parsed.headline ?? profile.headline,
bio: parsed.bio ?? profile.bio,
location: parsed.location ?? profile.location,
timezone: profile.timezone,
skills: parsed.skills ?? profile.skills ?? [],
chains: parsed.chains ?? profile.chains ?? [],
years_experience: parsed.years_experience ?? profile.years_experience,
seniority: parsed.seniority ?? profile.seniority,
comp_min_k: parsed.comp_min_k ?? profile.comp_min_k,
comp_max_k: parsed.comp_max_k ?? profile.comp_max_k,
open_to_offers: profile.open_to_offers,
github_url: parsed.github_url ?? profile.github_url,
twitter_url: parsed.twitter_url ?? profile.twitter_url,
linkedin_url: parsed.linkedin_url ?? profile.linkedin_url,
website_url: parsed.website_url ?? profile.website_url,
};
const saved = await window.W3J_API.apiFetch("/candidates/me", { method: "PUT", body });
onUploaded(saved);
setParsed(null);
};
return (
Resume
PDF or DOCX, up to 10 MB. Visible only to recruiters who match you to a role.
{profile.resume_key ? (
Resume on file
{profile.resume_key.split("/").slice(-1)[0]}
) : (
)}
upload(e.target.files?.[0])} />
{err &&
{err}
}
{parsed && (
Parsed from your resume
{Object.entries(parsed).filter(([, v]) => v != null && v !== "" && !(Array.isArray(v) && v.length === 0))
.map(([k, v]) => (
{k.replace(/_/g, " ")}
{Array.isArray(v) ? v.join(", ") : String(v)}
))}
)}
);
}
function ApplicationsList({ apps, roles, onChange }) {
const withdraw = async (id) => {
const updated = await window.W3J_API.apiFetch(`/applications/${id}/withdraw`, { method: "POST" });
onChange(apps.map((a) => (a.id === id ? updated : a)));
};
if (apps.length === 0) {
return No applications yet.
Browse open roles to get started.
;
}
return (
{apps.map((a) => {
const r = roles[a.role_id];
return (
{r?.public_id || "—"}
{r?.title || "Role removed"}
{r?.company_display || "—"} · applied {new Date(a.created_at).toLocaleDateString()}
{STATUS_LABEL[a.status] || a.status}
{r ? <>${r.min_comp_k}–{r.max_comp_k}k> : ""}
{a.status === "submitted" || a.status === "reviewed" ? (
) : }
);
})}
);
}
window.CandidateDashboard = CandidateDashboard;