/* 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 (
);
}
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 });