mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-17 00:30:05 +10:00
feat: add section heading icons to PDF templates (#3127)
* feat: add section heading icons to PDF templates Add customizable Phosphor icons before section titles in PDF output. Users can toggle visibility globally via a new "Hide section heading icons" switch (independent of item-level icons) and customize individual section icons through the builder sidebar icon picker. - Add `icon` field to `baseSectionSchema` and `summarySchema` - Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat) - Implement `SectionHeadingIcon` component with heading font-size scaling - Support "none" sentinel for per-section icon hiding - Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data - Add icon picker to builder sidebar sections and custom section dialogs Closes #2632 * test: add unit tests for section heading icons - Add tests for getResumeSectionIcon() covering built-in sections, summary, custom sections, "none" sentinel, and default fallbacks - Add schema tests for baseSectionSchema icon field, summarySchema icon, and pageSchema hideSectionIcons default behavior * refactor: minor updates to icon display * Update apps/web/locales/es-ES.po Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
baseSectionSchema,
|
||||
basicsSchema,
|
||||
customFieldSchema,
|
||||
experienceItemSchema,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
skillItemSchema,
|
||||
styleRuleSchema,
|
||||
styleRulesSchema,
|
||||
summarySchema,
|
||||
websiteSchema,
|
||||
} from "./data";
|
||||
import { defaultResumeData } from "./default";
|
||||
@@ -271,6 +273,41 @@ describe("pageSchema", () => {
|
||||
expect(pageSchema.safeParse(valid).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults hideSectionIcons to true when missing", () => {
|
||||
const { hideSectionIcons: _, ...pageWithout } = defaultResumeData.metadata.page;
|
||||
const result = pageSchema.safeParse(pageWithout);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.hideSectionIcons).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("baseSectionSchema", () => {
|
||||
it("defaults icon to empty string when missing", () => {
|
||||
const result = baseSectionSchema.safeParse({ title: "Test", columns: 1, hidden: false });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.icon).toBe("");
|
||||
});
|
||||
|
||||
it("accepts a custom icon value", () => {
|
||||
const result = baseSectionSchema.safeParse({ title: "Test", icon: "rocket", columns: 1, hidden: false });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.icon).toBe("rocket");
|
||||
});
|
||||
|
||||
it("accepts 'none' as a valid icon value", () => {
|
||||
const result = baseSectionSchema.safeParse({ title: "Test", icon: "none", columns: 1, hidden: false });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.icon).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("summarySchema", () => {
|
||||
it("defaults icon to empty string when missing", () => {
|
||||
const result = summarySchema.safeParse({ title: "", columns: 1, hidden: false, content: "" });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.icon).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("styleRulesSchema", () => {
|
||||
|
||||
@@ -94,6 +94,12 @@ export const basicsSchema = z.object({
|
||||
|
||||
export const summarySchema = z.object({
|
||||
title: z.string().describe("The title of the summary of the resume."),
|
||||
icon: z
|
||||
.string()
|
||||
.catch("")
|
||||
.describe(
|
||||
"Phosphor icon name to display before the summary section title in the PDF output. Empty string uses the default summary icon; 'none' hides the icon.",
|
||||
),
|
||||
columns: z.number().int().min(1).max(6).catch(1).describe("The number of columns the summary should span across."),
|
||||
hidden: z.boolean().describe("Whether to hide the summary from the resume."),
|
||||
content: z.string().describe("The content of the summary of the resume. This should be a HTML-formatted string."),
|
||||
@@ -271,6 +277,12 @@ export type CoverLetterItem = z.infer<typeof coverLetterItemSchema>;
|
||||
|
||||
export const baseSectionSchema = z.object({
|
||||
title: z.string().describe("The title of the section."),
|
||||
icon: z
|
||||
.string()
|
||||
.catch("")
|
||||
.describe(
|
||||
"Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon.",
|
||||
),
|
||||
columns: z.number().int().min(1).max(6).catch(1).describe("The number of columns the section should span across."),
|
||||
hidden: z.boolean().describe("Whether to hide the section from the resume."),
|
||||
});
|
||||
@@ -457,7 +469,11 @@ export const pageSchema = z.object({
|
||||
.string()
|
||||
.describe("The locale of the page. Used for displaying pre-translated section headings, if not overridden.")
|
||||
.catch("en-US"),
|
||||
hideIcons: z.boolean().describe("Whether to hide the icons of the sections.").catch(false),
|
||||
hideIcons: z.boolean().describe("Whether to hide the item-level icons (skills, profiles, interests).").catch(false),
|
||||
hideSectionIcons: z
|
||||
.boolean()
|
||||
.describe("Whether to hide the section heading icons displayed before section titles.")
|
||||
.catch(true),
|
||||
});
|
||||
|
||||
export const levelDesignSchema = z.object({
|
||||
|
||||
@@ -24,6 +24,7 @@ export const defaultResumeData: ResumeData = {
|
||||
},
|
||||
summary: {
|
||||
title: "",
|
||||
icon: "article",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
content: "",
|
||||
@@ -31,72 +32,84 @@ export const defaultResumeData: ResumeData = {
|
||||
sections: {
|
||||
profiles: {
|
||||
title: "",
|
||||
icon: "messenger-logo",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
experience: {
|
||||
title: "",
|
||||
icon: "briefcase",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
education: {
|
||||
title: "",
|
||||
icon: "graduation-cap",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
projects: {
|
||||
title: "",
|
||||
icon: "code-simple",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
skills: {
|
||||
title: "",
|
||||
icon: "compass-tool",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
languages: {
|
||||
title: "",
|
||||
icon: "translate",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
interests: {
|
||||
title: "",
|
||||
icon: "football",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
awards: {
|
||||
title: "",
|
||||
icon: "trophy",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
certifications: {
|
||||
title: "",
|
||||
icon: "certificate",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
publications: {
|
||||
title: "",
|
||||
icon: "books",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
volunteer: {
|
||||
title: "",
|
||||
icon: "hand-heart",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
},
|
||||
references: {
|
||||
title: "",
|
||||
icon: "phone",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
@@ -123,6 +136,7 @@ export const defaultResumeData: ResumeData = {
|
||||
format: "a4",
|
||||
locale: "en-US",
|
||||
hideIcons: false,
|
||||
hideSectionIcons: true,
|
||||
},
|
||||
design: {
|
||||
colors: {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
summary: {
|
||||
title: "",
|
||||
icon: "article",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
content:
|
||||
@@ -48,6 +49,7 @@ export const sampleResumeData: ResumeData = {
|
||||
sections: {
|
||||
profiles: {
|
||||
title: "",
|
||||
icon: "messenger-logo",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -81,6 +83,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
experience: {
|
||||
title: "",
|
||||
icon: "briefcase",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -104,6 +107,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
education: {
|
||||
title: "",
|
||||
icon: "graduation-cap",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -128,6 +132,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
projects: {
|
||||
title: "",
|
||||
icon: "code-simple",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -174,6 +179,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
skills: {
|
||||
title: "",
|
||||
icon: "compass-tool",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -241,6 +247,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
languages: {
|
||||
title: "",
|
||||
icon: "translate",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -262,6 +269,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
interests: {
|
||||
title: "",
|
||||
icon: "football",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -301,6 +309,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
awards: {
|
||||
title: "",
|
||||
icon: "trophy",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -336,6 +345,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
certifications: {
|
||||
title: "",
|
||||
icon: "certificate",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -369,6 +379,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
publications: {
|
||||
title: "",
|
||||
icon: "books",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -404,6 +415,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
volunteer: {
|
||||
title: "",
|
||||
icon: "hand-heart",
|
||||
columns: 2,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -439,6 +451,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
references: {
|
||||
title: "",
|
||||
icon: "phone",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [
|
||||
@@ -461,6 +474,7 @@ export const sampleResumeData: ResumeData = {
|
||||
customSections: [
|
||||
{
|
||||
title: "",
|
||||
icon: "briefcase",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
id: "019becaf-0b87-769d-98a6-46ccf558c0e8",
|
||||
@@ -502,6 +516,7 @@ export const sampleResumeData: ResumeData = {
|
||||
},
|
||||
{
|
||||
title: "Cover Letter",
|
||||
icon: "envelope-simple",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
id: "019bef5b-0b3d-7e2a-8a7c-12d9e23a4f6b",
|
||||
@@ -553,6 +568,7 @@ export const sampleResumeData: ResumeData = {
|
||||
format: "a4",
|
||||
locale: "en-US",
|
||||
hideIcons: false,
|
||||
hideSectionIcons: false,
|
||||
},
|
||||
design: {
|
||||
level: {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { CustomSectionType } from "./data";
|
||||
|
||||
export const defaultSectionIconNames = {
|
||||
summary: "article",
|
||||
profiles: "messenger-logo",
|
||||
experience: "briefcase",
|
||||
education: "graduation-cap",
|
||||
projects: "code-simple",
|
||||
skills: "compass-tool",
|
||||
languages: "translate",
|
||||
interests: "football",
|
||||
awards: "trophy",
|
||||
certifications: "certificate",
|
||||
publications: "books",
|
||||
volunteer: "hand-heart",
|
||||
references: "phone",
|
||||
"cover-letter": "envelope-simple",
|
||||
} as const satisfies Record<CustomSectionType, string>;
|
||||
|
||||
export const getDefaultSectionIconName = (sectionType: CustomSectionType): string =>
|
||||
defaultSectionIconNames[sectionType];
|
||||
Reference in New Issue
Block a user