/* career.jsx * * Career Planner — browse, compare, view-only detail. * * Three sub-views inside a single CareerPlanner component: * - 'catalog' : grid of all 10 career cards * - 'detail' : single career deep-dive (defaultCertId, salary, pros, cons, roles) * - 'compare' : 2-4 careers side-by-side * * Enrollment is deferred to Day 2 — this file is browse/compare only. * Careers whose default cert list contains a cert not in window.EXAMS are * locked (still viewable, no enroll button, "Tell us this matters" CTA). * * No external cert references, no competitor vendor links. * * Sets up window.CareerPlanner for app.jsx to mount as view='career'. */ (function () { "use strict"; // ─── Helpers ───────────────────────────────────────────────────────────── function fmt$(n) { return n.toLocaleString("en-US", {style:"currency", currency:"USD", maximumFractionDigits:0}); } function fmtSalary(n) { return "$" + Math.round(n/1000) + "k"; } function fmtRange(arr) { return fmtSalary(arr[0]) + " – " + fmtSalary(arr[1]); } function fmtHours(h) { return h + " hrs"; } function getCertMeta(certId) { return (window.CERT_META || {})[certId] || null; } function getCertPrice(certId) { const meta = getCertMeta(certId); if (!meta || !meta.price) return null; const m = meta.price.match(/\$([\d,]+)/); return m ? parseInt(m[1].replace(/,/g, ""), 10) : null; } function getCertLabel(certId) { const ex = (window.EXAMS || []).find(e => e.id === certId); if (ex) return ex.label; const meta = getCertMeta(certId); if (meta) return meta.label || meta.examCode || certId; return certId.toUpperCase(); } function isCertAvailable(certId) { return !!(window.EXAMS || []).find(e => e.id === certId); } function totalCost(certIds) { return certIds.reduce((sum, cid) => sum + (getCertPrice(cid) || 0), 0); } function totalHours(certIds) { const h = window.CERT_STUDY_HOURS || {}; return certIds.reduce((sum, cid) => sum + (h[cid] || 0), 0); } // A career unlocks once the first 2 certs in its RECOMMENDED LEARNING ORDER // (all certs, by `order` — A+1, A+2, Net+, Sec+, …, not just the `required` // ones) are built in CyberStudy. This lets users start the foundational // chunk now and trust later certs land before they reach them (a typical // user takes 6-12 months to clear the first few certs — that window is used // to build the rest of the path). The onboarding modal warns that later // certs are still in development so they enroll with eyes open. // // Edge case: paths with fewer than 2 certs unlock when ALL of them exist. const MIN_AVAILABLE_TO_UNLOCK = 2; function careerLockedCerts(career) { // All required certs in path order, those that aren't yet in EXAMS return career.defaultCerts .filter(d => d.required) .sort((a, b) => (a.order || 0) - (b.order || 0)) .map(d => d.certId) .filter(cid => !isCertAvailable(cid)); } // Returns the count of consecutive certs from the start of the recommended // learning order (ALL certs, not just required) that exist in EXAMS. Used by // isCareerLocked to gate enrollment. function careerLeadingAvailableCount(career) { const certs = (career.defaultCerts || []) .slice() .sort((a, b) => (a.order || 0) - (b.order || 0)); let streak = 0; for (const d of certs) { if (!isCertAvailable(d.certId)) break; streak++; } return streak; } function isCareerLocked(career) { const total = (career.defaultCerts || []).length; if (total === 0) return false; // Unlock once the first 2 certs (or all of them, for shorter paths) in // recommended order are available. return careerLeadingAvailableCount(career) < Math.min(MIN_AVAILABLE_TO_UNLOCK, total); } // Hover tooltip for a cert chip that isn't yet built in CyberStudy. // Single message — every "not yet" cert is the same kind of "not planned // yet, request it." The badge itself becomes an actionable "Request" CTA. function getMissingCertTitle(certId) { const label = getCertLabel(certId); return label + " — not planned yet. Click to request."; } // ─── Salary disclaimer (rendered as small text under salary blocks) ──── function SalaryDisclaimer() { return React.createElement( "p", { className: "cp-salary-disclaimer" }, window.CAREER_SALARY_DISCLAIMER || "" ); } // ─── Catalog: grid of all 10 career cards ──────────────────────────────── function CareerCatalog({ onSelect, selectedForCompare, onToggleCompare, onCompareGo, onRequestCareer, onTakeQuiz, hasQuizResult, onBackToHome }) { const careers = window.CAREER_PATHS || []; const compareCount = selectedForCompare.size; const compareReady = compareCount >= 2; const enrollment = window.UserData ? window.UserData.getCareerEnrollment() : null; return React.createElement( "div", { className: "cp-catalog" }, React.createElement("div", { className: "cp-catalog-head" }, onBackToHome && React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBackToHome, }, "← Home"), React.createElement("h1", { className: "cp-h1" }, "Career Planner"), React.createElement("p", { className: "cp-sub" }, "Compare IT career paths. See what each pays, what certs you need, and what the day-to-day actually looks like. Pick 2–4 to compare side-by-side." ) ), React.createElement("div", { className: "cp-disclaimer-block" }, React.createElement("p", { className: "cp-salary-disclaimer" }, window.CAREER_SALARY_DISCLAIMER || "") ), // Quiz banner — full-width prompt for indecisive users. // Different copy if they've already taken it (show "see prior result"). React.createElement("div", { className: "cp-quiz-banner", onClick: onTakeQuiz, role: "button", tabIndex: 0, onKeyDown: (ev) => { if (ev.key === "Enter" || ev.key === " ") onTakeQuiz(); }, }, React.createElement("div", { className: "cp-quiz-banner-icon" }, "🧭"), React.createElement("div", { className: "cp-quiz-banner-text" }, React.createElement("div", { className: "cp-quiz-banner-title" }, hasQuizResult ? "Want to retake the career-fit quiz?" : "Can't decide? Take the career-fit quiz." ), React.createElement("div", { className: "cp-quiz-banner-sub" }, hasQuizResult ? "Tap to see your last result or run it again." : "10 questions, ~2 minutes. We'll suggest the 3 paths that fit you best." ) ), React.createElement("div", { className: "cp-quiz-banner-arrow" }, "→") ), // Sticky compare bar (visible once user picks 1+) compareCount > 0 && React.createElement( "div", { className: "cp-compare-bar" }, React.createElement("span", { className: "cp-compare-bar-count" }, compareCount + " career" + (compareCount === 1 ? "" : "s") + " selected" ), React.createElement("span", { className: "cp-compare-bar-hint" }, compareCount < 2 ? "Pick at least one more to compare." : compareCount > 4 ? "Pick at most 4 careers to compare." : "Ready to compare." ), React.createElement("button", { className: "btn btn-primary btn-sm", disabled: !compareReady || compareCount > 4, onClick: onCompareGo, }, "Compare " + compareCount + " →") ), React.createElement( "div", { className: "cp-grid-scroll" }, React.createElement( "div", { className: "cp-grid" }, careers.map(c => { const locked = isCareerLocked(c); const certIds = c.defaultCerts.map(d => d.certId); const cost = totalCost(certIds); const hours = totalHours(certIds); const salaryLow = Math.min.apply(null, c.roles.map(r => r.salaryRange[0])); const salaryHigh = Math.max.apply(null, c.roles.map(r => r.salaryRange[1])); const inCompare = selectedForCompare.has(c.id); const isEnrolled = enrollment && enrollment.careerId === c.id; return React.createElement( "div", { key: c.id, className: "cp-card " + (locked ? "locked" : "") + " " + (inCompare ? "in-compare" : "") + " " + (isEnrolled ? "is-enrolled" : ""), role: "button", tabIndex: 0, onClick: () => onSelect(c.id), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(c.id); } }, }, React.createElement("div", { className: "cp-card-head" }, React.createElement("span", { className: "cp-card-icon" }, c.icon), React.createElement("span", { className: "cp-card-title" }, c.name), locked && React.createElement("span", { className: "cp-card-lock", title: "Some required certs aren't available in CyberStudy yet" }, "🔒"), isEnrolled && React.createElement("span", { className: "cp-card-enrolled-badge" }, "✓ Your path") ), React.createElement("p", { className: "cp-card-tagline" }, c.tagline), React.createElement("div", { className: "cp-card-stats" }, React.createElement("div", { className: "cp-stat" }, React.createElement("span", { className: "cp-stat-label" }, "Salary range"), React.createElement("span", { className: "cp-stat-val" }, fmtSalary(salaryLow) + " – " + fmtSalary(salaryHigh)) ), React.createElement("div", { className: "cp-stat" }, React.createElement("span", { className: "cp-stat-label" }, "Cost of certs"), React.createElement("span", { className: "cp-stat-val" }, cost > 0 ? fmt$(cost) : "—") ), React.createElement("div", { className: "cp-stat" }, React.createElement("span", { className: "cp-stat-label" }, "Study time"), React.createElement("span", { className: "cp-stat-val" }, hours > 0 ? fmtHours(hours) : "—") ) ), React.createElement("div", { className: "cp-card-certs" }, React.createElement("span", { className: "cp-certs-label" }, "Default path:"), certIds.map((cid, i) => React.createElement( "span", { key: cid, className: "cp-cert-chip " + (isCertAvailable(cid) ? "" : "missing"), title: isCertAvailable(cid) ? getCertLabel(cid) : getMissingCertTitle(cid), }, getCertLabel(cid) )) ), React.createElement("div", { className: "cp-card-actions" }, React.createElement("span", { className: "cp-card-hint" }, "Click to open →"), React.createElement("button", { className: "btn btn-ghost btn-sm cp-compare-toggle " + (inCompare ? "active" : ""), onClick: (e) => { e.stopPropagation(); onToggleCompare(c.id); }, }, inCompare ? "✓ In compare" : "+ Compare") ) ); }) ) ), React.createElement("div", { className: "cp-catalog-footer" }, React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: onRequestCareer, }, "+ Request another career path") ) ); } // ─── Detail: single-career deep dive ───────────────────────────────────── function CareerDetail({ career, onBack, onTellUs, onStartPath, onViewPlan, onRequestCert, requestedCerts }) { // Subscribe to owned-certs + enrollment changes so badges and start-banner // stay in sync. (Owned-certs can change via /profile while this page is // open, e.g. user adds a cert in another tab.) const [bump, setBump] = React.useState(0); React.useEffect(() => { const refresh = () => setBump(n => n + 1); window.addEventListener(window.UserData?.EVT_OWNED || "cyberstudy-owned-certs-change", refresh); window.addEventListener(window.UserData?.EVT_ENROLL || "cyberstudy-enrollment-change", refresh); window.addEventListener(window.UserData?.EVT_HYDRATE || "ccna-user-data-hydrated", refresh); return () => { window.removeEventListener(window.UserData?.EVT_OWNED || "cyberstudy-owned-certs-change", refresh); window.removeEventListener(window.UserData?.EVT_ENROLL || "cyberstudy-enrollment-change", refresh); window.removeEventListener(window.UserData?.EVT_HYDRATE || "ccna-user-data-hydrated", refresh); }; }, []); const certIds = career.defaultCerts.map(d => d.certId); const cost = totalCost(certIds); const hours = totalHours(certIds); const locked = isCareerLocked(career); const missingCerts = careerLockedCerts(career); // Read current enrollment so we can show "View your plan" vs "Start this path" const enrollment = window.UserData ? window.UserData.getCareerEnrollment() : null; const isEnrolledHere = enrollment && enrollment.careerId === career.id; // Owned cert lookup — used by both the cert-sequence badges and the // top-of-page progress summary. const ownedForCert = (cid) => window.UserData?.findOwnedCertByCertId(cid) || null; const ownedInPath = certIds.filter(cid => ownedForCert(cid)); // Representative salary: prefer mid-tier, then senior, then entry. // Picks one role to anchor the headline number — full ladder is shown // in the "Roles & salary ladder" section below. const repRole = career.roles.find(r => r.tier === "mid") || career.roles.find(r => r.tier === "senior") || career.roles.find(r => r.tier === "entry") || null; const repSalary = repRole ? repRole.medianSalary : null; const repTierLabel = repRole ? (repRole.tier === "mid" ? "mid-career median" : repRole.tier === "senior" ? "senior median" : "entry-level median") : ""; return React.createElement( "div", { className: "cp-detail" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack, }, "← Back to all careers"), React.createElement("div", { className: "cp-detail-head" }, React.createElement("span", { className: "cp-detail-icon" }, career.icon), React.createElement("div", null, React.createElement("h1", { className: "cp-h1" }, career.name), React.createElement("p", { className: "cp-sub" }, career.tagline) ) ), // Locked careers: show the cert-priority feedback CTA. // Unlocked careers: show "Start this career path" (or "View your plan" // if already enrolled here, or "Switch to this path" if enrolled elsewhere). locked && React.createElement( "div", { className: "cp-lock-banner" }, React.createElement("div", { className: "cp-lock-banner-title" }, "🔒 Enrollment locked" ), React.createElement("p", { className: "cp-lock-banner-body" }, "This career path requires ", React.createElement("strong", null, missingCerts.map(getCertLabel).join(" and ")), ", which ", missingCerts.length === 1 ? "isn't" : "aren't", " available in CyberStudy yet. You can still view the path details and explore cert costs, but enrollment will unlock when ", missingCerts.length === 1 ? "that cert" : "those certs", " ", missingCerts.length === 1 ? "is" : "are", " ready." ), React.createElement("button", { className: "btn btn-primary btn-sm", onClick: () => onTellUs(career, missingCerts), }, "Tell us this matters →") ), !locked && React.createElement( "div", { className: "cp-start-banner " + (isEnrolledHere ? "enrolled" : enrollment ? "switching" : "fresh") }, isEnrolledHere ? React.createElement(React.Fragment, null, React.createElement("div", { className: "cp-start-banner-title" }, "✓ You're on this path"), React.createElement("p", { className: "cp-start-banner-body" }, "Track your progress and customize the cert sequence in your plan view."), React.createElement("button", { className: "btn btn-primary", onClick: () => onViewPlan(career), }, "View your plan →") ) : enrollment ? React.createElement(React.Fragment, null, React.createElement("div", { className: "cp-start-banner-title" }, "Switch to this path?"), React.createElement("p", { className: "cp-start-banner-body" }, "You're currently on ", React.createElement("strong", null, (window.CAREER_PATHS || []).find(c => c.id === enrollment.careerId)?.name || enrollment.careerId), ". Switching keeps your other plan saved for later."), React.createElement("button", { className: "btn btn-primary", onClick: () => onStartPath(career), }, "Start this career path →") ) : React.createElement(React.Fragment, null, React.createElement("div", { className: "cp-start-banner-title" }, "Ready to start?"), React.createElement("p", { className: "cp-start-banner-body" }, "Enroll and we'll build you a personalized plan — track progress on each cert, customize the sequence, and see what you've already earned."), React.createElement("button", { className: "btn btn-primary", onClick: () => onStartPath(career), }, "Start this career path →") ) ), // Stats block — 4 boxes: Median salary, Total cost, Study time, Difficulty React.createElement("div", { className: "cp-detail-stats" }, React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Median salary"), React.createElement("span", { className: "cp-stat-big-val" }, repSalary ? fmtSalary(repSalary) : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, repTierLabel) ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Total cost"), React.createElement("span", { className: "cp-stat-big-val" }, cost > 0 ? fmt$(cost) : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, "voucher prices, all required certs") ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Study time"), React.createElement("span", { className: "cp-stat-big-val" }, hours > 0 ? hours + " hrs" : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, "across default path") ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Difficulty"), React.createElement("span", { className: "cp-stat-big-val" }, career.difficulty.charAt(0).toUpperCase() + career.difficulty.slice(1)), React.createElement("span", { className: "cp-stat-big-sub" }, "") ) ), // Day to day React.createElement("section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "What you'll actually do"), React.createElement("p", { className: "cp-paragraph" }, career.dayToDay) ), // Default cert sequence React.createElement("section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "Default certification sequence"), ownedInPath.length > 0 && React.createElement("p", { className: "cp-have-summary" }, "✓ You already have ", React.createElement("strong", null, ownedInPath.length, " of ", certIds.length), " certs in this path", ownedInPath.length === certIds.length ? " — nice!" : "." ), React.createElement("ol", { className: "cp-cert-seq" }, career.defaultCerts.map((d, i) => { const meta = getCertMeta(d.certId); const available = isCertAvailable(d.certId); const owned = ownedForCert(d.certId); return React.createElement("li", { key: d.certId, className: "cp-cert-step " + (available ? "" : "missing") + " " + (d.required ? "required" : "optional") + " " + (owned ? "have" : ""), }, React.createElement("span", { className: "cp-cert-step-num" }, owned ? "✓" : i + 1), React.createElement("div", { className: "cp-cert-step-body" }, React.createElement("div", { className: "cp-cert-step-title" }, getCertLabel(d.certId), owned && React.createElement("span", { className: "cp-tag-have" }, owned.verified ? "✓ Verified" : "✓ Already have" ), !owned && !d.required && React.createElement("span", { className: "cp-tag-optional" }, "optional"), // Not yet built in CyberStudy — show actionable "Request" // button. Once requested in this session, swap to a static // "✓ Requested" indicator. Single unified treatment for // every not-yet cert; users signal demand to drive build // priority. !available && !owned && ( requestedCerts && requestedCerts.has(d.certId) ? React.createElement("span", { className: "cp-tag-requested" }, "✓ Requested") : React.createElement("button", { className: "cp-tag-request", onClick: (e) => { e.stopPropagation(); onRequestCert && onRequestCert(d.certId); }, title: "Tell us to prioritize building this cert", }, "Not planned · Request now") ) ), meta && React.createElement("div", { className: "cp-cert-step-meta" }, meta.examCode + (meta.price ? " · " + meta.price : "") ), meta && meta.blurb && React.createElement("p", { className: "cp-cert-step-blurb" }, meta.blurb) ) ); }) ) ), // Pros / Cons (two columns) React.createElement("section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "Pros & cons"), React.createElement("div", { className: "cp-procon" }, React.createElement("div", { className: "cp-pros" }, React.createElement("div", { className: "cp-procon-label" }, "✓ Pros"), React.createElement("ul", null, career.pros.map((p, i) => React.createElement("li", { key: i }, p)) ) ), React.createElement("div", { className: "cp-cons" }, React.createElement("div", { className: "cp-procon-label" }, "✗ Cons"), React.createElement("ul", null, career.cons.map((p, i) => React.createElement("li", { key: i }, p)) ) ) ) ), // Love/Hate React.createElement("section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "Is this the right fit?"), React.createElement("div", { className: "cp-fit" }, React.createElement("div", { className: "cp-fit-block cp-fit-love" }, React.createElement("div", { className: "cp-fit-label" }, "You'll love this if..."), React.createElement("ul", null, career.loveIf.map((p, i) => React.createElement("li", { key: i }, p)) ) ), React.createElement("div", { className: "cp-fit-block cp-fit-hate" }, React.createElement("div", { className: "cp-fit-label" }, "You'll hate this if..."), React.createElement("ul", null, career.hateIf.map((p, i) => React.createElement("li", { key: i }, p)) ) ) ) ), // Roles & salary ladder React.createElement("section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "Roles & salary ladder"), React.createElement("div", { className: "cp-roles" }, career.roles.map(r => React.createElement( "div", { key: r.id, className: "cp-role cp-role-" + r.tier }, React.createElement("div", { className: "cp-role-head" }, React.createElement("span", { className: "cp-role-tier" }, r.tier), React.createElement("span", { className: "cp-role-title" }, r.title) ), React.createElement("p", { className: "cp-role-summary" }, r.summary), React.createElement("div", { className: "cp-role-salary" }, React.createElement("span", { className: "cp-role-salary-median" }, fmtSalary(r.medianSalary) + " median"), React.createElement("span", { className: "cp-role-salary-range" }, "(" + fmtRange(r.salaryRange) + ")") ), React.createElement("div", { className: "cp-role-certs" }, React.createElement("span", { className: "cp-role-certs-label" }, "Required:"), r.requiredCerts.map(cid => React.createElement( "span", { key: cid, className: "cp-cert-chip" }, getCertLabel(cid) )), r.preferredCerts.length > 0 && React.createElement("span", { className: "cp-role-certs-label preferred-label" }, "Recommended:"), r.preferredCerts.map(cid => React.createElement( "span", { key: cid, className: "cp-cert-chip preferred" }, getCertLabel(cid) )) ), React.createElement("div", { className: "cp-role-source" }, "Source: " + r.salarySource) )) ), React.createElement(SalaryDisclaimer) ), // Related careers career.relatedCareers && career.relatedCareers.length > 0 && React.createElement( "section", { className: "cp-section" }, React.createElement("h2", { className: "cp-h2" }, "You might also like"), React.createElement("div", { className: "cp-related" }, career.relatedCareers.map(relId => { const rel = (window.CAREER_PATHS || []).find(c => c.id === relId); if (!rel) return null; return React.createElement("button", { key: relId, className: "cp-related-card", onClick: () => { window.__cpNavigate && window.__cpNavigate(relId); }, }, React.createElement("span", { className: "cp-related-icon" }, rel.icon), React.createElement("span", { className: "cp-related-name" }, rel.name) ); }) ) ) ); } // ─── Onboarding modal: "Got any of these certs already?" ──────────────── // Fires the first time a user enrolls in a path. Surfaces the path's // cert sequence with checkboxes — checking one creates a self-attested // owned-cert entry (no PDF, no verification) so the plan view marks it // as "Already have." Skippable; user can manage certs on /profile later. function OnboardingModal({ career, onClose, onDone }) { const certIds = career.defaultCerts.map(d => d.certId); // Filter to certs the user doesn't already have recorded const alreadyOwned = new Set( (window.UserData ? window.UserData.getOwnedCerts() : []) .map(o => o.certId) ); const toShow = certIds.filter(cid => !alreadyOwned.has(cid)); // Surface certs in the path that aren't yet built in CyberStudy so we // can be upfront in the onboarding modal. Single bucket — all "not yet" // certs are treated the same; the user signals demand via the Request // buttons elsewhere on the plan view. const cm = window.CERT_META || {}; const allRequired = career.defaultCerts.filter(d => d.required).map(d => d.certId); const notPlannedYet = allRequired.filter(cid => { const m = cm[cid]; return m && m.comingSoon; }); const [picked, setPicked] = React.useState(() => new Set()); const [saving, setSaving] = React.useState(false); // If user already has every cert on the path recorded, skip the modal React.useEffect(() => { if (toShow.length === 0) { onDone(); } }, []); if (toShow.length === 0) return null; function toggle(cid) { setPicked(prev => { const next = new Set(prev); if (next.has(cid)) next.delete(cid); else next.add(cid); return next; }); } function handleSave() { setSaving(true); try { const cm = window.CERT_META || {}; picked.forEach(cid => { window.UserData.saveOwnedCert({ certId: cid, issuer: cm[cid]?.vendor || null, earnedDate: null, expirationDate: null, verificationId: null, }); }); } catch (_) { /* best-effort */ } onDone(); } return React.createElement("div", { className: "pf-modal-backdrop", onClick: onClose }, React.createElement("div", { className: "pf-modal", onClick: ev => ev.stopPropagation() }, React.createElement("div", { className: "pf-modal-head" }, React.createElement("h2", { className: "pf-modal-title" }, "Got any of these already?"), React.createElement("button", { className: "pf-modal-close", onClick: onClose, "aria-label": "Close", }, "×") ), React.createElement("div", { className: "pf-modal-body" }, // Roadmap notice — only if the path includes certs that aren't yet // built in CyberStudy. Single message: we plan to build them based // on demand. Users can hit "Request" on individual cert badges to // signal priority. No distinction between "ours to build" vs "third // party" — every "not yet" is requestable. notPlannedYet.length > 0 && React.createElement("div", { className: "cp-onboard-notice" }, React.createElement("div", { className: "cp-onboard-notice-title" }, "About your path" ), React.createElement("p", { className: "cp-onboard-notice-line" }, React.createElement("strong", null, "Not planned yet: "), notPlannedYet.map(cid => (cm[cid]?.label || cid)).join(", "), ". CyberStudy hasn't built study material for these yet. We'll prioritize them based on what users request — once you're enrolled, hit ", React.createElement("em", null, "Request"), " on any cert on your plan to signal demand. Typical users take 6-12 months to finish the first 2 certs, which gives us a window to build the rest." ) ), React.createElement("p", { className: "pf-help", style: { marginBottom: 14, fontSize: 13 } }, "Check any certs from this path you've already earned. We'll skip those on your plan. ", "You can add details (issuer, dates, PDF for verification) later from your profile." ), React.createElement("div", { className: "cp-onboard-list" }, toShow.map(cid => { const meta = (window.CERT_META || {})[cid] || {}; const isPicked = picked.has(cid); return React.createElement("label", { key: cid, className: "cp-onboard-row " + (isPicked ? "picked" : ""), }, React.createElement("input", { type: "checkbox", checked: isPicked, onChange: () => toggle(cid), }), React.createElement("div", { className: "cp-onboard-row-text" }, React.createElement("div", { className: "cp-onboard-row-label" }, meta.label || cid), meta.examCode && React.createElement("div", { className: "cp-onboard-row-sub" }, meta.examCode) ) ); }) ) ), React.createElement("div", { className: "pf-modal-foot" }, React.createElement("button", { className: "btn btn-ghost", onClick: onDone, disabled: saving, }, "Skip"), React.createElement("button", { className: "btn btn-primary", onClick: handleSave, disabled: saving, }, picked.size > 0 ? `Save ${picked.size} & continue →` : "Continue →") ) ) ); } // ─── AddCertModal — picker for adding a cert to the current plan ──────── // Lists all studyable certs in window.EXAMS that aren't already on the plan. // Also includes stub certs (comingSoon) so users can express interest by // adding them to their plan — the plan view's launchpad simply skips them // until they're built. function AddCertModal({ career, currentCertIds, onClose, onPick }) { const onPlan = new Set(currentCertIds); const allCertIds = [ ...((window.EXAMS || []).map(e => e.id)), ...["linuxplus", "cloudplus", "cysaplus", "pentestplus", "caspplus"] .filter(id => (window.CERT_META || {})[id]), ]; const candidates = allCertIds.filter(id => !onPlan.has(id)); return React.createElement("div", { className: "pf-modal-backdrop", onClick: onClose }, React.createElement("div", { className: "pf-modal", onClick: ev => ev.stopPropagation() }, React.createElement("div", { className: "pf-modal-head" }, React.createElement("h2", { className: "pf-modal-title" }, "Add a cert to your plan"), React.createElement("button", { className: "pf-modal-close", onClick: onClose, "aria-label": "Close", }, "×") ), React.createElement("div", { className: "pf-modal-body" }, candidates.length === 0 ? React.createElement("p", { className: "pf-help" }, "Every cert we know about is already on your plan. Use the cert request flow on the cert chooser if there's something missing.") : React.createElement("div", { className: "cp-add-list" }, candidates.map(cid => { const meta = (window.CERT_META || {})[cid] || {}; const hours = (window.CERT_STUDY_HOURS || {})[cid] || 0; return React.createElement("button", { key: cid, className: "cp-add-row", onClick: () => onPick(cid), }, React.createElement("div", { className: "cp-add-row-main" }, React.createElement("div", { className: "cp-add-row-label" }, meta.label || cid, // Inside the cert-picker modal, the user is already // choosing to add this cert to their plan — they're // informing themselves, not signaling demand. A // passive "Not planned yet" tag is enough here. // The actionable "Request" CTA lives on the plan // view itself and the career detail page. meta.comingSoon && React.createElement("span", { className: "cp-plan-row-coming" }, "Not planned yet") ), React.createElement("div", { className: "cp-add-row-sub" }, meta.examCode || "", meta.price ? " · " + meta.price : "", hours ? " · " + hours + " hrs" : "" ) ), React.createElement("span", { className: "cp-add-row-arrow" }, "+") ); }) ) ) ) ); } // ─── ImpactPreviewModal — confirm before applying a plan edit ────────── // Shows cost / time deltas + new totals so the user knows what they're // changing. Required-cert removal adds a red warning + double confirm. function ImpactPreviewModal({ edit, career, onConfirm, onCancel }) { const { action, label, deltas, warnRequiredRemoval } = edit; const sign = (n, dollars) => { if (n === 0) return dollars ? "$0" : "0 hrs"; const prefix = n > 0 ? "+" : "−"; const abs = Math.abs(n); return prefix + (dollars ? fmt$(abs) : abs + " hrs"); }; const title = action === "add" ? "Add " + label + " to plan?" : "Remove " + label + " from plan?"; return React.createElement("div", { className: "pf-modal-backdrop", onClick: onCancel }, React.createElement("div", { className: "pf-modal", onClick: ev => ev.stopPropagation() }, React.createElement("div", { className: "pf-modal-head" }, React.createElement("h2", { className: "pf-modal-title" }, title), React.createElement("button", { className: "pf-modal-close", onClick: onCancel, "aria-label": "Close", }, "×") ), React.createElement("div", { className: "pf-modal-body" }, warnRequiredRemoval && React.createElement("div", { className: "cp-impact-warn" }, React.createElement("strong", null, "⚠ This cert is marked required for the default " + career.name + " path."), React.createElement("br"), React.createElement("span", null, "Removing it makes your plan diverge from the recommended sequence. You can always add it back later.") ), React.createElement("div", { className: "cp-impact-grid" }, React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "Cost delta"), React.createElement("span", { className: "cp-impact-delta " + (deltas.cost > 0 ? "up" : deltas.cost < 0 ? "down" : ""), }, sign(deltas.cost, true)) ), React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "Time delta"), React.createElement("span", { className: "cp-impact-delta " + (deltas.hours > 0 ? "up" : deltas.hours < 0 ? "down" : ""), }, sign(deltas.hours, false)) ), // Salary tier change. "down" delta on add is impossible UNLESS we're // capped by ceiling. Inverse color logic vs cost: higher salary = green, lower = red. deltas.anchorDelta !== 0 && React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "Salary ceiling"), React.createElement("span", { className: "cp-impact-delta " + (deltas.anchorDelta > 0 ? "down" : "up"), }, sign(deltas.anchorDelta, true)) ), React.createElement("div", { className: "cp-impact-sep" }), React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "New total cost"), React.createElement("span", { className: "cp-impact-total" }, fmt$(deltas.newCost)) ), React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "New total time"), React.createElement("span", { className: "cp-impact-total" }, deltas.newHours + " hrs") ), React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "Path length"), React.createElement("span", { className: "cp-impact-total" }, deltas.newCount + " cert" + (deltas.newCount === 1 ? "" : "s")) ), deltas.newAnchor > 0 && React.createElement("div", { className: "cp-impact-row" }, React.createElement("span", { className: "cp-impact-label" }, "Projected ceiling"), React.createElement("span", { className: "cp-impact-total cp-impact-salary" }, fmt$(deltas.newAnchor), "/yr") ) ), deltas.unlocksHigherPath && React.createElement("div", { className: "cp-impact-unlock" }, React.createElement("strong", null, "💡 Opens higher-paying paths."), " This cert typically commands ", fmt$(deltas.newRawAnchor), "/yr in roles that match it, but ", career.name, " caps at ", fmt$(deltas.ceiling), ". Consider switching paths once you earn it." ), deltas.newAnchor > 0 && React.createElement("p", { className: "cp-impact-disclaimer" }, "Projection is capped by the top role on the ", career.name, " ladder. The cert anchor model uses Skillsoft IT Skills & Salary; real outcomes depend on experience, region, and employer." ) ), React.createElement("div", { className: "pf-modal-foot" }, React.createElement("button", { className: "btn btn-ghost", onClick: onCancel, }, "Cancel"), React.createElement("button", { className: "btn btn-primary " + (warnRequiredRemoval ? "btn-danger" : ""), onClick: onConfirm, }, action === "add" ? "Add to plan" : warnRequiredRemoval ? "Remove anyway" : "Remove from plan") ) ) ); } // ─── Career plan view (/career//plan) ───────────────────────────── // Customizable: reorder/remove/add certs, with impact preview on changes. // Reorder is instant; add/remove pops a preview modal so the user sees // cost/time deltas before committing. function CareerPlan({ career, onBack, onSwitchPath, onRequestCert, requestedCerts }) { const [bump, setBump] = React.useState(0); React.useEffect(() => { const refresh = () => setBump(n => n + 1); window.addEventListener(window.UserData?.EVT_OWNED || "cyberstudy-owned-certs-change", refresh); window.addEventListener(window.UserData?.EVT_PROG || "ccna-progress-change", refresh); window.addEventListener(window.UserData?.EVT_READ || "ccna-read-change", refresh); window.addEventListener(window.UserData?.EVT_PLAN || "cyberstudy-plan-change", refresh); return () => { window.removeEventListener(window.UserData?.EVT_OWNED || "cyberstudy-owned-certs-change", refresh); window.removeEventListener(window.UserData?.EVT_PROG || "ccna-progress-change", refresh); window.removeEventListener(window.UserData?.EVT_READ || "ccna-read-change", refresh); window.removeEventListener(window.UserData?.EVT_PLAN || "cyberstudy-plan-change", refresh); }; }, []); const enrollment = window.UserData ? window.UserData.getCareerEnrollment() : null; const customPlan = window.UserData ? window.UserData.getCareerPlan(career.id) : null; // The path the user is studying — custom if set, else the defaults. // Default path metadata (required/recommended/optional) flows through // for the default certs; for custom-added certs we treat them as required. const defaultMap = new Map(career.defaultCerts.map(d => [d.certId, d])); const certIds = customPlan ? customPlan.certIds : career.defaultCerts.map(d => d.certId); const totalCostNum = totalCost(certIds); const totalHoursNum = totalHours(certIds); // Projected median: max cert anchor in the plan, but CAPPED by the // ceiling of this career's role ladder. The cap is essential — having a // CCNA doesn't pay you $112k if you're in an IT support track; the cert // either qualifies you to switch paths, or it stays unused for salary // until you change roles. // // Cap = max salaryRange[1] across the career's roles (top of the ladder). // Honest read: "on this path, you can credibly aim for the top role, // which pays around X. To exceed it, you'd switch paths." const rawAnchor = certIds.reduce((m, cid) => { const a = (window.CERT_SALARY_ANCHORS || {})[cid]?.median || 0; return Math.max(m, a); }, 0); const careerCeiling = (career.roles || []).reduce((m, r) => { const top = (r.salaryRange && r.salaryRange[1]) || r.medianSalary || 0; return Math.max(m, top); }, 0); const planAnchor = careerCeiling > 0 ? Math.min(rawAnchor, careerCeiling) : rawAnchor; // If the cert anchor exceeds the path ceiling, surface a "switch path" // nudge — the cert qualifies them for a higher-paying path than they're on. const anchorExceedsCeiling = rawAnchor > 0 && careerCeiling > 0 && rawAnchor > careerCeiling; // Per-cert status. "Already have" wins; otherwise compute in-progress // from concepts read. We treat any reads as "in progress." When concepts // count for that cert isn't known, we fall back to 0%. function statusFor(certId) { const owned = window.UserData?.findOwnedCertByCertId(certId); if (owned) return { kind: "have", owned }; const allConcepts = (window.CONCEPTS || []).filter(c => (c.cert || "ccna") === certId); if (allConcepts.length === 0) return { kind: "none" }; const read = window.UserData ? window.UserData.getReadTopics(certId) : []; const readCount = (read || []).filter(t => allConcepts.find(c => c.topicId === t) ).length; if (readCount === 0) return { kind: "none" }; const pct = Math.round((readCount / allConcepts.length) * 100); return { kind: "progress", pct, read: readCount, total: allConcepts.length }; } function handleDrop() { if (!confirm("Drop the " + career.name + " path? Your customizations are saved if you come back.")) return; window.UserData.clearCareerEnrollment(); onBack(); } function handleStudy(certId) { if (window.__startStudyingCert) window.__startStudyingCert(certId); } // ─── Plan customization handlers ─────────────────────────────────── // All edits go through a single applyEdit() that: // 1. Builds the proposed new certIds array // 2. Pops the ImpactPreviewModal with cost/time deltas // 3. On confirm, persists via setCareerPlan(careerId, certIds) // 4. Required-cert removal additionally requires a second confirm // (handled inline in the preview modal via `confirmRequired`) const [pendingEdit, setPendingEdit] = React.useState(null); const [addModalOpen, setAddModalOpen] = React.useState(false); function buildEdit(action, certId, newCertIds, label) { // Compute deltas relative to current plan const oldCost = totalCost(certIds); const oldHours = totalHours(certIds); const newCost = totalCost(newCertIds); const newHours = totalHours(newCertIds); const defaultEntry = defaultMap.get(certId); const wasRequired = defaultEntry?.requirement === "required"; // Salary projection = max cert anchor in plan, CAPPED by career role // ceiling. The cap matters: a CCNA doesn't pay IT-support tier $112k. // The cert qualifies for a different path; it doesn't lift this path. const anchorFor = (cid) => (window.CERT_SALARY_ANCHORS || {})[cid]?.median || 0; const ceiling = (career.roles || []).reduce((m, r) => { const top = (r.salaryRange && r.salaryRange[1]) || r.medianSalary || 0; return Math.max(m, top); }, 0); const capAnchor = (anchor) => ceiling > 0 ? Math.min(anchor, ceiling) : anchor; const oldAnchor = capAnchor(certIds.reduce((m, cid) => Math.max(m, anchorFor(cid)), 0)); const newAnchor = capAnchor(newCertIds.reduce((m, cid) => Math.max(m, anchorFor(cid)), 0)); // Raw (uncapped) anchor lets us tell the user "this cert opens higher-paying // paths" even if the path-capped projection didn't change. const newRawAnchor = newCertIds.reduce((m, cid) => Math.max(m, anchorFor(cid)), 0); return { action, certId, newCertIds, label, deltas: { cost: newCost - oldCost, hours: newHours - oldHours, newCost, newHours, newCount: newCertIds.length, oldAnchor, newAnchor, anchorDelta: newAnchor - oldAnchor, newRawAnchor, ceiling, unlocksHigherPath: newRawAnchor > ceiling && ceiling > 0, }, warnRequiredRemoval: action === "remove" && wasRequired, }; } function moveCertUp(idx) { if (idx <= 0) return; const next = certIds.slice(); [next[idx-1], next[idx]] = [next[idx], next[idx-1]]; // Reordering doesn't change totals — apply directly (no preview) window.UserData.setCareerPlan(career.id, next); } function moveCertDown(idx) { if (idx >= certIds.length - 1) return; const next = certIds.slice(); [next[idx], next[idx+1]] = [next[idx+1], next[idx]]; window.UserData.setCareerPlan(career.id, next); } function requestRemove(certId) { const meta = (window.CERT_META || {})[certId] || {}; const next = certIds.filter(c => c !== certId); setPendingEdit(buildEdit("remove", certId, next, meta.label || certId)); } function requestAdd(certId) { const meta = (window.CERT_META || {})[certId] || {}; const next = certIds.concat([certId]); setAddModalOpen(false); setPendingEdit(buildEdit("add", certId, next, meta.label || certId)); } function commitEdit() { if (!pendingEdit) return; window.UserData.setCareerPlan(career.id, pendingEdit.newCertIds); setPendingEdit(null); } function cancelEdit() { setPendingEdit(null); } // Find the first not-yet-done cert that's actually studyable (not a stub // and not already "have"). That's the natural next action. function findNextToStudy() { for (const cid of certIds) { const s = statusFor(cid); if (s.kind === "have") continue; const exam = (window.EXAMS || []).find(e => e.id === cid); if (!exam) continue; // stub or unavailable return { certId: cid, status: s }; } return null; } const nextUp = findNextToStudy(); const completedCount = certIds.filter(cid => statusFor(cid).kind === "have").length; const allDone = completedCount === certIds.length; if (!enrollment || enrollment.careerId !== career.id) { // Defensive: shouldn't usually hit, but if the URL was bookmarked // and the user isn't enrolled here, show a CTA to enroll. return React.createElement("div", { className: "cp-detail" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack, }, "← Back to all careers"), React.createElement("h1", { className: "cp-h1" }, "Not enrolled in this path"), React.createElement("p", { className: "cp-sub" }, "Open the ", career.name, " details page and click \"Start this career path\" to enroll.") ); } return React.createElement("div", { className: "cp-detail cp-plan" }, React.createElement("div", { className: "cp-plan-nav" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: () => window.Router && window.Router.navigateCareer({ kind: "detail", careerId: career.id }), }, "← Back to " + career.name), React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn cp-back-all-careers", onClick: onBack, }, "All careers") ), React.createElement("div", { className: "cp-detail-head" }, React.createElement("span", { className: "cp-detail-icon" }, career.icon), React.createElement("div", null, React.createElement("h1", { className: "cp-h1" }, "Your ", career.name, " plan"), React.createElement("p", { className: "cp-sub" }, "Enrolled ", new Date(enrollment.enrolledAt).toLocaleDateString(), customPlan ? " · customized " + new Date(customPlan.modifiedAt).toLocaleDateString() : " · using default path" ) ) ), React.createElement("div", { className: "cp-plan-summary" }, React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Projected ceiling"), React.createElement("span", { className: "cp-stat-big-val cp-stat-big-salary" }, planAnchor > 0 ? fmt$(planAnchor) : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, "top of " + career.name + " ladder"), planAnchor > 0 && React.createElement("span", { className: "cp-stat-big-info", title: "Top of the salary ladder for this career path. The cert anchor model is capped by the career's role ceiling — having a higher-tier cert doesn't pay you more in this path; it qualifies you to switch to a higher-paying path.", }, "ⓘ") ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Cost of certs"), React.createElement("span", { className: "cp-stat-big-val" }, totalCostNum > 0 ? fmt$(totalCostNum) : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, "voucher prices") ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Study time"), React.createElement("span", { className: "cp-stat-big-val" }, totalHoursNum > 0 ? totalHoursNum + " hrs" : "—"), React.createElement("span", { className: "cp-stat-big-sub" }, "across path") ), React.createElement("div", { className: "cp-stat-big" }, React.createElement("span", { className: "cp-stat-big-label" }, "Path length"), React.createElement("span", { className: "cp-stat-big-val" }, certIds.length + " certs"), React.createElement("span", { className: "cp-stat-big-sub" }, customPlan ? "custom" : "default") ) ), // Launchpad: turn the plan into a "now do this" page. Big CTA at top, // per-row Study links inline with the sequence. The CTA targets the // first not-yet-done studyable cert; advances automatically as the // user completes certs and refreshes. allDone ? React.createElement("div", { className: "cp-launch-banner done" }, React.createElement("div", { className: "cp-launch-banner-eyebrow" }, "🎉 Path complete"), React.createElement("div", { className: "cp-launch-banner-title" }, "You've got every cert on the ", career.name, " path."), React.createElement("p", { className: "cp-launch-banner-body" }, "Nice work. Use the catalog to explore the next tier, or dive into another path entirely."), React.createElement("button", { className: "btn btn-primary", onClick: onBack, }, "Browse careers →") ) : nextUp ? React.createElement("div", { className: "cp-launch-banner" }, React.createElement("div", { className: "cp-launch-banner-eyebrow" }, completedCount > 0 ? `${completedCount} of ${certIds.length} done · next up` : "Next up" ), React.createElement("div", { className: "cp-launch-banner-title" }, ((window.CERT_META || {})[nextUp.certId]?.label || nextUp.certId), nextUp.status.kind === "progress" && React.createElement("span", { className: "cp-launch-banner-pct" }, " · ", nextUp.status.pct, "% done" ) ), React.createElement("p", { className: "cp-launch-banner-body" }, nextUp.status.kind === "progress" ? `Pick up where you left off — ${nextUp.status.read} of ${nextUp.status.total} concepts read.` : `${(window.CERT_STUDY_HOURS || {})[nextUp.certId] || 0} hours of study ahead. Let's go.` ), React.createElement("button", { className: "btn btn-primary cp-launch-cta", onClick: () => handleStudy(nextUp.certId), }, nextUp.status.kind === "progress" ? "Continue studying →" : "Start studying →") ) : React.createElement("div", { className: "cp-launch-banner waiting" }, React.createElement("div", { className: "cp-launch-banner-eyebrow" }, "Waiting on us"), React.createElement("div", { className: "cp-launch-banner-title" }, "No studyable cert up next"), React.createElement("p", { className: "cp-launch-banner-body" }, "The next cert in your sequence isn't available in CyberStudy yet. We'll get it built soon." ) ), // Path-mismatch nudge: if certs in plan are tier-higher than this path's // ceiling, suggest switching paths. Common case: adding CCNA to IT Support. anchorExceedsCeiling && React.createElement("div", { className: "cp-mismatch-banner" }, React.createElement("div", { className: "cp-mismatch-icon" }, "💡"), React.createElement("div", { className: "cp-mismatch-body" }, React.createElement("strong", null, "Your certs qualify for higher-paying roles than this path offers."), React.createElement("p", null, "The top role in ", career.name, " caps around ", fmt$(careerCeiling), "/yr. The certs on your plan typically command around ", fmt$(rawAnchor), "/yr in roles that match the credential. Consider switching to a path that aligns — Network Engineer, Cybersecurity Analyst, etc." ), React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: onSwitchPath, }, "Browse career paths →") ) ), React.createElement("h2", { className: "cp-h2" }, "Your cert sequence"), React.createElement("div", { className: "cp-plan-list" }, certIds.map((cid, idx) => { const meta = (window.CERT_META || {})[cid] || {}; const defaultEntry = defaultMap.get(cid); const status = statusFor(cid); const certHours = (window.CERT_STUDY_HOURS || {})[cid] || 0; const certPrice = meta.price || ""; const requirement = defaultEntry?.requirement || "required"; // Studyable = in our visible EXAMS list (not a stub) AND not "have" const studyable = (window.EXAMS || []).some(e => e.id === cid) && status.kind !== "have"; return React.createElement("div", { key: cid + "-" + idx, className: "cp-plan-row status-" + status.kind, }, React.createElement("div", { className: "cp-plan-row-num" }, idx + 1), React.createElement("div", { className: "cp-plan-row-main" }, React.createElement("div", { className: "cp-plan-row-head" }, React.createElement("span", { className: "cp-plan-row-label" }, meta.label || cid), React.createElement("span", { className: "cp-plan-row-req cp-req-" + requirement }, requirement.charAt(0).toUpperCase() + requirement.slice(1) ), // Single unified "Not planned · Request now" treatment for // any cert that isn't yet built in CyberStudy. After click, // swaps to "✓ Requested" for the session. Users signal // demand; we prioritize build order based on what people // actually want. meta.comingSoon && status.kind !== "have" && ( requestedCerts && requestedCerts.has(cid) ? React.createElement("span", { className: "cp-plan-row-requested" }, "✓ Requested") : React.createElement("button", { className: "cp-plan-row-request", onClick: (e) => { e.stopPropagation(); onRequestCert && onRequestCert(cid); }, title: "Tell us to prioritize building this cert", }, "Not planned · Request") ) ), meta.examCode && React.createElement("div", { className: "cp-plan-row-examcode" }, meta.examCode), React.createElement("div", { className: "cp-plan-row-meta" }, certHours > 0 && React.createElement("span", null, certHours, " hrs"), certHours > 0 && certPrice && React.createElement("span", { className: "cp-plan-row-sep" }, "·"), certPrice && React.createElement("span", null, certPrice) ), // Reorder + remove controls. Reorder is instant; remove pops // the impact-preview modal first. React.createElement("div", { className: "cp-plan-row-edit" }, React.createElement("button", { className: "cp-edit-btn", disabled: idx === 0, onClick: () => moveCertUp(idx), title: "Move up", "aria-label": "Move " + (meta.label || cid) + " up", }, "↑"), React.createElement("button", { className: "cp-edit-btn", disabled: idx === certIds.length - 1, onClick: () => moveCertDown(idx), title: "Move down", "aria-label": "Move " + (meta.label || cid) + " down", }, "↓"), React.createElement("button", { className: "cp-edit-btn cp-edit-remove", onClick: () => requestRemove(cid), title: "Remove from plan", "aria-label": "Remove " + (meta.label || cid), }, "✕") ) ), React.createElement("div", { className: "cp-plan-row-status" }, status.kind === "have" && React.createElement("span", { className: "cp-status-have" }, "✓ Already have"), status.kind === "progress" && React.createElement(React.Fragment, null, React.createElement("span", { className: "cp-status-progress" }, "📚 ", status.pct, "%"), React.createElement("span", { className: "cp-status-sub" }, status.read, "/", status.total, " concepts") ), status.kind === "none" && React.createElement("span", { className: "cp-status-none" }, "⬜ Not started"), studyable && React.createElement("button", { className: "btn btn-ghost btn-sm cp-plan-row-study", onClick: () => handleStudy(cid), }, status.kind === "progress" ? "Continue →" : "Study →") ) ); }) ), // Add-a-cert button anchors below the sequence React.createElement("div", { className: "cp-plan-add-wrap" }, React.createElement("button", { className: "btn btn-ghost cp-plan-add-btn", onClick: () => setAddModalOpen(true), }, "+ Add a cert") ), React.createElement("div", { className: "cp-plan-actions" }, React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: onSwitchPath, }, "Switch path"), customPlan && React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: () => { if (!confirm("Reset your custom plan to the default " + career.name + " sequence?")) return; window.UserData.clearCareerPlan(career.id); }, }, "Reset to default"), React.createElement("button", { className: "btn btn-ghost btn-sm cp-plan-drop", onClick: handleDrop, }, "Drop this path") ), // Modals addModalOpen && React.createElement(AddCertModal, { career, currentCertIds: certIds, onClose: () => setAddModalOpen(false), onPick: requestAdd, }), pendingEdit && React.createElement(ImpactPreviewModal, { edit: pendingEdit, career, onConfirm: commitEdit, onCancel: cancelEdit, }) ); } // ─── Compare view: 2-4 careers side-by-side ────────────────────────────── function CareerCompare({ careerIds, onBack, onSelect, onRemove }) { const careers = careerIds .map(id => (window.CAREER_PATHS || []).find(c => c.id === id)) .filter(Boolean); if (careers.length < 2) { return React.createElement("div", { className: "cp-compare-empty" }, React.createElement("p", null, "Pick at least 2 careers from the catalog to compare."), React.createElement("button", { className: "btn btn-secondary btn-sm", onClick: onBack }, "← Back to catalog") ); } function row(label, getter, opts) { opts = opts || {}; return React.createElement("tr", { className: "cp-comp-row " + (opts.divider ? "divider" : "") }, React.createElement("th", { className: "cp-comp-rowhead" }, label), careers.map((c, i) => React.createElement( "td", { key: c.id, className: "cp-comp-cell" }, getter(c) )) ); } return React.createElement( "div", { className: "cp-compare" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack, }, "← Back to catalog"), React.createElement("h1", { className: "cp-h1" }, "Comparing " + careers.length + " careers"), React.createElement("div", { className: "cp-disclaimer-block" }, React.createElement("p", { className: "cp-salary-disclaimer" }, window.CAREER_SALARY_DISCLAIMER || "") ), React.createElement("div", { className: "cp-comp-table-wrap" }, React.createElement("table", { className: "cp-comp-table" }, React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { className: "cp-comp-rowhead" }, ""), careers.map(c => React.createElement( "th", { key: c.id, className: "cp-comp-colhead" }, React.createElement("div", { className: "cp-comp-colhead-icon" }, c.icon), React.createElement("div", { className: "cp-comp-colhead-name" }, c.name), React.createElement("div", { className: "cp-comp-colhead-actions" }, React.createElement("button", { className: "btn btn-secondary btn-sm", onClick: () => onSelect(c.id), }, "Details"), React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: () => onRemove(c.id), }, "✕") ) )) ) ), React.createElement("tbody", null, row("Tagline", c => React.createElement("span", { className: "cp-comp-tagline" }, c.tagline)), row("Total cost", c => { const cost = totalCost(c.defaultCerts.map(d => d.certId)); return cost > 0 ? React.createElement("strong", null, fmt$(cost)) : "—"; }, {divider: true}), row("Study time", c => { const h = totalHours(c.defaultCerts.map(d => d.certId)); return h > 0 ? (h + " hrs") : "—"; }), row("Difficulty", c => c.difficulty.charAt(0).toUpperCase() + c.difficulty.slice(1)), row("Default certs", c => React.createElement("div", { className: "cp-comp-certs" }, c.defaultCerts.map(d => React.createElement( "span", { key: d.certId, className: "cp-cert-chip " + (isCertAvailable(d.certId) ? "" : "missing") + " " + (d.required ? "" : "optional"), title: d.required ? getCertLabel(d.certId) : getCertLabel(d.certId) + " (optional)", }, getCertLabel(d.certId) )) ), {divider: true}), row("Salary — entry", c => { const entry = c.roles.find(r => r.tier === "entry"); return entry ? fmtSalary(entry.medianSalary) : "—"; }), row("Salary — mid", c => { const mid = c.roles.find(r => r.tier === "mid"); return mid ? fmtSalary(mid.medianSalary) : "—"; }), row("Salary — senior", c => { const senior = c.roles.find(r => r.tier === "senior"); return senior ? fmtSalary(senior.medianSalary) : "—"; }), row("Pros", c => React.createElement("ul", { className: "cp-comp-list" }, c.pros.slice(0,3).map((p,i) => React.createElement("li", {key:i}, p)) ), {divider: true}), row("Cons", c => React.createElement("ul", { className: "cp-comp-list" }, c.cons.slice(0,3).map((p,i) => React.createElement("li", {key:i}, p)) )), row("Locked?", c => { const locked = isCareerLocked(c); return locked ? React.createElement("span", { className: "cp-comp-locked" }, "🔒 Enrollment locked") : React.createElement("span", { className: "cp-comp-ready" }, "✓ Available"); }, {divider: true}) ) ) ) ); } // ─── Career Quiz — 10-question fit recommender ────────────────────────── // Drives a question-at-a-time UI with a progress bar. Back button on // questions 2+ lets users change a prior answer (changes don't reset // subsequent answers, just lets you revise this one). Submit on the last // question scores via window.scoreCareerQuiz, persists the result, and // navigates to /career/quiz/results. function CareerQuiz({ onBack, onComplete }) { const questions = window.CAREER_QUIZ_QUESTIONS || []; const [idx, setIdx] = React.useState(0); const [answers, setAnswers] = React.useState({}); if (questions.length === 0) { // Should never happen, but defensive — quiz data didn't load return React.createElement("div", { className: "cp-detail" }, React.createElement("h1", { className: "cp-h1" }, "Career quiz isn't available"), React.createElement("p", null, "We couldn't load the quiz questions. Try refreshing — if it persists, let us know."), React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack }, "← Back to careers") ); } const q = questions[idx]; const isLast = idx === questions.length - 1; const isFirst = idx === 0; const pct = Math.round(((idx + 1) / questions.length) * 100); const answered = !!answers[q.id]; function pick(optionId) { setAnswers({ ...answers, [q.id]: optionId }); // No auto-advance — every question waits for the user to click Next. // Consistent behavior across binary + multi-choice, lets the user // change their mind before committing to the next question. } function next() { if (!answered) return; if (isLast) { // Score, persist, navigate const result = window.scoreCareerQuiz(answers); const stored = { answers, top3: result.top3, takenAt: new Date().toISOString(), }; if (window.UserData) window.UserData.setQuizResult(stored); onComplete(); } else { setIdx(idx + 1); } } function back() { if (isFirst) onBack(); else setIdx(idx - 1); } return React.createElement("div", { className: "cp-quiz" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack, }, "← Cancel quiz"), React.createElement("div", { className: "cp-quiz-head" }, React.createElement("h1", { className: "cp-h1" }, "Career-fit quiz"), React.createElement("p", { className: "cp-sub" }, "Answer honestly. No right/wrong — we use your picks to weight 10 IT career paths and surface the 3 that fit best." ) ), React.createElement("div", { className: "cp-quiz-progress" }, React.createElement("div", { className: "cp-quiz-progress-track" }, React.createElement("div", { className: "cp-quiz-progress-fill", style: { width: pct + "%" } }) ), React.createElement("div", { className: "cp-quiz-progress-label" }, "Question " + (idx + 1) + " of " + questions.length ) ), React.createElement("div", { className: "cp-quiz-card" }, React.createElement("h2", { className: "cp-quiz-question" }, q.text), React.createElement("div", { className: "cp-quiz-options " + (q.kind === "binary" ? "binary" : "choice"), }, q.options.map(opt => React.createElement("button", { key: opt.id, className: "cp-quiz-option " + (answers[q.id] === opt.id ? "picked" : ""), onClick: () => pick(opt.id), }, React.createElement("span", { className: "cp-quiz-option-marker" }, answers[q.id] === opt.id ? "●" : "○" ), React.createElement("span", { className: "cp-quiz-option-text" }, opt.text) )) ) ), React.createElement("div", { className: "cp-quiz-nav" }, React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: back, }, isFirst ? "Cancel" : "← Back"), React.createElement("button", { className: "btn btn-primary", disabled: !answered, onClick: next, }, isLast ? "See my results →" : "Next →") ) ); } // ─── Quiz results: top 3 cards with reasoning + "Start this path" CTA ──── function CareerQuizResults({ result, onRetake, onStartPath, onOpenCareer, onBack }) { if (!result || !result.top3 || result.top3.length === 0) { return React.createElement("div", { className: "cp-detail" }, React.createElement("h1", { className: "cp-h1" }, "No quiz result yet"), React.createElement("p", null, "Take the quiz first."), React.createElement("button", { className: "btn btn-primary", onClick: onRetake }, "Take the quiz →") ); } const tier1 = result.top3[0]; const tier2 = result.top3.slice(1); const careerById = (id) => (window.CAREER_PATHS || []).find(c => c.id === id); const t1Career = careerById(tier1.careerId); return React.createElement("div", { className: "cp-quiz-results" }, React.createElement("button", { className: "btn btn-ghost btn-sm cp-back-btn", onClick: onBack, }, "← Back to careers"), React.createElement("div", { className: "cp-quiz-head" }, React.createElement("h1", { className: "cp-h1" }, "Your top fits"), React.createElement("p", { className: "cp-sub" }, "These three career paths scored highest based on your answers. The top match is your strongest fit — but explore all three. Quiz results are a starting point, not a prescription." ) ), // Winner card — bigger, with primary CTA t1Career && React.createElement("div", { className: "cp-quiz-result-winner" }, React.createElement("div", { className: "cp-quiz-result-eyebrow" }, "Strongest match"), React.createElement("div", { className: "cp-quiz-result-name" }, React.createElement("span", null, t1Career.icon || "💼"), React.createElement("span", null, t1Career.name) ), React.createElement("p", { className: "cp-quiz-result-tagline" }, t1Career.tagline), tier1.reasons && tier1.reasons.length > 0 && React.createElement("div", { className: "cp-quiz-result-reasons" }, React.createElement("div", { className: "cp-quiz-result-reasons-label" }, "Why this fits:"), React.createElement("ul", null, tier1.reasons.map((r, i) => React.createElement("li", { key: i }, r)) ) ), React.createElement("div", { className: "cp-quiz-result-actions" }, React.createElement("button", { className: "btn btn-primary", onClick: () => onStartPath(t1Career), }, "Start this career path →"), React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: () => onOpenCareer(t1Career.id), }, "See details first") ) ), // Runners-up — smaller, exploratory tier2.length > 0 && React.createElement(React.Fragment, null, React.createElement("h2", { className: "cp-h2", style: { marginTop: 28 } }, "Also worth a look"), React.createElement("div", { className: "cp-quiz-result-others" }, tier2.map((t, i) => { const c = careerById(t.careerId); if (!c) return null; return React.createElement("button", { key: t.careerId, className: "cp-quiz-result-other", onClick: () => onOpenCareer(t.careerId), }, React.createElement("div", { className: "cp-quiz-result-other-head" }, React.createElement("span", { className: "cp-quiz-result-other-rank" }, "#" + (i + 2)), React.createElement("span", { className: "cp-quiz-result-other-icon" }, c.icon || "💼"), React.createElement("span", { className: "cp-quiz-result-other-name" }, c.name) ), React.createElement("p", { className: "cp-quiz-result-other-tagline" }, c.tagline), t.reasons && t.reasons.length > 0 && React.createElement("p", { className: "cp-quiz-result-other-reason" }, t.reasons[0] ) ); }) ) ), React.createElement("div", { className: "cp-quiz-result-foot" }, React.createElement("button", { className: "btn btn-ghost btn-sm", onClick: onRetake, }, "Retake the quiz"), result.takenAt && React.createElement("span", { className: "cp-quiz-result-taken" }, "Taken " + new Date(result.takenAt).toLocaleDateString() ) ) ); } // ─── Top-level CareerPlanner component ─────────────────────────────────── function CareerPlanner({ onBackToHome }) { // Derive view + activeId from the URL via the router. We sync to changes // (back/forward, in-app navigation) by subscribing. const [routeBump, setRouteBump] = React.useState(0); React.useEffect(() => { if (!window.Router) return; const unsub = window.Router.subscribe(() => setRouteBump(n => n + 1)); return unsub; }, []); // Subscribe to quiz writes so the catalog banner reflects whether a // result exists right after the user finishes the quiz. React.useEffect(() => { const refresh = () => setRouteBump(n => n + 1); const EVT_QUIZ = window.UserData?.EVT_QUIZ || "cyberstudy-quiz-change"; window.addEventListener(EVT_QUIZ, refresh); return () => window.removeEventListener(EVT_QUIZ, refresh); }, []); const route = (window.Router && window.Router.currentCareerRoute) ? (window.Router.currentCareerRoute() || { kind: "catalog" }) : { kind: "catalog" }; const view = route.kind === "detail" ? "detail" : route.kind === "compare" ? "compare" : route.kind === "plan" ? "plan" : route.kind === "quiz" ? "quiz" : route.kind === "quiz-results" ? "quiz-results" : "catalog"; const activeId = (route.kind === "detail" || route.kind === "plan") ? route.careerId : null; const [compareIds, setCompareIds] = React.useState(() => new Set()); const [tellUsStatus, setTellUsStatus] = React.useState(null); // null | 'sending' | 'sent' | 'error' const [tellUsMsg, setTellUsMsg] = React.useState(""); // Track certs the user has already clicked "Request" on, so we don't // re-spam the backend AND show "Requested ✓" feedback inline. This is // ephemeral per-session — refreshing the page clears it. That's fine; // the backend dedupes by issue_type + cert anyway. const [requestedCerts, setRequestedCerts] = React.useState(() => new Set()); // Onboarding modal — fires when user clicks "Start this career path". // Holds the career to onboard into; null = no modal. const [onboardingCareer, setOnboardingCareer] = React.useState(null); // Allow related-cards inside Detail to switch active career via the URL. React.useEffect(() => { window.__cpNavigate = (id) => { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "detail", careerId: id }); } }; return () => { delete window.__cpNavigate; }; }, []); function pickCareer(id) { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "detail", careerId: id }); } } function backToCatalog() { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "catalog" }); } } // Begin enrollment in a career path. If first-time-on-this-path (no // existing plan stored AND no prior enrollment in this career), show // the onboarding modal first. Otherwise just enroll + go to plan. function startPath(career) { if (!window.UserData) return; // Enrolling in a path is a signed-in feature — prompt sign-in when logged // out instead of writing local-only enrollment that won't surface. if (!(window.AuthStore && window.AuthStore.isLoggedIn)) { if (window.AuthNav && window.AuthNav.setView) window.AuthNav.setView('login'); else if (window.Router && window.Router.navigate) window.Router.navigate('login'); return; } const existingPlan = window.UserData.getCareerPlan(career.id); const currentEnrollment = window.UserData.getCareerEnrollment(); // First-time means: never had a plan stored AND not currently enrolled. const firstTime = !existingPlan && (!currentEnrollment || currentEnrollment.careerId !== career.id); if (firstTime) { setOnboardingCareer(career); return; } // Switching back to a previously-enrolled path: skip onboarding. window.UserData.setCareerEnrollment(career.id); viewPlan(career); } function viewPlan(career) { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "plan", careerId: career.id }); } } // Called by the onboarding modal when the user is done (saved or skipped). function completeOnboarding() { const career = onboardingCareer; setOnboardingCareer(null); if (!career) return; window.UserData.setCareerEnrollment(career.id); viewPlan(career); } function toggleCompare(id) { setCompareIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function compareGo() { if (compareIds.size >= 2 && compareIds.size <= 4) { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "compare" }); } } } function removeFromCompare(id) { setCompareIds(prev => { const next = new Set(prev); next.delete(id); if (next.size < 2 && view === "compare") { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "catalog" }); } } return next; }); } async function sendTellUs(career, missingCerts) { const user = window.AuthStore && window.AuthStore.user; if (!user) { alert("Sign in first to submit cert-priority feedback."); return; } setTellUsStatus("sending"); const api = window.AuthStore && window.AuthStore.api; try { const data = await api("/api/exam-feedback", { method: "POST", body: JSON.stringify({ cert: missingCerts[0] || "unknown", question_id: "career-unlock-request", source: "career_planner", question_stem: "User wants this cert prioritized to unlock the " + career.name + " career path.", issue_type: "career_unlock_request", description: "Career: " + career.name + " (" + career.id + ")\nMissing certs: " + missingCerts.join(", ") + "\nReason: User clicked 'Tell us this matters' on the career detail page.", }), }); if (data && data.ok) { setTellUsStatus("sent"); setTellUsMsg("Thanks — we'll prioritize " + missingCerts.map(getCertLabel).join(" and ") + "."); setTimeout(() => { setTellUsStatus(null); setTellUsMsg(""); }, 3500); } else { setTellUsStatus("error"); setTellUsMsg((data && data.message) || "Send failed."); } } catch (e) { setTellUsStatus("error"); setTellUsMsg(e.message || "Network error."); } } // "Request now" on a single cert badge. Signals user demand for content. // The backend logs it under the same exam-feedback table as other // requests so we can rank build priority by demand. Idempotent per // session — once clicked, we mark it requested and ignore re-clicks. async function requestCert(certId) { const user = window.AuthStore && window.AuthStore.user; if (!user) { alert("Sign in to request this cert. Your request signals which content to build next."); return; } if (requestedCerts.has(certId)) return; // already requested this session const certLabel = getCertLabel(certId); // Optimistic update — mark requested immediately so the badge changes // even if the network is slow. Revert on error. setRequestedCerts(prev => { const next = new Set(prev); next.add(certId); return next; }); setTellUsStatus("sending"); const api = window.AuthStore && window.AuthStore.api; try { const data = await api("/api/exam-feedback", { method: "POST", body: JSON.stringify({ cert: certId, question_id: "cert-content-request", source: "career_planner", question_stem: "User wants this cert built out in CyberStudy.", issue_type: "cert_content_request", description: "Cert: " + certLabel + " (" + certId + ")\nSource: cert badge click on career detail/plan view.", }), }); if (data && data.ok) { setTellUsStatus("sent"); setTellUsMsg("Thanks — we'll prioritize " + certLabel + "."); setTimeout(() => { setTellUsStatus(null); setTellUsMsg(""); }, 3500); } else { // Revert optimistic mark so user can retry setRequestedCerts(prev => { const next = new Set(prev); next.delete(certId); return next; }); setTellUsStatus("error"); setTellUsMsg((data && data.message) || "Send failed."); setTimeout(() => { setTellUsStatus(null); setTellUsMsg(""); }, 3500); } } catch (e) { setRequestedCerts(prev => { const next = new Set(prev); next.delete(certId); return next; }); setTellUsStatus("error"); setTellUsMsg(e.message || "Network error."); setTimeout(() => { setTellUsStatus(null); setTellUsMsg(""); }, 3500); } } // "Request another career path" — opens the same kind of modal used for // cert requests (via window.CareerPathRequestForm). Falls back to a // friendly alert if the feedback module isn't loaded for some reason. const [requestModalOpen, setRequestModalOpen] = React.useState(false); function openRequestCareer() { const user = window.AuthStore && window.AuthStore.user; if (!user) { alert("Sign in first to request a new career."); return; } if (!window.CareerPathRequestForm || !window.FeedbackModal) { alert("Feedback form isn't loaded — refresh the page and try again."); return; } setRequestModalOpen(true); } // Top-of-page status toast for tell-us const toast = tellUsStatus && React.createElement( "div", { className: "cp-toast " + (tellUsStatus === "sent" ? "ok" : tellUsStatus === "error" ? "error" : "sending"), }, tellUsStatus === "sending" ? "Sending…" : tellUsMsg ); // Request-career modal, mounted at the planner root so it can overlay // any sub-view. const requestModal = requestModalOpen && window.FeedbackModal && React.createElement( window.FeedbackModal, { open: requestModalOpen, onClose: () => setRequestModalOpen(false), ariaLabel: "Request a new career path", }, React.createElement(window.CareerPathRequestForm, { onClose: () => setRequestModalOpen(false), }) ); // Onboarding modal — overlaid when user starts a new path const onboardingModal = onboardingCareer && React.createElement(OnboardingModal, { career: onboardingCareer, onClose: () => setOnboardingCareer(null), onDone: completeOnboarding, }); // Quiz handlers function takeQuiz() { if (window.Router && window.Router.navigateCareer) { const existing = window.UserData?.getQuizResult(); // If they already have a result, go to results page (offer retake from there). // Otherwise, start a fresh quiz. window.Router.navigateCareer({ kind: existing ? "quiz-results" : "quiz" }); } } function startFreshQuiz() { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "quiz" }); } } function quizDone() { if (window.Router && window.Router.navigateCareer) { window.Router.navigateCareer({ kind: "quiz-results" }); } } function quizStartPath(career) { // Same as startPath() but called from results page. Delegate to existing. startPath(career); } function quizOpenCareer(careerId) { // Open the career's detail page from the results page pickCareer(careerId); } const hasQuizResult = !!(window.UserData && window.UserData.getQuizResult()); let body; if (view === "detail" && activeId) { const career = (window.CAREER_PATHS || []).find(c => c.id === activeId); body = career ? React.createElement(CareerDetail, { career, onBack: backToCatalog, onTellUs: sendTellUs, onStartPath: startPath, onViewPlan: viewPlan, onRequestCert: requestCert, requestedCerts: requestedCerts, }) : React.createElement("p", null, "Career not found."); } else if (view === "plan" && activeId) { const career = (window.CAREER_PATHS || []).find(c => c.id === activeId); body = career ? React.createElement(CareerPlan, { career, onBack: backToCatalog, onSwitchPath: backToCatalog, onRequestCert: requestCert, requestedCerts: requestedCerts, }) : React.createElement("p", null, "Career not found."); } else if (view === "compare") { body = React.createElement(CareerCompare, { careerIds: Array.from(compareIds), onBack: backToCatalog, onSelect: pickCareer, onRemove: removeFromCompare, }); } else if (view === "quiz") { body = React.createElement(CareerQuiz, { onBack: backToCatalog, onComplete: quizDone, }); } else if (view === "quiz-results") { const result = window.UserData?.getQuizResult(); body = React.createElement(CareerQuizResults, { result, onBack: backToCatalog, onRetake: startFreshQuiz, onStartPath: quizStartPath, onOpenCareer: quizOpenCareer, }); } else { body = React.createElement(CareerCatalog, { onSelect: pickCareer, selectedForCompare: compareIds, onToggleCompare: toggleCompare, onCompareGo: compareGo, onRequestCareer: openRequestCareer, onTakeQuiz: takeQuiz, hasQuizResult, onBackToHome, }); } return React.createElement("div", { className: "cp-page" }, toast, requestModal, onboardingModal, body); } window.CareerPlanner = CareerPlanner; })();