/* settings.jsx — Email template management settings page.
* Requires: app.jsx (Sidebar, Topbar, I, Avatar, Pill globals)
* email-store.jsx (loadTemplateStore, saveTemplateStore, etc.)
*/
/* ── Stage list for the settings page (minus "applied") ─────────────────── */
const ST_STAGE_GROUPS = [
{ id: "intro_screening", label: "Intro screening" },
{ id: "hiring_manager", label: "Hiring manager" },
{ id: "technical_test", label: "Technical test" },
{ id: "offer", label: "Offer" },
{ id: "hired", label: "Hired" },
{ id: "rejected", label: "Rejected" },
];
const STAGE_ACCENT = {
intro_screening: "var(--neutral-50)",
hiring_manager: "var(--indigo-50)",
technical_test: "var(--violet-50)",
offer: "var(--yellow-50)",
hired: "var(--green-50)",
rejected: "var(--red-50)",
};
/* Sample data used for live preview */
const PREVIEW_CTX = {
contact: { firstName: "Alex", lastName: "Rivera" },
posting: { title: "Senior backend engineer", location: "Remote · LatAm" },
stage: { label: "Intro screening" },
};
/* ── Variable insert chip ─────────────────────────────────────────────────── */
function VarChip({ label, onClick }) {
return (
);
}
/* ── TemplateEditor ──────────────────────────────────────────────────────── */
function TemplateEditor({ tpl, bindings, allTemplates, onSave, onDelete }) {
const { useState, useEffect, useRef } = React;
const [name, setName] = useState(tpl.name || "");
const [stageId, setStageId] = useState(tpl.stageId || "intro_screening");
const [subject, setSubject] = useState(tpl.subject || "");
const [body, setBody] = useState(tpl.body || "");
const [isDefault, setIsDefault] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const bodyRef = useRef(null);
const subjectRef = useRef(null);
useEffect(() => {
setName(tpl.name || "");
setStageId(tpl.stageId || "intro_screening");
setSubject(tpl.subject || "");
setBody(tpl.body || "");
setIsDefault(bindings[tpl.stageId] === tpl.id);
}, [tpl.id]);
useEffect(() => {
if (bodyRef.current) {
bodyRef.current.style.height = "auto";
bodyRef.current.style.height = bodyRef.current.scrollHeight + "px";
}
}, [body]);
const insertVar = (fieldRef, setter, varName) => {
const el = fieldRef.current;
if (!el) return;
const start = el.selectionStart || 0;
const end = el.selectionEnd || 0;
const val = el.value;
const token = `{{${varName}}}`;
const next = val.slice(0, start) + token + val.slice(end);
setter(next);
setTimeout(() => {
el.focus();
el.setSelectionRange(start + token.length, start + token.length);
}, 0);
};
const preview = showPreview
? renderEmailTemplate({ subject, body }, { ...PREVIEW_CTX, stage: { label: ST_STAGE_GROUPS.find(s => s.id === stageId)?.label || "" } })
: null;
const VARS = ["firstName", "lastName", "role", "stageName", "recruiter", "recruiterTitle"];
return (
{/* Stage binding row */}
{showPreview ? (
/* Live preview */
Preview with sample data (Alex Rivera → {ST_STAGE_GROUPS.find(s => s.id === stageId)?.label})
Subject
{preview.subject}
) : (
/* Edit form */
{/* Variable chips */}
Insert variable
{VARS.map(v => (
{
/* Insert into whichever field was last focused — default body */
insertVar(bodyRef, setBody, v);
}} />
))}
{/* Subject */}
setSubject(e.target.value)}
placeholder="Email subject line…"
onFocus={() => {}}
/>
{/* Body */}
)}
{/* Save footer */}
Changes are saved to your browser. Team sync coming soon.
);
}
/* ── SettingsPage ────────────────────────────────────────────────────────── */
function SettingsPage({ onNav }) {
const { useState, useCallback, useMemo } = React;
/* Load store */
function initStore() {
const stored = loadTemplateStore();
return {
templates: { ...DEFAULT_TEMPLATES, ...(stored?.templates || {}) },
bindings: { ...DEFAULT_STAGE_BINDINGS, ...(stored?.bindings || {}) },
};
}
const [store, setStore] = useState(initStore);
const [selectedId, setSelectedId] = useState(() => {
const st = initStore();
return Object.keys(st.templates)[0] || null;
});
const [activeSection, setActiveSection] = useState("templates"); // "templates" | "connectors"
const persist = useCallback((next) => {
setStore(next);
saveTemplateStore(next);
}, []);
/* Get the selected template */
const selectedTpl = selectedId ? store.templates[selectedId] : null;
/* Group templates by stage */
const grouped = useMemo(() => {
const map = {};
ST_STAGE_GROUPS.forEach(s => { map[s.id] = []; });
Object.values(store.templates).forEach(t => {
if (map[t.stageId]) map[t.stageId].push(t);
else map["intro_screening"].push(t);
});
return map;
}, [store.templates]);
/* Handlers */
const handleSave = useCallback((updatedTpl, makeDefaultForStage) => {
const next = {
templates: { ...store.templates, [updatedTpl.id]: updatedTpl },
bindings: { ...store.bindings },
};
if (makeDefaultForStage) {
next.bindings[makeDefaultForStage] = updatedTpl.id;
}
persist(next);
}, [store, persist]);
const handleDelete = useCallback((tplId) => {
if (!window.confirm("Delete this template? This cannot be undone.")) return;
const nextTemplates = { ...store.templates };
delete nextTemplates[tplId];
const nextBindings = { ...store.bindings };
Object.entries(nextBindings).forEach(([stageId, id]) => {
if (id === tplId) delete nextBindings[stageId];
});
const next = { templates: nextTemplates, bindings: nextBindings };
persist(next);
const remaining = Object.keys(nextTemplates);
setSelectedId(remaining[0] || null);
}, [store, persist]);
const handleNewTemplate = useCallback(() => {
const id = newTemplateId();
const tpl = {
id,
name: "New template",
stageId: "intro_screening",
subject: "Hello {{firstName}}",
body: "Hi {{firstName}},\n\nYour message here.\n\nBest,\n{{recruiter}}\n{{recruiterTitle}}",
};
const next = { ...store, templates: { ...store.templates, [id]: tpl } };
persist(next);
setSelectedId(id);
}, [store, persist]);
const handleResetToDefaults = useCallback(() => {
if (!window.confirm("Reset all templates to Airtm defaults? Custom templates will be lost.")) return;
resetTemplateStore();
const fresh = initStore();
setStore(fresh);
setSelectedId(Object.keys(fresh.templates)[0] || null);
}, []);
return (
{}} onSettings={() => {}} />
{/* page header */}
{" / "}
Settings
Settings
Manage email templates and ATS configuration.
{/* section tabs */}
{[{ id: "templates", label: "Email templates" }, { id: "connectors", label: "Connectors" }].map(tab => (
))}
{/* ── Templates section ── */}
{activeSection === "templates" && (
{/* left: template list */}
Templates
{ST_STAGE_GROUPS.map(stage => {
const tpls = grouped[stage.id] || [];
if (tpls.length === 0) return null;
return (
{stage.label}
{tpls.map(t => (
))}
);
})}
{/* right: editor */}
{selectedTpl ? (
) : (
Select a template from the list, or create a new one.
)}
)}
{/* ── Connectors section ── */}
{activeSection === "connectors" && (
{/* in-house DB */}
DB
In-house DB
PostgreSQL on RDS · live data
Connected
{/* other connectors */}
{["Greenhouse", "Workable", "Ashby", "CSV import"].map(name => (
))}
{/* AI model card */}
Airtm Rank v2 · trained on 142k hires
Calibrated weekly · 84% precision@10 over the last 90 days
Active
)}
);
}
window.SettingsPage = SettingsPage;