156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
import { joinLines, splitLines } from "./profileCv";
|
|
import { TailoredCvDraft } from "./types";
|
|
|
|
const DEFAULT_SECTION_ORDER = ["summary", "skills", "experience", "education", "custom"];
|
|
|
|
export function emptyTailoredCvDraft(): TailoredCvDraft {
|
|
return {
|
|
templateId: "ats-minimal",
|
|
headline: "",
|
|
summary: [],
|
|
selectedSkills: [],
|
|
experience: [],
|
|
education: [],
|
|
customSections: [],
|
|
renderOptions: {
|
|
showPhoto: false,
|
|
pageMode: "one-page",
|
|
accentColor: "slate",
|
|
sectionOrder: DEFAULT_SECTION_ORDER,
|
|
bulletDensity: "balanced",
|
|
},
|
|
status: "empty",
|
|
renderedText: "",
|
|
isLegacyFallback: false,
|
|
};
|
|
}
|
|
|
|
function formatDateRange(start?: string | null, end?: string | null, isCurrent?: boolean) {
|
|
const normalizedStart = start?.trim();
|
|
const normalizedEnd = end?.trim();
|
|
if (!normalizedStart && !normalizedEnd) return "";
|
|
if (!normalizedStart) return normalizedEnd ?? "";
|
|
return `${normalizedStart} - ${isCurrent ? "Present" : normalizedEnd || "Present"}`;
|
|
}
|
|
|
|
export function renderTailoredCvDraftText(source?: Partial<TailoredCvDraft> | null) {
|
|
const draft = emptyTailoredCvDraft();
|
|
const sourceSummary = source?.summary;
|
|
const sourceSelectedSkills = source?.selectedSkills;
|
|
const sourceExperience = source?.experience;
|
|
const sourceEducation = source?.education;
|
|
const sourceCustomSections = source?.customSections;
|
|
|
|
const summary = Array.isArray(sourceSummary) ? sourceSummary.filter(Boolean) : [];
|
|
const selectedSkills = Array.isArray(sourceSelectedSkills) ? sourceSelectedSkills.filter(Boolean) : [];
|
|
const experience = Array.isArray(sourceExperience) ? sourceExperience.filter(Boolean) : [];
|
|
const education = Array.isArray(sourceEducation) ? sourceEducation.filter(Boolean) : [];
|
|
const customSections = Array.isArray(sourceCustomSections) ? sourceCustomSections.filter(Boolean) : [];
|
|
|
|
const normalized = {
|
|
...draft,
|
|
...source,
|
|
summary,
|
|
selectedSkills,
|
|
experience,
|
|
education,
|
|
customSections,
|
|
};
|
|
|
|
const sections: string[] = [];
|
|
if (normalized.headline?.trim()) {
|
|
sections.push(normalized.headline.trim());
|
|
}
|
|
if (normalized.summary.length) {
|
|
sections.push(`Professional Summary\n${normalized.summary.map((item) => `- ${item.trim()}`).join("\n")}`);
|
|
}
|
|
if (normalized.selectedSkills.length) {
|
|
sections.push(`Core Skills\n${normalized.selectedSkills.map((item) => item.trim()).join("\n")}`);
|
|
}
|
|
if (normalized.experience.length) {
|
|
const body = normalized.experience.map((item) => {
|
|
const header = [item.title, item.company, item.location, formatDateRange(item.start, item.end, item.isCurrent)]
|
|
.map((value) => value?.trim())
|
|
.filter(Boolean)
|
|
.join(" | ");
|
|
const bullets = (item.bullets ?? []).filter(Boolean).map((bullet) => `- ${bullet.trim()}`).join("\n");
|
|
return [header, bullets].filter(Boolean).join("\n");
|
|
}).filter(Boolean).join("\n\n");
|
|
if (body) sections.push(`Experience\n${body}`);
|
|
}
|
|
if (normalized.education.length) {
|
|
const body = normalized.education.map((item) => {
|
|
const header = [item.qualification, item.institution, item.location, formatDateRange(item.start, item.end, false)]
|
|
.map((value) => value?.trim())
|
|
.filter(Boolean)
|
|
.join(" | ");
|
|
const details = (item.details ?? []).filter(Boolean).map((detail) => `- ${detail.trim()}`).join("\n");
|
|
return [header, details].filter(Boolean).join("\n");
|
|
}).filter(Boolean).join("\n\n");
|
|
if (body) sections.push(`Education\n${body}`);
|
|
}
|
|
normalized.customSections.forEach((section) => {
|
|
const title = section.title?.trim() || "Additional Information";
|
|
const items = (section.items ?? []).filter(Boolean).map((item) => item.trim()).join("\n");
|
|
if (items) sections.push(`${title}\n${items}`);
|
|
});
|
|
|
|
return sections.join("\n\n").trim();
|
|
}
|
|
|
|
export function normalizeTailoredCvDraft(source?: Partial<TailoredCvDraft> | null): TailoredCvDraft {
|
|
const empty = emptyTailoredCvDraft();
|
|
const sourceSummary = source?.summary;
|
|
const sourceSelectedSkills = source?.selectedSkills;
|
|
const sourceExperience = source?.experience;
|
|
const sourceEducation = source?.education;
|
|
const sourceCustomSections = source?.customSections;
|
|
const sourceRenderOptions = source?.renderOptions;
|
|
const sourceSectionOrder = sourceRenderOptions?.sectionOrder;
|
|
|
|
const normalized: TailoredCvDraft = {
|
|
...empty,
|
|
...source,
|
|
templateId: source?.templateId?.trim() || empty.templateId,
|
|
headline: source?.headline ?? "",
|
|
summary: Array.isArray(sourceSummary) ? sourceSummary.filter(Boolean) : [],
|
|
selectedSkills: Array.isArray(sourceSelectedSkills) ? sourceSelectedSkills.filter(Boolean) : [],
|
|
experience: Array.isArray(sourceExperience) ? sourceExperience.map((item) => ({
|
|
title: item?.title ?? "",
|
|
company: item?.company ?? "",
|
|
location: item?.location ?? "",
|
|
start: item?.start ?? "",
|
|
end: item?.end ?? "",
|
|
isCurrent: Boolean(item?.isCurrent),
|
|
bullets: Array.isArray(item?.bullets) ? item.bullets.filter(Boolean) : [],
|
|
})) : [],
|
|
education: Array.isArray(sourceEducation) ? sourceEducation.map((item) => ({
|
|
qualification: item?.qualification ?? "",
|
|
institution: item?.institution ?? "",
|
|
location: item?.location ?? "",
|
|
start: item?.start ?? "",
|
|
end: item?.end ?? "",
|
|
details: Array.isArray(item?.details) ? item.details.filter(Boolean) : [],
|
|
})) : [],
|
|
customSections: Array.isArray(sourceCustomSections) ? sourceCustomSections.map((item) => ({
|
|
title: item?.title ?? "",
|
|
items: Array.isArray(item?.items) ? item.items.filter(Boolean) : [],
|
|
})) : [],
|
|
renderOptions: {
|
|
...empty.renderOptions,
|
|
...sourceRenderOptions,
|
|
sectionOrder: Array.isArray(sourceSectionOrder) && sourceSectionOrder.length > 0
|
|
? sourceSectionOrder.filter(Boolean)
|
|
: DEFAULT_SECTION_ORDER,
|
|
},
|
|
status: source?.status?.trim() || empty.status,
|
|
renderedText: "",
|
|
isLegacyFallback: Boolean(source?.isLegacyFallback),
|
|
};
|
|
|
|
normalized.renderedText = renderTailoredCvDraftText(normalized);
|
|
return normalized;
|
|
}
|
|
|
|
export { joinLines, splitLines };
|