/* app.jsx — shell: topbar, section nav, live editor + preview, gallery, ATS drawer, tweaks */
const { useEffect, useRef, useLayoutEffect, useMemo, useCallback } = React;
const ACCENTS = {
"Brand Blue": "#1C9BE6",
"Brand Green": "#34C77F",
"Navy": "#0A0E39",
"Violet": "#6D5AE0",
"Slate": "#475569",
};
const NAV_FIXED = [
{ id: "basics", label: "Personal details", icon: "user" },
{ id: "summary", label: "Summary", icon: "text" },
];
const NAV_META = {
experience: { label: "Experience", icon: "briefcase", countKey: "experience" },
projects: { label: "Projects", icon: "folder", countKey: "projects" },
education: { label: "Education", icon: "cap", countKey: "education" },
skills: { label: "Skills", icon: "sparkle", countKey: "skills" },
certs: { label: "Certifications", icon: "award", countKey: "certs" },
languages: { label: "Languages", icon: "globe", countKey: "languages" },
activities: { label: "Extracurricular Activities",icon: "star", countKey: "activities" },
profdegrees: { label: "Professional Credentials", icon: "badge", countKey: "profdegrees" },
references: { label: "References", icon: "users", countKey: "references" },
};
const ADDABLE_SECTIONS = [
{ id: "languages", label: "Languages", desc: "English, French, Arabic…" },
{ id: "activities", label: "Extracurricular Activities",desc: "Clubs, sports, volunteering…" },
{ id: "profdegrees", label: "Professional Credentials", desc: "CA, CMA, PMP, CFA…" },
{ id: "references", label: "References", desc: "Contacts or 'Available upon request'" },
];
const DEFAULT_SECTION_ORDER = ["experience", "projects", "education", "skills", "certs"];
// keep for ATS lookup
const NAV = [
...NAV_FIXED,
...DEFAULT_SECTION_ORDER.map(id => ({ id, ...NAV_META[id] })),
];
function RingGauge({ value, size, stroke, color, track, children }) {
size = size || 30; stroke = stroke || 3.2; color = color || "#1C9BE6"; track = track || "#E3E6EF";
const r = (size - stroke) / 2, c = 2 * Math.PI * r;
const off = c * (1 - Math.max(0, Math.min(100, value)) / 100);
return (
);
}
/* Cute robot mascot whose expression reflects the ATS score.
90+ = happy, 80–89 = neutral, < 80 = sad. */
function ScoreRobot({ score, size }) {
size = size || 52;
const happy = score >= 90;
const sad = score < 80;
const main = sad ? "#8FB8D6" : "#1C9BE6";
const dark = sad ? "#6E9CBE" : "#1684C6";
const ink = "#0B2C46";
return (
{/* antenna + signal */}
{happy && <>
>}
{/* head */}
{/* face screen */}
{/* eyes */}
{happy
? <>
>
: <>
>}
{/* mouth */}
{happy && }
{!happy && !sad && }
{sad && }
{/* arms */}
{/* body */}
{/* lightning badge */}
{/* thruster */}
);
}
const scoreColor = (s) => s >= 80 ? "#16A36B" : s >= 60 ? "#1C9BE6" : s >= 40 ? "#E0A013" : "#E0533D";
const uidA = () => Math.random().toString(36).slice(2, 9);
const scoreLabel = (s) => s >= 80 ? "Excellent" : s >= 60 ? "Strong" : s >= 40 ? "Needs work" : "Getting started";
const RP_SECTIONS = [
{ id: "experience", label: "Experience", icon: "briefcase" },
{ id: "projects", label: "Projects", icon: "folder" },
{ id: "education", label: "Education", icon: "cap" },
{ id: "skills", label: "Skills", icon: "sparkle" },
{ id: "certs", label: "Certifications", icon: "award" },
{ id: "languages", label: "Languages", icon: "globe" },
{ id: "activities", label: "Extracurricular Activities",icon: "star" },
{ id: "profdegrees", label: "Professional Credentials", icon: "badge" },
{ id: "references", label: "References", icon: "users" },
];
function RpColLayout({ sidebarSections, setSidebarSections, sectionOrder }) {
const [dragId, setDragId] = React.useState(null);
const [dragTarget, setDragTarget] = React.useState(null);
const mainSections = sectionOrder.filter(id => !sidebarSections.includes(id));
const sidebarOrdered = sectionOrder.filter(id => sidebarSections.includes(id));
const moveTo = (id, col) => setSidebarSections(prev =>
col === "sidebar" ? (prev.includes(id) ? prev : [...prev, id]) : prev.filter(x => x !== id)
);
const handleDrop = (col) => { if (!dragId) return; moveTo(dragId, col); setDragId(null); setDragTarget(null); };
const ColBox = ({ col, ids, label, hint }) => (
{ e.preventDefault(); setDragTarget(col); }}
onDragLeave={() => setDragTarget(null)}
onDrop={() => handleDrop(col)}
style={{ border: "2px dashed", borderColor: dragTarget === col ? "var(--brand-blue)" : "var(--slate-200)", borderRadius: 8, padding: "8px 8px 4px", background: dragTarget === col ? "rgba(28,155,230,.05)" : "var(--slate-50)", marginBottom: 8, transition: "border-color .15s, background .15s", minHeight: 56 }}
>
{label}
{hint}
{ids.length === 0 &&
Drop here
}
{ids.map(id => {
const s = RP_SECTIONS.find(x => x.id === id); if (!s) return null;
return (
setDragId(id)}
onDragEnd={() => { setDragId(null); setDragTarget(null); }}
style={{ display: "flex", alignItems: "center", gap: 5, padding: "4px 7px", marginBottom: 4, borderRadius: 6, background: "#fff", border: "1px solid var(--slate-200)", cursor: "grab", opacity: dragId === id ? 0.4 : 1, fontSize: 12, fontWeight: 500, color: "var(--slate-700)" }}
>
{s.label}
moveTo(id, col === "main" ? "sidebar" : "main")}
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--slate-400)", fontSize: 13, lineHeight: 1, padding: "0 2px" }}
>{col === "main" ? "→" : "←"}
);
})}
);
return (
Drag sections between columns or click → ← to move.
);
}
const TWEAK_DEFAULTS = { "accent": "Brand Blue", "layout": "split", "density": "regular", "navy": "#0A0E39" };
const FREE_TEMPLATES = ["executive"]; // Prestige is the only free template
const FREE_DOWNLOAD_LIMIT = 3; // free users get 3 PDF downloads, then must upgrade
const LANDING_URL = (window.__LANDING_URL__ || "http://localhost:3401").replace(/\/$/, "");
/* Free-tier AI usage meter — shows how many free AI generations remain.
Updates live on every AI click via the "bdr-ai-uses-changed" event. */
function AIUsageBar() {
const LIMIT = window.AI_FREE_LIMIT || 10;
const read = () => {
const r = window.getAIUsesRemaining ? window.getAIUsesRemaining() : null;
if (r === Infinity) return LIMIT; // pro — handled by caller, won't render
if (r === null || isNaN(r)) return LIMIT; // not used yet — full allowance
return Math.max(0, Math.min(LIMIT, r));
};
const [remaining, setRemaining] = useState(read);
useEffect(() => {
const onChange = () => setRemaining(read());
window.addEventListener("bdr-ai-uses-changed", onChange);
return () => window.removeEventListener("bdr-ai-uses-changed", onChange);
}, []);
const used = LIMIT - remaining;
const pct = Math.round((used / LIMIT) * 100);
const out = remaining <= 0;
const low = remaining > 0 && remaining <= 3;
const barColor = out ? "var(--brand-blue)" : low ? "#F59E0B" : "var(--brand-green)";
return (
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
/* ── routing ──────────────────────────────────────────── */
const [mode, setMode] = useState("editor"); // "dashboard" | "editor"
/* ── document data ────────────────────────────────────── */
const load = (k, fb) => { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch { return fb; } };
const [docId, setDocId] = useState(() => load("bdr_current_doc", null));
const [data, setData] = useState(() => load("bdr_resume", SAMPLE_RESUME));
const [docName, setDocName] = useState(() => load("bdr_docname", "Maya Chen — Product Design"));
const [template, setTemplate] = useState(() => load("bdr_template", "modern"));
const [design, setDesign] = useState(() => load("bdr_design", { font: "poppins", accent: "#1C9BE6", scale: "normal", spacing: "normal" }));
const [coverLetter, setCoverLetter] = useState(() => load("bdr_coverletter", CL_DEFAULTS));
/* ── editor UI ────────────────────────────────────────── */
const [editorTab, setEditorTab] = useState("resume"); // "resume" | "coverletter"
const [section, setSection] = useState("basics");
const [openIds, setOpenIds] = useState({});
const [zoom, setZoom] = useState(1);
const [leftW, setLeftW] = useState(() => { try { const s = JSON.parse(localStorage.getItem("bdr_panel_w") || "{}"); return s.leftW || 290; } catch { return 290; } });
const [rightW, setRightW] = useState(() => { try { const s = JSON.parse(localStorage.getItem("bdr_panel_w") || "{}"); return s.rightW || 220; } catch { return 220; } });
const [plan, setPlan] = useState(() => { try { return localStorage.getItem("bdr_plan") || "free"; } catch { return "free"; } });
const [sectionOrder, setSectionOrder] = useState(() => { try { const v = localStorage.getItem("bdr_section_order"); return v ? JSON.parse(v) : DEFAULT_SECTION_ORDER; } catch { return DEFAULT_SECTION_ORDER; } });
const [sidebarSections, setSidebarSections] = useState(() => { try { const v = localStorage.getItem("bdr_sidebar_sections"); return v ? JSON.parse(v) : ["skills", "certs", "education"]; } catch { return ["skills", "certs", "education"]; } });
const [dragSecIdx, setDragSecIdx] = useState(null);
const [dragOverSecIdx, setDragOverSecIdx] = useState(null);
/* ── panel visibility ─────────────────────────────────── */
const [showGallery, setShowGallery] = useState(false);
const [showATS, setShowATS] = useState(false);
const [showJobMatch, setShowJobMatch] = useState(false);
const [showTailor, setShowTailor] = useState(false);
const [showShare, setShowShare] = useState(false);
const [showKeyboard, setShowKeyboard] = useState(false);
const [saved, setSaved] = useState(true);
const [pdfBusy, setPdfBusy] = useState(false);
const [pdfMenu, setPdfMenu] = useState(false);
const [shareCopied, setShareCopied] = useState("");
const [toast, setToast] = useState(null);
const [showAI, setShowAI] = useState(false);
const [showImport, setShowImport] = useState(false);
const [summaryBusy, setSummaryBusy] = useState(false);
const isPro = !!window.IS_PRO;
const [tabletPreview, setTabletPreview] = useState(false);
const [userMenu, setUserMenu] = useState(false);
const [aiNewSeen, setAiNewSeen] = useState(() => { try { return !!localStorage.getItem("bdr_ai_new_seen"); } catch { return false; } });
const [colLayoutSeen, setColLayoutSeen] = useState(() => { try { return !!localStorage.getItem("bdr_col_layout_seen"); } catch { return false; } });
const [sampleBanner, setSampleBanner] = useState(() => { try { return !localStorage.getItem("bdr_resume") && !localStorage.getItem("bdr_sample_seen"); } catch { return false; } });
const [showProExport, setShowProExport] = useState(false);
const [proReason, setProReason] = useState("template"); // "template" | "limit"
const [dlCount, setDlCount] = useState(() => { try { return parseInt(localStorage.getItem("bdr_dl_count") || "0", 10) || 0; } catch { return 0; } });
const [grammarBusy, setGrammarBusy] = useState(false);
const [grammarDone, setGrammarDone] = useState(false);
const [showInterviewPrep, setShowInterviewPrep] = useState(false);
const [showTranslate, setShowTranslate] = useState(false);
const [showSalary, setShowSalary] = useState(false);
const [showAchievement, setShowAchievement] = useState(false);
const [showColdEmail, setShowColdEmail] = useState(false);
const [recruiterScan, setRecruiterScan] = useState(false);
const [versions, setVersions] = useState(() => { try { return JSON.parse(localStorage.getItem("bdr_versions") || "[]"); } catch { return []; } });
const [showVersionName, setShowVersionName] = useState(false);
const [versionNameInput, setVersionNameInput] = useState("");
/* ── undo / redo history ──────────────────────────────── */
const histRef = useRef([data]);
const histIdx = useRef(0);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const refreshHist = () => {
setCanUndo(histIdx.current > 0);
setCanRedo(histIdx.current < histRef.current.length - 1);
};
const pushHistory = useCallback((next) => {
const h = histRef.current.slice(0, histIdx.current + 1);
h.push(next);
if (h.length > 60) h.shift();
histRef.current = h;
histIdx.current = h.length - 1;
refreshHist();
}, []);
const undo = useCallback(() => {
if (histIdx.current > 0) { histIdx.current--; setData(histRef.current[histIdx.current]); refreshHist(); }
}, []);
const redo = useCallback(() => {
if (histIdx.current < histRef.current.length - 1) { histIdx.current++; setData(histRef.current[histIdx.current]); refreshHist(); }
}, []);
const patch = useCallback((fn) => {
setData(d => { const next = fn(d); pushHistory(next); return next; });
}, [pushHistory]);
const showToast = (msg, action) => { setToast({ msg, action: action || null }); setTimeout(() => setToast(null), action ? 7000 : 2200); };
const signOut = () => {
const form = document.createElement("form");
form.method = "POST";
form.action = LANDING_URL + "/api/auth/logout";
document.body.appendChild(form);
form.submit();
};
const startPanelResize = (side) => (e) => {
e.preventDefault();
const x0 = e.clientX;
const startLeft = leftW, startRight = rightW;
const move = (ev) => {
const d = ev.clientX - x0;
// Left handle: drag right = wider. Right handle: drag right = narrower.
if (side === "left") setLeftW(Math.max(220, Math.min(460, startLeft + d)));
else setRightW(Math.max(180, Math.min(420, startRight - d)));
};
const up = () => {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", up);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
};
/* ── AI summary rewrite helper ────────────────────────────── */
const rewriteSummary = useCallback(async () => {
if (summaryBusy || !window.groqStream) return;
setSummaryBusy(true);
await window.groqStream({
messages: [
{ role: "system", content: window.GROQ_SYSTEM || "" },
{ role: "user", content: "Write a compelling 2–3 sentence professional summary for this résumé. Strong language, quantify if possible, no first-person pronouns. Return ONLY the summary text:\n\n" + (window.buildResumeCtx ? window.buildResumeCtx(data) : "") },
],
maxTokens: 200,
onChunk: (_, full) => patch(d => ({ ...d, summary: full })),
onDone: () => setSummaryBusy(false),
onError: (e) => {
setSummaryBusy(false);
showToast(e === "free_limit_reached"
? "Free AI limit reached — upgrade to Pro for unlimited use."
: (typeof e === "string" ? e : "AI error — couldn't rewrite summary."));
},
});
}, [summaryBusy, data, patch]);
const checkGrammar = useCallback(async () => {
if (grammarBusy || !window.groqStream) return;
setGrammarBusy(true);
setGrammarDone(false);
// Collect all plain-text snippets with keys
const items = [];
if (data.summary) items.push({ k: "summary", v: data.summary });
(data.experience || []).forEach((x, xi) =>
(x.bullets || []).forEach((b, bi) => { if (b.trim()) items.push({ k: `exp.${xi}.${bi}`, v: b }); })
);
(data.projects || []).forEach((x, xi) => {
if (x.detail) items.push({ k: `proj.${xi}.detail`, v: x.detail });
(x.bullets || []).forEach((b, bi) => { if (b.trim()) items.push({ k: `proj.${xi}.${bi}`, v: b }); });
});
(data.education || []).forEach((x, xi) => {
if (x.detail) items.push({ k: `edu.${xi}.detail`, v: x.detail });
});
(data.activities || []).forEach((a, ai) => {
if (a.detail) items.push({ k: `act.${ai}.detail`, v: a.detail });
});
(data.custom || []).forEach((c, ci) => {
if (c.body) items.push({ k: `cust.${ci}`, v: c.body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() });
});
if (!items.length) { setGrammarBusy(false); return; }
const numbered = items.map((item, i) => `${i + 1}. ${item.v}`).join("\n");
let raw = "";
await window.groqStream({
messages: [
{ role: "system", content: "You are a grammar and style editor. Fix grammar, spelling, punctuation, and clarity in each numbered item. Keep meaning and tone identical. Return ONLY a JSON array of corrected strings in the same order, no explanation." },
{ role: "user", content: "Fix grammar in each numbered item:\n\n" + numbered + "\n\nReturn as JSON array: [\"corrected 1\", \"corrected 2\", ...]" },
],
maxTokens: 2048,
onChunk: (_, full) => { raw = full; },
onDone: () => {
try {
const match = raw.match(/\[[\s\S]*\]/);
if (!match) throw new Error("no array");
const fixed = JSON.parse(match[0]);
patch(d => {
let nd = { ...d };
fixed.forEach((val, i) => {
if (!val || !items[i]) return;
const k = items[i].k;
if (k === "summary") { nd = { ...nd, summary: val }; return; }
const parts = k.split(".");
if (parts[0] === "exp") {
const xi = +parts[1]; const bi = +parts[2];
const exp = [...(nd.experience || [])];
const bullets = [...(exp[xi].bullets || [])];
bullets[bi] = val;
exp[xi] = { ...exp[xi], bullets };
nd = { ...nd, experience: exp };
} else if (parts[0] === "proj") {
const xi = +parts[1];
const projs = [...(nd.projects || [])];
if (parts[2] === "detail") projs[xi] = { ...projs[xi], detail: val };
else { const bi = +parts[2]; const bullets = [...(projs[xi].bullets||[])]; bullets[bi] = val; projs[xi] = { ...projs[xi], bullets }; }
nd = { ...nd, projects: projs };
} else if (parts[0] === "edu") {
const xi = +parts[1];
const edus = [...(nd.education || [])];
edus[xi] = { ...edus[xi], detail: val };
nd = { ...nd, education: edus };
} else if (parts[0] === "act") {
const ai = +parts[1];
const acts = [...(nd.activities || [])];
acts[ai] = { ...acts[ai], detail: val };
nd = { ...nd, activities: acts };
}
});
return nd;
});
} catch(e) {
setGrammarBusy(false);
showToast("AI error — couldn't parse grammar fixes. Try again.");
return;
}
setGrammarBusy(false);
setGrammarDone(true);
setTimeout(() => setGrammarDone(false), 3000);
},
onError: (e) => {
setGrammarBusy(false);
showToast(e === "free_limit_reached"
? "Free AI limit reached — upgrade to Pro for unlimited use."
: (typeof e === "string" ? e : "AI error — couldn't check grammar."));
},
});
}, [grammarBusy, data, patch]);
/* ── version snapshots ────────────────────────────────── */
const saveVersion = useCallback((name) => {
const snap = { id: Date.now().toString(36), name: name || ("Version " + new Date().toLocaleString()), data, template, design, savedAt: new Date().toISOString() };
setVersions(vs => {
const next = [snap, ...vs].slice(0, 20);
try { localStorage.setItem("bdr_versions", JSON.stringify(next)); } catch(_) {}
return next;
});
showToast("Version saved: " + snap.name);
}, [data, template, design]);
const restoreVersion = useCallback((snap) => {
patch(() => snap.data);
setTemplate(snap.template || template);
setDesign(snap.design || design);
showToast("Restored: " + snap.name);
}, [patch, template, design]);
const deleteVersion = useCallback((id) => {
setVersions(vs => {
const next = vs.filter(v => v.id !== id);
try { localStorage.setItem("bdr_versions", JSON.stringify(next)); } catch(_) {}
return next;
});
}, []);
/* ── multi-doc helpers ────────────────────────────────── */
const saveCurrentDoc = useCallback(() => {
const id = docId || uidA();
const payload = { data, docName, template, design, coverLetter, updatedAt: new Date().toISOString() };
saveDocData(id, payload);
const docs = loadAllDocs();
const idx = docs.findIndex(d => d.id === id);
const meta = { id, name: docName, template, updatedAt: new Date().toISOString() };
if (idx >= 0) docs[idx] = meta; else docs.unshift(meta);
saveAllDocs(docs);
localStorage.setItem("bdr_current_doc", JSON.stringify(id));
if (!docId) setDocId(id);
/* keep legacy keys in sync */
localStorage.setItem("bdr_resume", JSON.stringify(data));
localStorage.setItem("bdr_template", JSON.stringify(template));
localStorage.setItem("bdr_docname", JSON.stringify(docName));
localStorage.setItem("bdr_design", JSON.stringify(design));
localStorage.setItem("bdr_coverletter", JSON.stringify(coverLetter));
showToast("Saved");
return id;
}, [data, docName, template, design, coverLetter, docId]);
const openDoc = useCallback((id) => {
const p = loadDocData(id);
if (p) {
setData(p.data || SAMPLE_RESUME);
setDocName(p.docName || "Untitled");
setTemplate(p.template || "modern");
setDesign(p.design || { font: "auto", accent: "#1C9BE6" });
setCoverLetter(p.coverLetter || CL_DEFAULTS);
histRef.current = [p.data || SAMPLE_RESUME];
histIdx.current = 0;
refreshHist();
}
setDocId(id);
localStorage.setItem("bdr_current_doc", JSON.stringify(id));
setMode("editor"); setEditorTab("resume"); setSection("basics");
}, []);
const createNew = useCallback((tpl) => {
const id = uidA();
let tmpl = tpl || "modern";
const acc = (TEMPLATES[tmpl] && TEMPLATES[tmpl].defaultAccent) || "#1C9BE6";
setDocId(id); setData(SAMPLE_RESUME); setDocName("New Résumé");
setTemplate(tmpl); setDesign({ font: "poppins", accent: acc, scale: "normal", spacing: "normal" });
setCoverLetter(CL_DEFAULTS);
histRef.current = [SAMPLE_RESUME]; histIdx.current = 0; refreshHist();
localStorage.setItem("bdr_current_doc", JSON.stringify(id));
setMode("editor"); setEditorTab("resume"); setSection("basics");
}, []);
const goHome = useCallback(() => {
saveCurrentDoc();
setMode("dashboard");
}, [saveCurrentDoc]);
/* ── persistence ──────────────────────────────────────── */
useEffect(() => {
setSaved(false);
const id = setTimeout(() => { localStorage.setItem("bdr_resume", JSON.stringify(data)); setSaved(true); }, 500);
return () => clearTimeout(id);
}, [data]);
useEffect(() => { localStorage.setItem("bdr_template", JSON.stringify(template)); }, [template]);
const prevTemplateRef = useRef(template);
useEffect(() => {
const prev = prevTemplateRef.current;
prevTemplateRef.current = template;
const isNowTwoCol = template === "split" || template === "sidebar";
const wasTwoCol = prev === "split" || prev === "sidebar";
if (isNowTwoCol && !wasTwoCol && !colLayoutSeen) {
setTimeout(() => showToast("💡 Tip: Use 'Column layout' under Design to customise what goes in each column!"), 600);
}
}, [template]);
useEffect(() => { localStorage.setItem("bdr_docname", JSON.stringify(docName)); }, [docName]);
useEffect(() => { localStorage.setItem("bdr_design", JSON.stringify(design)); }, [design]);
useEffect(() => { localStorage.setItem("bdr_coverletter", JSON.stringify(coverLetter)); }, [coverLetter]);
useEffect(() => { try { localStorage.setItem("bdr_section_order", JSON.stringify(sectionOrder)); } catch {} }, [sectionOrder]);
useEffect(() => { try { localStorage.setItem("bdr_sidebar_sections", JSON.stringify(sidebarSections)); } catch {} }, [sidebarSections]);
useEffect(() => {
const id = setTimeout(() => {
localStorage.setItem("bdr_panel_w", JSON.stringify({ leftW, rightW }));
}, 400);
return () => clearTimeout(id);
}, [leftW, rightW]);
/* Read ?plan=pro from URL (set by landing when opening builder for Pro users) */
useEffect(() => {
const p = new URLSearchParams(window.location.search).get("plan");
if (p === "pro") {
localStorage.setItem("bdr_plan", "pro");
setPlan("pro");
history.replaceState(null, "", window.location.pathname);
}
}, []);
/* ── CSS variables ────────────────────────────────────── */
const accent = design.accent || "#1C9BE6";
useEffect(() => {
document.documentElement.style.setProperty("--accent", accent);
document.documentElement.style.setProperty("--density",
t.density === "compact" ? ".82" : t.density === "comfy" ? "1.18" : "1");
}, [accent, t.density]);
const ats = useMemo(() => computeATS(data), [data]);
/* ── keyboard shortcuts ───────────────────────────────── */
useEffect(() => {
const handler = (e) => {
if (mode !== "editor") return;
if (e.ctrlKey || e.metaKey) {
if (e.key === "z" && !e.shiftKey) { e.preventDefault(); undo(); }
if ((e.key === "z" && e.shiftKey) || e.key === "y") { e.preventDefault(); redo(); }
if (e.key === "s") { e.preventDefault(); saveCurrentDoc(); }
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [mode, undo, redo, saveCurrentDoc]);
/* ── preload PDF libs ─────────────────────────────────── */
useEffect(() => {
const idle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1500));
idle(() => { try { window.loadPdfLibs && window.loadPdfLibs(); } catch (e) {} });
}, []);
/* ── load from URL share link ─────────────────────────── */
useEffect(() => {
const hash = window.location.hash;
if (hash.startsWith("#resume=")) {
try {
const json = decodeURIComponent(atob(hash.slice("#resume=".length)));
const payload = JSON.parse(json);
if (payload.data) {
setData(payload.data);
if (payload.docName) setDocName(payload.docName);
if (payload.template) setTemplate(payload.template);
if (payload.design) setDesign(payload.design);
if (payload.coverLetter) setCoverLetter(payload.coverLetter);
history.replaceState(null, "", window.location.pathname);
showToast("Shared résumé loaded");
}
} catch (e) {}
}
}, []);
/* ── preview auto-fit scaling ─────────────────────────── */
const colRef = useRef(null);
const pageRef = useRef(null);
const [scale, setScale] = useState(0.7);
const [boxH, setBoxH] = useState(1056);
const recompute = () => {
if (!colRef.current) return;
const avail = colRef.current.clientWidth - 60;
const fit = Math.max(0.34, Math.min(1, avail / 816));
const s = fit * zoom;
setScale(s);
const h = pageRef.current ? pageRef.current.offsetHeight : 1056;
setBoxH(h * s);
};
useLayoutEffect(recompute, [zoom, data, template, design, t.layout, showATS, showJobMatch, editorTab]);
useEffect(() => {
const ro = new ResizeObserver(recompute);
if (colRef.current) ro.observe(colRef.current);
window.addEventListener("resize", recompute);
return () => { ro.disconnect(); window.removeEventListener("resize", recompute); };
}, [zoom]);
/* ── PDF export & download gating ─────────────────────── */
// Free users: only the Prestige template, and only FREE_DOWNLOAD_LIMIT total downloads.
const isProUser = plan === "pro" || isPro;
const downloadsLeft = Math.max(0, FREE_DOWNLOAD_LIMIT - dlCount);
const showDlRemaining = !isProUser && FREE_TEMPLATES.includes(template);
useEffect(() => {
try { localStorage.setItem("bdr_dl_count", String(dlCount)); } catch (e) {}
}, [dlCount]);
// Returns true if a download may proceed; otherwise opens the upgrade modal and returns false.
const checkDownloadAllowed = () => {
if (isProUser) return true;
if (!FREE_TEMPLATES.includes(template)) { setProReason("template"); setShowProExport(true); return false; }
if (dlCount >= FREE_DOWNLOAD_LIMIT) { setProReason("limit"); setShowProExport(true); return false; }
return true;
};
// Count a successful free download and tell the user how many remain.
const recordDownload = () => {
if (isProUser) return;
const n = dlCount + 1;
setDlCount(n);
const left = Math.max(0, FREE_DOWNLOAD_LIMIT - n);
showToast(left > 0
? left + " free download" + (left === 1 ? "" : "s") + " left"
: "That was your last free download — upgrade to Pro for unlimited.");
};
const exportPDF = async () => {
if (pdfBusy) return;
if (!checkDownloadAllowed()) return;
setPdfBusy(true);
try {
const ok = await window.downloadResumePDF(docName, { data, template, accent, design });
if (ok === false) showToast("PDF library unavailable — use Exact Copy instead");
else recordDownload();
}
catch (e) {
console.warn("PDF export failed", e);
showToast("PDF export failed — use Exact Copy instead");
}
finally { setPdfBusy(false); }
};
const exportExact = async () => {
if (pdfBusy) return;
if (!checkDownloadAllowed()) return;
setPdfBusy(true);
try {
const el = document.getElementById("resume-page");
if (!el) throw new Error("Resume element not found");
// Capture the fully-rendered resume HTML
const html = el.outerHTML;
// Collect all CSS rules from every loaded stylesheet
let css = "";
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) css += rule.cssText + "\n";
} catch (e) { /* cross-origin sheets */ }
}
// Collect CSS custom property overrides set on (e.g. --accent)
const rootStyle = document.documentElement.style;
let rootVars = ":root{";
for (let i = 0; i < rootStyle.length; i++) {
const p = rootStyle.item(i);
rootVars += p + ":" + rootStyle.getPropertyValue(p) + ";";
}
rootVars += "}";
// Google Fonts links needed for correct font rendering
const fontLinks = [...document.querySelectorAll("link[rel=stylesheet]")]
.map(l => l.href).filter(h => h.includes("googleapis.com") || h.includes("gstatic.com"));
let blob;
try {
const res = await fetch(LANDING_URL + "/api/pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html, css, rootVars, fontLinks, docName }),
});
if (!res.ok) throw new Error(await res.text());
blob = await res.blob();
} catch (serverErr) {
// The PDF server is unreachable (static/local hosting, CORS, or offline).
// Fall back to the in-browser exact-copy exporter, which saves directly.
console.warn("PDF server unavailable, using client-side export", serverErr);
const ok = await window.exportExactPDF(docName);
if (ok === false) throw new Error("client-side export unavailable");
recordDownload();
showToast("PDF downloaded!");
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = (docName || "resume").replace(/[^\w\s\-]/g, "").trim() + ".pdf";
a.click();
URL.revokeObjectURL(url);
recordDownload();
showToast("PDF downloaded!");
} catch (e) {
console.warn("PDF export failed", e);
showToast("PDF export failed — " + e.message);
} finally {
setPdfBusy(false);
}
};
/* ── share / export ───────────────────────────────────── */
const exportJSON = () => {
const payload = { data, docName, template, design, coverLetter };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = (docName || "resume").replace(/[^\w\s]/g, "").trim().replace(/\s+/g, "_") + ".json";
a.click();
URL.revokeObjectURL(url);
};
const copyShareLink = () => {
try {
const encoded = btoa(encodeURIComponent(JSON.stringify({ data, docName, template, design, coverLetter })));
const url = window.location.href.split("#")[0] + "#resume=" + encoded;
navigator.clipboard.writeText(url).then(() => {
setShareCopied("link"); setTimeout(() => setShareCopied(""), 2800);
});
} catch (e) { alert("Could not generate link."); }
};
const copyJSON = () => {
navigator.clipboard.writeText(JSON.stringify({ data, docName, template, design, coverLetter }, null, 2)).then(() => {
setShareCopied("json"); setTimeout(() => setShareCopied(""), 2800);
});
};
const pct = Math.round(scale * 100);
/* ── DASHBOARD mode ───────────────────────────────────── */
if (mode === "dashboard") {
return ;
}
/* ══════════════════════════════════════════════════════
EDITOR mode
══════════════════════════════════════════════════════ */
return (
{/* ── TOPBAR (light) ──────────────────────────────── */}
{/* Left group */}
bdRecruit.net
{saved ? "Saved" : "Saving…"}
{/* Center: doc title */}
{/* Right group */}
setTabletPreview(p => !p)}>
{ setShowATS(true); setShowJobMatch(false); setShowAI(false); }} title="ATS Analysis">
Analyze
{showDlRemaining && (
{downloadsLeft === 0 ? "No free downloads left" : downloadsLeft + " free left"}
)}
{pdfBusy ? <> Preparing…> : <> Download PDF>}
setUserMenu(m => !m)} title="Account">
{((data.basics?.name || "").trim().split(/\s+/).map(w => w[0]).slice(0, 2).join("").toUpperCase()) || "?"}
{userMenu && (
<>
setUserMenu(false)} style={{ position: "fixed", inset: 0, zIndex: 99 }} />
>
)}
{/* ── BODY (3-column) ─────────────────────────────── */}
{/* ── LEFT PANEL ──────────────────────────────── */}
{/* Builder / Templates tabs */}
{ setEditorTab("resume"); }}>Builder
setShowGallery(true)}>Templates
{/* Sample banner */}
{sampleBanner && (
👋 Sample data — edit any field to make it yours.
{
setSampleBanner(false);
try { localStorage.setItem("bdr_sample_seen", "1"); } catch {}
setShowImport(true);
}}>Import Resume
{ setSampleBanner(false); try { localStorage.setItem("bdr_sample_seen", "1"); } catch {} }}>
)}
{editorTab === "coverletter" ? (
/* ── Cover letter editor ── */
) : (
/* ── Section accordion ── */
{/* Fixed sections: Personal details + Summary */}
{NAV_FIXED.map(n => (
setSection(section === n.id ? "" : n.id)}>
{n.label}
{section === n.id ? "−" : "+"}
{section === n.id && (
)}
))}
{/* Draggable sections */}
{sectionOrder.map((id, idx) => {
const n = NAV_META[id]; if (!n) return null;
const ct = n.countKey ? (data[n.countKey] || []).length : null;
const isDragging = dragSecIdx === idx;
const isDragOver = dragOverSecIdx === idx && dragSecIdx !== idx;
return (
{ e.dataTransfer.effectAllowed = "move"; setDragSecIdx(idx); }}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverSecIdx(idx); }}
onDrop={e => { e.preventDefault(); if (dragSecIdx === null || dragSecIdx === idx) return; const next = [...sectionOrder]; const [moved] = next.splice(dragSecIdx, 1); next.splice(idx, 0, moved); setSectionOrder(next); setDragSecIdx(null); setDragOverSecIdx(null); }}
onDragEnd={() => { setDragSecIdx(null); setDragOverSecIdx(null); }}
onClick={() => setSection(section === id ? "" : id)}>
{n.label}
{ct > 0 && {ct} }
{section === id ? "−" : "+"}
{section === id && (
)}
);
})}
{/* Custom sections */}
{(data.custom || []).map(c => {
const cid = "custom:" + c.id;
return (
setSection(section === cid ? "" : cid)}>
{c.heading || "Custom section"}
{section === cid ? "−" : "+"}
{section === cid && (
)}
);
})}
{/* Add section */}
)}
{/* ── Tools ── */}
{(() => {
const [toolsOpen, setToolsOpen] = React.useState(true);
return (
setToolsOpen(o => !o)}
style={{ width:"100%", display:"flex", alignItems:"center", background:"none", border:"none", cursor:"pointer", padding:0 }}>
Tools
{toolsOpen && (
setEditorTab("coverletter")} title="Cover Letter">
Cover Letter
{ setShowJobMatch(true); setShowATS(false); setShowAI(false); setShowTailor(false); }} title="Job Match">
Job Match
{ setShowTailor(s => !s); setShowATS(false); setShowJobMatch(false); setShowAI(false); }} title="Tailor to job">
TailorPro
{ setShowAI(s => { if (!s) { setShowATS(false); setShowJobMatch(false); setShowTailor(false); } return !s; }); if (!aiNewSeen) { setAiNewSeen(true); try { localStorage.setItem("bdr_ai_new_seen", "1"); } catch {} } }} title="AI Coach">
AI Coach
setShowImport(true)} title="Import CV">
Import CV
setShowInterviewPrep(true)} title="Interview Prep">
Interview
setShowTranslate(true)} title="Translate Resume">
Translate
setShowSalary(true)} title="Salary Benchmarker">
Salary
setShowAchievement(true)} title="Quantify Bullets">
Quantify
setShowColdEmail(true)} title="Cold Email Generator">
Cold Email
)}
);
})()}
{/* Left panel resize handle */}
{/* ── PREVIEW (center) ────────────────────────── */}
{editorTab === "coverletter" ? "Cover Letter preview" : (TEMPLATES[template]?.name + " template")}
{editorTab !== "coverletter" && (
setRecruiterScan(s => !s)}
style={{ fontSize:11.5, fontWeight:700, padding:"4px 11px", borderRadius:7, border:"1.5px solid", cursor:"pointer", marginRight:8, transition:"all .15s",
borderColor: recruiterScan ? "#f59e0b" : "var(--slate-200)",
background: recruiterScan ? "rgba(245,158,11,.1)" : "transparent",
color: recruiterScan ? "#d97706" : "var(--slate-500)" }}
title="Show 6-second recruiter scan heatmap">
{recruiterScan ? "◉ Scan ON" : "◎ Recruiter Scan"}
)}
{editorTab !== "coverletter" && (
setZoom(z => Math.max(0.5, +(z - 0.1).toFixed(2)))}>
setZoom(1)}>{pct}%
setZoom(z => Math.min(1.6, +(z + 0.1).toFixed(2)))}>
)}
{editorTab === "coverletter" ? (
) : (
{/* Recruiter scan heatmap overlay */}
{recruiterScan && (
{/* Zone 1 — Name / Title: top 11% — hottest */}
{/* Zone 2 — Contact + first job header: 11–26% */}
{/* Zone 3 — First 2 bullets + skills: 26–52% */}
{/* Zone 4 — Rest: 52–100% — cold */}
{/* Legend */}
6-sec Recruiter Eye
{[["rgba(239,68,68,.6)","Name & Title","Hottest"],["rgba(249,115,22,.6)","Contact + Role","High"],["rgba(234,179,8,.5)","First bullets + Skills","Medium"],["rgba(99,102,241,.4)","Rest of resume","Low"]].map(([c,label,heat]) => (
))}
)}
)}
{/* Right panel resize handle */}
{/* ── RIGHT PANEL ─────────────────────────────── */}
{/* ATS Score card */}
ATS Score
{scoreLabel(ats.score)}
{ats.score}/100
ATS Readiness
{ setShowATS(true); setShowJobMatch(false); setShowAI(false); }}>
View full report →
{/* Template card */}
Template
{TEMPLATES[template]?.name || template}
setShowGallery(true)}>Change
{/* Design card */}
Accent color
{Object.entries(ACCENTS).map(([name, hex]) => (
setDesign(d => ({ ...d, accent: hex }))} title={name} />
))}
+
setDesign(d => ({ ...d, accent: e.target.value }))} />
Font
setDesign(d => ({ ...d, font: e.target.value }))}>
{(window.FONTS || []).map(f => (
{f.label}{f.note ? " · " + f.note : ""}
))}
Column layout
{(template === "split" || template === "sidebar") ? (
) : (
Switch to Split or Sidebar template to use columns.
)}
{/* Versions card */}
Version Snapshots
Save named versions to compare or restore.
{showVersionName ? (
setVersionNameInput(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") { saveVersion(versionNameInput || undefined); setShowVersionName(false); setVersionNameInput(""); } if (e.key === "Escape") { setShowVersionName(false); setVersionNameInput(""); } }}
/>
{ saveVersion(versionNameInput || undefined); setShowVersionName(false); setVersionNameInput(""); }}>Save
) : (
setShowVersionName(true)}>+ Save current version
)}
{versions.length > 0 && (
{versions.slice(0, 5).map(v => (
{v.name}
{new Date(v.savedAt).toLocaleDateString()}
restoreVersion(v)}>Restore
deleteVersion(v.id)}>
))}
{versions.length > 5 &&
+{versions.length-5} more
}
)}
{/* Free AI usage meter (free users only) */}
{!isPro &&
}
{/* Grammar check card */}
AI Writing Tools
Scan and fix grammar, spelling, and phrasing across your entire résumé in one click.
{grammarBusy
? <> Checking…>
: grammarDone ? "✓ Fixed!" : <> Check & Fix Grammar>}
{/* Keyboard shortcuts */}
setShowKeyboard(true)}>
⌨ Keyboard shortcuts
{showAI &&
{ setEditorTab("coverletter"); setShowAI(false); }}
onClose={() => setShowAI(false)} />}
{/* ── MODALS & DRAWERS ───────────────────────────── */}
{showGallery && (
{ setTemplate(k); const da = TEMPLATES[k] && TEMPLATES[k].defaultAccent; if (da) setDesign(d => ({ ...d, accent: da })); }}
onClose={() => setShowGallery(false)}
data={data} accent={accent} design={design} />
)}
{showATS && setShowATS(false)} setSection={setSection} />}
{showJobMatch && setShowJobMatch(false)} />}
{showTailor && { setEditorTab("coverletter"); setShowTailor(false); }} onClose={() => setShowTailor(false)} />}
{showInterviewPrep && setShowInterviewPrep(false)} />}
{showTranslate && setShowTranslate(false)} />}
{showSalary && setShowSalary(false)} />}
{showAchievement && setShowAchievement(false)} />}
{showColdEmail && setShowColdEmail(false)} />}
{showShare && (
setShowShare(false)}
onDownloadJSON={() => { exportJSON(); setShowShare(false); }}
onCopyLink={copyShareLink}
onCopyJSON={copyJSON}
onImport={(payload) => {
if (payload.data) setData(payload.data);
if (payload.docName) setDocName(payload.docName);
if (payload.template) setTemplate(payload.template);
if (payload.design) setDesign(payload.design);
if (payload.coverLetter) setCoverLetter(payload.coverLetter);
setShowShare(false); showToast("Résumé imported");
}}
/>
)}
{showKeyboard && setShowKeyboard(false)} />}
{showProExport && (
setShowProExport(false)}>
e.stopPropagation()}>
{proReason === "limit" ? "Free Downloads Used Up" : "Pro Access Needed to Export"}
{proReason === "limit"
? <>You've used all {FREE_DOWNLOAD_LIMIT} free downloads. Upgrade to Pro for unlimited PDF downloads and every premium template.>
: <>You're using {TEMPLATES[template]?.name} , a Pro template. The free plan includes the Prestige template with {FREE_DOWNLOAD_LIMIT} downloads — switch to it, or upgrade for every template.>}
Upgrade to Pro →
setShowProExport(false)}>Maybe later
)}
{showImport && {
const snapshot = data;
patch(() => imported);
setShowImport(false);
setSection("basics");
setEditorTab("resume");
showToast("CV imported — review and edit your details.", {
label: "Undo",
fn: () => { patch(() => snapshot); showToast("Import undone."); }
});
}}
onClose={() => setShowImport(false)} />}
{tabletPreview && (
setTabletPreview(false)} style={{ alignItems: "flex-start", padding: "52px 12px 12px" }}>
e.stopPropagation()} style={{ width: "100%", maxWidth: 640, background: "#EBEDF4", borderRadius: 14, padding: "48px 12px 16px", position: "relative" }}>
setTabletPreview(false)}
style={{ position: "absolute", top: 10, right: 10, background: "rgba(255,255,255,.7)" }}>
{ setTabletPreview(false); exportExact(); }}>
Exact PDF
{ setTabletPreview(false); exportPDF(); }}>
Quick ATS PDF
)}
{toast && (
{toast.msg}
{toast.action && (
{ toast.action.fn(); setToast(null); }}
style={{ marginLeft: 12, fontWeight: 700, color: "var(--brand-blue)", background: "none", border: "none", cursor: "pointer", fontSize: 13, padding: 0, textDecoration: "underline" }}>
{toast.action.label}
)}
)}
setTweak("layout", v)} />
setTweak("density", v)} />
{ setData(SAMPLE_RESUME); }} />
);
}
/* ── Share / Export modal ──────────────────────────────────── */
function ShareModal({ docName, onClose, onDownloadJSON, onCopyLink, onCopyJSON, onImport, copied }) {
const fileRef = useRef(null);
const handleImport = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = (ev) => {
try { onImport(JSON.parse(ev.target.result)); } catch { alert("Invalid résumé file."); }
};
reader.readAsText(f);
e.target.value = "";
};
return (
e.stopPropagation()}>
Share & Export
Export your résumé data or generate a shareable link.
Download JSON
Save all résumé data as a portable file. Import it back here any time.
fileRef.current && fileRef.current.click()}>
Import JSON
Load a previously exported résumé and replace the current one.
or share
{copied === "link" ? "Link copied!" : "Copy share link"}
Generates a URL with your résumé data embedded — share directly in a browser.
{copied === "json" ? "Copied!" : "Copy data to clipboard"}
Copy the raw JSON — paste it into another import dialog.
The share link embeds your full résumé data in the URL. Only share with people you trust.
);
}
/* ── Keyboard shortcuts modal ──────────────────────────────── */
function KeyboardShortcutsModal({ onClose }) {
const shortcuts = [
{ keys: ["Ctrl", "Z"], label: "Undo" },
{ keys: ["Ctrl", "Y"], label: "Redo" },
{ keys: ["Ctrl", "Shift", "Z"], label: "Redo (alternate)" },
{ keys: ["Ctrl", "S"], label: "Save to local storage" },
];
return (
e.stopPropagation()}>
{shortcuts.map((s, i) => (
{s.label}
{s.keys.map((k, j) => {k} )}
))}
Tip: Mac users, use ⌘ instead of Ctrl .
);
}
/* ── Add section menu ──────────────────────────────────────── */
function AddSectionMenu({ sectionOrder, setSectionOrder, setSection, setEditorTab, setData }) {
const [open, setOpen] = React.useState(false);
const addTyped = (id) => {
if (!sectionOrder.includes(id)) setSectionOrder(s => [...s, id]);
setEditorTab("resume"); setSection(id); setOpen(false);
};
const addCustom = () => {
const id = uidA();
setData(d => ({ ...d, custom: [...(d.custom || []), { id, heading: "Custom Section", body: "" }] }));
setEditorTab("resume"); setSection("custom:" + id); setOpen(false);
};
return (
{open &&
setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 49 }} />}
setOpen(o => !o)}>
Add section
{open && (
{ADDABLE_SECTIONS.map(s => {
const already = sectionOrder.includes(s.id);
return (
!already && addTyped(s.id)} disabled={already}>
{s.label}{already && ✓ }
{s.desc}
);
})}
Custom section
Blank section with rich text
)}
);
}
/* ── Template gallery ──────────────────────────────────────── */
function Gallery({ template, onPick, onClose, data, accent, design, plan }) {
const [filter, setFilter] = React.useState("all");
const [previewing, setPreviewing] = React.useState(template);
const [previewScale, setPreviewScale] = React.useState(0.85);
const canvasRef = React.useRef(null);
React.useLayoutEffect(() => {
if (!canvasRef.current) return;
const obs = new ResizeObserver(entries => {
const w = entries[0].contentRect.width - 48;
setPreviewScale(Math.min(1, w / 816));
});
obs.observe(canvasRef.current);
return () => obs.disconnect();
}, []);
React.useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
const allKeys = Object.keys(TEMPLATES);
const freeKeys = allKeys.filter(k => FREE_TEMPLATES.includes(k));
const proKeys = allKeys.filter(k => !FREE_TEMPLATES.includes(k));
const orderedAll = [...freeKeys, ...proKeys]; // free template(s) shown first
const visible = filter === "free" ? freeKeys : filter === "pro" ? proKeys : orderedAll;
const prevTpl = TEMPLATES[previewing];
const isProPrev = !FREE_TEMPLATES.includes(previewing);
return (
e.stopPropagation()}>
{/* ── Left panel: thumbnail list ── */}
Templates
{/* Filter tabs */}
{[["all","All",allKeys.length],["free","Free",freeKeys.length],["pro","Pro",proKeys.length]].map(([id,label,count]) => (
setFilter(id)}>
{label} {count}
))}
{/* Thumbnail grid */}
{visible.map(key => {
const isProTpl = !FREE_TEMPLATES.includes(key);
const isSel = template === key;
const isPrev = previewing === key;
return (
setPreviewing(key)}
onClick={() => { onPick(key); onClose(); }}>
{isSel &&
}
{isProTpl &&
Pro
}
{TEMPLATES[key].name}
);
})}
{/* Bottom CTA */}
{ onPick(previewing); onClose(); }}>
{template === previewing ? "Continue Editing" : "Use This Template →"}
{/* ── Right panel: full live preview ── */}
{prevTpl?.name}
{TEMPLATES[previewing]?.badge && {TEMPLATES[previewing].badge} }
{isProPrev && Pro }
ATS Ready
);
}
/* ── ATS report drawer ─────────────────────────────────────── */
const ATS_FIX_SECTIONS = {
"Add missing contact details": "basics",
"Parseable email address": "basics",
"Tighten or add a summary": "summary",
"Quantify more achievements": "experience",
"Start bullets with action verbs": "experience",
"Add more relevant keywords": "skills",
};
function ATSDrawer({ ats, onClose, setSection }) {
return (
<>
ATS report
How well a parser & recruiter will read this résumé.
{ats.score} / 100
{scoreLabel(ats.score)}
Fix the flagged items below to climb toward a 90+ score.
Category breakdown
{ats.categories.map(c => (
{c.key} {Math.round(c.score * 100)}%
))}
Checklist
{ats.checks.map((c, i) => {
const fixSection = c.state !== "ok" ? ATS_FIX_SECTIONS[c.title] : null;
return (
{c.title}
{fixSection && (
{ onClose(); setSection(fixSection); }}
style={{ fontSize: 11, fontWeight: 600, color: "var(--brand-blue)", background: "none", border: "none", cursor: "pointer", padding: "0 4px", flexShrink: 0 }}>
Fix it →
)}
{c.desc}
);
})}
>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(
);