Overhauled resume system
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m3s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m3s
This commit is contained in:
321
src/pages/api/resume/pdf.ts
Normal file
321
src/pages/api/resume/pdf.ts
Normal file
@ -0,0 +1,321 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import puppeteer from "puppeteer";
|
||||
import { siteConfig } from "../../../config/data";
|
||||
import * as TOML from "@iarna/toml";
|
||||
|
||||
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;
|
||||
}[];
|
||||
}
|
||||
|
||||
const generateResumeHTML = (data: ResumeData): string => {
|
||||
const resumeConfig = siteConfig.resume;
|
||||
|
||||
const skillsHTML =
|
||||
data.skills
|
||||
?.map((skill) => {
|
||||
const progressValue = skill.level * 20;
|
||||
return `
|
||||
<div class="mb-1">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<span class="text-xs font-medium text-gray-900">${skill.name}</span>
|
||||
<span class="text-xs text-gray-600">${skill.level}/5</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: ${progressValue}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("") || "";
|
||||
|
||||
const experienceHTML =
|
||||
data.experience
|
||||
?.map((exp) => {
|
||||
const descriptionList = exp.description
|
||||
.map((item) => `<li class="mb-1">${item}</li>`)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="mb-3 pl-2 border-l-2 border-blue-600">
|
||||
<h3 class="text-xs font-semibold text-gray-900 mb-1">${exp.position}</h3>
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
<span class="font-medium">${exp.company}</span>
|
||||
<span class="mx-1">•</span>
|
||||
<span>${exp.date}</span>
|
||||
<span class="mx-1">•</span>
|
||||
<span>${exp.location}</span>
|
||||
</div>
|
||||
<ul class="text-xs text-gray-700 leading-tight ml-3 list-disc">
|
||||
${descriptionList}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("") || "";
|
||||
|
||||
const educationHTML =
|
||||
data.education
|
||||
?.map((edu) => {
|
||||
const detailsList = edu.details
|
||||
? edu.details
|
||||
.map((detail) => `<li class="mb-1">${detail}</li>`)
|
||||
.join("")
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="mb-3 pl-2 border-l-2 border-green-600">
|
||||
<h3 class="text-xs font-semibold text-gray-900 mb-1">${edu.institution}</h3>
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
<span class="font-medium">${edu.degree} in ${edu.field}</span>
|
||||
<span class="mx-1">•</span>
|
||||
<span>${edu.date}</span>
|
||||
</div>
|
||||
${detailsList ? `<ul class="text-xs text-gray-700 leading-tight ml-3 list-disc">${detailsList}</ul>` : ""}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("") || "";
|
||||
|
||||
const volunteerHTML =
|
||||
data.volunteer
|
||||
?.map((vol) => {
|
||||
return `
|
||||
<div class="mb-2 pl-2 border-l-2 border-purple-600">
|
||||
<h3 class="text-xs font-semibold text-gray-900 mb-1">${vol.organization}</h3>
|
||||
<div class="text-xs text-gray-600">
|
||||
<span class="font-medium">${vol.position}</span>
|
||||
<span class="mx-1">•</span>
|
||||
<span>${vol.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("") || "";
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${data.basics.name} - Resume</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@media print {
|
||||
body {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
|
||||
.resume-container {
|
||||
max-width: 8.5in;
|
||||
min-height: 11in;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white text-gray-900 text-xs leading-tight p-3">
|
||||
<div class="resume-container mx-auto">
|
||||
<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.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>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
${
|
||||
data.summary && resumeConfig.sections.summary?.enabled
|
||||
? `
|
||||
<section class="mb-3">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${resumeConfig.sections.summary.title || "Summary"}
|
||||
</h2>
|
||||
<div class="text-xs text-gray-700 leading-tight">${data.summary.content}</div>
|
||||
</section>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<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>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</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>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.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.jsonFile}`);
|
||||
|
||||
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 = generateResumeHTML(resumeData);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(htmlContent, { waitUntil: "networkidle0" });
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: "A4",
|
||||
margin: {
|
||||
top: "0.2in",
|
||||
bottom: "0.2in",
|
||||
left: "0.2in",
|
||||
right: "0.2in",
|
||||
},
|
||||
printBackground: true,
|
||||
preferCSSPageSize: false,
|
||||
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": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating PDF:", error);
|
||||
return new Response("Error generating PDF", { status: 500 });
|
||||
}
|
||||
};
|
@ -2,62 +2,49 @@
|
||||
import { Icon } from "astro-icon/components";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import ResumeSkills from "../components/ResumeSkills";
|
||||
import PdfDownloadButton from "../components/PdfDownloadButton";
|
||||
import { siteConfig } from "../config/data";
|
||||
import "../styles/global.css";
|
||||
import * as TOML from "@iarna/toml";
|
||||
|
||||
interface ResumeData {
|
||||
basics: {
|
||||
name: string;
|
||||
email: string;
|
||||
url?: { href: string };
|
||||
};
|
||||
sections: {
|
||||
summary: { name: string; content: string };
|
||||
website?: string;
|
||||
profiles: {
|
||||
name: string;
|
||||
items: {
|
||||
network: string;
|
||||
username: string;
|
||||
url: { href: string };
|
||||
}[];
|
||||
};
|
||||
skills: {
|
||||
name: string;
|
||||
items: { id: string; name: string; level: number }[];
|
||||
};
|
||||
experience: {
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
date: string;
|
||||
location: string;
|
||||
summary: string;
|
||||
url?: { href: string };
|
||||
}[];
|
||||
};
|
||||
education: {
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
institution: string;
|
||||
studyType: string;
|
||||
area: string;
|
||||
date: string;
|
||||
summary: string;
|
||||
}[];
|
||||
};
|
||||
volunteer: {
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
organization: string;
|
||||
position: string;
|
||||
date: string;
|
||||
}[];
|
||||
};
|
||||
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;
|
||||
}[];
|
||||
}
|
||||
|
||||
let resumeData: ResumeData | undefined = undefined;
|
||||
@ -65,14 +52,14 @@ let fetchError: string | null = null;
|
||||
|
||||
// Check if resume JSON file is configured before attempting to fetch
|
||||
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) {
|
||||
return Astro.redirect('/');
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the base URL
|
||||
const baseUrl = Astro.url.origin;
|
||||
|
||||
// Fetch the JSON file from the public directory
|
||||
// Fetch the TOML file from the public directory
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`);
|
||||
|
||||
if (!response.ok) {
|
||||
@ -81,23 +68,12 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
resumeData = await response.json();
|
||||
|
||||
if (resumeData && resumeData.sections && resumeData.sections.skills) {
|
||||
const resumeSkills = resumeData.sections.skills;
|
||||
if (resumeSkills.items) {
|
||||
const tsSkill = resumeSkills.items.find(
|
||||
(s) => s.name === "Typescrpt",
|
||||
);
|
||||
if (tsSkill) {
|
||||
tsSkill.name = "Typescript";
|
||||
}
|
||||
}
|
||||
}
|
||||
const tomlContent = await response.text();
|
||||
resumeData = TOML.parse(tomlContent) as unknown as ResumeData;
|
||||
} catch (error) {
|
||||
console.error("Error loading resume data:", error);
|
||||
// Return to home page when resume data cannot be loaded
|
||||
return Astro.redirect('/');
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
const data = resumeData;
|
||||
@ -105,7 +81,7 @@ const resumeConfig = siteConfig.resume;
|
||||
|
||||
// At this point, data is guaranteed to exist since we redirect on error
|
||||
if (!data) {
|
||||
return Astro.redirect('/');
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
---
|
||||
|
||||
@ -115,91 +91,87 @@ if (!data) {
|
||||
{data.basics.name}
|
||||
</h1>
|
||||
|
||||
<div class="flex justify-center items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 mb-4 sm:mb-6">
|
||||
{data.basics.email && (
|
||||
<a
|
||||
href={`mailto:${data.basics.email}`}
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="mdi:email" /> {data.basics.email}
|
||||
</a>
|
||||
)}
|
||||
{data.sections.profiles.items.find(
|
||||
(p) => p.network === "GitHub",
|
||||
) && (
|
||||
<a
|
||||
href={
|
||||
data.sections.profiles.items.find(
|
||||
(p) => p.network === "GitHub",
|
||||
)!.url.href
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="simple-icons:github" /> GitHub
|
||||
</a>
|
||||
)}
|
||||
{data.sections.profiles.items.find(
|
||||
(p) => p.network === "linkedin",
|
||||
) && (
|
||||
<a
|
||||
href={
|
||||
data.sections.profiles.items.find(
|
||||
(p) => p.network === "linkedin",
|
||||
)!.url.href
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="simple-icons:linkedin" /> LinkedIn
|
||||
</a>
|
||||
)}
|
||||
<div
|
||||
class="flex justify-center items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 mb-4 sm:mb-6"
|
||||
>
|
||||
{
|
||||
data.basics.email && (
|
||||
<a
|
||||
href={`mailto:${data.basics.email}`}
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="mdi:email" /> {data.basics.email}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.basics.profiles.find((p) => p.network === "GitHub") && (
|
||||
<a
|
||||
href={
|
||||
data.basics.profiles.find(
|
||||
(p) => p.network === "GitHub",
|
||||
)!.url
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="simple-icons:github" /> GitHub
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.basics.profiles.find((p) => p.network === "LinkedIn") && (
|
||||
<a
|
||||
href={
|
||||
data.basics.profiles.find(
|
||||
(p) => p.network === "LinkedIn",
|
||||
)!.url
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="simple-icons:linkedin" /> LinkedIn
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{resumeConfig.pdfFile?.path && (
|
||||
<div class="text-center mb-6 sm:mb-8">
|
||||
<a
|
||||
href={resumeConfig.pdfFile.path}
|
||||
download={resumeConfig.pdfFile.filename}
|
||||
class="btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base"
|
||||
>
|
||||
<Icon name="mdi:download" /> {resumeConfig.pdfFile.displayText}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<PdfDownloadButton client:load />
|
||||
|
||||
{data.sections.summary && resumeConfig.sections.summary?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.summary.title || data.sections.summary.name || "Summary"}
|
||||
</h2>
|
||||
<div set:html={data.sections.summary.content} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.sections.profiles &&
|
||||
data.sections.profiles.items &&
|
||||
data.sections.profiles.items.length > 0 &&
|
||||
resumeConfig.sections.profiles?.enabled && (
|
||||
{
|
||||
data.summary && resumeConfig.sections.summary?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"}
|
||||
{resumeConfig.sections.summary.title || "Summary"}
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-3 sm:gap-4">
|
||||
{data.sections.profiles.items.map(
|
||||
(profile) => {
|
||||
<div>{data.summary.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
data.basics.profiles &&
|
||||
data.basics.profiles.length > 0 &&
|
||||
resumeConfig.sections.profiles?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.profiles.title ||
|
||||
"Profiles"}
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-3 sm:gap-4">
|
||||
{data.basics.profiles.map((profile) => {
|
||||
// Use Simple Icons directly based on network name
|
||||
// Convert network name to lowercase and use simple-icons format
|
||||
const iconName = `simple-icons:${profile.network.toLowerCase()}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={profile.url.href}
|
||||
href={profile.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||
@ -208,42 +180,47 @@ if (!data) {
|
||||
{profile.network}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{data.sections.skills &&
|
||||
data.sections.skills.items &&
|
||||
data.sections.skills.items.length > 0 &&
|
||||
resumeConfig.sections.skills?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.skills.title || data.sections.skills.name || "Skills"}
|
||||
</h2>
|
||||
<ResumeSkills
|
||||
skills={data.sections.skills.items}
|
||||
client:load
|
||||
/>
|
||||
{
|
||||
data.skills &&
|
||||
data.skills.length > 0 &&
|
||||
resumeConfig.sections.skills?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.skills.title || "Skills"}
|
||||
</h2>
|
||||
<ResumeSkills
|
||||
skills={data.skills.map((skill, index) => ({
|
||||
id: `skill-${index}`,
|
||||
name: skill.name,
|
||||
level: skill.level,
|
||||
}))}
|
||||
client:load
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{data.sections.experience &&
|
||||
data.sections.experience.items &&
|
||||
data.sections.experience.items.length > 0 &&
|
||||
resumeConfig.sections.experience?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"}
|
||||
</h2>
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
{data.sections.experience.items.map(
|
||||
(experience) => (
|
||||
{
|
||||
data.experience &&
|
||||
data.experience.length > 0 &&
|
||||
resumeConfig.sections.experience?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.experience.title ||
|
||||
"Experience"}
|
||||
</h2>
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
{data.experience.map((experience) => (
|
||||
<div class="border-l-2 border-primary pl-4 sm:pl-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold">
|
||||
{experience.position}
|
||||
@ -253,17 +230,18 @@ if (!data) {
|
||||
{experience.company}
|
||||
</span>
|
||||
<span>{experience.date}</span>
|
||||
<span>
|
||||
{experience.location}
|
||||
</span>
|
||||
<span>{experience.location}</span>
|
||||
</div>
|
||||
<div
|
||||
class="prose prose-sm sm:prose-base max-w-none"
|
||||
set:html={experience.summary}
|
||||
/>
|
||||
{experience.url && experience.url.href && (
|
||||
<ul class="list-disc list-inside space-y-1 text-sm sm:text-base">
|
||||
{experience.description.map(
|
||||
(item) => (
|
||||
<li>{item}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
{experience.url && (
|
||||
<a
|
||||
href={experience.url.href}
|
||||
href={experience.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2"
|
||||
@ -273,64 +251,67 @@ if (!data) {
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{data.sections.education &&
|
||||
data.sections.education.items &&
|
||||
data.sections.education.items.length > 0 &&
|
||||
resumeConfig.sections.education?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.education.title || data.sections.education.name || "Education"}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{data.sections.education.items.map(
|
||||
(education) => (
|
||||
{
|
||||
data.education &&
|
||||
data.education.length > 0 &&
|
||||
resumeConfig.sections.education?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.education.title ||
|
||||
"Education"}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{data.education.map((education) => (
|
||||
<div class="border-l-2 border-secondary pl-4 sm:pl-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold">
|
||||
{education.institution}
|
||||
</h3>
|
||||
<div class="text-sm sm:text-base text-base-content/70 mb-2">
|
||||
<span class="font-medium">
|
||||
{education.studyType} in{" "}
|
||||
{education.area}
|
||||
{education.degree} in{" "}
|
||||
{education.field}
|
||||
</span>
|
||||
<span class="block sm:inline sm:ml-4">
|
||||
{education.date}
|
||||
</span>
|
||||
</div>
|
||||
{education.summary && (
|
||||
<div
|
||||
class="prose prose-sm sm:prose-base max-w-none"
|
||||
set:html={education.summary}
|
||||
/>
|
||||
{education.details && (
|
||||
<ul class="list-disc list-inside space-y-1 text-sm sm:text-base">
|
||||
{education.details.map(
|
||||
(detail) => (
|
||||
<li>{detail}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{data.sections.volunteer &&
|
||||
data.sections.volunteer.items &&
|
||||
data.sections.volunteer.items.length > 0 &&
|
||||
resumeConfig.sections.volunteer?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{data.sections.volunteer.items.map(
|
||||
(volunteer) => (
|
||||
{
|
||||
data.volunteer &&
|
||||
data.volunteer.length > 0 &&
|
||||
resumeConfig.sections.volunteer?.enabled && (
|
||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6 break-words">
|
||||
<h2 class="card-title text-xl sm:text-2xl">
|
||||
{resumeConfig.sections.volunteer.title ||
|
||||
"Volunteer Work"}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{data.volunteer.map((volunteer) => (
|
||||
<div class="border-l-2 border-accent pl-4 sm:pl-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold">
|
||||
{volunteer.organization}
|
||||
@ -344,11 +325,11 @@ if (!data) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Layout>
|
||||
|
Reference in New Issue
Block a user