This commit is contained in:
@ -3,6 +3,41 @@ import puppeteer from "puppeteer";
|
||||
import { siteConfig } from "../../../config/data";
|
||||
import * as TOML from "@iarna/toml";
|
||||
|
||||
// Helper function to fetch and return SVG icon from Simple Icons CDN
|
||||
async function getSimpleIcon(iconName: string): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://cdn.jsdelivr.net/npm/simple-icons@v10/icons/${iconName.toLowerCase()}.svg`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch icon: ${iconName}`);
|
||||
return "";
|
||||
}
|
||||
const svgContent = await response.text();
|
||||
// Add inline styles for proper sizing and color
|
||||
return svgContent.replace(
|
||||
"<svg",
|
||||
'<svg style="width: 12px; height: 12px; display: inline-block; vertical-align: middle; fill: currentColor;"',
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching icon ${iconName}:`, error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get MDI icon SVG
|
||||
function getMdiIcon(iconName: string): string {
|
||||
const iconMap: { [key: string]: string } = {
|
||||
"mdi:email":
|
||||
'<svg style="width: 12px; height: 12px; display: inline-block; vertical-align: middle; fill: currentColor;" viewBox="0 0 24 24"><path d="M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C2.89,4 20,4.89 20,4Z"/></svg>',
|
||||
"mdi:download":
|
||||
'<svg style="width: 12px; height: 12px; display: inline-block; vertical-align: middle; fill: currentColor;" viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/></svg>',
|
||||
"mdi:link":
|
||||
'<svg style="width: 12px; height: 12px; display: inline-block; vertical-align: middle; fill: currentColor;" viewBox="0 0 24 24"><path d="M3.9,12C3.9,10.29 5.29,8.9 7,8.9H11V7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12M8,13H16V11H8V13M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.71 18.71,15.1 17,15.1H13V17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7Z"/></svg>',
|
||||
};
|
||||
return iconMap[iconName] || "";
|
||||
}
|
||||
|
||||
interface ResumeData {
|
||||
basics: {
|
||||
name: string;
|
||||
@ -49,9 +84,58 @@ interface ResumeData {
|
||||
}[];
|
||||
}
|
||||
|
||||
const generateResumeHTML = (data: ResumeData): string => {
|
||||
// Helper function to generate sections for a column
|
||||
function generateColumnSections(
|
||||
sectionNames: string[] = [],
|
||||
sectionData: { [key: string]: any },
|
||||
): string {
|
||||
return sectionNames
|
||||
.map((sectionName) => {
|
||||
const section = sectionData[sectionName];
|
||||
if (
|
||||
!section ||
|
||||
!section.data ||
|
||||
!section.data.length ||
|
||||
!section.enabled
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${section.title}
|
||||
</h2>
|
||||
<div class="${section.spacing}">
|
||||
${section.html}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const generateResumeHTML = async (data: ResumeData): Promise<string> => {
|
||||
const resumeConfig = siteConfig.resume;
|
||||
|
||||
// Get layout configuration with defaults
|
||||
const layout = resumeConfig.layout || {
|
||||
leftColumn: ["experience", "volunteer", "awards"],
|
||||
rightColumn: ["skills", "education"],
|
||||
};
|
||||
|
||||
// Pre-fetch icons for profiles
|
||||
const profileIcons: { [key: string]: string } = {};
|
||||
if (data.basics.profiles) {
|
||||
for (const profile of data.basics.profiles) {
|
||||
const iconName = profile.network.toLowerCase();
|
||||
profileIcons[profile.network] = await getSimpleIcon(iconName);
|
||||
}
|
||||
}
|
||||
|
||||
// Get email icon
|
||||
const emailIcon = getMdiIcon("mdi:email");
|
||||
|
||||
const skillsHTML =
|
||||
data.skills
|
||||
?.map((skill) => {
|
||||
@ -178,16 +262,17 @@ const generateResumeHTML = (data: ResumeData): string => {
|
||||
<header class="text-center mb-3 pb-2 border-b-2 border-gray-300">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-1">${data.basics.name}</h1>
|
||||
<div class="flex justify-center items-center flex-wrap gap-4 text-xs text-gray-600">
|
||||
${data.basics.email ? `<div class="flex items-center gap-1">📧 ${data.basics.email}</div>` : ""}
|
||||
${data.basics.email ? `<div class="flex items-center gap-1">${emailIcon} ${data.basics.email}</div>` : ""}
|
||||
${
|
||||
data.basics.profiles?.find((p) => p.network === "GitHub")
|
||||
? `<div class="flex items-center gap-1">🔗 github.com/${data.basics.profiles.find((p) => p.network === "GitHub")?.username}</div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
data.basics.profiles?.find((p) => p.network === "LinkedIn")
|
||||
? `<div class="flex items-center gap-1">💼 linkedin.com/in/${data.basics.profiles.find((p) => p.network === "LinkedIn")?.username}</div>`
|
||||
: ""
|
||||
data.basics.profiles
|
||||
?.map((profile) => {
|
||||
const icon = profileIcons[profile.network] || "";
|
||||
const displayUrl = profile.url
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/$/, "");
|
||||
return `<div class="flex items-center gap-1">${icon} ${displayUrl}</div>`;
|
||||
})
|
||||
.join("") || ""
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
@ -207,92 +292,95 @@ const generateResumeHTML = (data: ResumeData): string => {
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-4">
|
||||
${
|
||||
data.experience &&
|
||||
data.experience.length > 0 &&
|
||||
resumeConfig.sections.experience?.enabled
|
||||
? `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.experience.title || "Experience"}
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
${experienceHTML}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
data.volunteer &&
|
||||
data.volunteer.length > 0 &&
|
||||
resumeConfig.sections.volunteer?.enabled
|
||||
? `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.volunteer.title || "Volunteer Work"}
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
${volunteerHTML}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
data.awards &&
|
||||
data.awards.length > 0 &&
|
||||
resumeConfig.sections.awards?.enabled
|
||||
? `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.awards.title || "Awards & Recognition"}
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
${awardsHTML}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${generateColumnSections(layout.leftColumn, {
|
||||
experience: {
|
||||
data: data.experience,
|
||||
html: experienceHTML,
|
||||
title:
|
||||
resumeConfig.sections.experience?.title || "Experience",
|
||||
enabled: resumeConfig.sections.experience?.enabled,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
volunteer: {
|
||||
data: data.volunteer,
|
||||
html: volunteerHTML,
|
||||
title:
|
||||
resumeConfig.sections.volunteer?.title ||
|
||||
"Volunteer Work",
|
||||
enabled: resumeConfig.sections.volunteer?.enabled,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
awards: {
|
||||
data: data.awards,
|
||||
html: awardsHTML,
|
||||
title:
|
||||
resumeConfig.sections.awards?.title ||
|
||||
"Awards & Recognition",
|
||||
enabled: resumeConfig.sections.awards?.enabled,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
skills: {
|
||||
data: data.skills,
|
||||
html: skillsHTML,
|
||||
title: resumeConfig.sections.skills?.title || "Skills",
|
||||
enabled: resumeConfig.sections.skills?.enabled,
|
||||
spacing: "space-y-1",
|
||||
},
|
||||
education: {
|
||||
data: data.education,
|
||||
html: educationHTML,
|
||||
title:
|
||||
resumeConfig.sections.education?.title || "Education",
|
||||
enabled: resumeConfig.sections.education?.enabled,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
${
|
||||
data.skills &&
|
||||
data.skills.length > 0 &&
|
||||
resumeConfig.sections.skills?.enabled
|
||||
? `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.skills.title || "Skills"}
|
||||
</h2>
|
||||
<div class="space-y-1">
|
||||
${skillsHTML}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
data.education &&
|
||||
data.education.length > 0 &&
|
||||
resumeConfig.sections.education?.enabled
|
||||
? `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.education.title || "Education"}
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
${educationHTML}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${generateColumnSections(layout.rightColumn, {
|
||||
experience: {
|
||||
data: data.experience,
|
||||
html: experienceHTML,
|
||||
title:
|
||||
resumeConfig.sections.experience?.title || "Experience",
|
||||
enabled: resumeConfig.sections.experience?.enabled,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
volunteer: {
|
||||
data: data.volunteer,
|
||||
html: volunteerHTML,
|
||||
title:
|
||||
resumeConfig.sections.volunteer?.title ||
|
||||
"Volunteer Work",
|
||||
enabled: resumeConfig.sections.volunteer?.enabled,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
awards: {
|
||||
data: data.awards,
|
||||
html: awardsHTML,
|
||||
title:
|
||||
resumeConfig.sections.awards?.title ||
|
||||
"Awards & Recognition",
|
||||
enabled: resumeConfig.sections.awards?.enabled,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
skills: {
|
||||
data: data.skills,
|
||||
html: skillsHTML,
|
||||
title: resumeConfig.sections.skills?.title || "Skills",
|
||||
enabled: resumeConfig.sections.skills?.enabled,
|
||||
spacing: "space-y-1",
|
||||
},
|
||||
education: {
|
||||
data: data.education,
|
||||
html: educationHTML,
|
||||
title:
|
||||
resumeConfig.sections.education?.title || "Education",
|
||||
enabled: resumeConfig.sections.education?.enabled,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -323,7 +411,7 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
tomlContent,
|
||||
) as unknown as ResumeData;
|
||||
|
||||
const htmlContent = generateResumeHTML(resumeData);
|
||||
const htmlContent = await generateResumeHTML(resumeData);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
@ -366,7 +454,9 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `attachment; filename="Atridad_Lahiji_Resume.pdf"`,
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
Reference in New Issue
Block a user