/* global React, Icons, SECTION_DEFS, Field, AutoText */ // ============================================================ // Section organizer (drag & drop "segments") + editors for the // extra section types. Lets the user pick, drop, reorder and // remove résumé segments. Order is stored on resume.sectionOrder. // ============================================================ const { useState: useStateSE, useRef: useRefSE } = React; function uidSE() { return Math.random().toString(36).slice(2, 9); } // sensible starter content when a brand-new segment is added const SECTION_STARTERS = { summary: () => ({ summary: "" }), experience: () => ({ experience: [{ id: uidSE(), role: "Role", company: "Company", start: "2024", end: "Present", location: "", bullets: ["Describe what you did and the impact."] }] }), projects: () => ({ projects: [{ id: uidSE(), name: "Project name", desc: "What it is and why it matters." }] }), education: () => ({ education: [{ id: uidSE(), school: "School", degree: "Degree", start: "2020", end: "2024" }] }), skills: () => ({ skills: [{ name: "Skill", level: 3 }] }), certifications: () => ({ certifications: [{ id: uidSE(), name: "Certification", issuer: "Issuer", year: "2024" }] }), awards: () => ({ awards: [{ id: uidSE(), name: "Award", issuer: "", year: "2024" }] }), languages: () => ({ languages: [{ id: uidSE(), name: "Language", level: "Professional" }] }), publications: () => ({ publications: [{ id: uidSE(), title: "Publication title", venue: "Venue", year: "2024" }] }), volunteer: () => ({ volunteer: [{ id: uidSE(), role: "Role", company: "Organization", start: "2022", end: "Present", location: "", bullets: ["What you contributed."] }] }), courses: () => ({ courses: ["Course name"] }), interests: () => ({ interests: ["Interest"] }), references: () => ({ references: [{ id: uidSE(), name: "Reference name", title: "Title, Company", contact: "email@example.com" }] }), }; const DEFAULT_VISIBLE = ["summary", "experience", "projects", "skills", "certifications", "awards", "languages", "education"]; // the order to show when a résumé has no explicit sectionOrder yet: // the default segments that actually have content (matches the preview) function effectiveOrder(resume) { if (resume.sectionOrder && resume.sectionOrder.length) return resume.sectionOrder; const has = window.sectionHasData || (() => true); const base = DEFAULT_VISIBLE.filter((k) => has(k, resume)); return base.length ? base : DEFAULT_VISIBLE.slice(0, 4); } // ---------- the drag & drop organizer ---------- function SectionOrganizer({ resume, setResume }) { const order = effectiveOrder(resume); const setOrder = (o) => setResume((r) => ({ ...r, sectionOrder: o })); const drag = useRefSE(null); const [overKey, setOverKey] = useStateSE(null); const [dropActive, setDropActive] = useStateSE(false); const inactive = Object.keys(SECTION_DEFS).filter((k) => !order.includes(k)); const seed = (k) => { const starter = SECTION_STARTERS[k]; if (!starter) return; setResume((r) => { const empty = (k === "summary") ? !(r.summary && r.summary.trim()) : !(Array.isArray(r[k]) && r[k].length); return empty ? { ...r, ...starter() } : r; }); }; const addSection = (k) => { seed(k); if (!order.includes(k)) setOrder([...order, k]); }; const removeSection = (k) => setOrder(order.filter((x) => x !== k)); const onRowDragStart = (k) => (e) => { drag.current = { k, from: "list" }; e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", k); } catch (_) {} }; const onPalDragStart = (k) => (e) => { drag.current = { k, from: "pal" }; e.dataTransfer.effectAllowed = "copy"; try { e.dataTransfer.setData("text/plain", k); } catch (_) {} }; const onRowDragOver = (k) => (e) => { e.preventDefault(); const d = drag.current; if (!d) return; setOverKey(k); if (d.from !== "list" || d.k === k) return; const o = [...order]; const from = o.indexOf(d.k); const to = o.indexOf(k); if (from < 0 || to < 0) return; o.splice(from, 1); o.splice(to, 0, d.k); setOrder(o); }; const onListDrop = (e) => { e.preventDefault(); const d = drag.current; if (d && d.from === "pal" && !order.includes(d.k)) addSection(d.k); drag.current = null; setOverKey(null); setDropActive(false); }; const onDragEnd = () => { drag.current = null; setOverKey(null); setDropActive(false); }; return (

