// ============== jobs board ============== function Jobs({ go, focusId }) { const { CHAIN_FILTERS, TYPE_FILTERS, LEVEL_FILTERS, REMOTE_FILTERS, useRoles } = window.W3J; const { roles: ROLES, loading, source } = useRoles(); const [q, setQ] = React.useState(""); const [chain, setChain] = React.useState(new Set()); const [cat, setCat] = React.useState(new Set()); const [level, setLevel] = React.useState(new Set()); const [remote, setRemote] = React.useState(new Set()); const [minComp, setMinComp] = React.useState(0); const [sort, setSort] = React.useState("recent"); const [active, setActive] = React.useState(null); React.useEffect(() => { if (focusId) { const r = ROLES.find((x) => x.id === focusId); if (r) setActive(r); } }, [focusId, ROLES]); const toggle = (set, setSet, val) => { const next = new Set(set); if (next.has(val)) next.delete(val); else next.add(val); setSet(next); }; const filtered = React.useMemo(() => { const remoteMatch = (r) => { if (remote.size === 0) return true; return [...remote].some((rk) => r.remote.toLowerCase().startsWith(rk.toLowerCase())); }; let list = ROLES.filter((r) => { if (q) { const hay = (r.title + " " + r.company + " " + r.stack.join(" ") + " " + r.chain + " " + r.cat).toLowerCase(); if (!hay.includes(q.toLowerCase())) return false; } if (chain.size && !chain.has(r.chain)) return false; if (cat.size && !cat.has(r.cat)) return false; if (level.size && !level.has(r.level)) return false; if (!remoteMatch(r)) return false; if (r.min < minComp) return false; return true; }); if (sort === "recent") list = list.sort((a, b) => a.posted - b.posted); if (sort === "comp") list = list.sort((a, b) => b.max - a.max); if (sort === "title") list = list.sort((a, b) => a.title.localeCompare(b.title)); return list; }, [q, chain, cat, level, remote, minComp, sort, ROLES]); const counts = React.useMemo(() => { const c = (key) => Object.fromEntries( [...new Set(ROLES.map((r) => r[key]))].map((v) => [v, ROLES.filter((r) => r[key] === v).length]) ); return { chain: c("chain"), cat: c("cat"), level: c("level") }; }, [ROLES]); const clearAll = () => { setQ(""); setChain(new Set()); setCat(new Set()); setLevel(new Set()); setRemote(new Set()); setMinComp(0); }; const totalActive = chain.size + cat.size + level.size + remote.size + (q ? 1 : 0) + (minComp > 0 ? 1 : 0); return (
The job board · {ROLES.length} live roles{source === "mock" && " (preview)"}

Open roles.

Every role here is actively retained by Web3JOE. Companies are anonymized in public listings, full briefs are sent to qualified candidates after a short call.

setQ(e.target.value)} /> {q && }
{filtered.length} / {ROLES.length} roles
Sort
{loading ? (
Loading…
) : filtered.length === 0 ? (
No matches.

Try widening your filters — or clear all.

) : (
{filtered.map((r) => (
setActive(r)}> {r.id}
{r.title}
{r.company} · {r.remote} · posted {r.posted}d ago
{r.chain} {r.stack.slice(0, 2).map((s) => {s})}
${r.min}–{r.max}k
))}
)}
setActive(null)} go={go} />
); } function FilterGroup({ title, opts, active, onToggle, counts }) { return (
{title}
{opts.map((c) => (
onToggle(c)}> {c} {counts && {counts[c] || 0}}
))}
); } function JobDrawer({ role, onClose, go }) { const { user } = useAuth(); const [applied, setApplied] = React.useState(false); const [applying, setApplying] = React.useState(false); const [err, setErr] = React.useState(""); const [showApplyForm, setShowApplyForm] = React.useState(false); const [profile, setProfile] = React.useState(null); const [form, setForm] = React.useState(blankForm()); const [answers, setAnswers] = React.useState({}); const [resumeMode, setResumeMode] = React.useState("profile"); // "profile" | "upload" const [uploadedKey, setUploadedKey] = React.useState(null); const [uploading, setUploading] = React.useState(false); const fileInputRef = React.useRef(null); React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; if (role) window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [role]); React.useEffect(() => { setApplied(false); setShowApplyForm(false); setErr(""); setAnswers({}); setResumeMode("profile"); setUploadedKey(null); }, [role?.id]); React.useEffect(() => { if (!showApplyForm || !user || user.role !== "candidate") return; window.W3J_API.apiFetch("/candidates/me").then((p) => { setProfile(p); setForm({ full_name: p.full_name || "", email: user.email || "", linkedin_url: p.linkedin_url || "", twitter_url: p.twitter_url || "", location: p.location || "", expected_comp: "", }); setResumeMode(p.resume_key ? "profile" : "upload"); }).catch(() => {}); }, [showApplyForm, user]); const setField = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); const setAnswer = (qid) => (e) => setAnswers((a) => ({ ...a, [qid]: e.target.value })); const uploadResume = async (file) => { if (!file) return; setErr(""); setUploading(true); try { const fd = new FormData(); fd.append("file", file); const res = await window.W3J_API.apiFetch("/uploads/application-resume", { method: "POST", body: fd, isForm: true, }); setUploadedKey(res.resume_key); } catch (ex) { setErr(ex.message || "upload failed"); } finally { setUploading(false); } }; const apply = async (e) => { if (e) e.preventDefault(); setErr(""); setApplying(true); try { if (!role?.uuid) throw new Error("This is a preview role. Roles will be live once the recruiter creates them."); const resume_key = resumeMode === "upload" ? uploadedKey : (profile?.resume_key || null); if (!resume_key) { throw new Error("Please attach a resume — upload here or via your dashboard."); } const body = { role_id: role.uuid, full_name: form.full_name, email: form.email, linkedin_url: form.linkedin_url, twitter_url: form.twitter_url || null, location: form.location, expected_comp: form.expected_comp || null, resume_key, answers, }; await window.W3J_API.apiFetch("/applications", { method: "POST", body }); setApplied(true); } catch (ex) { if (ex.status === 409) setApplied(true); else setErr(ex.message); } finally { setApplying(false); } }; const questions = role?.questions || []; return ( <>