diff --git a/README.md b/README.md index 7e45a7b..2f2c0b5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ My personal website built with Astro and Preact! - **Resume Management** - **Blog Posts** - **Projects** -- **Talks & Presentations** +- **Talks** - **Terminal View** ## Resume Configuration @@ -34,6 +34,10 @@ Each section can be configured in `src/config/data.ts`: ```typescript export const resumeConfig: ResumeConfig = { tomlFile: "/files/resume.toml", + layout: { + leftColumn: ["experience", "volunteer", "awards"], + rightColumn: ["skills", "education"], + }, sections: { 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) Create a `resume.toml` file in the `public/files/` directory: @@ -114,50 +161,15 @@ date = "2023" 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 ```bash # Install dependencies -npm install +pnpm i # Start development server -npm run dev +pnpm dev # 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 diff --git a/public/files/resume.toml b/public/files/resume.toml index eaa26b7..0262337 100644 --- a/public/files/resume.toml +++ b/public/files/resume.toml @@ -3,11 +3,6 @@ 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" diff --git a/src/components/PdfDownloadButton.tsx b/src/components/ResumeDownloadButton.tsx similarity index 74% rename from src/components/PdfDownloadButton.tsx rename to src/components/ResumeDownloadButton.tsx index eeb16a5..c3c2a66 100644 --- a/src/components/PdfDownloadButton.tsx +++ b/src/components/ResumeDownloadButton.tsx @@ -1,12 +1,12 @@ import { useState } from "preact/hooks"; -interface PdfDownloadButtonProps { +interface ResumeDownloadButtonProps { className?: string; } -export default function PdfDownloadButton({ +export default function ResumeDownloadButton({ className = "", -}: PdfDownloadButtonProps) { +}: ResumeDownloadButtonProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -15,7 +15,7 @@ export default function PdfDownloadButton({ setError(null); try { - const response = await fetch("/api/resume/pdf"); + const response = await fetch(`/api/resume/pdf?t=${Date.now()}`); if (!response.ok) { throw new Error( @@ -57,18 +57,7 @@ export default function PdfDownloadButton({ Generating PDF... ) : ( - <> - - - - Generate PDF Resume - + <>Download Resume )} {error &&
{error}
} diff --git a/src/config/data.ts b/src/config/data.ts index 893f0f7..4efe55b 100644 --- a/src/config/data.ts +++ b/src/config/data.ts @@ -13,8 +13,8 @@ import type { // Import Lucide Icons import { Home, - NotebookPen, - BriefcaseBusiness, + Newspaper, + FileUser, CodeXml, Terminal as TerminalIcon, Megaphone, @@ -68,6 +68,10 @@ export const resumeConfig: ResumeConfig = { filename: "Atridad_Lahiji_Resume.pdf", displayText: "Download Resume (PDF)", }, + layout: { + leftColumn: ["experience", "awards"], + rightColumn: ["skills", "education", "volunteer"], + }, sections: { enabled: [ "summary", @@ -75,7 +79,6 @@ export const resumeConfig: ResumeConfig = { "education", "skills", "volunteer", - "profiles", "awards", ], summary: { @@ -98,10 +101,7 @@ export const resumeConfig: ResumeConfig = { title: "Volunteer Work", enabled: true, }, - profiles: { - title: "Professional Profiles", - enabled: true, - }, + awards: { title: "Awards & Recognition", enabled: true, @@ -297,7 +297,7 @@ export const navigationItems: NavigationItem[] = [ name: "Posts", path: "/posts", tooltip: "Posts", - icon: NotebookPen, + icon: Newspaper, enabled: true, isActive: (path: string) => path.startsWith("/posts") || path.startsWith("/post/"), @@ -307,7 +307,7 @@ export const navigationItems: NavigationItem[] = [ name: "Resume", path: "/resume", tooltip: "Resume", - icon: BriefcaseBusiness, + icon: FileUser, enabled: !!(resumeConfig.tomlFile && resumeConfig.tomlFile.trim()), }, { diff --git a/src/pages/api/resume/pdf.ts b/src/pages/api/resume/pdf.ts index 9308a1b..dd3274a 100644 --- a/src/pages/api/resume/pdf.ts +++ b/src/pages/api/resume/pdf.ts @@ -3,6 +3,41 @@ import puppeteer from "puppeteer"; import { siteConfig } from "../../../config/data"; import * as TOML from "@iarna/toml"; +// Helper function to fetch and return SVG icon from Simple Icons CDN +async function getSimpleIcon(iconName: string): Promise { + try { + const response = await fetch( + `https://cdn.jsdelivr.net/npm/simple-icons@v10/icons/${iconName.toLowerCase()}.svg`, + ); + if (!response.ok) { + console.warn(`Failed to fetch icon: ${iconName}`); + return ""; + } + const svgContent = await response.text(); + // Add inline styles for proper sizing and color + return svgContent.replace( + "', + "mdi:download": + '', + "mdi:link": + '', + }; + return iconMap[iconName] || ""; +} + interface ResumeData { basics: { name: string; @@ -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.title} +

+
+ ${section.html} +
+
+ `; + }) + .join(""); +} + +const generateResumeHTML = async (data: ResumeData): Promise => { const resumeConfig = siteConfig.resume; + // Get layout configuration with defaults + const layout = resumeConfig.layout || { + leftColumn: ["experience", "volunteer", "awards"], + rightColumn: ["skills", "education"], + }; + + // Pre-fetch icons for profiles + const profileIcons: { [key: string]: string } = {}; + if (data.basics.profiles) { + for (const profile of data.basics.profiles) { + const iconName = profile.network.toLowerCase(); + profileIcons[profile.network] = await getSimpleIcon(iconName); + } + } + + // Get email icon + const emailIcon = getMdiIcon("mdi:email"); + const skillsHTML = data.skills ?.map((skill) => { @@ -178,16 +262,17 @@ const generateResumeHTML = (data: ResumeData): string => {

${data.basics.name}

- ${data.basics.email ? `
📧 ${data.basics.email}
` : ""} + ${data.basics.email ? `
${emailIcon} ${data.basics.email}
` : ""} ${ - data.basics.profiles?.find((p) => p.network === "GitHub") - ? `
🔗 github.com/${data.basics.profiles.find((p) => p.network === "GitHub")?.username}
` - : "" - } - ${ - data.basics.profiles?.find((p) => p.network === "LinkedIn") - ? `
💼 linkedin.com/in/${data.basics.profiles.find((p) => p.network === "LinkedIn")?.username}
` - : "" + data.basics.profiles + ?.map((profile) => { + const icon = profileIcons[profile.network] || ""; + const displayUrl = profile.url + .replace(/^https?:\/\//, "") + .replace(/\/$/, ""); + return `
${icon} ${displayUrl}
`; + }) + .join("") || "" }
@@ -207,92 +292,95 @@ const generateResumeHTML = (data: ResumeData): string => {
- ${ - data.experience && - data.experience.length > 0 && - resumeConfig.sections.experience?.enabled - ? ` -
-

- ${resumeConfig.sections.experience.title || "Experience"} -

-
- ${experienceHTML} -
-
- ` - : "" - } - - ${ - data.volunteer && - data.volunteer.length > 0 && - resumeConfig.sections.volunteer?.enabled - ? ` -
-

- ${resumeConfig.sections.volunteer.title || "Volunteer Work"} -

-
- ${volunteerHTML} -
-
- ` - : "" - } - - ${ - data.awards && - data.awards.length > 0 && - resumeConfig.sections.awards?.enabled - ? ` -
-

- ${resumeConfig.sections.awards.title || "Awards & Recognition"} -

-
- ${awardsHTML} -
-
- ` - : "" - } + ${generateColumnSections(layout.leftColumn, { + experience: { + data: data.experience, + html: experienceHTML, + title: + resumeConfig.sections.experience?.title || "Experience", + enabled: resumeConfig.sections.experience?.enabled, + spacing: "space-y-3", + }, + volunteer: { + data: data.volunteer, + html: volunteerHTML, + title: + resumeConfig.sections.volunteer?.title || + "Volunteer Work", + enabled: resumeConfig.sections.volunteer?.enabled, + spacing: "space-y-2", + }, + awards: { + data: data.awards, + html: awardsHTML, + title: + resumeConfig.sections.awards?.title || + "Awards & Recognition", + enabled: resumeConfig.sections.awards?.enabled, + spacing: "space-y-2", + }, + skills: { + data: data.skills, + html: skillsHTML, + title: resumeConfig.sections.skills?.title || "Skills", + enabled: resumeConfig.sections.skills?.enabled, + spacing: "space-y-1", + }, + education: { + data: data.education, + html: educationHTML, + title: + resumeConfig.sections.education?.title || "Education", + enabled: resumeConfig.sections.education?.enabled, + spacing: "space-y-3", + }, + })}
- ${ - data.skills && - data.skills.length > 0 && - resumeConfig.sections.skills?.enabled - ? ` -
-

- ${resumeConfig.sections.skills.title || "Skills"} -

-
- ${skillsHTML} -
-
- ` - : "" - } - - ${ - data.education && - data.education.length > 0 && - resumeConfig.sections.education?.enabled - ? ` -
-

- ${resumeConfig.sections.education.title || "Education"} -

-
- ${educationHTML} -
-
- ` - : "" - } + ${generateColumnSections(layout.rightColumn, { + experience: { + data: data.experience, + html: experienceHTML, + title: + resumeConfig.sections.experience?.title || "Experience", + enabled: resumeConfig.sections.experience?.enabled, + spacing: "space-y-3", + }, + volunteer: { + data: data.volunteer, + html: volunteerHTML, + title: + resumeConfig.sections.volunteer?.title || + "Volunteer Work", + enabled: resumeConfig.sections.volunteer?.enabled, + spacing: "space-y-2", + }, + awards: { + data: data.awards, + html: awardsHTML, + title: + resumeConfig.sections.awards?.title || + "Awards & Recognition", + enabled: resumeConfig.sections.awards?.enabled, + spacing: "space-y-2", + }, + skills: { + data: data.skills, + html: skillsHTML, + title: resumeConfig.sections.skills?.title || "Skills", + enabled: resumeConfig.sections.skills?.enabled, + spacing: "space-y-1", + }, + education: { + data: data.education, + html: educationHTML, + title: + resumeConfig.sections.education?.title || "Education", + enabled: resumeConfig.sections.education?.enabled, + spacing: "space-y-3", + }, + })}
@@ -323,7 +411,7 @@ export const GET: APIRoute = async ({ request }) => { tomlContent, ) as unknown as ResumeData; - const htmlContent = generateResumeHTML(resumeData); + const htmlContent = await generateResumeHTML(resumeData); const browser = await puppeteer.launch({ headless: true, @@ -366,7 +454,9 @@ export const GET: APIRoute = async ({ request }) => { headers: { "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename="Atridad_Lahiji_Resume.pdf"`, - "Cache-Control": "public, max-age=3600", + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", }, }); } catch (error) { diff --git a/src/pages/resume.astro b/src/pages/resume.astro index 48cafab..ea66d6c 100644 --- a/src/pages/resume.astro +++ b/src/pages/resume.astro @@ -2,7 +2,7 @@ import { Icon } from "astro-icon/components"; import Layout from "../layouts/Layout.astro"; import ResumeSkills from "../components/ResumeSkills"; -import PdfDownloadButton from "../components/PdfDownloadButton"; +import ResumeDownloadButton from "../components/ResumeDownloadButton"; import { siteConfig } from "../config/data"; import "../styles/global.css"; import * as TOML from "@iarna/toml"; @@ -111,40 +111,24 @@ if (!data) { ) } { - data.basics.profiles.find((p) => p.network === "GitHub") && ( - p.network === "GitHub", - )!.url - } - target="_blank" - rel="noopener noreferrer" - class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base" - > - GitHub - - ) - } - { - data.basics.profiles.find((p) => p.network === "LinkedIn") && ( - p.network === "LinkedIn", - )!.url - } - target="_blank" - rel="noopener noreferrer" - class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base" - > - LinkedIn - - ) + data.basics.profiles.map((profile) => { + const iconName = `simple-icons:${profile.network.toLowerCase()}`; + return ( + + + {profile.network} + + ); + }) } - + { data.summary && resumeConfig.sections.summary?.enabled && ( @@ -159,40 +143,6 @@ if (!data) { ) } - { - data.basics.profiles && - data.basics.profiles.length > 0 && - resumeConfig.sections.profiles?.enabled && ( -
-
-

- {resumeConfig.sections.profiles.title || - "Profiles"} -

-
- {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 ( - - - {profile.network} - - ); - })} -
-
-
- ) - } - { data.skills && data.skills.length > 0 && diff --git a/src/types/index.ts b/src/types/index.ts index aa3b2b7..44e3f14 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -57,6 +57,10 @@ export interface ResumeConfig { filename: string; displayText: string; }; + layout?: { + leftColumn?: string[]; + rightColumn?: string[]; + }; sections: { enabled: string[]; summary?: {