diff --git a/apps/web/src/components/input/rich-input.tsx b/apps/web/src/components/input/rich-input.tsx
index 3bcbe36e2..38090f332 100644
--- a/apps/web/src/components/input/rich-input.tsx
+++ b/apps/web/src/components/input/rich-input.tsx
@@ -104,8 +104,6 @@ export function RichInput({ value, onChange, style, className, editorClassName,
const textDirection = isRTL(i18n.locale) ? "rtl" : undefined;
const [isFullscreen, setIsFullscreen] = useState(false);
- console.log(value);
-
const editor = useEditor({
...options,
extensions,
diff --git a/packages/pdf/src/templates/shared/filtering.test.ts b/packages/pdf/src/templates/shared/filtering.test.ts
index 1c51a903d..48efce041 100644
--- a/packages/pdf/src/templates/shared/filtering.test.ts
+++ b/packages/pdf/src/templates/shared/filtering.test.ts
@@ -27,6 +27,38 @@ describe("filterItems", () => {
const items = [{ hidden: false, name: "Alice", level: 4 }];
expect(filterItems(items)).toEqual([{ hidden: false, name: "Alice", level: 4 }]);
});
+
+ it("filters items with invalid primary titles when a section type is provided", () => {
+ const items = [
+ { hidden: false, company: " ", position: "Engineer" },
+ { hidden: false, company: "Acme", position: "Engineer" },
+ { hidden: false, company: "\n\t", position: "Manager" },
+ ];
+
+ expect(filterItems(items, "experience")).toEqual([{ hidden: false, company: "Acme", position: "Engineer" }]);
+ });
+
+ it("filters invalid experience roles by position", () => {
+ const items = [
+ {
+ hidden: false,
+ company: "Acme",
+ roles: [
+ { id: "role-1", position: " ", period: "2020", description: "" },
+ { id: "role-2", position: "Lead Engineer", period: "2021", description: "" },
+ { id: "role-3", position: "\n", period: "2022", description: "" },
+ ],
+ },
+ ];
+
+ expect(filterItems(items, "experience")).toEqual([
+ {
+ hidden: false,
+ company: "Acme",
+ roles: [{ id: "role-2", position: "Lead Engineer", period: "2021", description: "" }],
+ },
+ ]);
+ });
});
describe("hasVisibleItems", () => {
@@ -45,6 +77,10 @@ describe("hasVisibleItems", () => {
it("returns false for empty items", () => {
expect(hasVisibleItems({ hidden: false, items: [] })).toBe(false);
});
+
+ it("returns false when all items have invalid primary titles", () => {
+ expect(hasVisibleItems({ hidden: false, items: [{ hidden: false, school: " " }] }, "education")).toBe(false);
+ });
});
describe("isVisibleSummary", () => {
@@ -69,7 +105,7 @@ describe("isSectionVisible", () => {
const data = {
summary: { hidden: false, content: "
Hi
" },
sections: {
- experience: { hidden: false, items: [{ hidden: false }] },
+ experience: { hidden: false, items: [{ hidden: false, company: "Acme" }] },
skills: { hidden: false, items: [] },
education: { hidden: true, items: [{ hidden: false }] },
},
@@ -88,6 +124,15 @@ describe("isSectionVisible", () => {
expect(isSectionVisible("experience", data)).toBe(true);
});
+ it("returns false for built-in section when all items have invalid primary titles", () => {
+ expect(
+ isSectionVisible("experience", {
+ ...data,
+ sections: { experience: { hidden: false, items: [{ hidden: false, company: " " }] } },
+ }),
+ ).toBe(false);
+ });
+
it("returns false for built-in section with no items", () => {
expect(isSectionVisible("skills", data)).toBe(false);
});
@@ -100,6 +145,17 @@ describe("isSectionVisible", () => {
expect(isSectionVisible("ext-1", data)).toBe(true);
});
+ it("uses custom section type to validate item primary titles", () => {
+ expect(
+ isSectionVisible("ext-education", {
+ ...data,
+ customSections: [
+ { id: "ext-education", type: "education", hidden: false, items: [{ hidden: false, school: "" }] },
+ ],
+ }),
+ ).toBe(false);
+ });
+
it("returns false for unknown section id", () => {
expect(isSectionVisible("does-not-exist", data)).toBe(false);
});
@@ -109,7 +165,7 @@ describe("filterSections", () => {
const data = {
summary: { hidden: false, content: "Hi
" },
sections: {
- experience: { hidden: false, items: [{ hidden: false }] },
+ experience: { hidden: false, items: [{ hidden: false, company: "Acme" }] },
skills: { hidden: false, items: [] },
},
customSections: [],
diff --git a/packages/pdf/src/templates/shared/filtering.ts b/packages/pdf/src/templates/shared/filtering.ts
index 27101ee8b..593bd6bae 100644
--- a/packages/pdf/src/templates/shared/filtering.ts
+++ b/packages/pdf/src/templates/shared/filtering.ts
@@ -1,20 +1,43 @@
-import type { Summary } from "@reactive-resume/schema/resume/data";
+import type { CustomSectionType, Summary } from "@reactive-resume/schema/resume/data";
type HiddenItem = {
hidden: boolean;
+ [key: string]: unknown;
};
+type TitleBackedSectionType = Exclude;
+
type ItemSectionLike = {
hidden: boolean;
items: T[];
};
+type CustomSectionLike = ItemSectionLike & {
+ id: string;
+ type?: string;
+};
+
type FilterableData = {
summary: Pick;
sections: Partial>;
- customSections: Array;
+ customSections: CustomSectionLike[];
};
+const primaryTitleFields = {
+ profiles: "network",
+ experience: "company",
+ education: "school",
+ projects: "name",
+ skills: "name",
+ languages: "language",
+ interests: "name",
+ awards: "title",
+ certifications: "title",
+ publications: "title",
+ volunteer: "organization",
+ references: "name",
+} satisfies Record;
+
const isItemSection = (section: unknown): section is ItemSectionLike => {
return typeof section === "object" && section !== null && "items" in section;
};
@@ -23,12 +46,48 @@ const isSummarySection = (section: unknown): section is Summary => {
return typeof section === "object" && section !== null && "content" in section;
};
-export const filterItems = (items: T[]): T[] => {
- return items.filter((item) => !item.hidden);
+const hasText = (value: unknown): value is string => {
+ return typeof value === "string" && value.trim().length > 0;
};
-export const hasVisibleItems = (section: ItemSectionLike): boolean => {
- return !section.hidden && filterItems(section.items).length > 0;
+const getPrimaryTitleField = (sectionType?: string): string | undefined => {
+ return primaryTitleFields[sectionType as TitleBackedSectionType];
+};
+
+const hasValidPrimaryTitle = (item: HiddenItem, sectionType?: string): boolean => {
+ const titleField = getPrimaryTitleField(sectionType);
+
+ if (!titleField) return true;
+
+ return hasText((item as Record)[titleField]);
+};
+
+const hasVisibleExperienceRole = (role: unknown): boolean => {
+ return typeof role === "object" && role !== null && hasText((role as { position?: unknown }).position);
+};
+
+const filterExperienceRoles = (item: T, sectionType?: string): T => {
+ if (sectionType !== "experience") return item;
+
+ const roles = (item as { roles?: unknown }).roles;
+
+ if (!Array.isArray(roles)) return item;
+
+ const visibleRoles = roles.filter(hasVisibleExperienceRole);
+
+ if (visibleRoles.length === roles.length) return item;
+
+ return { ...item, roles: visibleRoles } as T;
+};
+
+export const filterItems = (items: T[], sectionType?: string): T[] => {
+ return items
+ .filter((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType))
+ .map((item) => filterExperienceRoles(item, sectionType));
+};
+
+export const hasVisibleItems = (section: ItemSectionLike, sectionType?: string): boolean => {
+ return !section.hidden && filterItems(section.items, sectionType).length > 0;
};
export const isVisibleSummary = (summary: Pick): boolean => {
@@ -41,12 +100,25 @@ const getSectionForFiltering = (sectionId: string, data: FilterableData) => {
return data.sections[sectionId] ?? data.customSections.find((section) => section.id === sectionId);
};
+const getSectionTypeForFiltering = (sectionId: string, section: unknown): string | undefined => {
+ if (sectionId === "summary") return "summary";
+
+ if (typeof section === "object" && section !== null && "type" in section) {
+ const type = (section as { type?: unknown }).type;
+
+ if (typeof type === "string") return type;
+ }
+
+ return sectionId;
+};
+
export const isSectionVisible = (sectionId: string, data: FilterableData): boolean => {
const section = getSectionForFiltering(sectionId, data);
+ const sectionType = getSectionTypeForFiltering(sectionId, section);
if (!section) return false;
if (isSummarySection(section)) return isVisibleSummary(section);
- if (isItemSection(section)) return hasVisibleItems(section);
+ if (isItemSection(section)) return hasVisibleItems(section, sectionType);
return false;
};
diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx
index 9c66eedff..2a6432a9f 100644
--- a/packages/pdf/src/templates/shared/sections.tsx
+++ b/packages/pdf/src/templates/shared/sections.tsx
@@ -51,10 +51,10 @@ const SectionItemsContext = createContext({ itemStyle:
const useSectionItemsContext = () => useContext(SectionItemsContext);
-const getVisibleItems = (section: ItemSection): T[] => {
- if (!hasVisibleItems(section)) return [];
+const getVisibleItems = (section: ItemSection, sectionType?: string): T[] => {
+ if (!hasVisibleItems(section, sectionType)) return [];
- return filterItems(section.items);
+ return filterItems(section.items, sectionType);
};
const SectionShell = ({
@@ -285,7 +285,7 @@ const ProfileSection = ({
} = {}) => {
const data = useRender();
const profiles = sectionData ?? data.sections.profiles;
- const items = getVisibleItems(profiles);
+ const items = getVisibleItems(profiles, "profiles");
const inlineStyle = useTemplateStyle("inline");
if (items.length === 0) return null;
@@ -318,7 +318,7 @@ const ExperienceSection = ({
} = {}) => {
const data = useRender();
const experience = sectionData ?? data.sections.experience;
- const items = getVisibleItems(experience);
+ const items = getVisibleItems(experience, "experience");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -400,7 +400,7 @@ const EducationSection = ({
} = {}) => {
const data = useRender();
const education = sectionData ?? data.sections.education;
- const items = getVisibleItems(education);
+ const items = getVisibleItems(education, "education");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -474,7 +474,7 @@ const ProjectsSection = ({
} = {}) => {
const data = useRender();
const projects = sectionData ?? data.sections.projects;
- const items = getVisibleItems(projects);
+ const items = getVisibleItems(projects, "projects");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
@@ -511,7 +511,7 @@ const SkillsSection = ({
} = {}) => {
const data = useRender();
const skills = sectionData ?? data.sections.skills;
- const items = getVisibleItems(skills);
+ const items = getVisibleItems(skills, "skills");
const inlineStyle = useTemplateStyle("inline");
const metrics = getTemplateMetrics(data.metadata.page);
@@ -551,7 +551,7 @@ const LanguagesSection = ({
} = {}) => {
const data = useRender();
const languages = sectionData ?? data.sections.languages;
- const items = getVisibleItems(languages);
+ const items = getVisibleItems(languages, "languages");
if (items.length === 0) return null;
@@ -581,7 +581,7 @@ const InterestsSection = ({
} = {}) => {
const data = useRender();
const interests = sectionData ?? data.sections.interests;
- const items = getVisibleItems(interests);
+ const items = getVisibleItems(interests, "interests");
const inlineStyle = useTemplateStyle("inline");
if (items.length === 0) return null;
@@ -615,7 +615,7 @@ const AwardsSection = ({
} = {}) => {
const data = useRender();
const awards = sectionData ?? data.sections.awards;
- const items = getVisibleItems(awards);
+ const items = getVisibleItems(awards, "awards");
const splitRowStyle = useTemplateStyle("splitRow");
const alignRightStyle = useTemplateStyle("alignRight");
@@ -652,7 +652,7 @@ const CertificationsSection = ({
} = {}) => {
const data = useRender();
const certifications = sectionData ?? data.sections.certifications;
- const items = getVisibleItems(certifications);
+ const items = getVisibleItems(certifications, "certifications");
if (items.length === 0) return null;
@@ -685,7 +685,7 @@ const PublicationsSection = ({
} = {}) => {
const data = useRender();
const publications = sectionData ?? data.sections.publications;
- const items = getVisibleItems(publications);
+ const items = getVisibleItems(publications, "publications");
if (items.length === 0) return null;
@@ -718,7 +718,7 @@ const VolunteerSection = ({
} = {}) => {
const data = useRender();
const volunteer = sectionData ?? data.sections.volunteer;
- const items = getVisibleItems(volunteer);
+ const items = getVisibleItems(volunteer, "volunteer");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -767,7 +767,7 @@ const ReferencesSection = ({
} = {}) => {
const data = useRender();
const references = sectionData ?? data.sections.references;
- const items = getVisibleItems(references);
+ const items = getVisibleItems(references, "references");
if (items.length === 0) return null;