diff --git a/src/pages/api/resume/generate.ts b/src/pages/api/resume/generate.ts index 7ca218a..9489ea5 100644 --- a/src/pages/api/resume/generate.ts +++ b/src/pages/api/resume/generate.ts @@ -1,94 +1,10 @@ import type { APIRoute } from "astro"; import { config } from "../../../config"; +import type { ResumeData } from "../../../types"; import * as TOML from "@iarna/toml"; import { renderToStream } from "@react-pdf/renderer"; import { ResumeDocument } from "../../../pdf/ResumeDocument"; -import { mdiEmail, mdiPhone, mdiDownload, mdiLink } from "@mdi/js"; -import * as simpleIcons from "simple-icons"; - -function getSimpleIconPath(network: string): string { - try { - const slug = network.toLowerCase().normalize("NFKD").replace(/[^\w]/g, ""); - const iconKey = `si${slug.charAt(0).toUpperCase()}${slug.slice(1)}`; - - const icon = (simpleIcons as any)[iconKey]; - return icon ? icon.path : ""; - } catch (error) { - console.warn(`Error finding icon for network: ${network}`, error); - return ""; - } -} - -function getMdiIconPath(iconName: string): string { - const iconMap: { [key: string]: string } = { - "mdi:email": mdiEmail, - "mdi:phone": mdiPhone, - "mdi:download": mdiDownload, - "mdi:link": mdiLink, - }; - return iconMap[iconName] || ""; -} - -interface ResumeData { - basics: { - name: string; - email: string; - phone?: string; - website?: string; - profiles: { - network: string; - username: string; - url: string; - }[]; - }; - layout?: { - left_column?: string[]; - right_column?: string[]; - }; - summary: { - content: string; - }; - experience: { - company: string; - position: string; - location: string; - date: string; - description: string[]; - url?: string; - }[]; - education: { - institution: string; - degree: string; - field: string; - date: string; - details?: string[]; - }[]; - skills: { - name: string; - level: number; - }[]; - volunteer: { - organization: string; - position: string; - date: string; - }[]; - awards: { - title: string; - organization: string; - date: string; - description?: string; - }[]; -} - -const fetchProfileIcons = (profiles: any[]) => { - const profileIcons: { [key: string]: string } = {}; - if (profiles) { - for (const profile of profiles) { - profileIcons[profile.network] = getSimpleIconPath(profile.network); - } - } - return profileIcons; -}; +import { getMdiIconPath, fetchProfileIcons } from "../../../utils/icons"; const generatePDF = async (data: ResumeData) => { const resumeConfig = config.resumeConfig; diff --git a/src/pdf/ResumeDocument.tsx b/src/pdf/ResumeDocument.tsx index 6941204..9d9603d 100644 --- a/src/pdf/ResumeDocument.tsx +++ b/src/pdf/ResumeDocument.tsx @@ -9,6 +9,7 @@ import { Svg, Path, } from "@react-pdf/renderer"; +import type { ResumeData } from "../types"; const styles = StyleSheet.create({ page: { @@ -153,22 +154,139 @@ const styles = StyleSheet.create({ }, }); -const Icon = ({ path, color = "currentColor" }: { path: string; color?: string }) => ( +const Icon = ({ + path, + color = "currentColor", +}: { + path: string; + color?: string; +}) => ( ); +const ExperienceSection = ({ + experience, +}: { + experience: ResumeData["experience"]; +}) => ( + <> + {experience?.map((exp, i) => ( + + {exp.position} + + {exp.company} • {exp.date} • {exp.location} + + + {exp.description?.map((desc, j) => ( + + + {desc} + + ))} + + + ))} + +); + +const SkillsSection = ({ skills }: { skills: ResumeData["skills"] }) => ( + <> + {skills?.map((skill, i) => ( + + + {skill.name} + {skill.level}/5 + + + + + + ))} + +); + +const EducationSection = ({ + education, +}: { + education: ResumeData["education"]; +}) => ( + <> + {education?.map((edu, i) => ( + + {edu.institution} + + {edu.degree} in {edu.field} • {edu.date} + + {edu.details && ( + + {edu.details.map((detail, j) => ( + + + {detail} + + ))} + + )} + + ))} + +); + +const VolunteerSection = ({ + volunteer, +}: { + volunteer: ResumeData["volunteer"]; +}) => ( + <> + {volunteer?.map((vol, i) => ( + + {vol.organization} + + {vol.position} • {vol.date} + + + ))} + +); + +const AwardsSection = ({ awards }: { awards: ResumeData["awards"] }) => ( + <> + {awards?.map((award, i) => ( + + {award.title} + + {award.organization} • {award.date} + + {award.description && ( + {award.description} + )} + + ))} + +); + interface ResumeDocumentProps { - data: any; + data: ResumeData; resumeConfig: any; icons: { [key: string]: string }; // Map of icon name to SVG path } -export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProps) => { +export const ResumeDocument = ({ + data, + resumeConfig, + icons, +}: ResumeDocumentProps) => { const layout = data.layout ? { - leftColumn: data.layout.left_column || ["experience", "volunteer", "awards"], + leftColumn: data.layout.left_column || [ + "experience", + "volunteer", + "awards", + ], rightColumn: data.layout.right_column || ["skills", "education"], } : resumeConfig.layout || { @@ -179,79 +297,15 @@ export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProp const renderSectionContent = (sectionName: string) => { switch (sectionName) { case "experience": - return data.experience?.map((exp: any, i: number) => ( - - {exp.position} - - {exp.company} • {exp.date} • {exp.location} - - - {exp.description?.map((desc: string, j: number) => ( - - - {desc} - - ))} - - - )); + return ; case "skills": - return data.skills?.map((skill: any, i: number) => ( - - - {skill.name} - {skill.level}/5 - - - - - - )); + return ; case "education": - return data.education?.map((edu: any, i: number) => ( - - {edu.institution} - - {edu.degree} in {edu.field} • {edu.date} - - {edu.details && ( - - {edu.details.map((detail: string, j: number) => ( - - - {detail} - - ))} - - )} - - )); + return ; case "volunteer": - return data.volunteer?.map((vol: any, i: number) => ( - - {vol.organization} - - {vol.position} • {vol.date} - - - )); + return ; case "awards": - return data.awards?.map((award: any, i: number) => ( - - {award.title} - - {award.organization} • {award.date} - - {award.description && ( - {award.description} - )} - - )); + return ; default: return null; } @@ -263,7 +317,9 @@ export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProp const content = renderSectionContent(name); // Check if section has content (simple check) - const hasContent = data[name] && data[name].length > 0; + const hasContent = + data[name as keyof ResumeData] && + (data[name as keyof ResumeData] as any[]).length > 0; if (!config || !hasContent || config.enabled === false) return null; @@ -297,8 +353,13 @@ export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProp )} {data.basics.profiles?.map((profile: any, i: number) => ( - {icons[profile.network] && } - + {icons[profile.network] && ( + + )} + {profile.url.replace(/^https?:\/\//, "").replace(/\/$/, "")} diff --git a/src/types.ts b/src/types.ts index 5b48ce4..e891198 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ import type { GiteaRepoInfo } from "./utils/gitea"; // Icon Types export type LucideIcon = Component; -export type AstroIconName = string; // For astro-icon string references like "mdi:email" +export type AstroIconName = string; export type CustomIconComponent = Component; export type IconType = LucideIcon | AstroIconName | CustomIconComponent; @@ -95,6 +95,57 @@ export interface ResumeConfig { }; } +export interface ResumeData { + basics: { + name: string; + email: string; + phone?: string; + website?: string; + profiles: { + network: string; + username: string; + url: string; + }[]; + }; + layout?: { + left_column?: string[]; + right_column?: string[]; + }; + summary: { + content: string; + }; + experience: { + company: string; + position: string; + location: string; + date: string; + description: string[]; + url?: string; + }[]; + education: { + institution: string; + degree: string; + field: string; + date: string; + details?: string[]; + }[]; + skills: { + name: string; + level: number; + }[]; + volunteer: { + organization: string; + position: string; + date: string; + }[]; + awards: { + title: string; + organization: string; + date: string; + description?: string; + }[]; +} + export interface PersonalInfo { name: string; profileImage: { diff --git a/src/utils/icons.ts b/src/utils/icons.ts new file mode 100644 index 0000000..687d796 --- /dev/null +++ b/src/utils/icons.ts @@ -0,0 +1,35 @@ +import { mdiEmail, mdiPhone, mdiDownload, mdiLink } from "@mdi/js"; +import * as simpleIcons from "simple-icons"; + +export function getSimpleIconPath(network: string): string { + try { + const slug = network.toLowerCase().normalize("NFKD").replace(/[^\w]/g, ""); + const iconKey = `si${slug.charAt(0).toUpperCase()}${slug.slice(1)}`; + + const icon = (simpleIcons as any)[iconKey]; + return icon ? icon.path : ""; + } catch (error) { + console.warn(`Error finding icon for network: ${network}`, error); + return ""; + } +} + +export function getMdiIconPath(iconName: string): string { + const iconMap: { [key: string]: string } = { + "mdi:email": mdiEmail, + "mdi:phone": mdiPhone, + "mdi:download": mdiDownload, + "mdi:link": mdiLink, + }; + return iconMap[iconName] || ""; +} + +export const fetchProfileIcons = (profiles: any[]) => { + const profileIcons: { [key: string]: string } = {}; + if (profiles) { + for (const profile of profiles) { + profileIcons[profile.network] = getSimpleIconPath(profile.network); + } + } + return profileIcons; +}; diff --git a/tsconfig.json b/tsconfig.json index 0dd6852..6485f64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,16 +2,13 @@ "extends": "astro/tsconfigs/strict", "compilerOptions": { "jsx": "react-jsx", - "jsxImportSource": "preact" + "jsxImportSource": "react-jsx", }, "include": [ ".astro/types.d.ts", "src/**/*.ts", "src/**/*.tsx", - "src/**/*.astro" + "src/**/*.astro", ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file + "exclude": ["node_modules", "dist"], +}