Skip to content

Commit 9c35aef

Browse files
committed
feat(cover-letter): enhance cover letter flow with new types and functions
- Introduced CoverLetterFlowContent and ProfessionalFlowItem types for better structure. - Added functions to build cover letter content and paginate items based on weight. - Refactored Veriworkly PDF and web templates to utilize new flow functions. - Improved handling of hidden sections in cover letter exports. - Updated tests to ensure correct rendering of cover letters with hidden sections and links.
1 parent 265f535 commit 9c35aef

19 files changed

Lines changed: 590 additions & 664 deletions

File tree

apps/blog-platform/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@veriworkly/blog-platform",
3-
"version": "3.10.0",
3+
"version": "3.10.2",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

apps/docs-platform/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@veriworkly/docs-platform",
3-
"version": "3.10.0",
3+
"version": "3.10.2",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@veriworkly/server",
3-
"version": "3.10.0",
3+
"version": "3.10.2",
44
"description": "VeriWorkly Resume Backend API",
55
"main": "dist/index.js",
66
"type": "module",

apps/site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@veriworkly/site",
3-
"version": "3.10.0",
3+
"version": "3.10.2",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

apps/studio/components/dashboard/StudioShell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface StudioShellProps {
3535
mainClassName?: string;
3636
}
3737

38-
const STUDIO_VERSION = "v3.10.0";
38+
const STUDIO_VERSION = "v3.10.2";
3939

4040
const StudioShell = ({ children, mainClassName }: StudioShellProps) => {
4141
const router = useRouter();

apps/studio/features/documents/rendering/resume-rendering.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ export interface RenderContactItem {
3838
href?: string;
3939
}
4040

41+
export interface ResumeRenderModel {
42+
style: ResumeRenderStyle;
43+
contactItems: RenderContactItem[];
44+
renderedLinks: ResumeLinkItem[];
45+
showBasics: boolean;
46+
showLinks: boolean;
47+
showSummary: boolean;
48+
showExperience: boolean;
49+
showEducation: boolean;
50+
showProjects: boolean;
51+
showSkills: boolean;
52+
visibleExperience: ResumeData["experience"];
53+
visibleEducation: ResumeData["education"];
54+
visibleProjects: ResumeData["projects"];
55+
visibleSkills: ResumeData["skills"];
56+
visibleCustomSections: ResumeData["customSections"];
57+
}
58+
4159
export function cleanResumeText(value: string | null | undefined): string {
4260
return stripEmoji(safeText(value ?? "")).replace(/\s+/g, " ");
4361
}
@@ -268,3 +286,28 @@ export function hasResumeSectionContent(resume: ResumeData, sectionId: ResumeSec
268286
.some((section) => hasCustomSectionContent(section));
269287
}
270288
}
289+
290+
export function getResumeRenderModel(resume: ResumeData): ResumeRenderModel {
291+
const style = getResumeRenderStyle(resume);
292+
293+
return {
294+
style,
295+
contactItems: getContactItems(resume.basics),
296+
renderedLinks: resume.links.items.filter((link) => normalizeLinkHref(link.url)),
297+
showBasics: hasResumeSectionContent(resume, "basics"),
298+
showLinks: hasResumeSectionContent(resume, "links"),
299+
showSummary: hasResumeSectionContent(resume, "summary"),
300+
showExperience: hasResumeSectionContent(resume, "experience"),
301+
showEducation: hasResumeSectionContent(resume, "education"),
302+
showProjects: hasResumeSectionContent(resume, "projects"),
303+
showSkills: hasResumeSectionContent(resume, "skills"),
304+
visibleExperience: resume.experience.filter(hasExperienceContent),
305+
visibleEducation: resume.education.filter(hasEducationContent),
306+
visibleProjects: resume.projects.filter(hasProjectContent),
307+
visibleSkills: resume.skills.filter(hasSkillGroupContent),
308+
visibleCustomSections: resume.customSections.filter(
309+
(section) =>
310+
hasResumeSectionContent(resume, section.kind) && hasCustomSectionContent(section),
311+
),
312+
};
313+
}

