/* 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 (
{children}
); } /* 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}
); })}
); 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 (
Free AI credits
{out ? "0 left" : remaining + " of " + LIMIT + " left"} {used}/{LIMIT} used
{out ? "You've used all your free AI generations." : "Each AI generation uses one credit."}
{out ? "Upgrade to Pro →" : "Go unlimited with Pro →"}
); } 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 */}
setDocName(e.target.value)} onFocus={e => e.target.select()} spellCheck={false} aria-label="Document name" />
{/* Right group */}
{showDlRemaining && ( {downloadsLeft === 0 ? "No free downloads left" : downloadsLeft + " free left"} )}
{userMenu && ( <>
setUserMenu(false)} style={{ position: "fixed", inset: 0, zIndex: 99 }} />
My account
)}
{/* ── BODY (3-column) ─────────────────────────────── */}
{/* ── LEFT PANEL ──────────────────────────────── */}
{/* Builder / Templates tabs */}
{/* Sample banner */} {sampleBanner && (
👋 Sample data — edit any field to make it yours.
)} {editorTab === "coverletter" ? ( /* ── Cover letter editor ── */
) : ( /* ── Section accordion ── */
{/* Fixed sections: Personal details + Summary */} {NAV_FIXED.map(n => (
{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 (
{section === id && (
)}
); })} {/* Custom sections */} {(data.custom || []).map(c => { const cid = "custom:" + c.id; return (
{section === cid && (
)}
); })} {/* Add section */}
)} {/* ── Tools ── */} {(() => { const [toolsOpen, setToolsOpen] = React.useState(true); return (
{toolsOpen && (
)}
); })()}
{/* Left panel resize handle */}
{/* ── PREVIEW (center) ────────────────────────── */}
{editorTab === "coverletter" ? "Cover Letter preview" : (TEMPLATES[template]?.name + " template")}
{editorTab !== "coverletter" && ( )} {editorTab !== "coverletter" && (
setZoom(1)}>{pct}%
)}
{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]) => (
{label} {heat}
))}
)}
)}
{/* Right panel resize handle */}
{/* ── RIGHT PANEL ─────────────────────────────── */}
{/* ATS Score card */}
ATS Score {scoreLabel(ats.score)}
{ats.score}/100
ATS Readiness
{/* Template card */}
Template
{TEMPLATES[template]?.name || template}
{/* Design card */}
Accent color
{Object.entries(ACCENTS).map(([name, hex]) => (
Font
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(""); } }} />
) : ( )} {versions.length > 0 && (
{versions.slice(0, 5).map(v => (
{v.name}
{new Date(v.savedAt).toLocaleDateString()}
))} {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.
{/* 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 →
)} {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" }}>
)} {toast && (
{toast.msg} {toast.action && ( )}
)} 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()}>

Keyboard shortcuts

{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 }} />} {open && (
{ADDABLE_SECTIONS.map(s => { const already = sectionOrder.includes(s.id); return ( ); })}
)}
); } /* ── 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]) => ( ))}
{/* 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 */}
{/* ── 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 && ( )}

{c.desc}

); })}
); } ReactDOM.createRoot(document.getElementById("root")).render();