More optimizations
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m6s

This commit is contained in:
2026-01-24 23:58:30 -07:00
parent 09fdbf7ec7
commit 0512645035
5 changed files with 230 additions and 170 deletions

View File

@@ -1,94 +1,10 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import { config } from "../../../config"; import { config } from "../../../config";
import type { ResumeData } from "../../../types";
import * as TOML from "@iarna/toml"; import * as TOML from "@iarna/toml";
import { renderToStream } from "@react-pdf/renderer"; import { renderToStream } from "@react-pdf/renderer";
import { ResumeDocument } from "../../../pdf/ResumeDocument"; import { ResumeDocument } from "../../../pdf/ResumeDocument";
import { mdiEmail, mdiPhone, mdiDownload, mdiLink } from "@mdi/js"; import { getMdiIconPath, fetchProfileIcons } from "../../../utils/icons";
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;
};
const generatePDF = async (data: ResumeData) => { const generatePDF = async (data: ResumeData) => {
const resumeConfig = config.resumeConfig; const resumeConfig = config.resumeConfig;

View File

@@ -9,6 +9,7 @@ import {
Svg, Svg,
Path, Path,
} from "@react-pdf/renderer"; } from "@react-pdf/renderer";
import type { ResumeData } from "../types";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
page: { 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;
}) => (
<Svg viewBox="0 0 24 24" style={styles.icon}> <Svg viewBox="0 0 24 24" style={styles.icon}>
<Path d={path} fill={color} /> <Path d={path} fill={color} />
</Svg> </Svg>
); );
const ExperienceSection = ({
experience,
}: {
experience: ResumeData["experience"];
}) => (
<>
{experience?.map((exp, i) => (
<View key={i} style={styles.experienceItem}>
<Text style={styles.itemTitle}>{exp.position}</Text>
<Text style={styles.itemSubtitle}>
{exp.company} {exp.date} {exp.location}
</Text>
<View style={styles.list}>
{exp.description?.map((desc, j) => (
<View key={j} style={styles.listItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.listContent}>{desc}</Text>
</View>
))}
</View>
</View>
))}
</>
);
const SkillsSection = ({ skills }: { skills: ResumeData["skills"] }) => (
<>
{skills?.map((skill, i) => (
<View key={i} style={styles.skillItem}>
<View style={styles.skillHeader}>
<Text style={{ fontSize: 8 }}>{skill.name}</Text>
<Text style={{ color: "#4B5563", fontSize: 8 }}>{skill.level}/5</Text>
</View>
<View style={styles.progressBarBg}>
<View
style={[styles.progressBarFill, { width: `${skill.level * 20}%` }]}
/>
</View>
</View>
))}
</>
);
const EducationSection = ({
education,
}: {
education: ResumeData["education"];
}) => (
<>
{education?.map((edu, i) => (
<View key={i} style={styles.educationItem}>
<Text style={styles.itemTitle}>{edu.institution}</Text>
<Text style={styles.itemSubtitle}>
{edu.degree} in {edu.field} {edu.date}
</Text>
{edu.details && (
<View style={styles.list}>
{edu.details.map((detail, j) => (
<View key={j} style={styles.listItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.listContent}>{detail}</Text>
</View>
))}
</View>
)}
</View>
))}
</>
);
const VolunteerSection = ({
volunteer,
}: {
volunteer: ResumeData["volunteer"];
}) => (
<>
{volunteer?.map((vol, i) => (
<View key={i} style={styles.volunteerItem}>
<Text style={styles.itemTitle}>{vol.organization}</Text>
<Text style={styles.itemSubtitle}>
{vol.position} {vol.date}
</Text>
</View>
))}
</>
);
const AwardsSection = ({ awards }: { awards: ResumeData["awards"] }) => (
<>
{awards?.map((award, i) => (
<View key={i} style={styles.awardItem}>
<Text style={styles.itemTitle}>{award.title}</Text>
<Text style={styles.itemSubtitle}>
{award.organization} {award.date}
</Text>
{award.description && (
<Text style={{ color: "#374151" }}>{award.description}</Text>
)}
</View>
))}
</>
);
interface ResumeDocumentProps { interface ResumeDocumentProps {
data: any; data: ResumeData;
resumeConfig: any; resumeConfig: any;
icons: { [key: string]: string }; // Map of icon name to SVG path 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 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"], rightColumn: data.layout.right_column || ["skills", "education"],
} }
: resumeConfig.layout || { : resumeConfig.layout || {
@@ -179,79 +297,15 @@ export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProp
const renderSectionContent = (sectionName: string) => { const renderSectionContent = (sectionName: string) => {
switch (sectionName) { switch (sectionName) {
case "experience": case "experience":
return data.experience?.map((exp: any, i: number) => ( return <ExperienceSection experience={data.experience} />;
<View key={i} style={styles.experienceItem}>
<Text style={styles.itemTitle}>{exp.position}</Text>
<Text style={styles.itemSubtitle}>
{exp.company} {exp.date} {exp.location}
</Text>
<View style={styles.list}>
{exp.description?.map((desc: string, j: number) => (
<View key={j} style={styles.listItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.listContent}>{desc}</Text>
</View>
))}
</View>
</View>
));
case "skills": case "skills":
return data.skills?.map((skill: any, i: number) => ( return <SkillsSection skills={data.skills} />;
<View key={i} style={styles.skillItem}>
<View style={styles.skillHeader}>
<Text style={{ fontSize: 8 }}>{skill.name}</Text>
<Text style={{ color: "#4B5563", fontSize: 8 }}>{skill.level}/5</Text>
</View>
<View style={styles.progressBarBg}>
<View
style={[
styles.progressBarFill,
{ width: `${skill.level * 20}%` },
]}
/>
</View>
</View>
));
case "education": case "education":
return data.education?.map((edu: any, i: number) => ( return <EducationSection education={data.education} />;
<View key={i} style={styles.educationItem}>
<Text style={styles.itemTitle}>{edu.institution}</Text>
<Text style={styles.itemSubtitle}>
{edu.degree} in {edu.field} {edu.date}
</Text>
{edu.details && (
<View style={styles.list}>
{edu.details.map((detail: string, j: number) => (
<View key={j} style={styles.listItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.listContent}>{detail}</Text>
</View>
))}
</View>
)}
</View>
));
case "volunteer": case "volunteer":
return data.volunteer?.map((vol: any, i: number) => ( return <VolunteerSection volunteer={data.volunteer} />;
<View key={i} style={styles.volunteerItem}>
<Text style={styles.itemTitle}>{vol.organization}</Text>
<Text style={styles.itemSubtitle}>
{vol.position} {vol.date}
</Text>
</View>
));
case "awards": case "awards":
return data.awards?.map((award: any, i: number) => ( return <AwardsSection awards={data.awards} />;
<View key={i} style={styles.awardItem}>
<Text style={styles.itemTitle}>{award.title}</Text>
<Text style={styles.itemSubtitle}>
{award.organization} {award.date}
</Text>
{award.description && (
<Text style={{ color: "#374151" }}>{award.description}</Text>
)}
</View>
));
default: default:
return null; return null;
} }
@@ -263,7 +317,9 @@ export const ResumeDocument = ({ data, resumeConfig, icons }: ResumeDocumentProp
const content = renderSectionContent(name); const content = renderSectionContent(name);
// Check if section has content (simple check) // 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; 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) => ( {data.basics.profiles?.map((profile: any, i: number) => (
<View key={i} style={styles.contactItem}> <View key={i} style={styles.contactItem}>
{icons[profile.network] && <Icon path={icons[profile.network]} />} {icons[profile.network] && (
<Link src={profile.url} style={{ color: "#4B5563", textDecoration: "none" }}> <Icon path={icons[profile.network]} />
)}
<Link
src={profile.url}
style={{ color: "#4B5563", textDecoration: "none" }}
>
{profile.url.replace(/^https?:\/\//, "").replace(/\/$/, "")} {profile.url.replace(/^https?:\/\//, "").replace(/\/$/, "")}
</Link> </Link>
</View> </View>

View File

@@ -4,7 +4,7 @@ import type { GiteaRepoInfo } from "./utils/gitea";
// Icon Types // Icon Types
export type LucideIcon = Component; 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 CustomIconComponent = Component;
export type IconType = LucideIcon | AstroIconName | CustomIconComponent; 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 { export interface PersonalInfo {
name: string; name: string;
profileImage: { profileImage: {

35
src/utils/icons.ts Normal file
View File

@@ -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;
};

View File

@@ -2,16 +2,13 @@
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "preact" "jsxImportSource": "react-jsx",
}, },
"include": [ "include": [
".astro/types.d.ts", ".astro/types.d.ts",
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.astro" "src/**/*.astro",
], ],
"exclude": [ "exclude": ["node_modules", "dist"],
"node_modules",
"dist"
]
} }