apps/studio/features/resume/editor/ResumePagedPreview.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ export function ResumePagedPreview({ children }: { children: ReactNode }) {
6161

6262
useLayoutEffect(() => {
6363
const frame = window.requestAnimationFrame(() => {
64-
const container = measureRef.current?.querySelector(
65-
"#resume-container",
66-
) as HTMLElement | null;
64+
const measureRoot = measureRef.current;
65+
const container = measureRoot?.querySelector("#resume-container") as HTMLElement | null;
6766

68-
if (!container) {
67+
if (!measureRoot || !container) {
6968
setPages([]);
7069
return;
7170
}
@@ -91,7 +90,7 @@ export function ResumePagedPreview({ children }: { children: ReactNode }) {
9190
width: `${RESUME_PAGE_WIDTH_PX}px`,
9291
});
9392

94-
measureRef.current.appendChild(probe);
93+
measureRoot.appendChild(probe);
9594

9695
const fitsPage = (elements: HTMLElement[]) => {
9796
probe.innerHTML = elements.map((element) => element.outerHTML).join("");

apps/studio/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@veriworkly/studio",
3-
"version": "3.10.0",
3+
"version": "3.10.2",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

apps/studio/templates/cover-letter/professional/pdf.tsx

Lines changed: 75 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import type { CoverLetterContent } from "@/features/cover-letter/types";
44
import { FONT_REGISTRY, normalizeFontFamilyId } from "@/features/documents/constants/fonts";
55

66
import {
7+
buildCoverLetterFlowContent,
8+
buildProfessionalFlowItems,
9+
getCoverLetterFlowSenderName,
710
pt,
811
PX_TO_PT,
9-
splitParagraphs,
10-
splitMarkdownLines,
1112
getCoverLetterLinks,
12-
splitRichTextBlocks,
1313
getCoverLetterLinkDisplayMode,
1414
isCoverLetterSectionVisible,
15+
type ProfessionalFlowItem,
1516
} from "../shared";
1617

1718
import {
@@ -152,7 +153,6 @@ export function ProfessionalCoverLetterPdf({ content }: { content: CoverLetterCo
152153
const showProfile = isCoverLetterSectionVisible(content, "profile");
153154
const showLinks = isCoverLetterSectionVisible(content, "links");
154155
const showTarget = isCoverLetterSectionVisible(content, "target");
155-
const showLetter = isCoverLetterSectionVisible(content, "letter");
156156
const font = FONT_REGISTRY[normalizeFontFamilyId(appearance.fontFamily)];
157157
const bodyLineHeight = appearance.lineHeight;
158158

@@ -169,7 +169,7 @@ export function ProfessionalCoverLetterPdf({ content }: { content: CoverLetterCo
169169
marginBottom: appearance.paragraphSpacing * PX_TO_PT,
170170
};
171171

172-
const senderName = content.senderName || content.signature || "Your Name";
172+
const senderName = getCoverLetterFlowSenderName(content);
173173

174174
const contact = showProfile
175175
? [
@@ -192,14 +192,76 @@ export function ProfessionalCoverLetterPdf({ content }: { content: CoverLetterCo
192192
].filter(Boolean)
193193
: [];
194194

195-
const bodyBlocks = showLetter
196-
? [
197-
...splitParagraphs(content.opening).map((text) => ({ type: "paragraph" as const, text })),
198-
...splitRichTextBlocks(content.body),
199-
]
200-
: [];
195+
const flowItems = buildProfessionalFlowItems(buildCoverLetterFlowContent(content), senderName);
196+
197+
function renderFlowItem(item: ProfessionalFlowItem) {
198+
if (item.type === "greeting") {
199+
return (
200+
<Text
201+
key={item.id}
202+
style={[styles.paragraph, paragraphStyle, { fontWeight: 600, color: "#09090b" }]}
203+
>
204+
{item.text}
205+
</Text>
206+
);
207+
}
208+
209+
if (item.type === "paragraph") {
210+
return (
211+
<Text key={item.id} style={[styles.paragraph, paragraphStyle]}>
212+
{item.text}
213+
</Text>
214+
);
215+
}
216+
217+
if (item.type === "body-list" || item.type === "proof-list") {
218+
return (
219+
<View key={item.id} style={[styles.bullets, listStyle]}>
220+
{item.items.map((listItem, index) => (
221+
<View
222+
key={`${listItem}-${index}`}
223+
style={[styles.bulletRow, index === item.items.length - 1 ? { marginBottom: 0 } : {}]}
224+
>
225+
<View style={styles.bulletDot}>
226+
<Svg width={pt(8)} height={pt(8)} viewBox="0 0 8 8">
227+
<Circle
228+
cx="4"
229+
cy="4"
230+
r="3.5"
231+
fill={item.type === "proof-list" ? "#09090b" : appearance.accentColor}
232+
/>
233+
</Svg>
234+
</View>
201235

202-
const highlights = showLetter ? splitMarkdownLines(content.highlights) : [];
236+
<Text style={styles.bulletText}>{listItem}</Text>
237+
</View>
238+
))}
239+
</View>
240+
);
241+
}
242+
243+
if (item.type === "closing") {
244+
return (
245+
<Text key={item.id} style={styles.closing}>
246+
{item.text}
247+
</Text>
248+
);
249+
}
250+
251+
if (item.type === "signature") {
252+
return (
253+
<Text key={item.id} style={styles.signature}>
254+
{item.text}
255+
</Text>
256+
);
257+
}
258+
259+
return (
260+
<Text key={item.id} style={styles.postscript}>
261+
P.S. {item.text}
262+
</Text>
263+
);
264+
}
203265

204266
return (
205267
<Document>
@@ -270,70 +332,7 @@ export function ProfessionalCoverLetterPdf({ content }: { content: CoverLetterCo
270332
) : null}
271333

272334
<View style={[styles.body, { fontSize: bodyFontSize, lineHeight: bodyLineHeight }]}>
273-
{showLetter && content.greeting ? (
274-
<Text style={[styles.paragraph, paragraphStyle, { fontWeight: 600, color: "#09090b" }]}>
275-
{content.greeting}
276-
</Text>
277-
) : null}
278-
279-
{bodyBlocks.map((block, index) =>
280-
block.type === "list" ? (
281-
<View key={`list-${index}`} style={[styles.bullets, listStyle]}>
282-
{block.items.map((item, i) => (
283-
<View
284-
key={i}
285-
style={[
286-
styles.bulletRow,
287-
i === block.items.length - 1 ? { marginBottom: 0 } : {},
288-
]}
289-
>
290-
<View style={styles.bulletDot}>
291-
<Svg width={pt(8)} height={pt(8)} viewBox="0 0 8 8">
292-
<Circle cx="4" cy="4" r="3.5" fill={appearance.accentColor} />
293-
</Svg>
294-
</View>
295-
296-
<Text style={styles.bulletText}>{item}</Text>
297-
</View>
298-
))}
299-
</View>
300-
) : (
301-
<Text key={`paragraph-${index}`} style={[styles.paragraph, paragraphStyle]}>
302-
{block.text}
303-
</Text>
304-
),
305-
)}
306-
307-
{highlights.length > 0 ? (
308-
<View style={[styles.bullets, listStyle]}>
309-
{highlights.map((highlight, i) => (
310-
<View
311-
key={i}
312-
style={[styles.bulletRow, i === highlights.length - 1 ? { marginBottom: 0 } : {}]}
313-
>
314-
<View style={styles.bulletDot}>
315-
<Svg width={pt(8)} height={pt(8)} viewBox="0 0 8 8">
316-
<Circle cx="4" cy="4" r="3.5" fill="#09090b" />
317-
</Svg>
318-
</View>
319-
320-
<Text style={styles.bulletText}>{highlight}</Text>
321-
</View>
322-
))}
323-
</View>
324-
) : null}
325-
326-
{showLetter && content.closing ? (
327-
<Text style={styles.closing}>{content.closing}</Text>
328-
) : null}
329-
330-
{showLetter ? (
331-
<Text style={styles.signature}>{content.signature || senderName}</Text>
332-
) : null}
333-
334-
{showLetter && content.postscript ? (
335-
<Text style={styles.postscript}>P.S. {content.postscript}</Text>
336-
) : null}
335+
{flowItems.map((item) => renderFlowItem(item))}
337336
</View>
338337
</Page>
339338
</Document>

0 commit comments

Comments
 (0)