Resume system overhaul :)
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m54s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m54s
This commit is contained in:
@@ -4,24 +4,32 @@ import { siteConfig } from "../../config/data";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check if resume TOML file is configured
|
||||
// Check if resume TOML content is configured
|
||||
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
|
||||
return new Response("Resume not configured", { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
let tomlContent: string;
|
||||
|
||||
// Fetch the TOML file from the public directory
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
// Check if tomlFile is a path (starts with /) or raw content
|
||||
if (siteConfig.resume.tomlFile.startsWith("/")) {
|
||||
// It's a file path - fetch it
|
||||
const url = new URL(request.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume TOML: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume TOML: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
tomlContent = await response.text();
|
||||
} else {
|
||||
// It's raw TOML content
|
||||
tomlContent = siteConfig.resume.tomlFile;
|
||||
}
|
||||
|
||||
const tomlContent = await response.text();
|
||||
const resumeData = TOML.parse(tomlContent);
|
||||
|
||||
return new Response(JSON.stringify(resumeData), {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { chromium } from 'playwright';
|
||||
import { chromium } from "playwright";
|
||||
import { siteConfig } from "../../../config/data";
|
||||
import * as TOML from "@iarna/toml";
|
||||
|
||||
@@ -48,6 +48,10 @@ interface ResumeData {
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
layout?: {
|
||||
left_column?: string[];
|
||||
right_column?: string[];
|
||||
};
|
||||
summary: {
|
||||
content: string;
|
||||
};
|
||||
@@ -84,7 +88,11 @@ interface ResumeData {
|
||||
}
|
||||
|
||||
// Template helper functions
|
||||
const createSection = (title: string, content: string, spacing = "space-y-3") => `
|
||||
const createSection = (
|
||||
title: string,
|
||||
content: string,
|
||||
spacing = "space-y-3",
|
||||
) => `
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
|
||||
${title}
|
||||
@@ -128,7 +136,9 @@ const createSkillItem = (skill: any) => {
|
||||
|
||||
const createEducationItem = (edu: any) => {
|
||||
const detailsList = edu.details
|
||||
? edu.details.map((detail: string) => `<li class="mb-1">${detail}</li>`).join("")
|
||||
? edu.details
|
||||
.map((detail: string) => `<li class="mb-1">${detail}</li>`)
|
||||
.join("")
|
||||
: "";
|
||||
|
||||
return `
|
||||
@@ -188,32 +198,42 @@ const createHead = (name: string) => `
|
||||
</head>
|
||||
`;
|
||||
|
||||
const createHeader = (basics: any, emailIcon: string, profileIcons: { [key: string]: string }) => `
|
||||
const createHeader = (
|
||||
basics: any,
|
||||
emailIcon: string,
|
||||
profileIcons: { [key: string]: string },
|
||||
) => `
|
||||
<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">${basics.name}</h1>
|
||||
<div class="flex justify-center items-center flex-wrap gap-4 text-xs text-gray-600">
|
||||
${basics.email ? `<div class="flex items-center gap-1">${emailIcon} ${basics.email}</div>` : ""}
|
||||
${basics.profiles
|
||||
?.map((profile: any) => {
|
||||
const icon = profileIcons[profile.network] || "";
|
||||
const displayUrl = profile.url
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/$/, "");
|
||||
return `<div class="flex items-center gap-1">${icon} ${displayUrl}</div>`;
|
||||
})
|
||||
.join("") || ""
|
||||
}
|
||||
${
|
||||
basics.profiles
|
||||
?.map((profile: any) => {
|
||||
const icon = profileIcons[profile.network] || "";
|
||||
const displayUrl = profile.url
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/$/, "");
|
||||
return `<div class="flex items-center gap-1">${icon} ${displayUrl}</div>`;
|
||||
})
|
||||
.join("") || ""
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
|
||||
const createSummarySection = (summary: any, resumeConfig: any) => {
|
||||
if (!summary || !resumeConfig.sections.summary?.enabled) return "";
|
||||
if (
|
||||
!summary ||
|
||||
!summary.content ||
|
||||
resumeConfig.sections.summary?.enabled === false
|
||||
)
|
||||
return "";
|
||||
|
||||
return `
|
||||
<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"}
|
||||
${resumeConfig.sections.summary?.title || "Summary"}
|
||||
</h2>
|
||||
<div class="text-xs text-gray-700 leading-tight">${summary.content}</div>
|
||||
</section>
|
||||
@@ -223,32 +243,32 @@ const createSummarySection = (summary: any, resumeConfig: any) => {
|
||||
const createColumnSections = (
|
||||
sectionNames: string[],
|
||||
sections: { [key: string]: string },
|
||||
resumeConfig: any
|
||||
resumeConfig: any,
|
||||
) => {
|
||||
const sectionConfig = {
|
||||
experience: {
|
||||
title: resumeConfig.sections.experience?.title || "Experience",
|
||||
enabled: resumeConfig.sections.experience?.enabled,
|
||||
enabled: resumeConfig.sections.experience?.enabled !== false,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
skills: {
|
||||
title: resumeConfig.sections.skills?.title || "Skills",
|
||||
enabled: resumeConfig.sections.skills?.enabled,
|
||||
enabled: resumeConfig.sections.skills?.enabled !== false,
|
||||
spacing: "space-y-1",
|
||||
},
|
||||
education: {
|
||||
title: resumeConfig.sections.education?.title || "Education",
|
||||
enabled: resumeConfig.sections.education?.enabled,
|
||||
enabled: resumeConfig.sections.education?.enabled !== false,
|
||||
spacing: "space-y-3",
|
||||
},
|
||||
volunteer: {
|
||||
title: resumeConfig.sections.volunteer?.title || "Volunteer Work",
|
||||
enabled: resumeConfig.sections.volunteer?.enabled,
|
||||
enabled: resumeConfig.sections.volunteer?.enabled !== false,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
awards: {
|
||||
title: resumeConfig.sections.awards?.title || "Awards & Recognition",
|
||||
enabled: resumeConfig.sections.awards?.enabled,
|
||||
enabled: resumeConfig.sections.awards?.enabled !== false,
|
||||
spacing: "space-y-2",
|
||||
},
|
||||
};
|
||||
@@ -258,10 +278,13 @@ const createColumnSections = (
|
||||
const config = sectionConfig[sectionName as keyof typeof sectionConfig];
|
||||
const content = sections[sectionName];
|
||||
|
||||
if (!config || !content || !config.enabled) return "";
|
||||
// Skip if section doesn't exist in config, has no content, or is disabled
|
||||
if (!config || !content || content.trim() === "" || !config.enabled)
|
||||
return "";
|
||||
|
||||
return createSection(config.title, content, config.spacing);
|
||||
})
|
||||
.filter((section) => section !== "")
|
||||
.join("");
|
||||
};
|
||||
|
||||
@@ -278,10 +301,20 @@ const fetchProfileIcons = async (profiles: any[]) => {
|
||||
|
||||
const generateResumeHTML = async (data: ResumeData): Promise<string> => {
|
||||
const resumeConfig = siteConfig.resume;
|
||||
const layout = resumeConfig.layout || {
|
||||
leftColumn: ["experience", "volunteer", "awards"],
|
||||
rightColumn: ["skills", "education"],
|
||||
};
|
||||
// Use layout from TOML data, fallback to site config, then to default
|
||||
const layout = data.layout
|
||||
? {
|
||||
leftColumn: data.layout.left_column || [
|
||||
"experience",
|
||||
"volunteer",
|
||||
"awards",
|
||||
],
|
||||
rightColumn: data.layout.right_column || ["skills", "education"],
|
||||
}
|
||||
: resumeConfig.layout || {
|
||||
leftColumn: ["experience", "volunteer", "awards"],
|
||||
rightColumn: ["skills", "education"],
|
||||
};
|
||||
|
||||
// Pre-fetch icons
|
||||
const profileIcons = await fetchProfileIcons(data.basics.profiles);
|
||||
@@ -289,11 +322,21 @@ const generateResumeHTML = async (data: ResumeData): Promise<string> => {
|
||||
|
||||
// Generate section content
|
||||
const sections = {
|
||||
experience: Array.isArray(data.experience) ? data.experience.map(createExperienceItem).join("") : "",
|
||||
skills: Array.isArray(data.skills) ? data.skills.map(createSkillItem).join("") : "",
|
||||
education: Array.isArray(data.education) ? data.education.map(createEducationItem).join("") : "",
|
||||
volunteer: Array.isArray(data.volunteer) ? data.volunteer.map(createVolunteerItem).join("") : "",
|
||||
awards: Array.isArray(data.awards) ? data.awards.map(createAwardItem).join("") : "",
|
||||
experience: Array.isArray(data.experience)
|
||||
? data.experience.map(createExperienceItem).join("")
|
||||
: "",
|
||||
skills: Array.isArray(data.skills)
|
||||
? data.skills.map(createSkillItem).join("")
|
||||
: "",
|
||||
education: Array.isArray(data.education)
|
||||
? data.education.map(createEducationItem).join("")
|
||||
: "",
|
||||
volunteer: Array.isArray(data.volunteer)
|
||||
? data.volunteer.map(createVolunteerItem).join("")
|
||||
: "",
|
||||
awards: Array.isArray(data.awards)
|
||||
? data.awards.map(createAwardItem).join("")
|
||||
: "",
|
||||
};
|
||||
|
||||
return `
|
||||
@@ -318,61 +361,77 @@ const generateResumeHTML = async (data: ResumeData): Promise<string> => {
|
||||
`;
|
||||
};
|
||||
|
||||
async function generatePDFFromToml(tomlContent: string): Promise<Uint8Array> {
|
||||
const resumeData: ResumeData = TOML.parse(
|
||||
tomlContent,
|
||||
) as unknown as ResumeData;
|
||||
const htmlContent = await generateResumeHTML(resumeData);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
||||
(process.env.NODE_ENV === "production"
|
||||
? "/usr/bin/chromium-browser"
|
||||
: undefined),
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--disable-web-security",
|
||||
"--disable-features=VizDisplayCompositor",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(htmlContent, { waitUntil: "networkidle" });
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: "A4",
|
||||
margin: {
|
||||
top: "0.2in",
|
||||
bottom: "0.2in",
|
||||
left: "0.2in",
|
||||
right: "0.2in",
|
||||
},
|
||||
printBackground: true,
|
||||
scale: 0.9,
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
return pdfBuffer;
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
|
||||
return new Response("Resume not configured", { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
let tomlContent: string;
|
||||
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
// Check if tomlFile is a path (starts with /) or raw content
|
||||
if (siteConfig.resume.tomlFile.startsWith("/")) {
|
||||
// It's a file path - fetch it
|
||||
const url = new URL(request.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
tomlContent = await response.text();
|
||||
} else {
|
||||
// It's raw TOML content
|
||||
tomlContent = siteConfig.resume.tomlFile;
|
||||
}
|
||||
|
||||
const tomlContent = await response.text();
|
||||
const resumeData: ResumeData = TOML.parse(
|
||||
tomlContent,
|
||||
) as unknown as ResumeData;
|
||||
|
||||
const htmlContent = await generateResumeHTML(resumeData);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
||||
(process.env.NODE_ENV === "production" ? "/usr/bin/chromium-browser" : undefined),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--disable-web-security',
|
||||
'--disable-features=VizDisplayCompositor'
|
||||
]
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: 'A4',
|
||||
margin: {
|
||||
top: '0.2in',
|
||||
bottom: '0.2in',
|
||||
left: '0.2in',
|
||||
right: '0.2in',
|
||||
},
|
||||
printBackground: true,
|
||||
scale: 0.9,
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
const pdfBuffer = await generatePDFFromToml(tomlContent);
|
||||
|
||||
return new Response(pdfBuffer, {
|
||||
headers: {
|
||||
@@ -387,4 +446,47 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
console.error("Error generating PDF:", error);
|
||||
return new Response("Error generating PDF", { status: 500 });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const tomlContent = await request.text();
|
||||
|
||||
if (!tomlContent.trim()) {
|
||||
return new Response("TOML content is required", { status: 400 });
|
||||
}
|
||||
|
||||
// Validate TOML content
|
||||
let resumeData: ResumeData;
|
||||
try {
|
||||
resumeData = TOML.parse(tomlContent) as unknown as ResumeData;
|
||||
} catch (parseError) {
|
||||
return new Response(
|
||||
`Invalid TOML format: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (!resumeData.basics?.name) {
|
||||
return new Response("Resume must include basics.name", { status: 400 });
|
||||
}
|
||||
|
||||
const pdfBuffer = await generatePDFFromToml(tomlContent);
|
||||
|
||||
const filename = `${resumeData.basics.name.replace(/[^a-zA-Z0-9]/g, "_")}_Resume.pdf`;
|
||||
|
||||
return new Response(pdfBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating PDF:", error);
|
||||
return new Response("Error generating PDF", { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
148
src/pages/api/resume/template.ts
Normal file
148
src/pages/api/resume/template.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const templateToml = `# Resume Template - Edit this file with your information
|
||||
# Save as .toml file and upload to the resume generator
|
||||
|
||||
[basics]
|
||||
name = "Your Full Name"
|
||||
email = "your.email@example.com"
|
||||
website = "https://yourwebsite.com"
|
||||
|
||||
# Add your social media profiles
|
||||
[[basics.profiles]]
|
||||
network = "GitHub"
|
||||
username = "yourusername"
|
||||
url = "https://github.com/yourusername"
|
||||
|
||||
[[basics.profiles]]
|
||||
network = "LinkedIn"
|
||||
username = "yourname"
|
||||
url = "https://linkedin.com/in/yourname"
|
||||
|
||||
[[basics.profiles]]
|
||||
network = "Bluesky"
|
||||
username = "yourusername"
|
||||
url = "https://bluesky.app/profile/yourusername"
|
||||
|
||||
# Layout Configuration - Customize which sections appear in each column
|
||||
[layout]
|
||||
left_column = ["experience", "volunteer"]
|
||||
right_column = ["skills", "education", "awards"]
|
||||
|
||||
[summary]
|
||||
content = """
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
"""
|
||||
|
||||
# Professional Experience
|
||||
[[experience]]
|
||||
company = "Company Name"
|
||||
position = "Your Job Title"
|
||||
location = "City, Province/Country"
|
||||
date = "Jan 2020 - Present"
|
||||
url = "https://company.com" # Optional
|
||||
description = [
|
||||
"Describe a key achievement or responsibility",
|
||||
"Quantify your impact with numbers when possible",
|
||||
"Use action verbs to describe what you accomplished",
|
||||
"Add more bullet points as needed",
|
||||
]
|
||||
|
||||
[[experience]]
|
||||
company = "Previous Company"
|
||||
position = "Previous Job Title"
|
||||
location = "City, Province/Country"
|
||||
date = "Jun 2018 - Dec 2019"
|
||||
description = [
|
||||
"Another achievement from your previous role",
|
||||
"Focus on results and impact",
|
||||
"Keep descriptions concise but informative",
|
||||
]
|
||||
|
||||
# Education
|
||||
[[education]]
|
||||
institution = "University Name"
|
||||
degree = "Bachelor of Science"
|
||||
field = "Computer Science"
|
||||
date = "2014 - 2018"
|
||||
details = [
|
||||
"Relevant coursework: Data Structures, Algorithms, Software Engineering",
|
||||
]
|
||||
|
||||
[[education]]
|
||||
institution = "Another Institution"
|
||||
degree = "Certificate"
|
||||
field = "Web Development"
|
||||
date = "2019"
|
||||
|
||||
# Skills (rate yourself 1-5)
|
||||
[[skills]]
|
||||
name = "JavaScript"
|
||||
level = 4
|
||||
|
||||
[[skills]]
|
||||
name = "Python"
|
||||
level = 5
|
||||
|
||||
[[skills]]
|
||||
name = "React"
|
||||
level = 4
|
||||
|
||||
[[skills]]
|
||||
name = "Node.js"
|
||||
level = 3
|
||||
|
||||
[[skills]]
|
||||
name = "SQL"
|
||||
level = 4
|
||||
|
||||
[[skills]]
|
||||
name = "Git"
|
||||
level = 4
|
||||
|
||||
[[skills]]
|
||||
name = "Docker"
|
||||
level = 3
|
||||
|
||||
[[skills]]
|
||||
name = "AWS"
|
||||
level = 2
|
||||
|
||||
# Volunteer Work (optional section)
|
||||
[[volunteer]]
|
||||
organization = "Local Tech Meetup"
|
||||
position = "Event Organizer"
|
||||
date = "2020 - Present"
|
||||
|
||||
[[volunteer]]
|
||||
organization = "Code for Good"
|
||||
position = "Volunteer Developer"
|
||||
date = "2019 - 2020"
|
||||
|
||||
# Awards and Recognition (optional section)
|
||||
[[awards]]
|
||||
title = "Employee of the Month"
|
||||
organization = "Current Company"
|
||||
date = "March 2023"
|
||||
description = "Recognized for outstanding contribution to the team project"
|
||||
|
||||
[[awards]]
|
||||
title = "Hackathon Winner"
|
||||
organization = "Local Tech Conference"
|
||||
date = "October 2022"
|
||||
description = "First place!"
|
||||
`;
|
||||
|
||||
return new Response(templateToml, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Disposition": 'attachment; filename="resume-template.toml"',
|
||||
"Cache-Control": "public, max-age=86400", // Cache for 1 day
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { Icon } from "astro-icon/components";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import ResumeSkills from "../components/ResumeSkills";
|
||||
import ResumeDownloadButton from "../components/ResumeDownloadButton";
|
||||
import ResumeSettingsModal from "../components/ResumeSettingsModal";
|
||||
import { siteConfig } from "../config/data";
|
||||
import "../styles/global.css";
|
||||
import * as TOML from "@iarna/toml";
|
||||
@@ -56,44 +57,48 @@ interface ResumeData {
|
||||
let resumeData: ResumeData | undefined = undefined;
|
||||
let fetchError: string | null = null;
|
||||
|
||||
// Check if resume TOML file is configured before attempting to fetch
|
||||
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the base URL
|
||||
const baseUrl = Astro.url.origin;
|
||||
let tomlContent: string;
|
||||
|
||||
// Fetch the TOML file from the public directory
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
if (siteConfig.resume.tomlFile.startsWith("/")) {
|
||||
const baseUrl = Astro.url.origin;
|
||||
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
tomlContent = await response.text();
|
||||
} else {
|
||||
tomlContent = siteConfig.resume.tomlFile;
|
||||
}
|
||||
|
||||
const tomlContent = await response.text();
|
||||
resumeData = TOML.parse(tomlContent) as unknown as ResumeData;
|
||||
} catch (error) {
|
||||
console.error("Error loading resume data:", error);
|
||||
// Return to home page when resume data cannot be loaded
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
const data = resumeData;
|
||||
const resumeConfig = siteConfig.resume;
|
||||
|
||||
// At this point, data is guaranteed to exist since we redirect on error
|
||||
if (!data) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Resume">
|
||||
<ResumeSettingsModal client:load />
|
||||
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-primary mb-4 sm:mb-6 text-center">
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl font-bold text-primary mb-4 sm:mb-6 text-center"
|
||||
>
|
||||
{data.basics.name}
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import TerminalComponent from "../components/Terminal.tsx";
|
||||
import TerminalComponent from "../components/Terminal";
|
||||
import "../styles/global.css";
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user