This commit is contained in:
90
README.md
90
README.md
@ -7,7 +7,7 @@ My personal website built with Astro and Preact!
|
|||||||
- **Resume Management**
|
- **Resume Management**
|
||||||
- **Blog Posts**
|
- **Blog Posts**
|
||||||
- **Projects**
|
- **Projects**
|
||||||
- **Talks & Presentations**
|
- **Talks**
|
||||||
- **Terminal View**
|
- **Terminal View**
|
||||||
|
|
||||||
## Resume Configuration
|
## Resume Configuration
|
||||||
@ -34,6 +34,10 @@ Each section can be configured in `src/config/data.ts`:
|
|||||||
```typescript
|
```typescript
|
||||||
export const resumeConfig: ResumeConfig = {
|
export const resumeConfig: ResumeConfig = {
|
||||||
tomlFile: "/files/resume.toml",
|
tomlFile: "/files/resume.toml",
|
||||||
|
layout: {
|
||||||
|
leftColumn: ["experience", "volunteer", "awards"],
|
||||||
|
rightColumn: ["skills", "education"],
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
summary: {
|
summary: {
|
||||||
title: "Professional Summary",
|
title: "Professional Summary",
|
||||||
@ -52,6 +56,49 @@ export const resumeConfig: ResumeConfig = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Layout Configuration
|
||||||
|
|
||||||
|
The resume layout is fully customizable. You can control which sections appear in which column and their order:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
layout: {
|
||||||
|
leftColumn: ["experience", "volunteer", "awards"],
|
||||||
|
rightColumn: ["skills", "education"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available sections for layout:**
|
||||||
|
- `experience` - Work experience
|
||||||
|
- `education` - Educational background
|
||||||
|
- `skills` - Technical and professional skills
|
||||||
|
- `volunteer` - Volunteer work
|
||||||
|
- `awards` - Awards and recognition
|
||||||
|
|
||||||
|
**Layout Rules:**
|
||||||
|
- Sections can be placed in either column
|
||||||
|
- Order within each column is determined by array order
|
||||||
|
- Missing sections are automatically excluded
|
||||||
|
- The `summary` section always appears at the top (full width)
|
||||||
|
- The `profiles` section appears in the header area
|
||||||
|
|
||||||
|
**Example Layouts:**
|
||||||
|
|
||||||
|
*Skills-focused layout:*
|
||||||
|
```typescript
|
||||||
|
layout: {
|
||||||
|
leftColumn: ["skills", "education"],
|
||||||
|
rightColumn: ["experience", "awards", "volunteer"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Experience-heavy layout:*
|
||||||
|
```typescript
|
||||||
|
layout: {
|
||||||
|
leftColumn: ["experience"],
|
||||||
|
rightColumn: ["skills", "education", "volunteer", "awards"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Resume Data Format (TOML)
|
### Resume Data Format (TOML)
|
||||||
|
|
||||||
Create a `resume.toml` file in the `public/files/` directory:
|
Create a `resume.toml` file in the `public/files/` directory:
|
||||||
@ -114,50 +161,15 @@ date = "2023"
|
|||||||
description = "Brief description of the award"
|
description = "Brief description of the award"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Section Field Details
|
|
||||||
|
|
||||||
#### Skills Section
|
|
||||||
- `level`: Integer from 1-5 representing proficiency level
|
|
||||||
- Displays as progress bars with visual indicators
|
|
||||||
|
|
||||||
#### Experience Section
|
|
||||||
- `description`: Array of strings for bullet points
|
|
||||||
- `url`: Optional company website link
|
|
||||||
|
|
||||||
#### Education Section
|
|
||||||
- `details`: Optional array of additional information (coursework, achievements, etc.)
|
|
||||||
|
|
||||||
#### Awards Section
|
|
||||||
- `description`: Optional additional details about the award
|
|
||||||
|
|
||||||
#### Profiles Section
|
|
||||||
- `network`: Used for icon selection (GitHub, LinkedIn, etc.)
|
|
||||||
- Icons automatically selected based on network name
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. **Configure Resume**: Edit `src/config/data.ts` to enable/disable sections
|
|
||||||
2. **Add Resume Data**: Create `public/files/resume.toml` with your information
|
|
||||||
3. **View Resume**: Navigate to `/resume` on your site
|
|
||||||
4. **Generate PDF**: Click "Generate PDF Resume" button for downloadable PDF
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
pnpm i
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
npm run dev
|
pnpm dev
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
npm run build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Resume PDF Generation
|
|
||||||
|
|
||||||
The system automatically generates PDFs using Puppeteer with:
|
|
||||||
- Optimized layout for A4 paper
|
|
||||||
- Print-friendly styling
|
|
||||||
- Consistent formatting across sections
|
|
||||||
- Proper page breaks and margins
|
|
||||||
|
@ -3,11 +3,6 @@ name = "Atridad Lahiji"
|
|||||||
email = "me@atri.dad"
|
email = "me@atri.dad"
|
||||||
website = "https://atri.dad"
|
website = "https://atri.dad"
|
||||||
|
|
||||||
[[basics.profiles]]
|
|
||||||
network = "GitHub"
|
|
||||||
username = "atridadl"
|
|
||||||
url = "https://github.com/atridadl"
|
|
||||||
|
|
||||||
[[basics.profiles]]
|
[[basics.profiles]]
|
||||||
network = "LinkedIn"
|
network = "LinkedIn"
|
||||||
username = "atridadl"
|
username = "atridadl"
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
interface PdfDownloadButtonProps {
|
interface ResumeDownloadButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PdfDownloadButton({
|
export default function ResumeDownloadButton({
|
||||||
className = "",
|
className = "",
|
||||||
}: PdfDownloadButtonProps) {
|
}: ResumeDownloadButtonProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export default function PdfDownloadButton({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/resume/pdf");
|
const response = await fetch(`/api/resume/pdf?t=${Date.now()}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -57,18 +57,7 @@ export default function PdfDownloadButton({
|
|||||||
Generating PDF...
|
Generating PDF...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>Download Resume</>
|
||||||
<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>
|
</button>
|
||||||
{error && <div class="mt-2 text-error text-sm">{error}</div>}
|
{error && <div class="mt-2 text-error text-sm">{error}</div>}
|
@ -13,8 +13,8 @@ import type {
|
|||||||
// Import Lucide Icons
|
// Import Lucide Icons
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
NotebookPen,
|
Newspaper,
|
||||||
BriefcaseBusiness,
|
FileUser,
|
||||||
CodeXml,
|
CodeXml,
|
||||||
Terminal as TerminalIcon,
|
Terminal as TerminalIcon,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
@ -68,6 +68,10 @@ export const resumeConfig: ResumeConfig = {
|
|||||||
filename: "Atridad_Lahiji_Resume.pdf",
|
filename: "Atridad_Lahiji_Resume.pdf",
|
||||||
displayText: "Download Resume (PDF)",
|
displayText: "Download Resume (PDF)",
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
leftColumn: ["experience", "awards"],
|
||||||
|
rightColumn: ["skills", "education", "volunteer"],
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
enabled: [
|
enabled: [
|
||||||
"summary",
|
"summary",
|
||||||
@ -75,7 +79,6 @@ export const resumeConfig: ResumeConfig = {
|
|||||||
"education",
|
"education",
|
||||||
"skills",
|
"skills",
|
||||||
"volunteer",
|
"volunteer",
|
||||||
"profiles",
|
|
||||||
"awards",
|
"awards",
|
||||||
],
|
],
|
||||||
summary: {
|
summary: {
|
||||||
@ -98,10 +101,7 @@ export const resumeConfig: ResumeConfig = {
|
|||||||
title: "Volunteer Work",
|
title: "Volunteer Work",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
profiles: {
|
|
||||||
title: "Professional Profiles",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
awards: {
|
awards: {
|
||||||
title: "Awards & Recognition",
|
title: "Awards & Recognition",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -297,7 +297,7 @@ export const navigationItems: NavigationItem[] = [
|
|||||||
name: "Posts",
|
name: "Posts",
|
||||||
path: "/posts",
|
path: "/posts",
|
||||||
tooltip: "Posts",
|
tooltip: "Posts",
|
||||||
icon: NotebookPen,
|
icon: Newspaper,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isActive: (path: string) =>
|
isActive: (path: string) =>
|
||||||
path.startsWith("/posts") || path.startsWith("/post/"),
|
path.startsWith("/posts") || path.startsWith("/post/"),
|
||||||
@ -307,7 +307,7 @@ export const navigationItems: NavigationItem[] = [
|
|||||||
name: "Resume",
|
name: "Resume",
|
||||||
path: "/resume",
|
path: "/resume",
|
||||||
tooltip: "Resume",
|
tooltip: "Resume",
|
||||||
icon: BriefcaseBusiness,
|
icon: FileUser,
|
||||||
enabled: !!(resumeConfig.tomlFile && resumeConfig.tomlFile.trim()),
|
enabled: !!(resumeConfig.tomlFile && resumeConfig.tomlFile.trim()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,41 @@ import puppeteer from "puppeteer";
|
|||||||
import { siteConfig } from "../../../config/data";
|
import { siteConfig } from "../../../config/data";
|
||||||
import * as TOML from "@iarna/toml";
|
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 {
|
interface ResumeData {
|
||||||
basics: {
|
basics: {
|
||||||
name: string;
|
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;
|
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 =
|
const skillsHTML =
|
||||||
data.skills
|
data.skills
|
||||||
?.map((skill) => {
|
?.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">
|
<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>
|
<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">
|
<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")
|
data.basics.profiles
|
||||||
? `<div class="flex items-center gap-1">🔗 github.com/${data.basics.profiles.find((p) => p.network === "GitHub")?.username}</div>`
|
?.map((profile) => {
|
||||||
: ""
|
const icon = profileIcons[profile.network] || "";
|
||||||
}
|
const displayUrl = profile.url
|
||||||
${
|
.replace(/^https?:\/\//, "")
|
||||||
data.basics.profiles?.find((p) => p.network === "LinkedIn")
|
.replace(/\/$/, "");
|
||||||
? `<div class="flex items-center gap-1">💼 linkedin.com/in/${data.basics.profiles.find((p) => p.network === "LinkedIn")?.username}</div>`
|
return `<div class="flex items-center gap-1">${icon} ${displayUrl}</div>`;
|
||||||
: ""
|
})
|
||||||
|
.join("") || ""
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -207,92 +292,95 @@ const generateResumeHTML = (data: ResumeData): string => {
|
|||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
${
|
${generateColumnSections(layout.leftColumn, {
|
||||||
data.experience &&
|
experience: {
|
||||||
data.experience.length > 0 &&
|
data: data.experience,
|
||||||
resumeConfig.sections.experience?.enabled
|
html: experienceHTML,
|
||||||
? `
|
title:
|
||||||
<section>
|
resumeConfig.sections.experience?.title || "Experience",
|
||||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
enabled: resumeConfig.sections.experience?.enabled,
|
||||||
${resumeConfig.sections.experience.title || "Experience"}
|
spacing: "space-y-3",
|
||||||
</h2>
|
},
|
||||||
<div class="space-y-3">
|
volunteer: {
|
||||||
${experienceHTML}
|
data: data.volunteer,
|
||||||
</div>
|
html: volunteerHTML,
|
||||||
</section>
|
title:
|
||||||
`
|
resumeConfig.sections.volunteer?.title ||
|
||||||
: ""
|
"Volunteer Work",
|
||||||
}
|
enabled: resumeConfig.sections.volunteer?.enabled,
|
||||||
|
spacing: "space-y-2",
|
||||||
${
|
},
|
||||||
data.volunteer &&
|
awards: {
|
||||||
data.volunteer.length > 0 &&
|
data: data.awards,
|
||||||
resumeConfig.sections.volunteer?.enabled
|
html: awardsHTML,
|
||||||
? `
|
title:
|
||||||
<section>
|
resumeConfig.sections.awards?.title ||
|
||||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
"Awards & Recognition",
|
||||||
${resumeConfig.sections.volunteer.title || "Volunteer Work"}
|
enabled: resumeConfig.sections.awards?.enabled,
|
||||||
</h2>
|
spacing: "space-y-2",
|
||||||
<div class="space-y-2">
|
},
|
||||||
${volunteerHTML}
|
skills: {
|
||||||
</div>
|
data: data.skills,
|
||||||
</section>
|
html: skillsHTML,
|
||||||
`
|
title: resumeConfig.sections.skills?.title || "Skills",
|
||||||
: ""
|
enabled: resumeConfig.sections.skills?.enabled,
|
||||||
}
|
spacing: "space-y-1",
|
||||||
|
},
|
||||||
${
|
education: {
|
||||||
data.awards &&
|
data: data.education,
|
||||||
data.awards.length > 0 &&
|
html: educationHTML,
|
||||||
resumeConfig.sections.awards?.enabled
|
title:
|
||||||
? `
|
resumeConfig.sections.education?.title || "Education",
|
||||||
<section>
|
enabled: resumeConfig.sections.education?.enabled,
|
||||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
spacing: "space-y-3",
|
||||||
${resumeConfig.sections.awards.title || "Awards & Recognition"}
|
},
|
||||||
</h2>
|
})}
|
||||||
<div class="space-y-2">
|
|
||||||
${awardsHTML}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
${
|
${generateColumnSections(layout.rightColumn, {
|
||||||
data.skills &&
|
experience: {
|
||||||
data.skills.length > 0 &&
|
data: data.experience,
|
||||||
resumeConfig.sections.skills?.enabled
|
html: experienceHTML,
|
||||||
? `
|
title:
|
||||||
<section>
|
resumeConfig.sections.experience?.title || "Experience",
|
||||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
enabled: resumeConfig.sections.experience?.enabled,
|
||||||
${resumeConfig.sections.skills.title || "Skills"}
|
spacing: "space-y-3",
|
||||||
</h2>
|
},
|
||||||
<div class="space-y-1">
|
volunteer: {
|
||||||
${skillsHTML}
|
data: data.volunteer,
|
||||||
</div>
|
html: volunteerHTML,
|
||||||
</section>
|
title:
|
||||||
`
|
resumeConfig.sections.volunteer?.title ||
|
||||||
: ""
|
"Volunteer Work",
|
||||||
}
|
enabled: resumeConfig.sections.volunteer?.enabled,
|
||||||
|
spacing: "space-y-2",
|
||||||
${
|
},
|
||||||
data.education &&
|
awards: {
|
||||||
data.education.length > 0 &&
|
data: data.awards,
|
||||||
resumeConfig.sections.education?.enabled
|
html: awardsHTML,
|
||||||
? `
|
title:
|
||||||
<section>
|
resumeConfig.sections.awards?.title ||
|
||||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
"Awards & Recognition",
|
||||||
${resumeConfig.sections.education.title || "Education"}
|
enabled: resumeConfig.sections.awards?.enabled,
|
||||||
</h2>
|
spacing: "space-y-2",
|
||||||
<div class="space-y-3">
|
},
|
||||||
${educationHTML}
|
skills: {
|
||||||
</div>
|
data: data.skills,
|
||||||
</section>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -323,7 +411,7 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
tomlContent,
|
tomlContent,
|
||||||
) as unknown as ResumeData;
|
) as unknown as ResumeData;
|
||||||
|
|
||||||
const htmlContent = generateResumeHTML(resumeData);
|
const htmlContent = await generateResumeHTML(resumeData);
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
@ -366,7 +454,9 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/pdf",
|
"Content-Type": "application/pdf",
|
||||||
"Content-Disposition": `attachment; filename="Atridad_Lahiji_Resume.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) {
|
} catch (error) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
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 ResumeDownloadButton from "../components/ResumeDownloadButton";
|
||||||
import { siteConfig } from "../config/data";
|
import { siteConfig } from "../config/data";
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
import * as TOML from "@iarna/toml";
|
import * as TOML from "@iarna/toml";
|
||||||
@ -111,40 +111,24 @@ if (!data) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data.basics.profiles.find((p) => p.network === "GitHub") && (
|
data.basics.profiles.map((profile) => {
|
||||||
<a
|
const iconName = `simple-icons:${profile.network.toLowerCase()}`;
|
||||||
href={
|
return (
|
||||||
data.basics.profiles.find(
|
<a
|
||||||
(p) => p.network === "GitHub",
|
href={profile.url}
|
||||||
)!.url
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
<Icon name={iconName} />
|
||||||
>
|
{profile.network}
|
||||||
<Icon name="simple-icons:github" /> GitHub
|
</a>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<PdfDownloadButton client:load />
|
<ResumeDownloadButton client:load />
|
||||||
|
|
||||||
{
|
{
|
||||||
data.summary && resumeConfig.sections.summary?.enabled && (
|
data.summary && resumeConfig.sections.summary?.enabled && (
|
||||||
@ -159,40 +143,6 @@ if (!data) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
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}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<Icon name={iconName} />
|
|
||||||
{profile.network}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
data.skills &&
|
data.skills &&
|
||||||
data.skills.length > 0 &&
|
data.skills.length > 0 &&
|
||||||
|
@ -57,6 +57,10 @@ export interface ResumeConfig {
|
|||||||
filename: string;
|
filename: string;
|
||||||
displayText: string;
|
displayText: string;
|
||||||
};
|
};
|
||||||
|
layout?: {
|
||||||
|
leftColumn?: string[];
|
||||||
|
rightColumn?: string[];
|
||||||
|
};
|
||||||
sections: {
|
sections: {
|
||||||
enabled: string[];
|
enabled: string[];
|
||||||
summary?: {
|
summary?: {
|
||||||
|
Reference in New Issue
Block a user