239 lines
9.3 KiB
TypeScript
239 lines
9.3 KiB
TypeScript
import { Head } from "$fresh/runtime.ts";
|
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
|
import {
|
|
LuMail,
|
|
LuGithub,
|
|
LuLinkedin,
|
|
LuGlobe,
|
|
LuGitBranch,
|
|
LuDownload,
|
|
} from "@preact-icons/lu";
|
|
|
|
interface ResumeData {
|
|
basics: {
|
|
name: string;
|
|
email: string;
|
|
url?: { href: string };
|
|
};
|
|
sections: {
|
|
summary: { name: string; content: string };
|
|
profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
|
|
skills: { name: string; items: { id: string; name: string; level: number }[] };
|
|
experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
|
|
education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
|
|
volunteer: { name: string; items: { id: string; organization: string; position: string; date: string /* No summary here */ }[] };
|
|
};
|
|
}
|
|
|
|
export const handler: Handlers<ResumeData> = {
|
|
async GET(_req, ctx) {
|
|
try {
|
|
const resp = await fetch(new URL("/files/resume.json", ctx.url).href);
|
|
if (!resp.ok) {
|
|
console.error(`Error fetching resume.json: ${resp.status} ${resp.statusText}`);
|
|
return ctx.render(undefined);
|
|
}
|
|
const resumeData: ResumeData = await resp.json();
|
|
const skillsSection = resumeData.sections.skills;
|
|
if (skillsSection && skillsSection.items) {
|
|
const tsSkill = skillsSection.items.find(s => s.name === "Typescrpt");
|
|
if (tsSkill) {
|
|
tsSkill.name = "Typescript";
|
|
}
|
|
}
|
|
return ctx.render(resumeData);
|
|
} catch (error) {
|
|
console.error("Error processing resume data:", error);
|
|
return ctx.render(undefined);
|
|
}
|
|
},
|
|
};
|
|
|
|
export default function ResumePage({ data }: PageProps<ResumeData | undefined>) {
|
|
if (!data) {
|
|
return (
|
|
<>
|
|
<Head><title>Error Loading Resume</title></Head>
|
|
<div class="container mx-auto p-4 max-w-4xl text-center">
|
|
<h1 class="text-2xl font-bold text-error">Error loading resume data.</h1>
|
|
<p>Please try refreshing the page.</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const { basics, sections } = data;
|
|
const { summary, profiles, skills, experience, education, volunteer } = sections;
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{basics.name} - Resume</title>
|
|
<meta name="description" content={`${basics.name}'s professional resume.`} />
|
|
</Head>
|
|
<div class="container mx-auto p-4 max-w-4xl">
|
|
<h1 class="text-4xl font-bold mb-6 text-center">{basics.name}</h1>
|
|
|
|
{/* Contact Info */}
|
|
<div class="flex justify-center items-center flex-wrap gap-x-4 gap-y-2 mb-6">
|
|
{basics.email && (
|
|
<a href={`mailto:${basics.email}`} class="link link-hover inline-flex items-center gap-1">
|
|
<LuMail /> {basics.email}
|
|
</a>
|
|
)}
|
|
{profiles.items.find(p => p.network === "GitHub") && (
|
|
<a href={profiles.items.find(p => p.network === "GitHub")!.url.href} target="_blank" rel="noopener noreferrer" class="link link-hover inline-flex items-center gap-1">
|
|
<LuGithub /> GitHub
|
|
</a>
|
|
)}
|
|
{profiles.items.find(p => p.network === "linkedin") && (
|
|
<a href={profiles.items.find(p => p.network === "linkedin")!.url.href} target="_blank" rel="noopener noreferrer" class="link link-hover inline-flex items-center gap-1">
|
|
<LuLinkedin /> LinkedIn
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Download Resume Button */}
|
|
<div class="text-center mb-8">
|
|
<a
|
|
href="/files/Atridad_Lahiji_Resume_Public.pdf"
|
|
download="Atridad_Lahiji_Resume.pdf"
|
|
class="btn btn-primary inline-flex items-center gap-2"
|
|
>
|
|
<LuDownload /> Download Resume (PDF)
|
|
</a>
|
|
</div>
|
|
|
|
{/* Summary Card */}
|
|
{summary && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{summary.name || "Summary"}</h2>
|
|
<div dangerouslySetInnerHTML={{ __html: summary.content }}></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Profiles Card */}
|
|
{profiles && profiles.items && profiles.items.length > 0 && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{profiles.name || "Profiles"}</h2>
|
|
<div class="flex flex-wrap gap-4">
|
|
{profiles.items.map((profile) => {
|
|
let IconComponent = LuGlobe;
|
|
const networkLower = profile.network.toLowerCase();
|
|
if (networkLower === "github") IconComponent = LuGithub;
|
|
else if (networkLower === "linkedin") IconComponent = LuLinkedin;
|
|
else if (networkLower === "forgejo") IconComponent = LuGitBranch;
|
|
|
|
return (
|
|
<a
|
|
key={profile.network}
|
|
href={profile.url.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="link link-hover inline-flex items-center gap-1"
|
|
>
|
|
<IconComponent /> {profile.network} ({profile.username})
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Skills Card */}
|
|
{skills && skills.items && skills.items.length > 0 && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{skills.name || "Skills"}</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{skills.items.map((skill) => (
|
|
<div key={skill.id || skill.name}>
|
|
<label class="label">
|
|
<span class="label-text">{skill.name}</span>
|
|
</label>
|
|
<progress
|
|
class="progress progress-primary w-full"
|
|
value={skill.level * 20}
|
|
max="100"
|
|
>
|
|
</progress>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Experience Card */}
|
|
{experience && experience.items && experience.items.length > 0 && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{experience.name || "Experience"}</h2>
|
|
<div class="space-y-4">
|
|
{experience.items.map((exp, index) => (
|
|
<div key={exp.id || index} class="collapse collapse-arrow bg-base-100">
|
|
<input type="radio" name="resume-accordion-experience" checked={index === 0} readOnly />
|
|
<div class="collapse-title text-xl font-medium">
|
|
{exp.position} at {exp.company} ({exp.date})
|
|
{exp.location && (
|
|
<span class="text-sm font-normal float-right pt-1">{exp.location}</span>
|
|
)}
|
|
</div>
|
|
<div class="collapse-content">
|
|
{exp.url && exp.url.href && (
|
|
<a href={exp.url.href} target="_blank" rel="noopener noreferrer" class="link link-primary block mb-2">{exp.url.href}</a>
|
|
)}
|
|
<div dangerouslySetInnerHTML={{ __html: exp.summary }}></div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Education Card */}
|
|
{education && education.items && education.items.length > 0 && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{education.name || "Education"}</h2>
|
|
<div class="space-y-4">
|
|
{education.items.map((edu, index) => (
|
|
<div key={edu.id || index}>
|
|
<h3 class="text-lg font-semibold">{edu.institution}</h3>
|
|
<p>{edu.studyType} - {edu.area} ({edu.date})</p>
|
|
{edu.summary && (
|
|
<div class="ml-4 text-sm mt-1" dangerouslySetInnerHTML={{ __html: edu.summary }}></div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Volunteering Card */}
|
|
{volunteer && volunteer.items && volunteer.items.length > 0 && (
|
|
<div class="card bg-base-200 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{volunteer.name || "Volunteering"}</h2>
|
|
<div class="space-y-4">
|
|
{volunteer.items.map((vol, index) => (
|
|
<div key={vol.id || index}>
|
|
<h3 class="text-lg font-semibold">{vol.organization}</h3>
|
|
<p>{vol.position} ({vol.date})</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</>
|
|
);
|
|
}
|