mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 17:34:52 +10:00
803 lines
22 KiB
TypeScript
803 lines
22 KiB
TypeScript
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
|
import type { Template } from "@reactive-resume/schema/templates";
|
|
import z, { flattenError, ZodError } from "zod";
|
|
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
|
import { templateSchema } from "@reactive-resume/schema/templates";
|
|
import { parseColorString } from "@reactive-resume/utils/color";
|
|
import { generateId } from "@reactive-resume/utils/string";
|
|
|
|
function colorToRgba(color: string): string {
|
|
const parsed = parseColorString(color);
|
|
if (!parsed) return "rgba(0, 0, 0, 1)";
|
|
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${parsed.a})`;
|
|
}
|
|
|
|
const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
|
|
|
|
const nonNegative = (value: number): number => Math.max(0, value);
|
|
|
|
const pxToPt = (px: number): number => px * 0.75;
|
|
|
|
const clampPictureSize = (size: number): number => clamp(size, 32, 512);
|
|
|
|
const clampRotation = (rotation: number): number => clamp(rotation, 0, 360);
|
|
|
|
const clampAspectRatio = (ratio: number): number => clamp(ratio, 0.5, 2.5);
|
|
|
|
const clampBorderRadius = (radius: number): number => clamp(radius, 0, 100);
|
|
|
|
const clampFontSize = (size: number): number => clamp(size, 6, 24);
|
|
|
|
const clampLineHeight = (height: number): number => clamp(height, 0.5, 4);
|
|
|
|
const clampSidebarWidth = (width: number): number => clamp(width, 10, 50);
|
|
|
|
const convertAndClampFontSize = (px: number): number => clampFontSize(pxToPt(px));
|
|
|
|
const isValidEmail = (email: string): boolean => {
|
|
if (!email) return false;
|
|
return z.email().safeParse(email).success;
|
|
};
|
|
|
|
const sanitizeEmail = (email: string | undefined): string => {
|
|
if (!email) return "";
|
|
return isValidEmail(email) ? email : "";
|
|
};
|
|
|
|
type FontWeight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
|
|
|
|
const FONT_WEIGHT_MAP: Record<string, FontWeight> = {
|
|
regular: "400",
|
|
italic: "400",
|
|
"100": "100",
|
|
"200": "200",
|
|
"300": "300",
|
|
"400": "400",
|
|
"500": "500",
|
|
"600": "600",
|
|
"700": "700",
|
|
"800": "800",
|
|
"900": "900",
|
|
bold: "700",
|
|
"bold-italic": "700",
|
|
};
|
|
|
|
const convertFontVariantToWeight = (variant: string, defaultWeight: FontWeight = "400"): FontWeight =>
|
|
FONT_WEIGHT_MAP[variant.toLowerCase()] ?? defaultWeight;
|
|
|
|
const convertFontVariants = (variants: string[] | undefined, defaultWeight: FontWeight = "400"): FontWeight[] => {
|
|
if (!variants || variants.length === 0) return [defaultWeight];
|
|
return variants.map((v) => convertFontVariantToWeight(v, defaultWeight));
|
|
};
|
|
|
|
const convertFontVariantsForHeading = (variants: string[] | undefined): FontWeight[] => {
|
|
const weights = convertFontVariants(variants, "600");
|
|
const filtered = weights.filter((w) => Number.parseInt(w, 10) >= 600);
|
|
return filtered.length > 0 ? filtered : ["600"];
|
|
};
|
|
|
|
type V4ResumeData = {
|
|
basics: {
|
|
name: string;
|
|
headline: string;
|
|
email: string;
|
|
phone: string;
|
|
location: string;
|
|
url: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
customFields: Array<{
|
|
id?: string;
|
|
icon?: string;
|
|
text?: string;
|
|
}>;
|
|
picture: {
|
|
url: string;
|
|
size: number;
|
|
aspectRatio: number;
|
|
borderRadius: number;
|
|
effects: {
|
|
hidden: boolean;
|
|
border: boolean;
|
|
grayscale: boolean;
|
|
};
|
|
};
|
|
};
|
|
sections: {
|
|
summary: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
content: string;
|
|
};
|
|
awards: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
title?: string;
|
|
awarder?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
certifications: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
issuer?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
education: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
institution?: string;
|
|
studyType?: string;
|
|
area?: string;
|
|
score?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
experience: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
company?: string;
|
|
position?: string;
|
|
location?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
volunteer: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
organization?: string;
|
|
position?: string;
|
|
location?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
interests: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
keywords?: string[];
|
|
}>;
|
|
};
|
|
languages: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
description?: string;
|
|
level?: number;
|
|
}>;
|
|
};
|
|
profiles: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
network?: string;
|
|
username?: string;
|
|
icon?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
projects: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
description?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
keywords?: string[];
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
publications: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
publisher?: string;
|
|
date?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
references: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
description?: string;
|
|
summary?: string;
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
};
|
|
skills: {
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
description?: string;
|
|
level?: number;
|
|
keywords?: string[];
|
|
}>;
|
|
};
|
|
custom?: Record<
|
|
string,
|
|
{
|
|
name: string;
|
|
columns: number;
|
|
separateLinks: boolean;
|
|
visible: boolean;
|
|
id: string;
|
|
items: Array<{
|
|
id: string;
|
|
visible: boolean;
|
|
name?: string;
|
|
description?: string;
|
|
date?: string;
|
|
location?: string;
|
|
summary?: string;
|
|
keywords?: string[];
|
|
url?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
}>;
|
|
}
|
|
>;
|
|
};
|
|
metadata: {
|
|
template: string;
|
|
layout: Array<Array<string[]>>;
|
|
css: {
|
|
value: string;
|
|
visible: boolean;
|
|
};
|
|
page: {
|
|
margin: number;
|
|
format: "a4" | "letter";
|
|
options: {
|
|
breakLine: boolean;
|
|
pageNumbers: boolean;
|
|
};
|
|
};
|
|
theme: {
|
|
background: string;
|
|
text: string;
|
|
primary: string;
|
|
};
|
|
typography: {
|
|
font: {
|
|
family: string;
|
|
subset: string;
|
|
variants: string[];
|
|
size: number;
|
|
};
|
|
lineHeight: number;
|
|
hideIcons: boolean;
|
|
underlineLinks: boolean;
|
|
};
|
|
notes: string;
|
|
};
|
|
};
|
|
|
|
// Transform layout section ID from V4 format to new format
|
|
// V4 uses "custom.{id}" for custom sections, new format just uses "{id}"
|
|
const transformLayoutSectionId = (id: string): string => {
|
|
if (id.startsWith("custom.")) return id.slice(7);
|
|
return id;
|
|
};
|
|
|
|
// Transform layout column by stripping "custom." prefix from section IDs
|
|
const transformLayoutColumn = (column: string[]): string[] => {
|
|
return column
|
|
.filter((id) => id !== "summary") // Summary is handled separately
|
|
.map(transformLayoutSectionId);
|
|
};
|
|
|
|
export class ReactiveResumeV4JSONImporter {
|
|
parse(json: string): ResumeData {
|
|
try {
|
|
const v4Data = JSON.parse(json) as V4ResumeData;
|
|
|
|
const transformed: ResumeData = {
|
|
picture: {
|
|
hidden: v4Data.basics.picture?.effects?.hidden ?? false,
|
|
url: v4Data.basics.picture?.url ?? "",
|
|
size: clampPictureSize(v4Data.basics.picture?.size ?? 80),
|
|
rotation: clampRotation(0),
|
|
aspectRatio: clampAspectRatio(v4Data.basics.picture?.aspectRatio ?? 1),
|
|
borderRadius: clampBorderRadius(v4Data.basics.picture?.borderRadius ?? 0),
|
|
borderColor: v4Data.basics.picture?.effects?.border ? "rgba(0, 0, 0, 0.5)" : "rgba(0, 0, 0, 0)",
|
|
borderWidth: nonNegative(v4Data.basics.picture?.effects?.border ? 1 : 0),
|
|
shadowColor: "rgba(0, 0, 0, 0.5)",
|
|
shadowWidth: nonNegative(0),
|
|
},
|
|
basics: {
|
|
name: v4Data.basics.name ?? "",
|
|
headline: v4Data.basics.headline ?? "",
|
|
email: sanitizeEmail(v4Data.basics.email),
|
|
phone: v4Data.basics.phone ?? "",
|
|
location: v4Data.basics.location ?? "",
|
|
website: {
|
|
url: v4Data.basics.url?.href ?? "",
|
|
label: v4Data.basics.url?.label ?? "",
|
|
},
|
|
customFields: (v4Data.basics.customFields ?? []).map((field) => ({
|
|
id: field.id ?? generateId(),
|
|
icon: field.icon ?? "",
|
|
text: field.text ?? "",
|
|
link: "",
|
|
})),
|
|
},
|
|
summary: {
|
|
title: v4Data.sections.summary?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.summary?.columns ?? 1,
|
|
hidden: !(v4Data.sections.summary?.visible ?? true),
|
|
content: v4Data.sections.summary?.content ?? "",
|
|
},
|
|
sections: {
|
|
profiles: {
|
|
title: v4Data.sections.profiles?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.profiles?.columns ?? 1,
|
|
hidden: !(v4Data.sections.profiles?.visible ?? true),
|
|
items: (v4Data.sections.profiles?.items ?? [])
|
|
.filter((item) => item.network && item.network.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
icon: item.icon ?? "",
|
|
iconColor: "",
|
|
network: item.network ?? "",
|
|
username: item.username ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
})),
|
|
},
|
|
experience: {
|
|
title: v4Data.sections.experience?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.experience?.columns ?? 1,
|
|
hidden: !(v4Data.sections.experience?.visible ?? true),
|
|
items: (v4Data.sections.experience?.items ?? [])
|
|
.filter((item) => item.company && item.company.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
company: item.company ?? "",
|
|
position: item.position ?? "",
|
|
location: item.location ?? "",
|
|
period: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
roles: [],
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
education: {
|
|
title: v4Data.sections.education?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.education?.columns ?? 1,
|
|
hidden: !(v4Data.sections.education?.visible ?? true),
|
|
items: (v4Data.sections.education?.items ?? [])
|
|
.filter((item) => item.institution && item.institution.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
school: item.institution ?? "",
|
|
degree: item.studyType ?? "",
|
|
area: item.area ?? "",
|
|
grade: item.score ?? "",
|
|
location: "",
|
|
period: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
projects: {
|
|
title: v4Data.sections.projects?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.projects?.columns ?? 1,
|
|
hidden: !(v4Data.sections.projects?.visible ?? true),
|
|
items: (v4Data.sections.projects?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
name: item.name ?? "",
|
|
period: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? item.description ?? "",
|
|
})),
|
|
},
|
|
skills: {
|
|
title: v4Data.sections.skills?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.skills?.columns ?? 1,
|
|
hidden: !(v4Data.sections.skills?.visible ?? true),
|
|
items: (v4Data.sections.skills?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
icon: "",
|
|
iconColor: "",
|
|
name: item.name ?? "",
|
|
proficiency: item.description ?? "",
|
|
// v4 stored skill level as 0-10; scale down to v5's 0-5 range
|
|
level: Math.round(clamp(item.level ?? 0, 0, 10) / 2),
|
|
keywords: item.keywords ?? [],
|
|
})),
|
|
},
|
|
languages: {
|
|
title: v4Data.sections.languages?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.languages?.columns ?? 1,
|
|
hidden: !(v4Data.sections.languages?.visible ?? true),
|
|
items: (v4Data.sections.languages?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
language: item.name ?? "",
|
|
fluency: item.description ?? "",
|
|
// v4 stored language level as 0-10; scale down to v5's 0-5 range
|
|
level: Math.round(clamp(item.level ?? 0, 0, 10) / 2),
|
|
})),
|
|
},
|
|
interests: {
|
|
title: v4Data.sections.interests?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.interests?.columns ?? 1,
|
|
hidden: !(v4Data.sections.interests?.visible ?? true),
|
|
items: (v4Data.sections.interests?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
icon: "",
|
|
iconColor: "",
|
|
name: item.name ?? "",
|
|
keywords: item.keywords ?? [],
|
|
})),
|
|
},
|
|
awards: {
|
|
title: v4Data.sections.awards?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.awards?.columns ?? 1,
|
|
hidden: !(v4Data.sections.awards?.visible ?? true),
|
|
items: (v4Data.sections.awards?.items ?? [])
|
|
.filter((item) => item.title && item.title.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
title: item.title ?? "",
|
|
awarder: item.awarder ?? "",
|
|
date: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
certifications: {
|
|
title: v4Data.sections.certifications?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.certifications?.columns ?? 1,
|
|
hidden: !(v4Data.sections.certifications?.visible ?? true),
|
|
items: (v4Data.sections.certifications?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
title: item.name ?? "",
|
|
issuer: item.issuer ?? "",
|
|
date: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
publications: {
|
|
title: v4Data.sections.publications?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.publications?.columns ?? 1,
|
|
hidden: !(v4Data.sections.publications?.visible ?? true),
|
|
items: (v4Data.sections.publications?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
title: item.name ?? "",
|
|
publisher: item.publisher ?? "",
|
|
date: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
volunteer: {
|
|
title: v4Data.sections.volunteer?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.volunteer?.columns ?? 1,
|
|
hidden: !(v4Data.sections.volunteer?.visible ?? true),
|
|
items: (v4Data.sections.volunteer?.items ?? [])
|
|
.filter((item) => item.organization && item.organization.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
organization: item.organization ?? "",
|
|
location: item.location ?? "",
|
|
period: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
references: {
|
|
title: v4Data.sections.references?.name ?? "",
|
|
icon: "",
|
|
columns: v4Data.sections.references?.columns ?? 1,
|
|
hidden: !(v4Data.sections.references?.visible ?? true),
|
|
items: (v4Data.sections.references?.items ?? [])
|
|
.filter((item) => item.name && item.name.length > 0)
|
|
.map((item) => ({
|
|
id: item.id ?? generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
name: item.name ?? "",
|
|
position: item.description ?? "",
|
|
phone: "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
description: item.summary ?? "",
|
|
})),
|
|
},
|
|
},
|
|
customSections: Object.entries(v4Data.sections.custom ?? {}).map(([sectionId, section]) => ({
|
|
id: section.id || sectionId,
|
|
title: section.name ?? "",
|
|
icon: "",
|
|
type: "experience" as const, // Default to experience type as it has the most compatible fields
|
|
columns: section.columns ?? 1,
|
|
hidden: !(section.visible ?? true),
|
|
items: section.items.map((item, index) => {
|
|
const hasName = Boolean(item.name?.trim());
|
|
return {
|
|
id: item.id || generateId(),
|
|
hidden: !(item.visible ?? true),
|
|
company: item.name?.trim() || `#${index + 1}`,
|
|
// Only use description as subtitle when item has a name;
|
|
// otherwise description IS the primary content and goes to the body below
|
|
position: hasName ? (item.description ?? "") : "",
|
|
location: item.location ?? "",
|
|
period: item.date ?? "",
|
|
website: {
|
|
url: item.url?.href ?? "",
|
|
label: item.url?.label ?? "",
|
|
inlineLink: false,
|
|
},
|
|
roles: [],
|
|
// Prefer HTML summary; fall back to plain description
|
|
// (for description-only items, description IS the body content)
|
|
description: item.summary ?? item.description ?? "",
|
|
};
|
|
}),
|
|
})),
|
|
metadata: {
|
|
template: (templateSchema.safeParse(v4Data.metadata.template).success
|
|
? v4Data.metadata.template
|
|
: "onyx") as Template,
|
|
layout: {
|
|
sidebarWidth: clampSidebarWidth(35),
|
|
pages: (v4Data.metadata.layout ?? []).map((page) => {
|
|
const main = transformLayoutColumn(page[0] ?? []);
|
|
const sidebar = transformLayoutColumn(page[1] ?? []);
|
|
return {
|
|
fullWidth: sidebar.length === 0,
|
|
main,
|
|
sidebar,
|
|
};
|
|
}),
|
|
},
|
|
page: {
|
|
gapX: nonNegative(4),
|
|
gapY: nonNegative(6),
|
|
marginX: nonNegative(v4Data.metadata.page?.margin ?? 14),
|
|
marginY: nonNegative(v4Data.metadata.page?.margin ?? 14),
|
|
format: v4Data.metadata.page?.format ?? "a4",
|
|
locale: "en-US",
|
|
hideLinkUnderline: v4Data.metadata.typography?.underlineLinks === false,
|
|
hideIcons: v4Data.metadata.typography?.hideIcons ?? false,
|
|
hideSectionIcons: true,
|
|
},
|
|
design: {
|
|
colors: {
|
|
primary: v4Data.metadata.theme?.primary
|
|
? colorToRgba(v4Data.metadata.theme.primary)
|
|
: "rgba(220, 38, 38, 1)",
|
|
text: v4Data.metadata.theme?.text ? colorToRgba(v4Data.metadata.theme.text) : "rgba(0, 0, 0, 1)",
|
|
background: v4Data.metadata.theme?.background
|
|
? colorToRgba(v4Data.metadata.theme.background)
|
|
: "rgba(255, 255, 255, 1)",
|
|
},
|
|
level: {
|
|
icon: "star",
|
|
type: "circle",
|
|
},
|
|
},
|
|
typography: {
|
|
body: {
|
|
fontFamily: v4Data.metadata.typography?.font?.family ?? "IBM Plex Serif",
|
|
fontWeights: convertFontVariants(v4Data.metadata.typography?.font?.variants),
|
|
fontSize: convertAndClampFontSize(v4Data.metadata.typography?.font?.size ?? 14.67),
|
|
lineHeight: clampLineHeight(v4Data.metadata.typography?.lineHeight ?? 1.5),
|
|
},
|
|
heading: {
|
|
fontFamily: v4Data.metadata.typography?.font?.family ?? "IBM Plex Serif",
|
|
fontWeights: convertFontVariantsForHeading(v4Data.metadata.typography?.font?.variants),
|
|
fontSize: clampFontSize(convertAndClampFontSize(v4Data.metadata.typography?.font?.size ?? 14.67) + 3),
|
|
lineHeight: clampLineHeight(v4Data.metadata.typography?.lineHeight ?? 1.5),
|
|
},
|
|
},
|
|
notes: v4Data.metadata.notes ?? "",
|
|
styleRules: [],
|
|
},
|
|
};
|
|
|
|
if (v4Data.sections.summary?.visible && v4Data.sections.summary?.content) {
|
|
const firstPage = transformed.metadata.layout.pages[0];
|
|
if (firstPage) {
|
|
firstPage.main.unshift("summary");
|
|
}
|
|
}
|
|
|
|
return resumeDataSchema.parse(transformed);
|
|
} catch (error: unknown) {
|
|
if (error instanceof ZodError) {
|
|
const errors = flattenError(error);
|
|
throw new Error(JSON.stringify(errors));
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
}
|