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"],
+}