mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 23:32:19 +10:00
fix: render cover letter exports without resume chrome
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
WidthType,
|
WidthType,
|
||||||
} from "docx";
|
} from "docx";
|
||||||
import { parseColorString } from "@reactive-resume/utils/color";
|
import { parseColorString } from "@reactive-resume/utils/color";
|
||||||
|
import { shouldShowResumeHeader } from "./cover-letter";
|
||||||
import { toSafeDocxLink } from "./link-utils";
|
import { toSafeDocxLink } from "./link-utils";
|
||||||
import { renderBuiltInSection, renderCustomSection, renderSummary, setRenderConfig } from "./section-renderers";
|
import { renderBuiltInSection, renderCustomSection, renderSummary, setRenderConfig } from "./section-renderers";
|
||||||
|
|
||||||
@@ -413,9 +414,10 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
|
|||||||
textColorHex,
|
textColorHex,
|
||||||
primaryColorHex: colorHex,
|
primaryColorHex: colorHex,
|
||||||
};
|
};
|
||||||
|
const showHeader = shouldShowResumeHeader(data);
|
||||||
|
|
||||||
// Header placement depends on template
|
// Header placement depends on template
|
||||||
if (templateConfig.headerPosition === "full-width") {
|
if (templateConfig.headerPosition === "full-width" && showHeader) {
|
||||||
setRenderConfig(mainConfig);
|
setRenderConfig(mainConfig);
|
||||||
documentChildren.push(...buildHeader(data, colorHex, textColorHex));
|
documentChildren.push(...buildHeader(data, colorHex, textColorHex));
|
||||||
}
|
}
|
||||||
@@ -434,7 +436,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
|
|||||||
setRenderConfig(mainConfig);
|
setRenderConfig(mainConfig);
|
||||||
|
|
||||||
const mainParagraphs: Paragraph[] = [];
|
const mainParagraphs: Paragraph[] = [];
|
||||||
if (templateConfig.headerPosition === "main-only") {
|
if (templateConfig.headerPosition === "main-only" && showHeader) {
|
||||||
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
|
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
|
||||||
}
|
}
|
||||||
for (const sectionId of layoutPage.main) {
|
for (const sectionId of layoutPage.main) {
|
||||||
@@ -445,7 +447,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
|
|||||||
setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex });
|
setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex });
|
||||||
|
|
||||||
const sidebarParagraphs: Paragraph[] = [];
|
const sidebarParagraphs: Paragraph[] = [];
|
||||||
if (templateConfig.headerPosition === "sidebar-only") {
|
if (templateConfig.headerPosition === "sidebar-only" && showHeader) {
|
||||||
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
|
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
|
||||||
}
|
}
|
||||||
for (const sectionId of layoutPage.sidebar) {
|
for (const sectionId of layoutPage.sidebar) {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const builderSource = readFileSync(fileURLToPath(new URL("./builder.ts", import.meta.url)), "utf8");
|
||||||
|
|
||||||
|
describe("cover letter DOCX layout", () => {
|
||||||
|
it("gates resume headers for cover-letter-only documents", () => {
|
||||||
|
expect(builderSource).toContain("shouldShowResumeHeader(data)");
|
||||||
|
expect(builderSource).toContain('templateConfig.headerPosition === "full-width" && showHeader');
|
||||||
|
expect(builderSource).toContain('templateConfig.headerPosition === "main-only" && showHeader');
|
||||||
|
expect(builderSource).toContain('templateConfig.headerPosition === "sidebar-only" && showHeader');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||||
|
|
||||||
|
const isCoverLetterSection = (data: ResumeData, sectionId: string) => {
|
||||||
|
const section = data.customSections.find((customSection) => customSection.id === sectionId);
|
||||||
|
|
||||||
|
return section?.type === "cover-letter" && !section.hidden && section.items.some((item) => !item.hidden);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCoverLetterOnlyDocument = (data: ResumeData) => {
|
||||||
|
const sectionIds = data.metadata.layout.pages.flatMap((page) => [...page.main, ...page.sidebar]);
|
||||||
|
|
||||||
|
return sectionIds.length > 0 && sectionIds.every((sectionId) => isCoverLetterSection(data, sectionId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldShowResumeHeader = (data: ResumeData) => !isCoverLetterOnlyDocument(data);
|
||||||
@@ -150,6 +150,7 @@ describe("renderCustomSection", () => {
|
|||||||
const section: CustomSection = {
|
const section: CustomSection = {
|
||||||
...baseCustom,
|
...baseCustom,
|
||||||
type: "cover-letter",
|
type: "cover-letter",
|
||||||
|
title: "Cover Letter",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "x",
|
id: "x",
|
||||||
@@ -160,8 +161,7 @@ describe("renderCustomSection", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const paragraphs = renderCustomSection(section, HEX);
|
const paragraphs = renderCustomSection(section, HEX);
|
||||||
// 1 heading + at least two paragraphs (recipient + body)
|
expect(paragraphs).toHaveLength(2);
|
||||||
expect(paragraphs.length).toBeGreaterThanOrEqual(3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ export function renderCustomSection(section: CustomSection, colorHex: string): P
|
|||||||
|
|
||||||
// Cover letter type — render recipient + content
|
// Cover letter type — render recipient + content
|
||||||
if (sectionType === "cover-letter") {
|
if (sectionType === "cover-letter") {
|
||||||
const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)];
|
const paragraphs: Paragraph[] = [];
|
||||||
for (const item of visibleItems) {
|
for (const item of visibleItems) {
|
||||||
if ("recipient" in item && item.recipient) {
|
if ("recipient" in item && item.recipient) {
|
||||||
paragraphs.push(...htmlToParagraphs(item.recipient, getHtmlStyle()));
|
paragraphs.push(...htmlToParagraphs(item.recipient, getHtmlStyle()));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -64,7 +65,7 @@ export const AzurillPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -76,7 +77,7 @@ export const BronzorPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sections = getBronzorSections({ mainSections, sidebarSections, fullWidth: page.fullWidth });
|
const sections = getBronzorSections({ mainSections, sidebarSections, fullWidth: page.fullWidth });
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -54,7 +55,7 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const hasPicture = hasTemplatePicture(picture);
|
const hasPicture = hasTemplatePicture(picture);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { basename } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const pageFiles = [
|
||||||
|
"azurill/AzurillPage.tsx",
|
||||||
|
"bronzor/BronzorPage.tsx",
|
||||||
|
"chikorita/ChikoritaPage.tsx",
|
||||||
|
"ditgar/DitgarPage.tsx",
|
||||||
|
"ditto/DittoPage.tsx",
|
||||||
|
"gengar/GengarPage.tsx",
|
||||||
|
"glalie/GlaliePage.tsx",
|
||||||
|
"kakuna/KakunaPage.tsx",
|
||||||
|
"lapras/LaprasPage.tsx",
|
||||||
|
"leafish/LeafishPage.tsx",
|
||||||
|
"meowth/MeowthPage.tsx",
|
||||||
|
"onyx/OnyxPage.tsx",
|
||||||
|
"pikachu/PikachuPage.tsx",
|
||||||
|
"rhyhorn/RhyhornPage.tsx",
|
||||||
|
"scizor/ScizorPage.tsx",
|
||||||
|
];
|
||||||
|
|
||||||
|
const readTemplate = (file: string) => {
|
||||||
|
const path = fileURLToPath(new URL(file, import.meta.url));
|
||||||
|
return readFileSync(path, "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("cover letter PDF layout", () => {
|
||||||
|
it.each(pageFiles)("%s suppresses the resume header for cover-letter-only documents", (file) => {
|
||||||
|
const source = readTemplate(file);
|
||||||
|
|
||||||
|
expect(source, basename(file)).toContain('from "../shared/cover-letter"');
|
||||||
|
expect(source, basename(file)).toContain("shouldShowResumeHeader(data, pageIndex)");
|
||||||
|
expect(source, basename(file)).not.toContain("const showHeader = pageIndex === 0;");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
@@ -64,7 +65,7 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const showSidebar = !page.fullWidth || showHeader;
|
const showSidebar = !page.fullWidth || showHeader;
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -59,7 +60,7 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const hasPicture = hasTemplatePicture(picture);
|
const hasPicture = hasTemplatePicture(picture);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
@@ -59,7 +60,7 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const showSidebar = !page.fullWidth || showHeader;
|
const showSidebar = !page.fullWidth || showHeader;
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -61,7 +62,7 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const showSidebar = !page.fullWidth || showHeader;
|
const showSidebar = !page.fullWidth || showHeader;
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -52,7 +53,7 @@ export const KakunaPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -51,7 +52,7 @@ export const LaprasPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -57,7 +58,7 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data).filter((section) => section !== "summary");
|
const mainSections = filterSections(page.main, data).filter((section) => section !== "summary");
|
||||||
const sidebarSections = filterSections(page.sidebar, data).filter((section) => section !== "summary");
|
const sidebarSections = filterSections(page.sidebar, data).filter((section) => section !== "summary");
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -56,7 +57,7 @@ export const MeowthPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -51,7 +52,7 @@ export const OnyxPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -61,7 +62,7 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const showSidebar = !page.fullWidth;
|
const showSidebar = !page.fullWidth;
|
||||||
const hasPicture = hasTemplatePicture(picture);
|
const hasPicture = hasTemplatePicture(picture);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -54,7 +55,7 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = filterSections(page.sidebar, data);
|
const sidebarSections = filterSections(page.sidebar, data);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WebsiteContactItem,
|
WebsiteContactItem,
|
||||||
} from "../shared/contact-item";
|
} from "../shared/contact-item";
|
||||||
import { TemplateProvider } from "../shared/context";
|
import { TemplateProvider } from "../shared/context";
|
||||||
|
import { shouldShowResumeHeader } from "../shared/cover-letter";
|
||||||
import { filterSections } from "../shared/filtering";
|
import { filterSections } from "../shared/filtering";
|
||||||
import { getTemplateMetrics } from "../shared/metrics";
|
import { getTemplateMetrics } from "../shared/metrics";
|
||||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||||
@@ -52,7 +53,7 @@ export const ScizorPage = ({ page, pageIndex }: TemplatePageProps) => {
|
|||||||
const metrics = getTemplateMetrics(metadata.page);
|
const metrics = getTemplateMetrics(metadata.page);
|
||||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||||
const showHeader = pageIndex === 0;
|
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||||
const mainSections = filterSections(page.main, data);
|
const mainSections = filterSections(page.main, data);
|
||||||
const sidebarSections = page.fullWidth ? [] : filterSections(page.sidebar, data);
|
const sidebarSections = page.fullWidth ? [] : filterSections(page.sidebar, data);
|
||||||
const sections = [...mainSections, ...sidebarSections];
|
const sections = [...mainSections, ...sidebarSections];
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||||
|
import { shouldShowResumeHeader } from "./cover-letter";
|
||||||
|
|
||||||
|
const createCoverLetterOnlyData = () => {
|
||||||
|
const data = structuredClone(sampleResumeData);
|
||||||
|
const coverLetter = data.customSections.find((section) => section.type === "cover-letter");
|
||||||
|
|
||||||
|
if (!coverLetter) throw new Error("sample resume must include a cover letter");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customSections: [coverLetter],
|
||||||
|
metadata: {
|
||||||
|
...data.metadata,
|
||||||
|
layout: {
|
||||||
|
...data.metadata.layout,
|
||||||
|
pages: [{ fullWidth: true, main: [coverLetter.id], sidebar: [] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("shouldShowResumeHeader", () => {
|
||||||
|
it("hides the header when every visible layout section is a cover letter", () => {
|
||||||
|
expect(shouldShowResumeHeader(createCoverLetterOnlyData(), 0)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the first-page header for normal resume documents", () => {
|
||||||
|
expect(shouldShowResumeHeader(sampleResumeData, 0)).toBe(true);
|
||||||
|
expect(shouldShowResumeHeader(sampleResumeData, 1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||||
|
import { filterSections } from "./filtering";
|
||||||
|
|
||||||
|
const isCoverLetterSection = (data: ResumeData, sectionId: string) =>
|
||||||
|
data.customSections.some((section) => section.id === sectionId && section.type === "cover-letter");
|
||||||
|
|
||||||
|
const isCoverLetterOnlyDocument = (data: ResumeData) => {
|
||||||
|
const visibleSections = data.metadata.layout.pages.flatMap((page) =>
|
||||||
|
filterSections([...page.main, ...page.sidebar], data),
|
||||||
|
);
|
||||||
|
|
||||||
|
return visibleSections.length > 0 && visibleSections.every((sectionId) => isCoverLetterSection(data, sectionId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldShowResumeHeader = (data: ResumeData, pageIndex: number) =>
|
||||||
|
pageIndex === 0 && !isCoverLetterOnlyDocument(data);
|
||||||
@@ -50,4 +50,13 @@ describe("buildMarkdown", () => {
|
|||||||
expect(cover.length).toBeGreaterThan(0);
|
expect(cover.length).toBeGreaterThan(0);
|
||||||
expect(cover).not.toBe(md);
|
expect(cover).not.toBe(md);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the cover-letter scope without resume header or section heading", () => {
|
||||||
|
const cover = buildMarkdown(getResumeExportData(sampleResumeData, "cover-letter"));
|
||||||
|
|
||||||
|
expect(cover).toContain("Dear Hiring Manager");
|
||||||
|
expect(cover).not.toContain(`# ${sampleResumeData.basics.name}`);
|
||||||
|
expect(cover).not.toContain(`_${sampleResumeData.basics.headline}_`);
|
||||||
|
expect(cover).not.toContain("## Cover Letter");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,13 +9,24 @@ type Sections = ResumeData["sections"];
|
|||||||
*/
|
*/
|
||||||
export type SectionTitleResolver = (sectionId: string) => string | undefined;
|
export type SectionTitleResolver = (sectionId: string) => string | undefined;
|
||||||
|
|
||||||
|
function isCoverLetterSection(data: ResumeData, sectionId: string): boolean {
|
||||||
|
const section = data.customSections.find((customSection) => customSection.id === sectionId);
|
||||||
|
return section?.type === "cover-letter" && visibleItems(section).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCoverLetterOnlyDocument(data: ResumeData): boolean {
|
||||||
|
const sectionIds = data.metadata.layout.pages.flatMap((page) => [...page.main, ...page.sidebar]);
|
||||||
|
|
||||||
|
return sectionIds.length > 0 && sectionIds.every((sectionId) => isCoverLetterSection(data, sectionId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes resume data to Markdown — a compact, readable format well suited for feeding
|
* Serializes resume data to Markdown — a compact, readable format well suited for feeding
|
||||||
* into AI agents. Scope the input first with `getResumeExportData(data, target)` to emit only
|
* into AI agents. Scope the input first with `getResumeExportData(data, target)` to emit only
|
||||||
* the resume or the cover letter.
|
* the resume or the cover letter.
|
||||||
*/
|
*/
|
||||||
export function buildMarkdown(data: ResumeData, resolveTitle?: SectionTitleResolver): string {
|
export function buildMarkdown(data: ResumeData, resolveTitle?: SectionTitleResolver): string {
|
||||||
const blocks: string[] = [...renderHeader(data.basics)];
|
const blocks: string[] = isCoverLetterOnlyDocument(data) ? [] : [...renderHeader(data.basics)];
|
||||||
|
|
||||||
for (const page of data.metadata.layout.pages) {
|
for (const page of data.metadata.layout.pages) {
|
||||||
for (const sectionId of [...page.main, ...page.sidebar]) {
|
for (const sectionId of [...page.main, ...page.sidebar]) {
|
||||||
@@ -274,7 +285,7 @@ function renderCustomSection(section: CustomSection): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sectionType === "cover-letter") {
|
if (sectionType === "cover-letter") {
|
||||||
const blocks = [heading(section.title)];
|
const blocks: string[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if ("recipient" in item && item.recipient) blocks.push(htmlToMarkdown(item.recipient));
|
if ("recipient" in item && item.recipient) blocks.push(htmlToMarkdown(item.recipient));
|
||||||
if ("content" in item && item.content) blocks.push(htmlToMarkdown(item.content));
|
if ("content" in item && item.content) blocks.push(htmlToMarkdown(item.content));
|
||||||
|
|||||||
Reference in New Issue
Block a user