/* global React, Icons */ // ============================================================ // Section registry + shared section renderers // Every résumé section is data-driven so the builder can let the // user drag, drop, add and remove "segments" in any order, and the // unified template engine (ResumePreview.jsx) renders them. // ============================================================ // key -> { label, icon, kind, blurb } const SECTION_DEFS = { summary: { label: "Summary", icon: "pen", kind: "text", blurb: "A short professional intro" }, experience: { label: "Experience", icon: "brief", kind: "jobs", blurb: "Roles, companies & impact" }, projects: { label: "Projects", icon: "folder", kind: "projects",blurb: "Selected work & side projects" }, education: { label: "Education", icon: "cap", kind: "edu", blurb: "Degrees & schools" }, skills: { label: "Skills", icon: "star", kind: "skills", blurb: "Tools & competencies" }, certifications: { label: "Certifications", icon: "cert", kind: "named", blurb: "Licenses & credentials" }, awards: { label: "Awards", icon: "trophy", kind: "named", blurb: "Honors & recognition" }, languages: { label: "Languages", icon: "lang", kind: "leveled", blurb: "Spoken & written languages" }, publications: { label: "Publications", icon: "book", kind: "pubs", blurb: "Papers, articles & talks" }, volunteer: { label: "Volunteering", icon: "heart", kind: "jobs", blurb: "Community & pro-bono work" }, courses: { label: "Coursework", icon: "list", kind: "chips", blurb: "Relevant courses" }, interests: { label: "Interests", icon: "spark", kind: "chips", blurb: "Hobbies & personal" }, references: { label: "References", icon: "user", kind: "refs", blurb: "Referees or 'on request'" }, }; // natural fallback order if a résumé has no explicit sectionOrder const DEFAULT_ORDER = [ "summary", "experience", "projects", "skills", "certifications", "awards", "languages", "education", "volunteer", "publications", "courses", "interests", "references", ]; const sectionLabel = (k) => (SECTION_DEFS[k] ? SECTION_DEFS[k].label : k); // does this résumé hold any content for section k? function sectionHasData(k, d) { if (k === "summary") return !!(d.summary && d.summary.trim()); if (k === "references") { const r = d.references; return d.referencesOnRequest || (Array.isArray(r) && r.length > 0); } const v = d[k]; return Array.isArray(v) && v.length > 0; } // the effective, ordered, non-empty list of section keys to render. // When an explicit sectionOrder exists it is authoritative (so removing a // segment in the organizer truly removes it). Only fall back to DEFAULT_ORDER // when no order has been set yet. function activeSections(d) { if (d.sectionOrder && d.sectionOrder.length) { return d.sectionOrder.filter((k) => SECTION_DEFS[k] && sectionHasData(k, d)); } return DEFAULT_ORDER.filter((k) => SECTION_DEFS[k] && sectionHasData(k, d)); } // ---- contacts ---- const CICONS = { email: Icons.mail, phone: Icons.phone, website: Icons.globe, location: Icons.pin }; function contactList(d, keys) { return (keys || ["email", "phone", "website", "location"]) .map((k) => ({ k, Ic: CICONS[k], val: d[k] })) .filter((x) => x.val); } const initials = (name) => (name || "?").trim().split(" ").map((w) => w[0]).slice(0, 2).join(""); // ---- photo with monogram fallback ---- // When a real photo URL is set we layer it over the accent gradient, so a // slow / failed remote load still reads as an intentional colored avatar // rather than an empty white box. function Photo({ d, shape = "circle", className = "" }) { if (d.photo) return (
); return
{initials(d.name)}
; } // ---- proficiency scale ("vector") model --------------------------------- // A skill is stored as either a plain string (legacy) or { name, level } // where level is 1..SKILL_MAX. The scale is how much you've used a tool / // language — rendered as dots, segments or signal bars instead of plain text. const SKILL_MAX = 5; const LEVEL_WORDS = ["", "Novice", "Familiar", "Proficient", "Advanced", "Expert"]; // level -> fill % for the bar / ring variants (reads naturally, never 0/100) const LVL_PCT = { 1: 50, 2: 65, 3: 78, 4: 90, 5: 98 }; const clampLvl = (n) => { n = Math.round(Number(n)); return Math.max(1, Math.min(SKILL_MAX, Number.isFinite(n) && n ? n : 4)); }; const SKILL_LVLS = [94, 88, 96, 82, 90, 85, 92, 80, 87, 83]; const lvl = (i) => SKILL_LVLS[i % SKILL_LVLS.length]; // legacy strings get a varied-but-plausible level so old résumés still look alive const legacyTier = (i) => clampLvl(Math.round(lvl(i) / 20)); const skillName = (s) => (s && typeof s === "object" ? s.name : s); const normSkill = (s, i = 0) => s && typeof s === "object" ? { name: s.name || "", level: clampLvl(s.level) } : { name: String(s ?? ""), level: legacyTier(i) }; // spoken-language proficiency words -> tier on the same 1..5 scale const LANG_TIER = { native: 5, bilingual: 5, mother: 5, fluent: 5, "full professional": 4, professional: 4, advanced: 4, "working professional": 4, conversational: 3, intermediate: 3, limited: 2, "limited working": 2, basic: 2, elementary: 2, beginner: 1, }; const langTier = (level) => { if (level == null) return 3; const k = String(level).toLowerCase().trim(); if (LANG_TIER[k] != null) return LANG_TIER[k]; const hit = Object.keys(LANG_TIER).find((w) => k.includes(w)); return hit ? LANG_TIER[hit] : 3; }; // ---- the scale renderers (the "vectors") ---- function ScaleDots({ level, max = SKILL_MAX }) { return {Array.from({ length: max }, (_, i) => )}; } function ScaleSegs({ level, max = SKILL_MAX }) { return {Array.from({ length: max }, (_, i) => )}; } function ScaleSignal({ level, max = SKILL_MAX }) { return {Array.from({ length: max }, (_, i) => )}; } function Scale({ kind, level, max }) { if (kind === "segments") return ; if (kind === "signal") return ; return ; } // one labelled scale row (name on the left, scale on the right) function ScaleRow({ name, level, kind, showWord, editPath }) { return (
{name} {showWord && {LEVEL_WORDS[level]}}
); } // ---- skill visualisations ---- function SkillBar({ name, pct, word, editPath }) { return (
{name}{word || pct + "%"}
); } function SkillRing({ name, pct, editPath }) { const R = 22, C = 2 * Math.PI * R; return (
{pct}% {name}
); } function RSkills({ items, variant, showWord }) { const norm = (items || []).map((s, i) => normSkill(s, i)); if (variant === "bars") return
{norm.slice(0, 9).map((s, i) => )}
; if (variant === "rings") return
{norm.slice(0, 6).map((s, i) => )}
; if (variant === "dots" || variant === "segments" || variant === "signal") return
{norm.slice(0, 12).map((s, i) => )}
; if (variant === "list") return
{norm.map((s, i) => {s.name})}
; return
{norm.map((s, i) => {s.name})}
; } // ---- experience / volunteer entry ---- function RJob({ j, secKey = "experience" }) { return (
{j.role} {j.start}{j.end ? ` – ${j.end}` : ""}
{j.company} {j.location ? <> · {j.location} : ""}
{j.bullets && j.bullets.filter(Boolean).length > 0 && (
    {j.bullets.map((b, i) => (b && b.trim() ?
  • {b}
  • : null))}
)}
); } function REdu({ e }) { return (
{e.school}{e.start}{e.end ? ` – ${e.end}` : ""}
{e.degree &&
{e.degree}
}
); } // ---- the body of one section (heading is added by the engine) ---- function SectionBody({ k, d, variant, tpl }) { const def = SECTION_DEFS[k]; if (!def) return null; // graphic templates show a proficiency scale; ATS / list templates stay text-only const graphic = tpl && tpl.skills && tpl.skills !== "list"; const showWord = !!(tpl && tpl.scaleWord); switch (def.kind) { case "text": return

