refactor: drop dead render props, duplicate template tables and unused import helpers

This commit is contained in:
Amruth Pillai
2026-09-29 10:19:02 +02:00
parent a42cacc057
commit 0f6e08a922
16 changed files with 45 additions and 313 deletions
+21 -51
View File
@@ -58,33 +58,13 @@ const NO_BORDERS = {
right: { style: BorderStyle.NONE, size: 0 },
} as const;
// --- Template layout config ---
interface TemplateConfig {
/** Which side the sidebar appears on */
sidebarSide: "left" | "right" | "none";
/** Sidebar background: "solid" = full primary color, "tint" = 20% opacity, "none" = no background */
sidebarBackground: "solid" | "tint" | "none";
/** Where the header is rendered */
headerPosition: "full-width" | "main-only" | "sidebar-only";
}
export const TEMPLATE_CONFIGS: Record<Template, TemplateConfig> = {
azurill: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
bronzor: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
chikorita: { sidebarSide: "right", sidebarBackground: "solid", headerPosition: "main-only" },
ditgar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
ditto: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
gengar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
glalie: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
kakuna: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
lapras: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
leafish: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
meowth: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
onyx: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
pikachu: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "main-only" },
rhyhorn: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
scizor: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
// Sidebar and header placement come from `templateLayouts`, as in the PDF; only the sidebar's background is DOCX's
// own: "solid" fills it with the primary colour (text inverts), "tint" with 20% of it.
const SIDEBAR_BACKGROUND: Partial<Record<Template, "solid" | "tint">> = {
chikorita: "solid",
ditgar: "tint",
gengar: "tint",
glalie: "tint",
};
type PagePlan = { kind: "single"; sections: string[] } | { kind: "split" };
@@ -296,7 +276,7 @@ function buildTwoColumnTable(
sidebarParagraphs: Paragraph[],
sidebarWidthPct: number,
gapXTwips: number,
sidebarSide: "left" | "right" | "none",
sidebarSide: "left" | "right",
sidebarShadingHex?: string,
): Table {
const mainWidthPct = 100 - sidebarWidthPct;
@@ -311,11 +291,8 @@ function buildTwoColumnTable(
const margins: { right?: number; left?: number } = {};
if (sidebarSide === "left") {
margins.right = gapXTwips;
} else if (sidebarSide === "right") {
margins.left = gapXTwips;
}
if (sidebarSide === "left") margins.right = gapXTwips;
else margins.left = gapXTwips;
const sidebarCell = new TableCell({
width: { size: sidebarWidthPct, type: WidthType.PERCENTAGE },
@@ -376,20 +353,13 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
const sidebarWidth = data.metadata.layout.sidebarWidth;
// Template-aware layout config
const templateConfig = TEMPLATE_CONFIGS[data.metadata.template];
// Compute sidebar background shading hex
let sidebarShadingHex: string | undefined;
if (templateConfig.sidebarBackground === "solid") {
sidebarShadingHex = colorHex;
} else if (templateConfig.sidebarBackground === "tint") {
sidebarShadingHex = blendWithWhite(colorHex, 0.2);
}
// Determine sidebar text colors — inverted when sidebar has a solid background
const sidebarTextColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : textColorHex;
const sidebarHeadingColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : colorHex;
const layout = templateLayouts[data.metadata.template];
const background = SIDEBAR_BACKGROUND[data.metadata.template];
const sidebarShadingHex =
background === "solid" ? colorHex : background === "tint" ? blendWithWhite(colorHex, 0.2) : undefined;
// Text on a solid sidebar inverts.
const sidebarTextColorHex = background === "solid" ? bgColorHex : textColorHex;
const sidebarHeadingColorHex = background === "solid" ? bgColorHex : colorHex;
// Configure heading typography for section renderers
const headingFont = data.metadata.typography.heading.fontFamily || "Calibri";
@@ -409,7 +379,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
const showHeader = shouldShowResumeHeader(data);
// Header placement depends on template
if (templateConfig.headerPosition === "full-width" && showHeader) {
if (layout.headerPlacement === "full-width" && showHeader) {
setRenderConfig(mainConfig);
documentChildren.push(...buildHeader(data, colorHex, textColorHex));
}
@@ -428,7 +398,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
setRenderConfig(mainConfig);
const mainParagraphs: Paragraph[] = [];
if (templateConfig.headerPosition === "main-only" && showHeader) {
if (layout.headerPlacement === "main-only" && showHeader) {
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
}
for (const sectionId of layoutPage.main) {
@@ -439,7 +409,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex });
const sidebarParagraphs: Paragraph[] = [];
if (templateConfig.headerPosition === "sidebar-only" && showHeader) {
if (layout.headerPlacement === "sidebar-only" && showHeader) {
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
}
for (const sectionId of layoutPage.sidebar) {
@@ -454,7 +424,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
sidebarWidth,
gapXTwips,
// The side chosen in Design, else the template's own.
data.metadata.layout.sidebarSide ?? templateConfig.sidebarSide,
data.metadata.layout.sidebarSide ?? layout.sidebarSide ?? "left",
sidebarShadingHex,
),
);
@@ -7,8 +7,8 @@ const builderSource = readFileSync(fileURLToPath(new URL("./builder.ts", import.
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');
expect(builderSource).toContain('layout.headerPlacement === "full-width" && showHeader');
expect(builderSource).toContain('layout.headerPlacement === "main-only" && showHeader');
expect(builderSource).toContain('layout.headerPlacement === "sidebar-only" && showHeader');
});
});
+1 -15
View File
@@ -1,19 +1,5 @@
import { describe, expect, it } from "vitest";
import { templateLayouts } from "@reactive-resume/schema/templates";
import { planPageColumns, TEMPLATE_CONFIGS } from "./builder";
describe("DOCX template configs", () => {
it("put the sidebar and header where the PDF does for two-column templates", () => {
for (const [template, layout] of Object.entries(templateLayouts)) {
if (layout.columns !== 2) continue;
const config = TEMPLATE_CONFIGS[template as keyof typeof TEMPLATE_CONFIGS];
expect({ sidebarSide: config.sidebarSide, headerPosition: config.headerPosition }, template).toEqual({
sidebarSide: layout.sidebarSide,
headerPosition: layout.headerPlacement,
});
}
});
});
import { planPageColumns } from "./builder";
describe("planPageColumns", () => {
const page = { fullWidth: false, main: ["experience"], sidebar: ["skills"] };
+1 -61
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { formatDate, formatPeriod, formatSingleDate, toRangeDates, toSingleDates, toYearMonth } from "./date";
import { formatDate, toRangeDates, toSingleDates, toYearMonth } from "./date";
describe("formatDate", () => {
it("formats YYYY-MM as 'Month Year'", () => {
@@ -10,10 +10,6 @@ describe("formatDate", () => {
expect(formatDate("2024-03-15")).toBe("March 2024");
});
it("formats YYYY-MM-DD with includeDay=true as 'Month DD, Year'", () => {
expect(formatDate("2024-03-15", true)).toBe("March 15, 2024");
});
it("returns YYYY unchanged when only year provided", () => {
expect(formatDate("2024")).toBe("2024");
});
@@ -26,68 +22,12 @@ describe("formatDate", () => {
expect(formatDate("2024-12")).toBe("December 2024");
});
it("includes day even with single-digit days", () => {
expect(formatDate("2024-03-05", true)).toBe("March 05, 2024");
});
it("returns 'undefined' month name when out-of-range month is supplied", () => {
// Defensive: behavior documents what happens with bad input
expect(formatDate("2024-13")).toBe("undefined 2024");
});
});
describe("formatPeriod", () => {
it("returns empty string when both dates missing", () => {
expect(formatPeriod()).toBe("");
});
it("returns end date alone when start is missing", () => {
expect(formatPeriod(undefined, "2024-05")).toBe("2024-05");
});
it("returns start - Present when end is missing", () => {
expect(formatPeriod("2024-01")).toBe("January 2024 - Present");
});
it("formats both ends when both provided", () => {
expect(formatPeriod("2020-06", "2024-03")).toBe("June 2020 - March 2024");
});
it("treats empty string start same as undefined", () => {
expect(formatPeriod("", "2024-05")).toBe("2024-05");
});
it("treats empty string end same as undefined", () => {
expect(formatPeriod("2024-01", "")).toBe("January 2024 - Present");
});
it("returns empty when start empty and end empty", () => {
expect(formatPeriod("", "")).toBe("");
});
});
describe("formatSingleDate", () => {
it("returns empty string when date is undefined", () => {
expect(formatSingleDate()).toBe("");
});
it("returns empty string when date is empty string", () => {
expect(formatSingleDate("")).toBe("");
});
it("formats full date with day included", () => {
expect(formatSingleDate("2024-03-15")).toBe("March 15, 2024");
});
it("formats year-month even with includeDay flag implied", () => {
expect(formatSingleDate("2024-03")).toBe("March 2024");
});
it("returns plain year when only year present", () => {
expect(formatSingleDate("2024")).toBe("2024");
});
});
describe("structured dates", () => {
it("keeps the year and month of ISO dates", () => {
expect(toYearMonth("2024-01-15")).toBe("2024-01");
+4 -34
View File
@@ -9,49 +9,19 @@ function getMonthName(month: string | undefined): string {
return fmt.format(new Date(2000, index, 1));
}
/**
* Formats a partial ISO 8601 date string (YYYY, YYYY-MM, or YYYY-MM-DD)
* into a human-readable format like "January 2024" or "January 15, 2024".
*/
export function formatDate(date: string, includeDay = false): string {
/** Formats a partial ISO 8601 date (YYYY, YYYY-MM or YYYY-MM-DD) as "January 2024", or the year alone. */
export function formatDate(date: string): string {
const parts = date.split("-");
if (parts.length >= 2) {
const [year, month] = parts;
const monthName = getMonthName(month);
if (parts.length === 3 && includeDay) {
return `${monthName} ${parts[2]}, ${year}`;
}
return `${monthName} ${year}`;
return `${getMonthName(month)} ${year}`;
}
// YYYY only
return date;
}
/**
* Formats a date range from start and end dates.
* Returns "Start - End", "Start - Present" if no end, or just the end date if no start.
*/
export function formatPeriod(startDate?: string, endDate?: string): string {
if (!startDate && !endDate) return "";
if (!startDate) return endDate || "";
if (!endDate) return `${formatDate(startDate)} - Present`;
return `${formatDate(startDate)} - ${formatDate(endDate)}`;
}
/**
* Formats a single date with day included (e.g., "January 15, 2024").
* Falls back to month-year or year-only for partial dates.
*/
export function formatSingleDate(date?: string): string {
if (!date) return "";
return formatDate(date, true);
}
/** The year and month of an ISO 8601 date ("2024-01-15" → "2024-01"); null when it isn't one. */
export function toYearMonth(date?: string): YearMonth | null {
const match = /^(\d{4})(?:-(0[1-9]|1[0-2]))?/.exec(date?.trim() ?? "");
@@ -59,7 +29,7 @@ export function toYearMonth(date?: string): YearMonth | null {
return match[2] ? `${match[1]}-${match[2]}` : match[1];
}
/** Structured dates for a range of ISO dates; a start with no end is ongoing, as `formatPeriod` prints it. */
/** Structured dates for a range of ISO dates; a start with no end is ongoing. */
export function toRangeDates(startDate?: string, endDate?: string): ResumeDates {
const start = toYearMonth(startDate);
const end = toYearMonth(endDate);
+1 -1
View File
@@ -91,7 +91,7 @@ describe("parseJSONResume", () => {
expect(item.company).toBe("Acme Corp");
expect(item.position).toBe("Senior Engineer");
expect(item.location).toBe("Berlin");
expect(item.period.length).toBeGreaterThan(0);
expect(item.dates?.start).toBeTruthy();
expect(item.description).toContain("Shipped X");
});
+10 -11
View File
@@ -1,10 +1,10 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { ZodError, z } from "zod";
import { z } from "zod";
import { getNetworkIcon } from "@reactive-resume/resume/icons";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { generateId } from "@reactive-resume/utils/string";
import { formatPeriod, formatSingleDate, toRangeDates, toSingleDates } from "./date";
import { toRangeDates, toSingleDates } from "./date";
import { rethrowAsImportError } from "./error";
import { arrayToHtmlList, toHtmlDescription } from "./html";
import { parseLevel } from "./level";
@@ -220,7 +220,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
company: work.name || "",
position: work.position || "",
location: work.location || "",
period: formatPeriod(work.startDate, work.endDate),
period: "",
dates: toRangeDates(work.startDate, work.endDate),
website: createItemWebsite(work.url),
roles: [],
@@ -243,7 +243,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
area: edu.area || "",
grade: edu.score || "",
location: "",
period: formatPeriod(edu.startDate, edu.endDate),
period: "",
dates: toRangeDates(edu.startDate, edu.endDate),
website: createItemWebsite(edu.url),
description: edu.courses && edu.courses.length > 0 ? arrayToHtmlList(edu.courses) : "",
@@ -261,7 +261,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
id: generateId(),
hidden: false,
name: project.name || "",
period: formatPeriod(project.startDate, project.endDate),
period: "",
dates: toRangeDates(project.startDate, project.endDate),
website: createItemWebsite(project.url),
description: toHtmlDescription(project.description, project.highlights),
@@ -332,7 +332,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
hidden: false,
title: award.title || "",
awarder: award.awarder || "",
date: formatSingleDate(award.date),
date: "",
dates: toSingleDates(award.date),
website: createItemWebsite(),
description: award.summary ? `<p>${award.summary}</p>` : "",
@@ -351,7 +351,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
hidden: false,
title: cert.name || "",
issuer: cert.issuer || "",
date: formatSingleDate(cert.date),
date: "",
dates: toSingleDates(cert.date),
website: createItemWebsite(cert.url),
description: "",
@@ -370,7 +370,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
hidden: false,
title: pub.name || "",
publisher: pub.publisher || "",
date: formatSingleDate(pub.releaseDate),
date: "",
dates: toSingleDates(pub.releaseDate),
website: createItemWebsite(pub.url),
description: pub.summary ? `<p>${pub.summary}</p>` : "",
@@ -389,7 +389,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData {
hidden: false,
organization: vol.organization || "",
location: "",
period: formatPeriod(vol.startDate, vol.endDate),
period: "",
dates: toRangeDates(vol.startDate, vol.endDate),
website: createItemWebsite(vol.url),
description: toHtmlDescription(vol.summary, vol.highlights),
@@ -441,7 +441,6 @@ export function parseJSONResume(json: string): ResumeData {
const jsonResume = jsonResumeSchema.parse(JSON.parse(json));
return convertJSONResume(jsonResume);
} catch (error) {
if (error instanceof ZodError) rethrowAsImportError(error);
throw error;
rethrowAsImportError(error);
}
}
+1 -3
View File
@@ -1,5 +1,4 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { ZodError } from "zod";
import { resumeDataSchema, sectionTypeSchema } from "@reactive-resume/schema/resume/data";
import { rethrowAsImportError } from "./error";
@@ -69,7 +68,6 @@ export function parseReactiveResumeJSON(json: string): ResumeData {
const parsed = resumeDataSchema.parse(JSON.parse(json));
return resumeDataSchema.parse(normalizeBuiltInSectionsInLayout(parsed));
} catch (error) {
if (error instanceof ZodError) rethrowAsImportError(error);
throw error;
rethrowAsImportError(error);
}
}
-4
View File
@@ -44,7 +44,6 @@ export const ResumeDocument = ({
semanticRuntime,
}: ResumeDocumentProps) => {
const TemplatePageComponent = getTemplatePage(template);
const creationDate = useMemo(() => new Date(), []);
const hasCjkContent = useMemo(() => resumeContentContainsCJK(data), [data]);
const scripts = useMemo(() => resumeContentScripts(data), [data]);
const typography = useMemo(
@@ -85,9 +84,6 @@ export const ResumeDocument = ({
renderOptions={renderOptions}
>
<Document
pageMode="useNone"
creationDate={creationDate}
producer="Reactive Resume"
title={resumeData.basics.name}
author={resumeData.basics.name}
creator={resumeData.basics.name}
+1 -2
View File
@@ -59,8 +59,7 @@ export function imageSources(tree: HostNode[]): string[] {
if ("text" in node) return;
if (node.type === HOST.image) {
const { src } = node.props;
const url = typeof src === "string" ? src : (src as { uri?: unknown } | undefined)?.uri;
if (typeof url === "string" && url.length > 0) sources.add(url);
if (typeof src === "string" && src.length > 0) sources.add(src);
}
node.children.forEach(visit);
};
+1 -9
View File
@@ -21,9 +21,6 @@ type FlowProps = {
fixed?: boolean | undefined;
/** react-pdf only; Forme has no equivalent, so it's accepted and ignored. */
minPresenceAhead?: number | undefined;
debug?: boolean | undefined;
id?: string | undefined;
bookmark?: string | undefined;
children?: ReactNode;
} & DataAttributes;
@@ -37,11 +34,9 @@ export type TextProps = FlowProps & {
export type LinkProps = TextProps & { src: string };
export type ImageProps = {
src: string | { uri: string } | undefined;
src: string | undefined;
style?: StyleProp | undefined;
fixed?: boolean | undefined;
debug?: boolean | undefined;
cache?: boolean | undefined;
} & DataAttributes;
export type SvgProps = {
@@ -68,12 +63,9 @@ export type DocumentProps = {
author?: string | undefined;
subject?: string | undefined;
creator?: string | undefined;
producer?: string | undefined;
language?: string | undefined;
/** "auto" hyphenates words by the document language; "manual" breaks only at soft hyphens. */
hyphenation?: "auto" | "manual" | undefined;
creationDate?: Date | undefined;
pageMode?: string | undefined;
children?: ReactNode;
};
-3
View File
@@ -51,8 +51,5 @@ export function pdf(element: ReactElement) {
const [root] = renderHostTree(element);
return {
container: { document: root ? toRenderedNode(root) : null },
toBuffer: () => renderToBuffer(element),
toBlob: async () =>
new Blob([(await renderToBuffer(element)) as Uint8Array<ArrayBuffer>], { type: "application/pdf" }),
};
}
+1 -6
View File
@@ -180,11 +180,7 @@ const flowStyle = (props: Record<string, unknown>, style: FormeStyle): FormeStyl
return next;
};
const imageSource = (src: unknown): string | undefined => {
if (typeof src === "string") return src;
if (src && typeof src === "object" && "uri" in src && typeof src.uri === "string") return src.uri;
return undefined;
};
const imageSource = (src: unknown): string | undefined => (typeof src === "string" ? src : undefined);
const number = (value: unknown, context: Context): number | undefined => {
const points = toPoints(value, context);
@@ -418,7 +414,6 @@ function convertNode(node: HostNode, parentContext: Context, key: number): React
{
key,
style: bordered.style,
...(typeof props.bookmark === "string" ? { bookmark: props.bookmark } : {}),
},
...bordered.children,
);
-1
View File
@@ -12,7 +12,6 @@
"./markdown": "./src/markdown.ts",
"./patch": "./src/patch.ts",
"./proposals": "./src/proposals.ts",
"./section-availability": "./src/section-availability.ts",
"./section-sort": "./src/section-sort.ts",
"./social-meta": "./src/social-meta.ts",
"./stylesheet": "./src/stylesheet/index.ts",
@@ -1,70 +0,0 @@
import { describe, expect, it } from "vitest";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
import { getSectionAvailability } from "./section-availability";
describe("section availability", () => {
it("includes every printable section and records every authored location", () => {
const data = structuredClone(sampleResumeData);
const customSection = data.customSections[0];
const laterPage = data.metadata.layout.pages[2];
if (!customSection || !laterPage) throw new Error("Sample resume lacks expected section fixtures.");
const customSectionId = customSection.id;
laterPage.sidebar.push(customSectionId, customSectionId, "unknown-section");
const availability = getSectionAvailability(data);
const sectionIds = availability.map((entry) => entry.sectionId);
expect(sectionIds).toEqual([
"summary",
...Object.keys(data.sections),
...data.customSections.map((section) => section.id),
]);
expect(sectionIds).not.toContain("picture");
expect(sectionIds).not.toContain("basics");
expect(sectionIds).not.toContain("custom");
expect(sectionIds).not.toContain("unknown-section");
expect(availability.find((entry) => entry.sectionId === customSectionId)?.locations).toEqual([
{ pageIndex: 1, columnId: "main" },
{ pageIndex: 2, columnId: "sidebar" },
{ pageIndex: 2, columnId: "sidebar" },
]);
});
it("derives hidden and placement state independently without mutation", () => {
const data = structuredClone(sampleResumeData);
data.sections.experience.hidden = true;
data.sections.awards.items = [];
for (const page of data.metadata.layout.pages) {
page.main = page.main.filter((id) => id !== "experience");
page.sidebar = page.sidebar.filter((id) => id !== "experience");
page.main = page.main.filter((id) => id !== "projects");
page.sidebar = page.sidebar.filter((id) => id !== "projects");
}
data.summary.hidden = true;
const before = structuredClone(data);
const availability = getSectionAvailability(data);
expect(availability.find((entry) => entry.sectionId === "experience")).toEqual({
sectionId: "experience",
hidden: true,
locations: [],
});
expect(availability.find((entry) => entry.sectionId === "summary")).toEqual({
sectionId: "summary",
hidden: true,
locations: [{ pageIndex: 0, columnId: "main" }],
});
expect(availability.find((entry) => entry.sectionId === "projects")).toEqual({
sectionId: "projects",
hidden: false,
locations: [],
});
expect(availability.find((entry) => entry.sectionId === "awards")).toEqual({
sectionId: "awards",
hidden: false,
locations: [{ pageIndex: 1, columnId: "main" }],
});
expect(data).toEqual(before);
});
});
@@ -1,39 +0,0 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
export type SectionLocation = {
pageIndex: number;
columnId: "main" | "sidebar";
};
export type SectionAvailability = {
sectionId: string;
hidden: boolean;
locations: SectionLocation[];
};
function getSectionLocations(data: ResumeData, sectionId: string): SectionLocation[] {
const locations: SectionLocation[] = [];
for (const [pageIndex, page] of data.metadata.layout.pages.entries()) {
for (const columnId of ["main", "sidebar"] as const) {
for (const id of page[columnId]) {
if (id === sectionId) locations.push({ pageIndex, columnId });
}
}
}
return locations;
}
export function getSectionAvailability(data: ResumeData): SectionAvailability[] {
const sections = [
{ sectionId: "summary", hidden: data.summary.hidden },
...Object.entries(data.sections).map(([sectionId, section]) => ({ sectionId, hidden: section.hidden })),
...data.customSections.map((section) => ({ sectionId: section.id, hidden: section.hidden })),
];
return sections.map((section) => ({
...section,
locations: getSectionLocations(data, section.sectionId),
}));
}