import type { APIRoute } from "astro"; import { chromium } from 'playwright'; 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 { 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( "', "mdi:download": '', "mdi:link": '', }; return iconMap[iconName] || ""; } interface ResumeData { basics: { name: string; email: string; website?: string; profiles: { network: string; username: string; url: 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; }[]; } // 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.title}

${section.html}
`; }) .join(""); } const generateResumeHTML = async (data: ResumeData): Promise => { 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) => { const progressValue = skill.level * 20; return `
${skill.name} ${skill.level}/5
`; }) .join("") || ""; const experienceHTML = data.experience ?.map((exp) => { const descriptionList = exp.description .map((item) => `
  • ${item}
  • `) .join(""); return `

    ${exp.position}

    ${exp.company} ${exp.date} ${exp.location}
      ${descriptionList}
    `; }) .join("") || ""; const educationHTML = data.education ?.map((edu) => { const detailsList = edu.details ? edu.details .map((detail) => `
  • ${detail}
  • `) .join("") : ""; return `

    ${edu.institution}

    ${edu.degree} in ${edu.field} ${edu.date}
    ${detailsList ? `
      ${detailsList}
    ` : ""}
    `; }) .join("") || ""; const volunteerHTML = data.volunteer ?.map((vol) => { return `

    ${vol.organization}

    ${vol.position} ${vol.date}
    `; }) .join("") || ""; const awardsHTML = data.awards ?.map((award) => { return `

    ${award.title}

    ${award.organization} ${award.date}
    ${award.description ? `
    ${award.description}
    ` : ""}
    `; }) .join("") || ""; return ` ${data.basics.name} - Resume

    ${data.basics.name}

    ${data.basics.email ? `
    ${emailIcon} ${data.basics.email}
    ` : ""} ${data.basics.profiles ?.map((profile) => { const icon = profileIcons[profile.network] || ""; const displayUrl = profile.url .replace(/^https?:\/\//, "") .replace(/\/$/, ""); return `
    ${icon} ${displayUrl}
    `; }) .join("") || "" }
    ${data.summary && resumeConfig.sections.summary?.enabled ? `

    ${resumeConfig.sections.summary.title || "Summary"}

    ${data.summary.content}
    ` : "" }
    ${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", }, })}
    ${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", }, })}
    `; }; export const GET: APIRoute = async ({ request }) => { try { if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) { return new Response("Resume not configured", { status: 404 }); } const url = new URL(request.url); const baseUrl = `${url.protocol}//${url.host}`; const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`); if (!response.ok) { throw new Error( `Failed to fetch resume: ${response.status} ${response.statusText}`, ); } const tomlContent = await response.text(); const resumeData: ResumeData = TOML.parse( tomlContent, ) as unknown as ResumeData; const htmlContent = await generateResumeHTML(resumeData); // Launch browser with Playwright const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || (process.env.NODE_ENV === "production" ? "/usr/bin/google-chrome" : undefined), args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-web-security', '--disable-features=VizDisplayCompositor' ] }); const page = await browser.newPage(); await page.setContent(htmlContent, { waitUntil: 'networkidle' }); const pdfBuffer = await page.pdf({ format: 'A4', margin: { top: '0.2in', bottom: '0.2in', left: '0.2in', right: '0.2in', }, printBackground: true, scale: 0.9, }); await browser.close(); return new Response(pdfBuffer, { headers: { "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename="Atridad_Lahiji_Resume.pdf"`, "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); } catch (error) { console.error("Error generating PDF:", error); return new Response("Error generating PDF", { status: 500 }); } };