{d.summary}

; case "jobs": { const arr = k === "experience" ? d.experience : d.volunteer; return
{(arr || []).map((j) => )}
; } case "projects": return (
{d.projects.map((p) => (
{p.name}{p.desc ? {p.desc} : null}
))}
); case "edu": return
{d.education.map((e) => )}
; case "skills": return ; case "named": { const arr = d[k] || []; return (
{arr.map((x) => (
{x.name} {(x.issuer || x.year) && {[x.issuer, x.year].filter(Boolean).join(" · ")}}
))}
); } case "leveled": { // languages get the same scale 'vector' on graphic templates; keep the // word too (CEFR-ish proficiency reads clearer than dots alone here) const lkind = (tpl && (tpl.skills === "segments" || tpl.skills === "signal")) ? tpl.skills : "dots"; return (
{d.languages.map((l) => (
{l.name} {graphic ? {l.level && {l.level}} : (l.level && {l.level})}
))}
); } case "pubs": return (
{d.publications.map((p) => (
{p.title}{p.venue ? <>. {p.venue} : null}{p.year ? `, ${p.year}` : ""}.
))}
); case "chips": { const arr = d[k] || []; return
{arr.map((s, i) => {s})}
; } case "refs": if (d.referencesOnRequest && !(d.references && d.references.length)) { return

Available upon request.

; } return (
{(d.references || []).map((r) => (
{r.name}{r.title && {r.title}}{r.contact && {r.contact}}
))}
); default: return null; } } Object.assign(window, { SECTION_DEFS, DEFAULT_ORDER, sectionLabel, sectionHasData, activeSections, contactList, initials, Photo, SkillBar, SkillRing, RSkills, RJob, REdu, SectionBody, lvl, SKILL_MAX, LEVEL_WORDS, LVL_PCT, clampLvl, skillName, normSkill, langTier, ScaleDots, ScaleSegs, ScaleSignal, Scale, ScaleRow, });