diff --git a/libs/parser/src/reactive-resume-v3/index.ts b/libs/parser/src/reactive-resume-v3/index.ts index c3f1b8d6..ed1b42e4 100644 --- a/libs/parser/src/reactive-resume-v3/index.ts +++ b/libs/parser/src/reactive-resume-v3/index.ts @@ -59,13 +59,15 @@ export class ReactiveResumeV3Parser implements Parser { const result = JSON.parse(JSON.stringify(defaultResumeData)) as ResumeData; // Basics - result.basics.name = data.basics.name; + result.basics.name = data.basics.name ?? ""; result.basics.email = data.basics.email; - result.basics.phone = data.basics.phone; - result.basics.headline = data.basics.headline; - result.basics.location = data.basics.location.address; - result.sections.summary.content = data.basics.summary; - result.basics.picture.url = isUrl(data.basics.photo.url) ? data.basics.photo.url : ""; + result.basics.phone = data.basics.phone ?? ""; + result.basics.headline = data.basics.headline ?? ""; + result.basics.location = data.basics.location.address ?? ""; + result.sections.summary.content = + (typeof data.basics.summary === "string" ? data.basics.summary : data.basics.summary?.body) ?? + ""; + result.basics.picture.url = isUrl(data.basics.photo.url) ? data.basics.photo.url! : ""; // Profiles if (data.basics.profiles) { @@ -73,10 +75,10 @@ export class ReactiveResumeV3Parser implements Parser { result.sections.profiles.items.push({ ...defaultProfile, id: createId(), - network: profile.network, - username: profile.username, - icon: profile.network.toLocaleLowerCase(), - url: { ...defaultProfile.url, href: isUrl(profile.url) ? profile.url : "" }, + network: profile.network ?? "", + username: profile.username ?? "", + icon: (profile.network ?? "").toLocaleLowerCase(), + url: { ...defaultProfile.url, href: isUrl(profile.url) ? profile.url! : "" }, }); } } @@ -84,14 +86,16 @@ export class ReactiveResumeV3Parser implements Parser { // Work if (data.sections.work.items) { for (const work of data.sections.work.items) { + if (!work) continue; + result.sections.experience.items.push({ ...defaultExperience, id: createId(), - company: work.name, - position: work.position, - summary: work.summary, - date: `${work.date.start} - ${work.date.end}`, - url: { ...defaultExperience.url, href: isUrl(work.url) ? work.url : "" }, + company: work.name ?? "", + position: work.position ?? "", + summary: work.summary ?? "", + date: `${work.date?.start} - ${work.date?.end}`, + url: { ...defaultExperience.url, href: isUrl(work.url) ? work.url! : "" }, }); } } @@ -99,14 +103,16 @@ export class ReactiveResumeV3Parser implements Parser { // Awards if (data.sections.awards.items) { for (const award of data.sections.awards.items) { + if (!award) continue; + result.sections.awards.items.push({ ...defaultAward, id: createId(), - title: award.title, - awarder: award.awarder, - date: award.date, - summary: award.summary, - url: { ...defaultAward.url, href: isUrl(award.url) ? award.url : "" }, + title: award.title ?? "", + awarder: award.awarder ?? "", + date: award.date ?? "", + summary: award.summary ?? "", + url: { ...defaultAward.url, href: isUrl(award.url) ? award.url! : "" }, }); } } @@ -114,13 +120,17 @@ export class ReactiveResumeV3Parser implements Parser { // Skills if (data.sections.skills.items) { for (const skill of data.sections.skills.items) { + if (!skill) continue; + result.sections.skills.items.push({ ...defaultSkill, id: createId(), - name: skill.name, + name: skill.name ?? "", level: Math.floor(skill.levelNum / 2), - description: skill.level, - keywords: skill.keywords, + description: skill.level ?? "", + keywords: Array.isArray(skill.keywords) + ? (skill.keywords.filter(Boolean) as string[]) + : [], }); } } @@ -128,15 +138,19 @@ export class ReactiveResumeV3Parser implements Parser { // Projects if (data.sections.projects.items) { for (const project of data.sections.projects.items) { + if (!project) continue; + result.sections.projects.items.push({ ...defaultProject, id: createId(), - name: project.name, - summary: project.summary, - description: project.description, - date: `${project.date.start} - ${project.date.end}`, - keywords: project.keywords, - url: { ...defaultProject.url, href: isUrl(project.url) ? project.url : "" }, + name: project.name ?? "", + summary: project.summary ?? "", + description: project.description ?? "", + date: `${project.date?.start} - ${project.date?.end}`, + keywords: Array.isArray(project.keywords) + ? (project.keywords.filter(Boolean) as string[]) + : [], + url: { ...defaultProject.url, href: isUrl(project.url) ? project.url! : "" }, }); } } @@ -144,16 +158,18 @@ export class ReactiveResumeV3Parser implements Parser { // Education if (data.sections.education.items) { for (const education of data.sections.education.items) { + if (!education) continue; + result.sections.education.items.push({ ...defaultEducation, id: createId(), - institution: education.institution, - studyType: education.degree, - area: education.area, - score: education.score, - summary: education.summary, - date: `${education.date.start} - ${education.date.end}`, - url: { ...defaultEducation.url, href: isUrl(education.url) ? education.url : "" }, + institution: education.institution ?? "", + studyType: education.degree ?? "", + area: education.area ?? "", + score: education.score ?? "", + summary: education.summary ?? "", + date: `${education.date?.start} - ${education.date?.end}`, + url: { ...defaultEducation.url, href: isUrl(education.url) ? education.url! : "" }, }); } } @@ -161,11 +177,15 @@ export class ReactiveResumeV3Parser implements Parser { // Interests if (data.sections.interests.items) { for (const interest of data.sections.interests.items) { + if (!interest) continue; + result.sections.interests.items.push({ ...defaultInterest, id: createId(), - name: interest.name, - keywords: interest.keywords, + name: interest.name ?? "", + keywords: Array.isArray(interest.keywords) + ? (interest.keywords.filter(Boolean) as string[]) + : [], }); } } @@ -173,11 +193,13 @@ export class ReactiveResumeV3Parser implements Parser { // Languages if (data.sections.languages.items) { for (const language of data.sections.languages.items) { + if (!language) continue; + result.sections.languages.items.push({ ...defaultLanguage, id: createId(), - name: language.name, - description: language.level, + name: language.name ?? "", + description: language.level ?? "", level: Math.floor(language.levelNum / 2), }); } @@ -186,14 +208,16 @@ export class ReactiveResumeV3Parser implements Parser { // Volunteer if (data.sections.volunteer.items) { for (const volunteer of data.sections.volunteer.items) { + if (!volunteer) continue; + result.sections.volunteer.items.push({ ...defaultVolunteer, id: createId(), - organization: volunteer.organization, - position: volunteer.position, - summary: volunteer.summary, - date: `${volunteer.date.start} - ${volunteer.date.end}`, - url: { ...defaultVolunteer.url, href: isUrl(volunteer.url) ? volunteer.url : "" }, + organization: volunteer.organization ?? "", + position: volunteer.position ?? "", + summary: volunteer.summary ?? "", + date: `${volunteer.date?.start} - ${volunteer.date?.end}`, + url: { ...defaultVolunteer.url, href: isUrl(volunteer.url) ? volunteer.url! : "" }, }); } } @@ -201,12 +225,14 @@ export class ReactiveResumeV3Parser implements Parser { // References if (data.sections.references.items) { for (const reference of data.sections.references.items) { + if (!reference) continue; + result.sections.references.items.push({ ...defaultReference, id: createId(), - name: reference.name, - summary: reference.summary, - description: reference.relationship, + name: reference.name ?? "", + summary: reference.summary ?? "", + description: reference.relationship ?? "", }); } } @@ -214,13 +240,15 @@ export class ReactiveResumeV3Parser implements Parser { // Publications if (data.sections.publications.items) { for (const publication of data.sections.publications.items) { + if (!publication) continue; + result.sections.publications.items.push({ ...defaultPublication, id: createId(), - name: publication.name, - summary: publication.summary, - date: publication.date, - url: { ...defaultPublication.url, href: isUrl(publication.url) ? publication.url : "" }, + name: publication.name ?? "", + summary: publication.summary ?? "", + date: publication.date ?? "", + url: { ...defaultPublication.url, href: isUrl(publication.url) ? publication.url! : "" }, }); } } @@ -228,16 +256,18 @@ export class ReactiveResumeV3Parser implements Parser { // Certifications if (data.sections.certifications.items) { for (const certification of data.sections.certifications.items) { + if (!certification) continue; + result.sections.certifications.items.push({ ...defaultCertification, id: createId(), - name: certification.name, - issuer: certification.issuer, - summary: certification.summary, - date: certification.date, + name: certification.name ?? "", + issuer: certification.issuer ?? "", + summary: certification.summary ?? "", + date: certification.date ?? "", url: { ...defaultCertification.url, - href: isUrl(certification.url) ? certification.url : "", + href: isUrl(certification.url) ? certification.url! : "", }, }); } diff --git a/libs/parser/src/reactive-resume-v3/schema.ts b/libs/parser/src/reactive-resume-v3/schema.ts index 9fcc6f04..1bc2c575 100644 --- a/libs/parser/src/reactive-resume-v3/schema.ts +++ b/libs/parser/src/reactive-resume-v3/schema.ts @@ -1,36 +1,47 @@ import { z } from "zod"; -const dateSchema = z.object({ start: z.string(), end: z.string() }); +const dateSchema = z + .object({ start: z.string().optional(), end: z.string().optional() }) + .optional(); const profileSchema = z.object({ - id: z.string(), - url: z.string(), - network: z.string(), - username: z.string(), + id: z.string().optional(), + url: z.string().optional(), + network: z.string().optional(), + username: z.string().optional(), }); const basicsSchema = z.object({ - name: z.string(), + name: z.string().optional(), email: z.literal("").or(z.string().email()), - phone: z.string(), - headline: z.string(), - summary: z.string(), + phone: z.string().optional(), + headline: z.string().optional(), + summary: z + .string() + .or( + z.object({ + body: z.string().optional(), + visible: z.boolean().default(true), + heading: z.string().optional(), + }), + ) + .optional(), birthdate: z.string().optional(), - website: z.string(), + website: z.string().optional(), profiles: z.array(profileSchema), location: z.object({ - address: z.string(), - postalCode: z.string(), - city: z.string(), - country: z.string(), - region: z.string(), + address: z.string().optional(), + postalCode: z.string().optional(), + city: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), }), photo: z.object({ visible: z.boolean(), - url: z.string(), + url: z.string().optional(), filters: z.object({ - shape: z.string(), - size: z.number(), + shape: z.string().nullable().optional(), + size: z.coerce.number(), border: z.boolean(), grayscale: z.boolean(), }), @@ -38,122 +49,158 @@ const basicsSchema = z.object({ }); const sectionSchema = z.object({ - id: z.string(), - name: z.string(), + id: z.string().optional(), + name: z.string().optional(), type: z.enum(["basic", "work", "custom"]), - columns: z.number().or(z.null()), + columns: z.coerce.number().or(z.null()).default(1), visible: z.boolean(), }); -const workSchema = z.object({ - id: z.string(), - url: z.string(), - date: dateSchema, - name: z.string(), - position: z.string(), - summary: z.string(), -}); +const workSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + date: dateSchema, + name: z.string().optional(), + position: z.string().optional(), + summary: z.string().nullable().optional(), + }) + .nullable(); -const awardSchema = z.object({ - id: z.string(), - url: z.string(), - date: z.string(), - title: z.string(), - awarder: z.string(), - summary: z.string(), -}); +const awardSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + date: z.string().optional(), + title: z.string().optional(), + awarder: z.string().optional(), + summary: z.string().nullable().optional(), + }) + .nullable(); -const skillSchema = z.object({ - id: z.string(), - name: z.string(), - level: z.string(), - keywords: z.array(z.string()), - levelNum: z.number(), -}); +const skillSchema = z + .object({ + id: z.string().optional(), + name: z.string().optional(), + level: z.coerce.string().optional(), + keywords: z.array(z.string().nullable()).optional(), + levelNum: z.coerce.number(), + }) + .nullable(); -const projectSchema = z.object({ - id: z.string(), - url: z.string(), - date: dateSchema, - name: z.string(), - summary: z.string(), - keywords: z.array(z.string()), - description: z.string(), -}); +const projectSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + date: dateSchema, + name: z.string().optional(), + summary: z.string().nullable().optional(), + keywords: z.array(z.string().nullable()).optional(), + description: z.string().optional(), + }) + .nullable(); -const educationSchema = z.object({ - id: z.string(), - url: z.string(), - area: z.string(), - date: dateSchema, - score: z.string(), - degree: z.string(), - courses: z.array(z.string()), - summary: z.string(), - institution: z.string(), -}); +const educationSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + area: z.string().optional(), + date: dateSchema, + score: z.string().optional(), + degree: z.string().optional(), + courses: z.array(z.string().nullable()).optional(), + summary: z.string().nullable().optional(), + institution: z.string().optional(), + }) + .nullable(); -const interestSchema = z.object({ - id: z.string(), - name: z.string(), - keywords: z.array(z.string()), -}); +const interestSchema = z + .object({ + id: z.string().optional(), + name: z.string().optional(), + keywords: z.array(z.string().nullable()).optional(), + }) + .nullable(); -const languageSchema = z.object({ - id: z.string(), - name: z.string(), - level: z.string(), - levelNum: z.number(), -}); +const languageSchema = z + .object({ + id: z.string().optional(), + name: z.string().optional(), + level: z.string().optional(), + levelNum: z.coerce.number(), + }) + .nullable(); -const volunteerSchema = z.object({ - id: z.string(), - organization: z.string(), - position: z.string(), - date: dateSchema, - url: z.string(), - summary: z.string(), -}); +const volunteerSchema = z + .object({ + id: z.string().optional(), + organization: z.string().optional(), + position: z.string().optional(), + date: dateSchema, + url: z.string().optional(), + summary: z.string().nullable().optional(), + }) + .nullable(); -const referenceSchema = z.object({ - id: z.string(), - name: z.string(), - email: z.string(), - phone: z.string(), - summary: z.string(), - relationship: z.string(), -}); +const referenceSchema = z + .object({ + id: z.string().optional(), + name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), + summary: z.string().nullable().optional(), + relationship: z.string().optional(), + }) + .nullable(); -const publicationSchema = z.object({ - id: z.string(), - url: z.string(), - date: z.string(), - name: z.string(), - publisher: z.string(), - summary: z.string(), -}); +const publicationSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + date: z.string().optional(), + name: z.string().optional(), + publisher: z.string().optional(), + summary: z.string().nullable().optional(), + }) + .nullable(); -const certificationSchema = z.object({ - id: z.string(), - url: z.string(), - date: z.string(), - name: z.string(), - issuer: z.string(), - summary: z.string(), -}); +const certificationSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + date: z.string().optional(), + name: z.string().optional(), + issuer: z.string().optional(), + summary: z.string().nullable().optional(), + }) + .nullable(); -const metadataSchema = z.object({ - css: z.object({ value: z.string(), visible: z.boolean() }), - date: z.object({ format: z.string() }), - theme: z.object({ text: z.string(), primary: z.string(), background: z.string() }), - layout: z.array(z.array(z.array(z.string()))), - locale: z.string().optional(), - template: z.string(), - typography: z.object({ - size: z.object({ body: z.number(), heading: z.number() }), - family: z.object({ body: z.string(), heading: z.string() }), - }), -}); +const metadataSchema = z + .object({ + css: z.object({ value: z.string().optional(), visible: z.boolean() }).optional(), + date: z.object({ format: z.string().optional() }).optional(), + theme: z + .object({ + text: z.string().optional(), + primary: z.string().optional(), + background: z.string().optional(), + }) + .optional(), + layout: z.array(z.array(z.array(z.string().nullable()))).optional(), + locale: z.string().optional(), + template: z.string().optional(), + typography: z + .object({ + size: z + .object({ body: z.coerce.number().optional(), heading: z.coerce.number().optional() }) + .optional(), + family: z + .object({ body: z.string().optional(), heading: z.string().optional() }) + .optional(), + }) + .optional(), + }) + .optional(); export const reactiveResumeV3Schema = z.object({ public: z.boolean(), diff --git a/libs/utils/src/namespaces/string.ts b/libs/utils/src/namespaces/string.ts index 61e702c5..7aec1d5b 100644 --- a/libs/utils/src/namespaces/string.ts +++ b/libs/utils/src/namespaces/string.ts @@ -9,7 +9,9 @@ export const getInitials = (name: string) => { return ((initials.shift()?.[1] || "") + (initials.pop()?.[1] || "")).toUpperCase(); }; -export const isUrl = (string: string) => { +export const isUrl = (string: string | null | undefined) => { + if (!string) return false; + const urlRegex = /https?:\/\/[^ \n]+/i; return urlRegex.test(string);