Drag segments to reorder how they appear on your résumé. Add or remove the ones relevant to you.

{ if (drag.current && drag.current.from === "pal") { e.preventDefault(); setDropActive(true); } }} onDragLeave={(e) => { if (e.currentTarget === e.target) setDropActive(false); }} onDrop={onListDrop} > {order.filter((k) => SECTION_DEFS[k]).map((k) => { const def = SECTION_DEFS[k]; const Ic = Icons[def.icon] || Icons.tag; return (
{def.label} {def.blurb}
); })}
{inactive.length > 0 && ( <>
Add a segment
{inactive.map((k) => { const def = SECTION_DEFS[k]; const Ic = Icons[def.icon] || Icons.tag; return ( ); })}
)}
); } // ---------- editors for the extra section types ---------- function CardRow({ onDelete, children }) { return (
{children}
); } function NamedEditor({ items, onChange, nameLabel, addLabel }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}> set(it.id, { name: v })} />
set(it.id, { issuer: v })} /> set(it.id, { year: v })} mono />
))} ); } function LanguageEditor({ items, onChange }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); const LEVELS = ["Native", "Fluent", "Professional", "Conversational", "Basic"]; return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}>
set(it.id, { name: v })} />
))} ); } function PublicationEditor({ items, onChange }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}> set(it.id, { title: v })} />
set(it.id, { venue: v })} /> set(it.id, { year: v })} mono />
))} ); } function ReferenceEditor({ items, onChange }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}> set(it.id, { name: v })} /> set(it.id, { title: v })} /> set(it.id, { contact: v })} /> ))} ); } function ProjectEditor({ items, onChange }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}> set(it.id, { name: v })} /> ))} ); } function VolunteerEditor({ items, onChange }) { const set = (id, patch) => onChange(items.map((it) => it.id === id ? { ...it, ...patch } : it)); const setBullet = (id, i, v) => { const it = items.find((x) => x.id === id); set(id, { bullets: it.bullets.map((b, j) => j === i ? v : b) }); }; return ( <> {(items || []).map((it) => ( onChange(items.filter((x) => x.id !== it.id))}> set(it.id, { role: v })} />
set(it.id, { company: v })} /> { const [s, e] = v.split("–").map((x) => x.trim()); set(it.id, { start: s || "", end: e || "" }); }} mono />
Highlights {(it.bullets || []).map((b, i) => (
setBullet(it.id, i, v)} placeholder="What you contributed…" />
))}
))} ); } function ChipEditor({ items, onChange, placeholder }) { const [draft, setDraft] = useStateSE(""); const add = () => { const v = draft.trim(); if (v) { onChange([...(items || []), v]); setDraft(""); } }; return ( <>
{(items || []).map((s, i) => ( {s} ))}
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} />
); } // map a section key to its editor (only the "extra" ones — core ones live in Builder) function ExtraSectionEditor({ k, resume, setResume }) { const set = (key, val) => setResume((r) => ({ ...r, [key]: val })); switch (k) { case "projects": return set("projects", v)} />; case "certifications": return set("certifications", v)} nameLabel="Certification" addLabel="Add certification" />; case "awards": return set("awards", v)} nameLabel="Award" addLabel="Add award" />; case "languages": return set("languages", v)} />; case "publications": return set("publications", v)} />; case "volunteer": return set("volunteer", v)} />; case "references": return set("references", v)} />; case "courses": return set("courses", v)} placeholder="Add a course and press Enter" />; case "interests": return set("interests", v)} placeholder="Add an interest and press Enter" />; default: return null; } } const EXTRA_KEYS = ["projects", "certifications", "awards", "languages", "publications", "volunteer", "courses", "interests", "references"]; Object.assign(window, { SectionOrganizer, ExtraSectionEditor, EXTRA_KEYS, DEFAULT_VISIBLE, effectiveOrder });