mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 23:32:19 +10:00
feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown export (#3217)
* feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown Let people export the resume and cover letter as distinct documents, and add a Markdown format alongside PDF / DOCX / JSON (handy for AI agents). - Server/API: scope PDF generation and download URLs to a resume/cover-letter target. - Export domain: getResumeExportData + resumeHasCoverLetter in @reactive-resume/resume. - Redesign the download dialog: one global "What to export" scope toggle (Tabs) plus flattened per-format rows, reusing existing UI components and design language. - Add Markdown export (@reactive-resume/resume/markdown) with a small tiptap-HTML converter. - Fix blank section headings in DOCX and Markdown by injecting the locale-aware section-title resolver (titles are stored empty and resolved at render time). - Locale catalogs updated for the new strings. * test(e2e): open the download dialog before exporting JSON The JSON export moved into the redesigned download dialog, so the spec now opens the dialog from the Export sidebar section before clicking "Download JSON".
This commit is contained in:
@@ -122,23 +122,36 @@ function blendWithWhite(hex: string, opacity: number): string {
|
||||
|
||||
// --- Section rendering dispatch ---
|
||||
|
||||
function renderSection(sectionId: string, data: ResumeData, colorHex: string): Paragraph[] {
|
||||
/** Resolves a section's (usually empty) stored title from its id — locale-aware, supplied by the caller. */
|
||||
export type SectionTitleResolver = (sectionId: string) => string | undefined;
|
||||
|
||||
function renderSection(
|
||||
sectionId: string,
|
||||
data: ResumeData,
|
||||
colorHex: string,
|
||||
resolveTitle?: SectionTitleResolver,
|
||||
): Paragraph[] {
|
||||
const titled = <T extends { title: string }>(section: T): T => ({
|
||||
...section,
|
||||
title: resolveTitle?.(sectionId)?.trim() || section.title,
|
||||
});
|
||||
|
||||
if (sectionId === "summary") {
|
||||
return renderSummary(data.summary, colorHex);
|
||||
return renderSummary(titled(data.summary), colorHex);
|
||||
}
|
||||
|
||||
// ponytail: data.sections keys are the source of truth; no need to maintain a parallel Set
|
||||
if (sectionId in data.sections) {
|
||||
const section = data.sections[sectionId as SectionType];
|
||||
if (section) {
|
||||
return renderBuiltInSection(sectionId as SectionType, section, colorHex);
|
||||
return renderBuiltInSection(sectionId as SectionType, titled(section), colorHex);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const customSection = data.customSections.find((cs) => cs.id === sectionId);
|
||||
if (customSection) {
|
||||
return renderCustomSection(customSection, colorHex);
|
||||
return renderCustomSection(titled(customSection), colorHex);
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -352,7 +365,7 @@ function buildTwoColumnTable(
|
||||
* - Page margins and fixed DOCX page format from `metadata.page`; free-form exports as A4
|
||||
* - Primary, text, and background colors from `metadata.design.colors`
|
||||
*/
|
||||
export function buildDocument(data: ResumeData): Document {
|
||||
export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResolver): Document {
|
||||
const colorHex = getColorHex(data.metadata.design.colors.primary, "DC2626");
|
||||
const textColorHex = getColorHex(data.metadata.design.colors.text, "000000");
|
||||
const bgColorHex = getColorHex(data.metadata.design.colors.background, "FFFFFF");
|
||||
@@ -414,7 +427,7 @@ export function buildDocument(data: ResumeData): Document {
|
||||
if (isFullWidth) {
|
||||
setRenderConfig(mainConfig);
|
||||
for (const sectionId of [...layoutPage.main, ...layoutPage.sidebar]) {
|
||||
documentChildren.push(...renderSection(sectionId, data, colorHex));
|
||||
documentChildren.push(...renderSection(sectionId, data, colorHex, resolveTitle));
|
||||
}
|
||||
} else {
|
||||
// Render main sections with normal colors
|
||||
@@ -425,7 +438,7 @@ export function buildDocument(data: ResumeData): Document {
|
||||
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
|
||||
}
|
||||
for (const sectionId of layoutPage.main) {
|
||||
mainParagraphs.push(...renderSection(sectionId, data, colorHex));
|
||||
mainParagraphs.push(...renderSection(sectionId, data, colorHex, resolveTitle));
|
||||
}
|
||||
|
||||
// Render sidebar sections with potentially inverted colors
|
||||
@@ -436,7 +449,7 @@ export function buildDocument(data: ResumeData): Document {
|
||||
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
|
||||
}
|
||||
for (const sectionId of layoutPage.sidebar) {
|
||||
sidebarParagraphs.push(...renderSection(sectionId, data, sidebarHeadingColorHex));
|
||||
sidebarParagraphs.push(...renderSection(sectionId, data, sidebarHeadingColorHex, resolveTitle));
|
||||
}
|
||||
|
||||
if (mainParagraphs.length > 0 || sidebarParagraphs.length > 0) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SectionTitleResolver } from "./builder";
|
||||
import { Packer } from "docx";
|
||||
import { buildDocument } from "./builder";
|
||||
|
||||
/** Builds a DOCX file from resume data and returns it as a Blob. */
|
||||
export async function buildDocx(data: ResumeData): Promise<Blob> {
|
||||
const doc = buildDocument(data);
|
||||
/**
|
||||
* Builds a DOCX file from resume data and returns it as a Blob. Pass `resolveTitle` to fill in
|
||||
* locale-aware section headings (titles are stored empty and resolved at render time).
|
||||
*/
|
||||
export async function buildDocx(data: ResumeData, resolveTitle?: SectionTitleResolver): Promise<Blob> {
|
||||
const doc = buildDocument(data, resolveTitle);
|
||||
return Packer.toBlob(doc);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user