Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/profile-dev.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{
"company": "Clarion Systems",
"role": "Staff Software Engineer",
"location": "Remote",
"start": "2022-07",
"end": null,
"bullets": [
Expand All @@ -28,6 +29,7 @@
{
"company": "Beacon Labs",
"role": "Senior Software Engineer",
"location": "Austin, TX",
"start": "2019-04",
"end": "2022-06",
"bullets": [
Expand All @@ -40,6 +42,7 @@
{
"company": "Trove Digital",
"role": "Software Engineer",
"location": "Austin, TX",
"start": "2016-08",
"end": "2019-03",
"bullets": [
Expand Down
3 changes: 3 additions & 0 deletions data/profile.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{
"company": "Meridian Software",
"role": "Director of Product Design",
"location": "Remote",
"start": "2023-03",
"end": null,
"bullets": [
Expand All @@ -28,6 +29,7 @@
{
"company": "Lumen Analytics",
"role": "Senior Product Designer",
"location": "Chicago, IL",
"start": "2020-06",
"end": "2023-02",
"bullets": [
Expand All @@ -41,6 +43,7 @@
{
"company": "Clearpath Digital",
"role": "UX Designer",
"location": "Chicago, IL",
"start": "2017-08",
"end": "2020-05",
"bullets": [
Expand Down
36 changes: 36 additions & 0 deletions lib/__tests__/resume-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,42 @@ describe("formatDateRange", () => {
// Assert
expect(range).toBe("December 2011 – January 2012");
});

it("omits the location segment entirely when location is undefined", () => {
// Arrange
const start = "2024-10";
const end = null;

// Act
const range = formatDateRange(start, end, undefined);

// Assert
expect(range).toBe("October 2024 – Present");
});

it("omits the location segment entirely when location is an empty string", () => {
// Arrange
const start = "2024-10";
const end = null;

// Act
const range = formatDateRange(start, end, "");

// Assert
expect(range).toBe("October 2024 – Present");
});

it("appends a single location after the date range separated by ' | '", () => {
// Arrange
const start = "2023-01";
const end = "2024-01";

// Act
const range = formatDateRange(start, end, "Chicago, IL");

// Assert
expect(range).toBe("January 2023 – January 2024 | Chicago, IL");
});
});

describe("linesToBullets", () => {
Expand Down
66 changes: 66 additions & 0 deletions lib/__tests__/resume-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ describe("buildResumeHtml", () => {
expect(html).toContain("<li>Shipped the thing.</li>");
});

it("appends the role's location after the date range when location is present", () => {
// Arrange
const profile: ProfileData = {
experience: [
{
company: "Anthropic",
role: "Staff Designer",
location: "Chicago, IL",
start: "2023-01",
end: null,
bullets: [],
},
],
};

// Act
const html = buildResumeHtml(profile, candidate);

// Assert
expect(html).toContain("January 2023 – Present | Chicago, IL");
});

it("omits the location segment when the role has no location", () => {
// Arrange
const profile: ProfileData = {
experience: [
{ company: "Anthropic", role: "Staff Designer", start: "2023-01", end: null, bullets: [] },
],
};

// Act
const html = buildResumeHtml(profile, candidate);

// Assert
expect(html).toContain("January 2023 – Present</span>");
});

it("renders multiple experience roles in the given order", () => {
// Arrange
const profile: ProfileData = {
Expand Down Expand Up @@ -309,6 +346,35 @@ describe("buildResumeHtml", () => {
expect(html).not.toContain("<script>");
});

it("renders each education entry as a single line joining institution, degree, honors, and date with ' | '", () => {
// Arrange
const profile: ProfileData = {
education: [
{ institution: "State University", degree: "B.A. Design", graduated: "2010-05", honors: "Cum Laude" },
],
};

// Act
const html = buildResumeHtml(profile, candidate);

// Assert
expect(html).toMatch(/<b>State University<\/b>\s*\|\s*B\.A\. Design\s*\|\s*Cum Laude\s*\|\s*2010-05/);
});

it("gives the first section header a larger top margin than subsequent section headers", () => {
// Arrange
const profile: ProfileData = {
summary: "Fifteen years of design leadership.",
strengths: ["Design Systems"],
};

// Act
const html = buildResumeHtml(profile, candidate);

// Assert
expect(html).toMatch(/h2:first-of-type\s*\{[^}]*margin-top:\s*18px/);
});

it("embeds Inter as a real base64 font-face, not a bare font-family reference", () => {
// Arrange
const profile: ProfileData = {};
Expand Down
3 changes: 2 additions & 1 deletion lib/docx-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ProfileData {
experience?: {
company: string;
role: string;
location?: string;
start: string;
end: string | null;
bullets: string[];
Expand Down Expand Up @@ -207,7 +208,7 @@ export async function generateResumeDOCX(
children: [
run(job.company, { bold: true, color: body }),
new TextRun({ children: ["\t"], font: { ascii: font, hAnsi: font } }),
run(formatDateRange(job.start, job.end), { color: meta, size: 18 }),
run(formatDateRange(job.start, job.end, job.location), { color: meta, size: 18 }),
],
tabStops: [{ type: TabStopType.RIGHT, position: contentWidth }],
spacing: { after: 60 },
Expand Down
1 change: 1 addition & 0 deletions lib/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface ExperienceEntry {
company: string;
role: string;
location?: string;
start: string;
end: string | null;
bullets: string[];
Expand Down
5 changes: 3 additions & 2 deletions lib/resume-format.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export function formatDateRange(start: string, end: string | null): string {
export function formatDateRange(start: string, end: string | null, location?: string): string {
const fmt = (d: string) => {
const [y, m] = d.split("-");
return new Date(Number(y), Number(m) - 1).toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
};
return `${fmt(start)} – ${end ? fmt(end) : "Present"}`;
const range = `${fmt(start)} – ${end ? fmt(end) : "Present"}`;
return location ? `${range} | ${location}` : range;
}

export function companySlug(company: string): string {
Expand Down
31 changes: 14 additions & 17 deletions lib/resume-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,26 @@ function renderStyle(): string {
* { box-sizing: border-box; }
body { font-family: 'Inter', -apple-system, sans-serif; color: var(--body); margin: 0; padding: 0; }
h1 { font-size: 27px; font-weight: 700; color: var(--ink); letter-spacing: -0.01em; line-height: 1.2; margin: 0; }
.headline { font-size: 14px; font-weight: 500; color: var(--accent); letter-spacing: 0.01em; margin: 4px 8px 0 0; text-align: justify;}
.contact { font-size: 12px; font-weight: 400; color: var(--secondary); line-height: 1.3; margin: 6px 6px 0 0; }
.headline { font-size: 13px; font-weight: 500; color: var(--accent); letter-spacing: 0.01em; margin: 6px 0 0; }
.contact { font-size: 12px; font-weight: 400; color: var(--secondary); line-height: 1.6; margin: 8px 0 0; }
h2 {
font-size: 11px; font-weight: 600; color: var(--accent); text-transform: uppercase;
letter-spacing: 0.12em; padding-bottom: 5px; border-bottom: 1px solid var(--rule); margin: 17px 0 8px;
letter-spacing: 0.12em; padding-bottom: 5px; border-bottom: 1px solid var(--rule); margin: 16px 0 7px;
}
h2:first-of-type { margin-top: 18px; }
h3 { font-size: 13.5px; font-weight: 700; color: var(--ink); line-height: 1.4; margin: 0; }
.role-row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin-top: 2px; }
.employer { font-size: 12.5px; font-weight: 500; color: var(--secondary); }
.dates { font-size: 12px; font-weight: 400; color: var(--muted); white-space: nowrap; }
.role-block { margin-bottom: 16px; }
.role-block { margin-bottom: 12px; }
.role-head { break-inside: avoid; }
ul { padding-left: 18px; margin: 5px 0 0; }
li { font-size: 13px; line-height: 1.45; margin-bottom: 4px; }
p.body-text { font-size: 13px; line-height: 1.45; margin: 0; }
.skills-line { font-size: 13px; line-height: 1.3; margin: 4px 0; }
ul { padding-left: 18px; margin: 4px 0 0; }
li { font-size: 12.25px; line-height: 1.5; margin-bottom: 4px; }
p.body-text { font-size: 12.25px; line-height: 1.5; margin: 0; }
.skills-line { font-size: 12.25px; line-height: 1.5; margin: 4px 0; }
.skills-line b { font-weight: 600; color: var(--ink); }
.edu-entry { break-inside: avoid; margin-bottom: 10px; }
.edu-entry b { font-weight: 700; color: var(--ink); font-size: 13px; }
.edu-entry .details { font-size: 12px; color: var(--secondary); }
.edu-entry { break-inside: avoid; margin-bottom: 10px; font-size: 12.25px; }
.edu-entry b { font-weight: 700; color: var(--ink); }
</style>`;
}

Expand Down Expand Up @@ -138,7 +138,7 @@ function renderExperienceSection(profile: ProfileData, tailoredBullets?: Record<
<h3>${escapeHtml(job.role)}</h3>
<div class="role-row">
<span class="employer">${escapeHtml(job.company)}</span>
<span class="dates">${formatDateRange(job.start, job.end)}</span>
<span class="dates">${escapeHtml(formatDateRange(job.start, job.end, job.location))}</span>
</div>
</div>
<ul>${bullets.map((b) => `<li>${escapeHtml(b)}</li>`).join("")}</ul>
Expand All @@ -155,11 +155,8 @@ function renderAwardsSection(profile: ProfileData): string {
function renderEducationSection(profile: ProfileData): string {
if (!profile.education?.length) return "";
const entries = profile.education.map((edu) => {
const details = [edu.degree, edu.honors, edu.graduated].filter(Boolean).map(String).map(escapeHtml).join(" | ");
return `<div class="edu-entry">
<div><b>${escapeHtml(edu.institution)}</b></div>
<div class="details">${details}</div>
</div>`;
const rest = [edu.degree, edu.honors, edu.graduated].filter(Boolean).map(String).map(escapeHtml).join(" | ");
return `<div class="edu-entry"><b>${escapeHtml(edu.institution)}</b>${rest ? ` | ${rest}` : ""}</div>`;
});
return `<h2>Education</h2>\n${entries.join("\n")}`;
}
Expand Down
Loading