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:
@ -13,13 +13,16 @@
|
|||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@astrojs/preact": "^4.1.0",
|
"@astrojs/preact": "^4.1.0",
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/rss": "^4.0.12",
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
"@preact/signals": "^2.2.0",
|
"@preact/signals": "^2.2.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
|
"@types/puppeteer": "^7.0.4",
|
||||||
"astro": "^5.10.0",
|
"astro": "^5.10.0",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
"lucide-preact": "^0.518.0",
|
"lucide-preact": "^0.518.0",
|
||||||
"preact": "^10.26.9",
|
"preact": "^10.26.9",
|
||||||
|
"puppeteer": "^24.10.2",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10"
|
||||||
},
|
},
|
||||||
@ -31,7 +34,8 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"sharp"
|
"sharp",
|
||||||
|
"puppeteer"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
595
pnpm-lock.yaml
generated
595
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,483 +0,0 @@
|
|||||||
{
|
|
||||||
"basics": {
|
|
||||||
"name": "Atridad Lahiji",
|
|
||||||
"headline": "",
|
|
||||||
"email": "me@atri.dad",
|
|
||||||
"phone": "",
|
|
||||||
"location": "",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": "https://atri.dad"
|
|
||||||
},
|
|
||||||
"customFields": [],
|
|
||||||
"picture": {
|
|
||||||
"url": "",
|
|
||||||
"size": 64,
|
|
||||||
"aspectRatio": 1,
|
|
||||||
"borderRadius": 0,
|
|
||||||
"effects": {
|
|
||||||
"hidden": false,
|
|
||||||
"border": false,
|
|
||||||
"grayscale": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"summary": {
|
|
||||||
"name": "Summary",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "summary",
|
|
||||||
"content": "<p>I am a full-stack web developer and researcher with a background maintaining and developing for large-scale enterprise software systems. I am in the process of completing my Master of Science in Computer Science under the supervision of Dr. Nathaniel Osgood at the University of Saskatchewan. I have completed my course work and am now moving into writing my thesis which can be done asynchronously.</p><p></p>"
|
|
||||||
},
|
|
||||||
"awards": {
|
|
||||||
"name": "Awards",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "awards",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"certifications": {
|
|
||||||
"name": "Certifications",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "certifications",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"education": {
|
|
||||||
"name": "Education",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "education",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "xtkfnu2zq3myh09pumehphx9",
|
|
||||||
"visible": true,
|
|
||||||
"institution": "University of Saskatchewan",
|
|
||||||
"studyType": "Masters",
|
|
||||||
"area": "Computer Science",
|
|
||||||
"score": "",
|
|
||||||
"date": "2024 – Present",
|
|
||||||
"summary": "<p style=\"text-align: left\">Supervisor: Dr. Nathaniel Osgood</p><ul><li><p style=\"text-align: left\">CMPT 838: Computer Security</p></li><li><p style=\"text-align: left\">CMPT 815: Computer Systems and Performance Evaluation</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "o4my8au0d7c6bf09vlqwxvyw",
|
|
||||||
"visible": true,
|
|
||||||
"institution": "University of Saskatchewan",
|
|
||||||
"studyType": "Bachelors (3 Year)",
|
|
||||||
"area": "Computer Science",
|
|
||||||
"score": "",
|
|
||||||
"date": "2016 – 2019",
|
|
||||||
"summary": "",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "pnwpsei7ag1yldmtv9f4kt4e",
|
|
||||||
"visible": true,
|
|
||||||
"institution": "University of Saskatchewan",
|
|
||||||
"studyType": "Bachelors",
|
|
||||||
"area": "Computer Engineering",
|
|
||||||
"score": "",
|
|
||||||
"date": "2012 – 2017",
|
|
||||||
"summary": "",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"experience": {
|
|
||||||
"name": "Experience",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "experience",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "gn67fi9oygi5tz1x3p3r7mbf",
|
|
||||||
"visible": true,
|
|
||||||
"company": "Atash Consulting",
|
|
||||||
"position": "Owner/Developer",
|
|
||||||
"location": "Edmonton, Alberta",
|
|
||||||
"date": "June 2019 – Present",
|
|
||||||
"summary": "<ul><li><p>Builds mobile and web applications for small-medium sized businesses</p></li><li><p>Provides consulting on as application development, system architecture, DevOps, etc</p></li><li><p>Hosting websites for small-medium sized businesses</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": "https://atash.dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "x8ok2hutceh7lroyhwa7kj0h",
|
|
||||||
"visible": true,
|
|
||||||
"company": "University of Saskatchewan CEPHIL Lab",
|
|
||||||
"position": "Research Technician",
|
|
||||||
"location": "Saskatoon, Saskatchewan",
|
|
||||||
"date": "November 2023 – Present",
|
|
||||||
"summary": "<ul><li><p>Developing mobile and web applications</p></li><li><p>Coordinating with other grant researchers to deliver a minimum viable product</p></li><li><p>Gathering requirements from stakeholders to craft a product timeline</p></li><li><p>Acting as a technical lead and supervisor to a developer intern</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "f0kyaxcy3syb8wazs3ye662i",
|
|
||||||
"visible": true,
|
|
||||||
"company": "Alberta Motor Association",
|
|
||||||
"position": "Software Developer II",
|
|
||||||
"location": "Edmonton, Alberta",
|
|
||||||
"date": "August 2021 – November 2023",
|
|
||||||
"summary": "<ul><li><p>Developed and maintained internal enterprise-level business applications leveraging Amazon Web Services (AWS)</p></li><li><p>Used React and Create React App (CRA) for standalone applications and micro-front-ends</p></li><li><p>Developed an in-house payment gateway for all AMA services that integrates with Stripe</p></li><li><p>Provided tier 3 support support for internal service</p></li><li><p>Participated in a bi-monthly 24/7 on-call rotation</p></li><li><p>Mentored students in the organization’s Developer in Training program</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "yikqef72i068lfiy8iiwjm45",
|
|
||||||
"visible": true,
|
|
||||||
"company": "University of Alberta IST",
|
|
||||||
"position": "Software Developer",
|
|
||||||
"location": "Edmonton, Alberta",
|
|
||||||
"date": "October 2019 – August 2021",
|
|
||||||
"summary": "<ul><li><p>Front-end development of web applications using Vue.js</p></li><li><p>Leveraged Amazon Web Services to adopt a serverless architecture</p></li><li><p>Maintained a secure exam application developed in-house</p></li><li><p>Monitored and maintained an exam scheduling system hosted on-premises</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "wzqfv3h8rxs6574z5hlvrhm7",
|
|
||||||
"visible": true,
|
|
||||||
"company": "University of Alberta IST",
|
|
||||||
"position": "Support Analyst",
|
|
||||||
"location": "Edmonton, Alberta",
|
|
||||||
"date": "July 2017 – October 2019",
|
|
||||||
"summary": "<ul><li><p>Provided support for our Moodle installation to students, faculty, and staff</p></li><li><p>Front-end development of web applications using Vue.js</p></li></ul>",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"volunteer": {
|
|
||||||
"name": "Volunteering",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "volunteer",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "xhg1p7exqggrjkldszplj1wk",
|
|
||||||
"visible": true,
|
|
||||||
"organization": "Big Brother Big Sisters",
|
|
||||||
"position": "Mentor",
|
|
||||||
"location": "",
|
|
||||||
"date": "2021 – 2022",
|
|
||||||
"summary": "",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"interests": {
|
|
||||||
"name": "Interests",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "interests",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"name": "Languages",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "languages",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"name": "Profiles",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "profiles",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "zuto1s9atwo6tdx9qfa9ggug",
|
|
||||||
"visible": true,
|
|
||||||
"network": "GitHub",
|
|
||||||
"username": "atridadl",
|
|
||||||
"icon": "github",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": "https://github.com/atridadl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "satbehrw5da07dmi8y8j70kl",
|
|
||||||
"visible": true,
|
|
||||||
"network": "linkedin",
|
|
||||||
"username": "atridadl",
|
|
||||||
"icon": "linkedin",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": "https://www.linkedin.com/in/atridadl/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "yorfn8ku98u5o0jzvumo9q2v",
|
|
||||||
"visible": true,
|
|
||||||
"network": "Gitea",
|
|
||||||
"username": "atridad",
|
|
||||||
"icon": "gitea",
|
|
||||||
"url": {
|
|
||||||
"label": "",
|
|
||||||
"href": "https://git.atri.dad/atridad"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"projects": {
|
|
||||||
"name": "Projects",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "projects",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"publications": {
|
|
||||||
"name": "Publications",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "publications",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"name": "References",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "references",
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"skills": {
|
|
||||||
"name": "Skills",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "skills",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "lpwyb43emmmukje3c49yupu7",
|
|
||||||
"visible": true,
|
|
||||||
"name": "HTML + CSS + JavaScript",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "c5qu0q3wct06oj1wa3u3tkar",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Typescrpt",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "qtq2qfeoa0bskykwhfzmlpng",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Vitest, Jest, and Playwright",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "b6k6q4r592uesacsz03dtvyk",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Docker + Docker Compose",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "lc3eu9r8vvqhsst1mkeqxgse",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Go (Golang)",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "lme3ob0kfpe5hgsuar42nmi6",
|
|
||||||
"visible": true,
|
|
||||||
"name": "SQL (PostgreSQL, MySQL, SQLite)",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "f58rq48rtsgdftfcbpt785is",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Python",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ht9fn1i89gm0e3gf5mfde0os",
|
|
||||||
"visible": true,
|
|
||||||
"name": "SCRUM",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "vtpxeg6r0os9ygjmg384wo7f",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Amazon Web Services (AWS)",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tk3i1xdw92vny0fk7001rrj7",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Ruby",
|
|
||||||
"description": "",
|
|
||||||
"level": 2,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "jqy6vkxl8hed4vgow0z12vwy",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Test Driven Development",
|
|
||||||
"description": "",
|
|
||||||
"level": 3,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "oalwcevey6plalwasugwf4q7",
|
|
||||||
"visible": true,
|
|
||||||
"name": "C#",
|
|
||||||
"description": "",
|
|
||||||
"level": 3,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "skhsek829sf8012wbwd38fl8",
|
|
||||||
"visible": true,
|
|
||||||
"name": "PHP",
|
|
||||||
"description": "",
|
|
||||||
"level": 2,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "feotadkdeli1ukx3u3ix86ig",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Time Management",
|
|
||||||
"description": "",
|
|
||||||
"level": 4,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "a993l06kuyinj9l88uz3ztux",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Problem Solving",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rhyu2toaznnidknrz244klqq",
|
|
||||||
"visible": true,
|
|
||||||
"name": "Attention to Detail",
|
|
||||||
"description": "",
|
|
||||||
"level": 5,
|
|
||||||
"keywords": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"b5li7wh27iylvqlsmeavvkzh": {
|
|
||||||
"name": "Custom Section",
|
|
||||||
"columns": 1,
|
|
||||||
"separateLinks": true,
|
|
||||||
"visible": true,
|
|
||||||
"id": "b5li7wh27iylvqlsmeavvkzh",
|
|
||||||
"items": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"template": "glalie",
|
|
||||||
"layout": [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"summary",
|
|
||||||
"education",
|
|
||||||
"experience",
|
|
||||||
"projects",
|
|
||||||
"references",
|
|
||||||
"custom.b5li7wh27iylvqlsmeavvkzh"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"profiles",
|
|
||||||
"skills",
|
|
||||||
"volunteer",
|
|
||||||
"interests",
|
|
||||||
"certifications",
|
|
||||||
"awards",
|
|
||||||
"publications",
|
|
||||||
"languages"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"css": {
|
|
||||||
"value": ".text-2xl {\n\tfont-size: 30px;\n}",
|
|
||||||
"visible": true
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"margin": 16,
|
|
||||||
"format": "letter",
|
|
||||||
"options": {
|
|
||||||
"breakLine": true,
|
|
||||||
"pageNumbers": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"background": "#ffffff",
|
|
||||||
"text": "#000000",
|
|
||||||
"primary": "#0284c7"
|
|
||||||
},
|
|
||||||
"typography": {
|
|
||||||
"font": {
|
|
||||||
"family": "Lato",
|
|
||||||
"subset": "latin",
|
|
||||||
"variants": [
|
|
||||||
"regular"
|
|
||||||
],
|
|
||||||
"size": 14
|
|
||||||
},
|
|
||||||
"lineHeight": 0.95,
|
|
||||||
"hideIcons": false,
|
|
||||||
"underlineLinks": true
|
|
||||||
},
|
|
||||||
"notes": ""
|
|
||||||
}
|
|
||||||
}
|
|
174
public/files/resume.toml
Normal file
174
public/files/resume.toml
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
[basics]
|
||||||
|
name = "Atridad Lahiji"
|
||||||
|
email = "me@atri.dad"
|
||||||
|
website = "https://atri.dad"
|
||||||
|
|
||||||
|
[[basics.profiles]]
|
||||||
|
network = "GitHub"
|
||||||
|
username = "atridadl"
|
||||||
|
url = "https://github.com/atridadl"
|
||||||
|
|
||||||
|
[[basics.profiles]]
|
||||||
|
network = "LinkedIn"
|
||||||
|
username = "atridadl"
|
||||||
|
url = "https://www.linkedin.com/in/atridadl/"
|
||||||
|
|
||||||
|
[[basics.profiles]]
|
||||||
|
network = "Gitea"
|
||||||
|
username = "atridad"
|
||||||
|
url = "https://git.atri.dad/atridad"
|
||||||
|
|
||||||
|
[summary]
|
||||||
|
content = "I am a full-stack web developer and researcher with a background maintaining and developing for large-scale enterprise software systems."
|
||||||
|
|
||||||
|
[[experience]]
|
||||||
|
company = "Atash Consulting"
|
||||||
|
position = "Owner/Developer"
|
||||||
|
location = "Edmonton, Alberta"
|
||||||
|
date = "June 2019 – Present"
|
||||||
|
description = [
|
||||||
|
"Builds mobile and web applications for small-medium sized businesses",
|
||||||
|
"Provides consulting on application development, system architecture, DevOps, etc",
|
||||||
|
"Hosting websites for small-medium sized businesses",
|
||||||
|
]
|
||||||
|
url = "https://atash.dev"
|
||||||
|
|
||||||
|
[[experience]]
|
||||||
|
company = "University of Saskatchewan CEPHIL Lab"
|
||||||
|
position = "Research Technician"
|
||||||
|
location = "Saskatoon, Saskatchewan"
|
||||||
|
date = "November 2023 – Present"
|
||||||
|
description = [
|
||||||
|
"Developing mobile and web applications",
|
||||||
|
"Coordinating with other grant researchers to deliver a minimum viable product",
|
||||||
|
"Gathering requirements from stakeholders to craft a product timeline",
|
||||||
|
"Acting as a technical lead and supervisor to a developer intern",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[experience]]
|
||||||
|
company = "Alberta Motor Association"
|
||||||
|
position = "Software Developer II"
|
||||||
|
location = "Edmonton, Alberta"
|
||||||
|
date = "August 2021 – November 2023"
|
||||||
|
description = [
|
||||||
|
"Developed and maintained internal enterprise-level business applications leveraging Amazon Web Services (AWS)",
|
||||||
|
"Used React and Create React App (CRA) for standalone applications and micro-front-ends",
|
||||||
|
"Developed an in-house payment gateway for all AMA services that integrates with Stripe",
|
||||||
|
"Provided tier 3 support for internal services",
|
||||||
|
"Participated in a bi-monthly 24/7 on-call rotation",
|
||||||
|
"Mentored students in the organization's Developer in Training program",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[experience]]
|
||||||
|
company = "University of Alberta IST"
|
||||||
|
position = "Software Developer"
|
||||||
|
location = "Edmonton, Alberta"
|
||||||
|
date = "October 2019 – August 2021"
|
||||||
|
description = [
|
||||||
|
"Front-end development of web applications using Vue.js",
|
||||||
|
"Leveraged Amazon Web Services to adopt a serverless architecture",
|
||||||
|
"Maintained a secure exam application developed in-house",
|
||||||
|
"Monitored and maintained an exam scheduling system hosted on-premises",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[experience]]
|
||||||
|
company = "University of Alberta IST"
|
||||||
|
position = "Support Analyst"
|
||||||
|
location = "Edmonton, Alberta"
|
||||||
|
date = "July 2017 – October 2019"
|
||||||
|
description = [
|
||||||
|
"Provided support for our Moodle installation to students, faculty, and staff",
|
||||||
|
"Front-end development of web applications using Vue.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[education]]
|
||||||
|
institution = "University of Saskatchewan"
|
||||||
|
degree = "Masters"
|
||||||
|
field = "Computer Science"
|
||||||
|
date = "2024 – Present"
|
||||||
|
details = [
|
||||||
|
"Supervisor: Dr. Nathaniel Osgood",
|
||||||
|
"CMPT 838: Computer Security",
|
||||||
|
"CMPT 815: Computer Systems and Performance Evaluation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[education]]
|
||||||
|
institution = "University of Saskatchewan"
|
||||||
|
degree = "Bachelors (3 Year)"
|
||||||
|
field = "Computer Science"
|
||||||
|
date = "2016 – 2019"
|
||||||
|
|
||||||
|
[[education]]
|
||||||
|
institution = "University of Saskatchewan"
|
||||||
|
degree = "Bachelors"
|
||||||
|
field = "Computer Engineering"
|
||||||
|
date = "2012 – 2017"
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "HTML + CSS + JavaScript"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "TypeScript"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Vitest, Jest, and Playwright"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Docker + Docker Compose"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Go (Golang)"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "SQL (PostgreSQL, MySQL, SQLite)"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Python"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "SCRUM"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Amazon Web Services (AWS)"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Ruby"
|
||||||
|
level = 2
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Test Driven Development"
|
||||||
|
level = 3
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "C#"
|
||||||
|
level = 3
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "PHP"
|
||||||
|
level = 2
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Time Management"
|
||||||
|
level = 4
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Problem Solving"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[skills]]
|
||||||
|
name = "Attention to Detail"
|
||||||
|
level = 5
|
||||||
|
|
||||||
|
[[volunteer]]
|
||||||
|
organization = "Big Brother Big Sisters"
|
||||||
|
position = "Mentor"
|
||||||
|
date = "2021 – 2022"
|
77
src/components/PdfDownloadButton.tsx
Normal file
77
src/components/PdfDownloadButton.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
interface PdfDownloadButtonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PdfDownloadButton({
|
||||||
|
className = "",
|
||||||
|
}: PdfDownloadButtonProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/resume/pdf");
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to generate PDF: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a temporary link element and trigger download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = "Atridad_Lahiji_Resume.pdf";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error downloading PDF:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to download PDF");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="text-center mb-6 sm:mb-8">
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={isLoading}
|
||||||
|
class={`btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base ${className}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<span class="loading loading-spinner"></span>
|
||||||
|
Generating PDF...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
|
||||||
|
</svg>
|
||||||
|
Generate PDF Resume
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{error && <div class="mt-2 text-error text-sm">{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -62,7 +62,7 @@ export const homepageSections: HomepageSections = {
|
|||||||
|
|
||||||
// Resume Configuration
|
// Resume Configuration
|
||||||
export const resumeConfig: ResumeConfig = {
|
export const resumeConfig: ResumeConfig = {
|
||||||
jsonFile: "/files/resume.json",
|
jsonFile: "/files/resume.toml",
|
||||||
pdfFile: {
|
pdfFile: {
|
||||||
path: "/files/Atridad_Lahiji_Resume.pdf",
|
path: "/files/Atridad_Lahiji_Resume.pdf",
|
||||||
filename: "Atridad_Lahiji_Resume.pdf",
|
filename: "Atridad_Lahiji_Resume.pdf",
|
||||||
|
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 { Icon } from "astro-icon/components";
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import ResumeSkills from "../components/ResumeSkills";
|
import ResumeSkills from "../components/ResumeSkills";
|
||||||
|
import PdfDownloadButton from "../components/PdfDownloadButton";
|
||||||
import { siteConfig } from "../config/data";
|
import { siteConfig } from "../config/data";
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
|
import * as TOML from "@iarna/toml";
|
||||||
|
|
||||||
interface ResumeData {
|
interface ResumeData {
|
||||||
basics: {
|
basics: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
url?: { href: string };
|
website?: string;
|
||||||
};
|
|
||||||
sections: {
|
|
||||||
summary: { name: string; content: string };
|
|
||||||
profiles: {
|
profiles: {
|
||||||
name: string;
|
network: string;
|
||||||
items: {
|
username: string;
|
||||||
network: string;
|
url: 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;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
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;
|
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
|
// Check if resume JSON file is configured before attempting to fetch
|
||||||
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) {
|
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) {
|
||||||
return Astro.redirect('/');
|
return Astro.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the base URL
|
// Get the base URL
|
||||||
const baseUrl = Astro.url.origin;
|
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}`);
|
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -81,23 +68,12 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeData = await response.json();
|
const tomlContent = await response.text();
|
||||||
|
resumeData = TOML.parse(tomlContent) as unknown as ResumeData;
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading resume data:", error);
|
console.error("Error loading resume data:", error);
|
||||||
// Return to home page when resume data cannot be loaded
|
// Return to home page when resume data cannot be loaded
|
||||||
return Astro.redirect('/');
|
return Astro.redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = resumeData;
|
const data = resumeData;
|
||||||
@ -105,7 +81,7 @@ const resumeConfig = siteConfig.resume;
|
|||||||
|
|
||||||
// At this point, data is guaranteed to exist since we redirect on error
|
// At this point, data is guaranteed to exist since we redirect on error
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return Astro.redirect('/');
|
return Astro.redirect("/");
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -115,91 +91,87 @@ if (!data) {
|
|||||||
{data.basics.name}
|
{data.basics.name}
|
||||||
</h1>
|
</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">
|
<div
|
||||||
{data.basics.email && (
|
class="flex justify-center items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 mb-4 sm:mb-6"
|
||||||
<a
|
>
|
||||||
href={`mailto:${data.basics.email}`}
|
{
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
data.basics.email && (
|
||||||
>
|
<a
|
||||||
<Icon name="mdi:email" /> {data.basics.email}
|
href={`mailto:${data.basics.email}`}
|
||||||
</a>
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
)}
|
>
|
||||||
{data.sections.profiles.items.find(
|
<Icon name="mdi:email" /> {data.basics.email}
|
||||||
(p) => p.network === "GitHub",
|
</a>
|
||||||
) && (
|
)
|
||||||
<a
|
}
|
||||||
href={
|
{
|
||||||
data.sections.profiles.items.find(
|
data.basics.profiles.find((p) => p.network === "GitHub") && (
|
||||||
(p) => p.network === "GitHub",
|
<a
|
||||||
)!.url.href
|
href={
|
||||||
}
|
data.basics.profiles.find(
|
||||||
target="_blank"
|
(p) => p.network === "GitHub",
|
||||||
rel="noopener noreferrer"
|
)!.url
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
}
|
||||||
>
|
target="_blank"
|
||||||
<Icon name="simple-icons:github" /> GitHub
|
rel="noopener noreferrer"
|
||||||
</a>
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
)}
|
>
|
||||||
{data.sections.profiles.items.find(
|
<Icon name="simple-icons:github" /> GitHub
|
||||||
(p) => p.network === "linkedin",
|
</a>
|
||||||
) && (
|
)
|
||||||
<a
|
}
|
||||||
href={
|
{
|
||||||
data.sections.profiles.items.find(
|
data.basics.profiles.find((p) => p.network === "LinkedIn") && (
|
||||||
(p) => p.network === "linkedin",
|
<a
|
||||||
)!.url.href
|
href={
|
||||||
}
|
data.basics.profiles.find(
|
||||||
target="_blank"
|
(p) => p.network === "LinkedIn",
|
||||||
rel="noopener noreferrer"
|
)!.url
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
}
|
||||||
>
|
target="_blank"
|
||||||
<Icon name="simple-icons:linkedin" /> LinkedIn
|
rel="noopener noreferrer"
|
||||||
</a>
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
)}
|
>
|
||||||
|
<Icon name="simple-icons:linkedin" /> LinkedIn
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{resumeConfig.pdfFile?.path && (
|
<PdfDownloadButton client:load />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.sections.summary && resumeConfig.sections.summary?.enabled && (
|
{
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
data.summary && resumeConfig.sections.summary?.enabled && (
|
||||||
<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 && (
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
<div class="card-body p-4 sm:p-6 break-words">
|
<div class="card-body p-4 sm:p-6 break-words">
|
||||||
<h2 class="card-title text-xl sm:text-2xl">
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
{resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"}
|
{resumeConfig.sections.summary.title || "Summary"}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-wrap gap-3 sm:gap-4">
|
<div>{data.summary.content}</div>
|
||||||
{data.sections.profiles.items.map(
|
</div>
|
||||||
(profile) => {
|
</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
|
// Use Simple Icons directly based on network name
|
||||||
// Convert network name to lowercase and use simple-icons format
|
// Convert network name to lowercase and use simple-icons format
|
||||||
const iconName = `simple-icons:${profile.network.toLowerCase()}`;
|
const iconName = `simple-icons:${profile.network.toLowerCase()}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={profile.url.href}
|
href={profile.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
@ -208,42 +180,47 @@ if (!data) {
|
|||||||
{profile.network}
|
{profile.network}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{data.sections.skills &&
|
{
|
||||||
data.sections.skills.items &&
|
data.skills &&
|
||||||
data.sections.skills.items.length > 0 &&
|
data.skills.length > 0 &&
|
||||||
resumeConfig.sections.skills?.enabled && (
|
resumeConfig.sections.skills?.enabled && (
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
<div class="card-body p-4 sm:p-6 break-words">
|
<div class="card-body p-4 sm:p-6 break-words">
|
||||||
<h2 class="card-title text-xl sm:text-2xl">
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
{resumeConfig.sections.skills.title || data.sections.skills.name || "Skills"}
|
{resumeConfig.sections.skills.title || "Skills"}
|
||||||
</h2>
|
</h2>
|
||||||
<ResumeSkills
|
<ResumeSkills
|
||||||
skills={data.sections.skills.items}
|
skills={data.skills.map((skill, index) => ({
|
||||||
client:load
|
id: `skill-${index}`,
|
||||||
/>
|
name: skill.name,
|
||||||
|
level: skill.level,
|
||||||
|
}))}
|
||||||
|
client:load
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{data.sections.experience &&
|
{
|
||||||
data.sections.experience.items &&
|
data.experience &&
|
||||||
data.sections.experience.items.length > 0 &&
|
data.experience.length > 0 &&
|
||||||
resumeConfig.sections.experience?.enabled && (
|
resumeConfig.sections.experience?.enabled && (
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
<div class="card-body p-4 sm:p-6 break-words">
|
<div class="card-body p-4 sm:p-6 break-words">
|
||||||
<h2 class="card-title text-xl sm:text-2xl">
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
{resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"}
|
{resumeConfig.sections.experience.title ||
|
||||||
</h2>
|
"Experience"}
|
||||||
<div class="space-y-4 sm:space-y-6">
|
</h2>
|
||||||
{data.sections.experience.items.map(
|
<div class="space-y-4 sm:space-y-6">
|
||||||
(experience) => (
|
{data.experience.map((experience) => (
|
||||||
<div class="border-l-2 border-primary pl-4 sm:pl-6">
|
<div class="border-l-2 border-primary pl-4 sm:pl-6">
|
||||||
<h3 class="text-lg sm:text-xl font-semibold">
|
<h3 class="text-lg sm:text-xl font-semibold">
|
||||||
{experience.position}
|
{experience.position}
|
||||||
@ -253,17 +230,18 @@ if (!data) {
|
|||||||
{experience.company}
|
{experience.company}
|
||||||
</span>
|
</span>
|
||||||
<span>{experience.date}</span>
|
<span>{experience.date}</span>
|
||||||
<span>
|
<span>{experience.location}</span>
|
||||||
{experience.location}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<ul class="list-disc list-inside space-y-1 text-sm sm:text-base">
|
||||||
class="prose prose-sm sm:prose-base max-w-none"
|
{experience.description.map(
|
||||||
set:html={experience.summary}
|
(item) => (
|
||||||
/>
|
<li>{item}</li>
|
||||||
{experience.url && experience.url.href && (
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
{experience.url && (
|
||||||
<a
|
<a
|
||||||
href={experience.url.href}
|
href={experience.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2"
|
class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2"
|
||||||
@ -273,64 +251,67 @@ if (!data) {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{data.sections.education &&
|
{
|
||||||
data.sections.education.items &&
|
data.education &&
|
||||||
data.sections.education.items.length > 0 &&
|
data.education.length > 0 &&
|
||||||
resumeConfig.sections.education?.enabled && (
|
resumeConfig.sections.education?.enabled && (
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
<div class="card-body p-4 sm:p-6 break-words">
|
<div class="card-body p-4 sm:p-6 break-words">
|
||||||
<h2 class="card-title text-xl sm:text-2xl">
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
{resumeConfig.sections.education.title || data.sections.education.name || "Education"}
|
{resumeConfig.sections.education.title ||
|
||||||
</h2>
|
"Education"}
|
||||||
<div class="space-y-4">
|
</h2>
|
||||||
{data.sections.education.items.map(
|
<div class="space-y-4">
|
||||||
(education) => (
|
{data.education.map((education) => (
|
||||||
<div class="border-l-2 border-secondary pl-4 sm:pl-6">
|
<div class="border-l-2 border-secondary pl-4 sm:pl-6">
|
||||||
<h3 class="text-lg sm:text-xl font-semibold">
|
<h3 class="text-lg sm:text-xl font-semibold">
|
||||||
{education.institution}
|
{education.institution}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="text-sm sm:text-base text-base-content/70 mb-2">
|
<div class="text-sm sm:text-base text-base-content/70 mb-2">
|
||||||
<span class="font-medium">
|
<span class="font-medium">
|
||||||
{education.studyType} in{" "}
|
{education.degree} in{" "}
|
||||||
{education.area}
|
{education.field}
|
||||||
</span>
|
</span>
|
||||||
<span class="block sm:inline sm:ml-4">
|
<span class="block sm:inline sm:ml-4">
|
||||||
{education.date}
|
{education.date}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{education.summary && (
|
{education.details && (
|
||||||
<div
|
<ul class="list-disc list-inside space-y-1 text-sm sm:text-base">
|
||||||
class="prose prose-sm sm:prose-base max-w-none"
|
{education.details.map(
|
||||||
set:html={education.summary}
|
(detail) => (
|
||||||
/>
|
<li>{detail}</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{data.sections.volunteer &&
|
{
|
||||||
data.sections.volunteer.items &&
|
data.volunteer &&
|
||||||
data.sections.volunteer.items.length > 0 &&
|
data.volunteer.length > 0 &&
|
||||||
resumeConfig.sections.volunteer?.enabled && (
|
resumeConfig.sections.volunteer?.enabled && (
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
<div class="card-body p-4 sm:p-6 break-words">
|
<div class="card-body p-4 sm:p-6 break-words">
|
||||||
<h2 class="card-title text-xl sm:text-2xl">
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
{resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"}
|
{resumeConfig.sections.volunteer.title ||
|
||||||
</h2>
|
"Volunteer Work"}
|
||||||
<div class="space-y-4">
|
</h2>
|
||||||
{data.sections.volunteer.items.map(
|
<div class="space-y-4">
|
||||||
(volunteer) => (
|
{data.volunteer.map((volunteer) => (
|
||||||
<div class="border-l-2 border-accent pl-4 sm:pl-6">
|
<div class="border-l-2 border-accent pl-4 sm:pl-6">
|
||||||
<h3 class="text-lg sm:text-xl font-semibold">
|
<h3 class="text-lg sm:text-xl font-semibold">
|
||||||
{volunteer.organization}
|
{volunteer.organization}
|
||||||
@ -344,11 +325,11 @@ if (!data) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
Reference in New Issue
Block a user