// Nuance ATS — shared chrome and primitives
const { useState, useMemo, useEffect, useCallback, useRef } = React;
// =================== Ephemeral toast =============
// Lightweight hook: renders one inline toast inside the current screen.
// Usage:
// const [toast, showToast, ToastHost] = useAtsToast();
//
// {ToastHost}
function useAtsToast() {
const [toast, setToast] = useState(null);
const timerRef = React.useRef(null);
const show = useCallback((message, opts = {}) => {
if (timerRef.current) clearTimeout(timerRef.current);
setToast({ message, tone: opts.tone || "neutral", icon: opts.icon });
timerRef.current = setTimeout(() => setToast(null), opts.duration || 2800);
}, []);
useEffect(() => () => timerRef.current && clearTimeout(timerRef.current), []);
const host = toast ? (
{toast.message}
) : null;
return [toast, show, host];
}
// =================== Viewer avatar menu =========
function ViewerAvatarMenu() {
const [open, setOpen] = useState(false);
const buttonRef = useRef(null);
const menuRef = useRef(null);
// Use ref to ensure portal container is stable (fixes React 18 portal cleanup issue)
const portalRootRef = useRef(null);
if (!portalRootRef.current) {
portalRootRef.current = document.getElementById('portal-root') || document.body;
}
// Get current viewer from window
const currentViewer = typeof window.__atsCurrentViewer !== 'undefined'
? window.__atsCurrentViewer
: VIEWERS[0];
useEffect(() => {
if (!open) return;
const onDoc = (e) => {
if (buttonRef.current && buttonRef.current.contains(e.target)) return;
if (menuRef.current && !menuRef.current.contains(e.target)) setOpen(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const handleViewerChange = (viewer) => {
if (typeof window.__atsSetViewer === 'function') {
window.__atsSetViewer(viewer);
}
setOpen(false);
};
const handleLogout = () => {
setOpen(false);
if (typeof window.__atsLogout === 'function') {
window.__atsLogout();
}
};
const isAuthenticated = !!localStorage.getItem('ats_token');
const authUser = window.__atsAuthUser;
const isRealAdmin = authUser && authUser.role === 'admin';
const viewAsRoles = [
{ role: "admin", label: "Admin", desc: "Full access", accent: ROLES.admin?.accent || "#1D3140" },
{ role: "interviewer", label: "Interviewer", desc: "Limited visibility", accent: ROLES.interviewer?.accent || "#7595D5" },
{ role: "external", label: "External Agent", desc: "Jon Luzha's view", accent: ROLES.external?.accent || "#9C6B4F" },
];
const switchRole = (role) => {
if (!authUser || typeof window.__atsSetViewer !== 'function') return;
if (role === "admin") {
const nameParts = (authUser.full_name || "").split(" ");
const initials = nameParts.map(p => p[0] || "").join("").slice(0, 2).toUpperCase();
window.__atsSetViewer({
id: authUser.id, name: authUser.full_name, initials,
color: "#7595D5", role: "admin", title: authUser.email,
avatar_url: authUser.avatar_url,
});
} else if (role === "interviewer") {
const nameParts = (authUser.full_name || "").split(" ");
const initials = nameParts.map(p => p[0] || "").join("").slice(0, 2).toUpperCase();
window.__atsSetViewer({
id: authUser.id, name: authUser.full_name, initials,
color: "#7595D5", role: "interviewer", title: authUser.email,
avatar_url: authUser.avatar_url,
});
} else if (role === "external") {
window.__atsSetViewer({
id: "ext-jon-luzha", name: "Jon Luzha", initials: "JL",
color: "#9C6B4F", role: "external", title: "Alexander Chapman",
sourceTag: "Jon Luzha",
});
}
setOpen(false);
};
// Get button position for dropdown placement
const [buttonRect, setButtonRect] = useState(null);
useEffect(() => {
if (open && buttonRef.current) {
setButtonRect(buttonRef.current.getBoundingClientRect());
}
}, [open]);
return (
<>
{open && buttonRect && ReactDOM.createPortal(
{authUser ? authUser.full_name : currentViewer.name}
{authUser ? authUser.email : currentViewer.title}
{isRealAdmin && (
View as
{viewAsRoles.map(v => (
))}
)}
,
portalRootRef.current
)}
>
);
}
Object.assign(window, { useAtsToast, ViewerAvatarMenu });
// =================== Brand mark =================
function Mark({ size = 22 }) {
return (
);
}
// =================== Avatar =====================
function Avatar({ src, name, size = 24, ring = false }) {
const initials = (name || "?")
.split(" ").map(s => s[0]).slice(0, 2).join("");
const [err, setErr] = useState(false);
const dim = { width: size, height: size, fontSize: Math.max(9, size * 0.42) };
if (!src || err) {
return (
{initials}
);
}
return (
setErr(true)} />
);
}
// Tiny initials disc (recruiter etc.)
function InitialDisc({ initials, color, size = 18, title, avatar }) {
const [imgErr, setImgErr] = useState(false);
if (avatar && !imgErr) {
return (
setImgErr(true)} style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }} />
);
}
return (
{initials}
);
}
// =================== Stage chip =================
function StageChip({ stage, dense = false }) {
return (
{dense ? stage.short : stage.label}
);
}
// =================== Source pill ================
function SourcePill({ source }) {
const s = SOURCES.find(x => x.id === source) || SOURCES[0];
return {s.label};
}
// =================== Search bar ====================
function SearchBar() {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState(0);
const inputRef = useRef(null);
const wrapRef = useRef(null);
const portalRef = useRef(null);
if (!portalRef.current) {
portalRef.current = document.getElementById('portal-root') || document.body;
}
const results = useMemo(() => {
const q = query.trim().toLowerCase();
if (q.length < 2) return [];
const terms = q.split(/\s+/);
const matched = [];
for (const c of CANDIDATES) {
const hay = c._searchHay;
if (terms.every(t => hay.includes(t))) {
matched.push(c);
if (matched.length >= 12) break;
}
}
return matched;
}, [query]);
const onChange = (e) => {
setQuery(e.target.value);
setOpen(true);
setSelected(0);
};
useEffect(() => {
const handler = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
inputRef.current?.focus();
inputRef.current?.select();
setOpen(true);
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
useEffect(() => {
if (!open) return;
const handler = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) {
const dropdown = document.querySelector('.ats-search-dropdown');
if (dropdown && dropdown.contains(e.target)) return;
setOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
const goTo = (c) => {
setOpen(false);
setQuery("");
if (typeof window.__atsNavigate === "function") {
window.__atsNavigate("pipeline");
}
setTimeout(() => {
window.location.hash = "#candidate/" + c.id;
window.dispatchEvent(new CustomEvent('ats-open-candidate', { detail: c.id }));
}, 50);
};
const onKeyDown = (e) => {
if (!open || results.length === 0) {
if (e.key === 'Escape') { setOpen(false); inputRef.current?.blur(); }
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelected(s => Math.min(s + 1, results.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelected(s => Math.max(s - 1, 0));
} else if (e.key === 'Enter') {
e.preventDefault();
if (results[selected]) goTo(results[selected]);
} else if (e.key === 'Escape') {
setOpen(false);
inputRef.current?.blur();
}
};
const showDropdown = open && query.trim().length >= 2;
const inputRect = showDropdown && wrapRef.current
? wrapRef.current.getBoundingClientRect() : null;
return (
{ if (query.trim().length >= 2) setOpen(true); }}
/>
{!open &&
⌘K}
{showDropdown && inputRect && ReactDOM.createPortal(
{results.length === 0 ? (
No candidates found
) : (
results.map((c, i) => (
))
)}
,
portalRef.current
)}
);
}
// =================== Top nav =====================
function TopNav({ activeTab = "Pipeline" }) {
const viewer = useViewer();
const isAdmin = viewer && viewer.role === "admin";
const tabs = isAdmin
? ["My candidates", "Pipeline", "Permissions"]
: ["My candidates", "Pipeline"];
const resolvedActive =
(activeTab === "Candidates" || activeTab === "All candidates")
? "Pipeline"
: activeTab;
const handleTabClick = (t) => {
if (typeof window.__atsNavigate === "function") {
if (t === "Pipeline") window.__atsNavigate("pipeline");
else if (t === "My candidates") window.__atsNavigate("my-candidates");
else if (t === "Permissions") window.__atsNavigate("settings");
}
};
return (
Nuance
);
}
// =================== Sidebar (jobs nav) ==========
function ViewIcon({ id }) {
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" };
switch (id) {
case "board": // kanban columns
return (
);
case "table": // rows
return (
);
case "calendar":
return (
);
case "stalled": // clock
return (
);
case "referrals": // sparkle / source
return (
);
case "highrating": // star
return (
);
default:
return null;
}
}
const PIPELINE_VIEWS = [
{ id: "board", label: "Board view", color: "var(--nl-ink)" },
{ id: "table", label: "Table view", color: "var(--nl-ink)" },
{ id: "calendar", label: "Calendar view", color: "var(--nl-blue)" },
];
function SavedViewRow({ saved, isActive, onSelect, onRename, onDelete }) {
const [editing, setEditing] = useState(false);
const [name, setName] = useState(saved.name);
const [menuOpen, setMenuOpen] = useState(false);
const inputRef = useRef(null);
const menuRef = useRef(null);
useEffect(() => { setName(saved.name); }, [saved.name]);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
useEffect(() => {
if (!menuOpen) return;
const onDoc = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [menuOpen]);
const commit = () => {
const trimmed = name.trim();
if (!trimmed) {
setName(saved.name);
setEditing(false);
return;
}
if (trimmed !== saved.name) onRename(trimmed);
setEditing(false);
};
return (
{editing ? (
setName(e.target.value)}
onBlur={commit}
onKeyDown={(e) => {
if (e.key === "Enter") commit();
else if (e.key === "Escape") { setName(saved.name); setEditing(false); }
}}
maxLength={48}
/>
) : (
)}
{menuOpen && (
)}
);
}
function timeAgo(iso) {
const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
if (s < 60) return "just now";
const m = Math.floor(s / 60);
if (m < 60) return m + "m ago";
const h = Math.floor(m / 60);
if (h < 24) return h + "h ago";
const d = Math.floor(h / 24);
return d + "d ago";
}
function JobSidebar({ activeJobId, onSelect, view, onViewSelect, savedViews, activeSavedViewId, onSavedViewRename, onSavedViewDelete }) {
const viewer = useViewer();
const isAdmin = viewer && viewer.role === "admin";
const [jobTick, setJobTick] = useState(0);
useEffect(() => {
const h = () => setJobTick(t => t + 1);
window.addEventListener("jobs-updated", h);
return () => window.removeEventListener("jobs-updated", h);
}, []);
const openJobs = JOBS.filter(j => j.status === "open" || j.status === "active");
const inactiveJobs = JOBS.filter(j => j.status !== "open" && j.status !== "active");
const [showInactive, setShowInactive] = useState(false);
const [newJobFlash, setNewJobFlash] = useState(false);
const [dropTarget, setDropTarget] = useState(null);
const [intStatus, setIntStatus] = useState(null);
const [tick, setTick] = useState(0);
const [retrying, setRetrying] = useState(false);
const loadStatus = () => fetchIntegrationStatus().then(d => { if (d) setIntStatus(d); });
useEffect(() => {
loadStatus();
const iv = setInterval(() => { loadStatus(); setTick(t => t + 1); }, 30000);
return () => clearInterval(iv);
}, []);
// Local active state — always tracked so clicks give visual feedback
// even when the parent screen doesn't (yet) re-render on selection.
const [localActive, setLocalActive] = useState(activeJobId);
const effectiveActive = localActive !== undefined ? localActive : activeJobId;
const handleSelect = (id) => {
setLocalActive(id);
if (onSelect) onSelect(id);
};
const filledLabel = (j) => {
if (j.status === "filled") return j.filledBy;
if (j.status === "paused") return j.pauseReason || "Paused";
return "Closed";
};
return (
);
}
Object.assign(window, {
Mark, Avatar, InitialDisc, StageChip, SourcePill, SearchBar, TopNav, JobSidebar
});