This commit is contained in:
@ -15,13 +15,13 @@
|
|||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/rss": "^4.0.12",
|
||||||
"@preact/signals": "^2.2.0",
|
"@preact/signals": "^2.2.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"astro": "^5.9.2",
|
"astro": "^5.9.2",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
"lucide-preact": "^0.513.0",
|
"lucide-preact": "^0.514.0",
|
||||||
"preact": "^10.26.8",
|
"preact": "^10.26.9",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"tailwindcss": "^4.1.8"
|
"tailwindcss": "^4.1.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
|
490
pnpm-lock.yaml
generated
490
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/files/DevEdmonton_Talk_HATEOAS.pdf
Normal file
BIN
public/files/DevEdmonton_Talk_HATEOAS.pdf
Normal file
Binary file not shown.
@ -1,6 +1,13 @@
|
|||||||
import { useComputed, useSignal } from "@preact/signals";
|
import { useComputed, useSignal } from "@preact/signals";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import { Home, NotebookPen, FileText, CodeXml, Terminal as TerminalIcon } from 'lucide-preact';
|
import {
|
||||||
|
Home,
|
||||||
|
NotebookPen,
|
||||||
|
FileText,
|
||||||
|
CodeXml,
|
||||||
|
Terminal as TerminalIcon,
|
||||||
|
Megaphone,
|
||||||
|
} from "lucide-preact";
|
||||||
|
|
||||||
interface NavigationBarProps {
|
interface NavigationBarProps {
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
@ -35,28 +42,29 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Listen for astro:page-load event which fires after navigation completes
|
// Listen for astro:page-load event which fires after navigation completes
|
||||||
document.addEventListener('astro:page-load', handleAstroNavigation);
|
document.addEventListener("astro:page-load", handleAstroNavigation);
|
||||||
|
|
||||||
// Also listen for astro:after-swap as a backup
|
// Also listen for astro:after-swap as a backup
|
||||||
document.addEventListener('astro:after-swap', handleAstroNavigation);
|
document.addEventListener("astro:after-swap", handleAstroNavigation);
|
||||||
|
|
||||||
// Listen for regular navigation events as fallback
|
// Listen for regular navigation events as fallback
|
||||||
window.addEventListener('popstate', updatePath);
|
window.addEventListener("popstate", updatePath);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('astro:page-load', handleAstroNavigation);
|
document.removeEventListener("astro:page-load", handleAstroNavigation);
|
||||||
document.removeEventListener('astro:after-swap', handleAstroNavigation);
|
document.removeEventListener("astro:after-swap", handleAstroNavigation);
|
||||||
window.removeEventListener('popstate', updatePath);
|
window.removeEventListener("popstate", updatePath);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Use the client path
|
// Use the client path
|
||||||
const activePath = currentClientPath.value;
|
const activePath = currentClientPath.value;
|
||||||
|
|
||||||
// Normalize path by removing trailing slashes for consistent comparison
|
// Normalize path
|
||||||
const normalizedPath = activePath.endsWith('/') && activePath.length > 1
|
const normalizedPath =
|
||||||
? activePath.slice(0, -1)
|
activePath.endsWith("/") && activePath.length > 1
|
||||||
: activePath;
|
? activePath.slice(0, -1)
|
||||||
|
: activePath;
|
||||||
|
|
||||||
const isPostsPath = (path: string) => {
|
const isPostsPath = (path: string) => {
|
||||||
return path.startsWith("/posts") || path.startsWith("/post/");
|
return path.startsWith("/posts") || path.startsWith("/post/");
|
||||||
@ -125,7 +133,9 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
<li class="mx-0.5 sm:mx-1">
|
<li class="mx-0.5 sm:mx-1">
|
||||||
<a
|
<a
|
||||||
href="/projects"
|
href="/projects"
|
||||||
class={normalizedPath.startsWith("/projects") ? "menu-active" : ""}
|
class={
|
||||||
|
normalizedPath.startsWith("/projects") ? "menu-active" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div class="tooltip" data-tip="Projects">
|
<div class="tooltip" data-tip="Projects">
|
||||||
<CodeXml size={18} class="sm:w-5 sm:h-5" />
|
<CodeXml size={18} class="sm:w-5 sm:h-5" />
|
||||||
@ -133,6 +143,17 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="mx-0.5 sm:mx-1">
|
||||||
|
<a
|
||||||
|
href="/talks"
|
||||||
|
class={normalizedPath.startsWith("/talks") ? "menu-active" : ""}
|
||||||
|
>
|
||||||
|
<div class="tooltip" data-tip="Talks">
|
||||||
|
<Megaphone size={18} class="sm:w-5 sm:h-5" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="mx-0.5 sm:mx-1">
|
<li class="mx-0.5 sm:mx-1">
|
||||||
<a
|
<a
|
||||||
href="/terminal"
|
href="/terminal"
|
||||||
|
@ -7,12 +7,12 @@ interface Skill {
|
|||||||
level: number;
|
level: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SkillsSectionProps {
|
interface ResumeSkillsProps {
|
||||||
title: string;
|
title: string;
|
||||||
skills: Skill[];
|
skills: Skill[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkillsSection({ title, skills }: SkillsSectionProps) {
|
export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
|
||||||
const animatedLevels = useSignal<{ [key: string]: number }>({});
|
const animatedLevels = useSignal<{ [key: string]: number }>({});
|
||||||
const hasAnimated = useSignal(false);
|
const hasAnimated = useSignal(false);
|
||||||
|
|
||||||
@ -29,10 +29,10 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ threshold: 0.3 }
|
{ threshold: 0.3 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const skillsElement = document.getElementById('skills-section');
|
const skillsElement = document.getElementById("skills-section");
|
||||||
if (skillsElement) {
|
if (skillsElement) {
|
||||||
observer.observe(skillsElement);
|
observer.observe(skillsElement);
|
||||||
}
|
}
|
||||||
@ -45,8 +45,7 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
|
|||||||
}, [skills]);
|
}, [skills]);
|
||||||
|
|
||||||
const animateSkill = (skillId: string, targetLevel: number) => {
|
const animateSkill = (skillId: string, targetLevel: number) => {
|
||||||
const duration = 1500; // 1.5 seconds
|
const steps = 60;
|
||||||
const steps = 60; // 60 frames for smooth animation
|
|
||||||
const increment = targetLevel / steps;
|
const increment = targetLevel / steps;
|
||||||
let currentStep = 0;
|
let currentStep = 0;
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
|
|||||||
const currentValue = Math.min(increment * currentStep, targetLevel);
|
const currentValue = Math.min(increment * currentStep, targetLevel);
|
||||||
animatedLevels.value = {
|
animatedLevels.value = {
|
||||||
...animatedLevels.value,
|
...animatedLevels.value,
|
||||||
[skillId]: currentValue
|
[skillId]: currentValue,
|
||||||
};
|
};
|
||||||
currentStep++;
|
currentStep++;
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@ -77,14 +76,15 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<div key={skill.id}>
|
<div key={skill.id}>
|
||||||
<label class="label p-1 sm:p-2">
|
<label class="label p-1 sm:p-2">
|
||||||
<span class="label-text text-sm sm:text-base">{skill.name}</span>
|
<span class="label-text text-sm sm:text-base">
|
||||||
|
{skill.name}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<progress
|
<progress
|
||||||
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
|
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
|
||||||
value={progressValue}
|
value={progressValue}
|
||||||
max="100"
|
max="100"
|
||||||
>
|
></progress>
|
||||||
</progress>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
44
src/components/TalkCard.astro
Normal file
44
src/components/TalkCard.astro
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
|
interface Talk {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
talk: Talk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { talk } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"
|
||||||
|
>
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<h2
|
||||||
|
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
|
||||||
|
>
|
||||||
|
{talk.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-center break-words my-4 text-base-100">
|
||||||
|
{talk.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<a
|
||||||
|
href={talk.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-circle btn-sm bg-base-100 hover:bg-base-200 text-accent"
|
||||||
|
aria-label={`Visit ${talk.name}`}
|
||||||
|
>
|
||||||
|
<Icon name="mdi:link" class="text-lg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,223 +1,389 @@
|
|||||||
---
|
---
|
||||||
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 SkillsSection from '../components/SkillsSection.tsx';
|
import ResumeSkills from "../components/ResumeSkills";
|
||||||
import '../styles/global.css';
|
import "../styles/global.css";
|
||||||
|
|
||||||
interface ResumeData {
|
interface ResumeData {
|
||||||
basics: {
|
basics: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
url?: { href: string };
|
url?: { href: string };
|
||||||
};
|
};
|
||||||
sections: {
|
sections: {
|
||||||
summary: { name: string; content: string };
|
summary: { name: string; content: string };
|
||||||
profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
|
profiles: {
|
||||||
skills: { name: string; items: { id: string; name: string; level: number }[] };
|
name: string;
|
||||||
experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
|
items: {
|
||||||
education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
|
network: string;
|
||||||
volunteer: { name: string; items: { id: string; organization: string; position: string; date: 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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let resumeData: ResumeData | undefined = undefined;
|
let resumeData: ResumeData | undefined = undefined;
|
||||||
let fetchError: string | null = null;
|
let fetchError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the base URL for the current request
|
// Get the base URL for the current request
|
||||||
const baseUrl = Astro.url.origin;
|
const baseUrl = Astro.url.origin;
|
||||||
|
|
||||||
// Fetch the JSON file from the public directory
|
// Fetch the JSON file from the public directory
|
||||||
const response = await fetch(`${baseUrl}/files/resume.json`);
|
const response = await fetch(`${baseUrl}/files/resume.json`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch resume: ${response.status} ${response.statusText}`);
|
throw new Error(
|
||||||
}
|
`Failed to fetch resume: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
resumeData = await response.json();
|
}
|
||||||
|
|
||||||
if (resumeData && resumeData.sections && resumeData.sections.skills) {
|
resumeData = await response.json();
|
||||||
const skillsSection = resumeData.sections.skills;
|
|
||||||
if (skillsSection.items) {
|
if (resumeData && resumeData.sections && resumeData.sections.skills) {
|
||||||
const tsSkill = skillsSection.items.find(s => s.name === "Typescrpt");
|
const resumeSkills = resumeData.sections.skills;
|
||||||
if (tsSkill) {
|
if (resumeSkills.items) {
|
||||||
tsSkill.name = "Typescript";
|
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);
|
||||||
fetchError = "Unable to load resume data. Please make sure the resume.json file exists in /public/files/";
|
fetchError =
|
||||||
resumeData = undefined;
|
"Unable to load resume data. Please make sure the resume.json file exists in /public/files/";
|
||||||
|
resumeData = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = resumeData;
|
const data = resumeData;
|
||||||
---
|
---
|
||||||
|
|
||||||
{(!data || fetchError) && (
|
{
|
||||||
<Layout>
|
(!data || fetchError) && (
|
||||||
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl text-center w-full">
|
<Layout>
|
||||||
<h1 class="text-2xl font-bold text-red-600">Error loading resume data.</h1>
|
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl text-center w-full">
|
||||||
<p>{fetchError || "Please try refreshing the page."}</p>
|
<h1 class="text-2xl font-bold text-red-600">
|
||||||
</div>
|
Error loading resume data.
|
||||||
</Layout>
|
</h1>
|
||||||
)}
|
<p>{fetchError || "Please try refreshing the page."}</p>
|
||||||
|
|
||||||
{data && !fetchError && (
|
|
||||||
<Layout>
|
|
||||||
<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 mb-4 sm:mb-6 text-center">{data.basics.name}</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">
|
|
||||||
{data.basics.email && (
|
|
||||||
<a href={`mailto:${data.basics.email}`} class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base">
|
|
||||||
<Icon name="mdi:email" /> {data.basics.email}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{data.sections.profiles.items.find(p => p.network === "GitHub") && (
|
|
||||||
<a href={data.sections.profiles.items.find(p => p.network === "GitHub")!.url.href} target="_blank" rel="noopener noreferrer" class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base">
|
|
||||||
<Icon name="simple-icons:github" /> GitHub
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{data.sections.profiles.items.find(p => p.network === "linkedin") && (
|
|
||||||
<a href={data.sections.profiles.items.find(p => p.network === "linkedin")!.url.href} 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 class="text-center mb-6 sm:mb-8">
|
|
||||||
<a
|
|
||||||
href="/files/Atridad_Lahiji_Resume.pdf"
|
|
||||||
download="Atridad_Lahiji_Resume.pdf"
|
|
||||||
class="btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:download" /> Download Resume (PDF)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.sections.summary && (
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
|
||||||
<div class="card-body p-4 sm:p-6">
|
|
||||||
<h2 class="card-title text-xl sm:text-2xl">{data.sections.summary.name || "Summary"}</h2>
|
|
||||||
<div set:html={data.sections.summary.content}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.sections.profiles && data.sections.profiles.items && data.sections.profiles.items.length > 0 && (
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
|
||||||
<div class="card-body p-4 sm:p-6">
|
|
||||||
<h2 class="card-title text-xl sm:text-2xl">{data.sections.profiles.name || "Profiles"}</h2>
|
|
||||||
<div class="flex flex-wrap gap-3 sm:gap-4">
|
|
||||||
{data.sections.profiles.items.map((profile) => {
|
|
||||||
let iconName = "mdi:web";
|
|
||||||
const networkLower = profile.network.toLowerCase();
|
|
||||||
if (networkLower === "github") iconName = "simple-icons:github";
|
|
||||||
else if (networkLower === "linkedin") iconName = "simple-icons:linkedin";
|
|
||||||
else if (networkLower === "gitea") iconName = "simple-icons:gitea";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={profile.url.href}
|
|
||||||
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} ({profile.username})
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{data.sections.skills && data.sections.skills.items && data.sections.skills.items.length > 0 && (
|
{
|
||||||
<SkillsSection
|
data && !fetchError && (
|
||||||
title={data.sections.skills.name}
|
<Layout>
|
||||||
skills={data.sections.skills.items}
|
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full">
|
||||||
client:visible
|
<h1 class="text-3xl sm:text-4xl font-bold mb-4 sm:mb-6 text-center">
|
||||||
/>
|
{data.basics.name}
|
||||||
)}
|
</h1>
|
||||||
|
|
||||||
{data.sections.experience && data.sections.experience.items && data.sections.experience.items.length > 0 && (
|
<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 class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
{data.basics.email && (
|
||||||
<div class="card-body p-4 sm:p-6">
|
<a
|
||||||
<h2 class="card-title text-xl sm:text-2xl">{data.sections.experience.name || "Experience"}</h2>
|
href={`mailto:${data.basics.email}`}
|
||||||
<div class="space-y-3 sm:space-y-4">
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
{data.sections.experience.items.map((exp, index) => (
|
>
|
||||||
<details class="collapse collapse-arrow bg-base-100" open={index === 0 ? true : undefined}>
|
<Icon name="mdi:email" /> {data.basics.email}
|
||||||
<summary class="collapse-title text-lg sm:text-xl font-medium p-3 sm:p-4">
|
</a>
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2">
|
)}
|
||||||
<span class="font-semibold">{exp.position} at {exp.company}</span>
|
{data.sections.profiles.items.find(
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 text-sm sm:text-base font-normal">
|
(p) => p.network === "GitHub",
|
||||||
<span>{exp.date}</span>
|
) && (
|
||||||
{exp.location && (
|
<a
|
||||||
<span class="text-base-content/70">{exp.location}</span>
|
href={
|
||||||
)}
|
data.sections.profiles.items.find(
|
||||||
</div>
|
(p) => p.network === "GitHub",
|
||||||
</div>
|
)!.url.href
|
||||||
</summary>
|
}
|
||||||
<div class="collapse-content p-3 sm:p-4">
|
target="_blank"
|
||||||
{exp.url && exp.url.href && (
|
rel="noopener noreferrer"
|
||||||
<a href={exp.url.href} target="_blank" rel="noopener noreferrer" class="link link-primary block mb-2 text-sm sm:text-base break-all">{exp.url.href}</a>
|
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
<Icon name="simple-icons:github" /> GitHub
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{data.sections.profiles.items.find(
|
||||||
|
(p) => p.network === "linkedin",
|
||||||
|
) && (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
data.sections.profiles.items.find(
|
||||||
|
(p) => p.network === "linkedin",
|
||||||
|
)!.url.href
|
||||||
|
}
|
||||||
|
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 class="mt-2">
|
|
||||||
<ul class="list space-y-1">
|
|
||||||
{exp.summary.replace(/<\/?ul>|<\/?p>/g, '')
|
|
||||||
.split('<li>')
|
|
||||||
.filter(item => item.trim() !== '')
|
|
||||||
.map(item => (
|
|
||||||
<li class="list-row text-sm sm:text-base">
|
|
||||||
{item.replace('</li>', '')}
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.sections.education && data.sections.education.items && data.sections.education.items.length > 0 && (
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
|
||||||
<div class="card-body p-4 sm:p-6">
|
|
||||||
<h2 class="card-title text-xl sm:text-2xl">{data.sections.education.name || "Education"}</h2>
|
|
||||||
<div class="space-y-3 sm:space-y-4">
|
|
||||||
{data.sections.education.items.map((edu, index) => (
|
|
||||||
<div>
|
|
||||||
<h3 class="text-base sm:text-lg font-semibold">{edu.institution}</h3>
|
|
||||||
<p class="text-sm sm:text-base">{edu.studyType} - {edu.area} ({edu.date})</p>
|
|
||||||
{edu.summary && (
|
|
||||||
<div class="ml-2 sm:ml-4 text-xs sm:text-sm mt-1" set:html={edu.summary}></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.sections.volunteer && data.sections.volunteer.items && data.sections.volunteer.items.length > 0 && (
|
<div class="text-center mb-6 sm:mb-8">
|
||||||
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
<a
|
||||||
<div class="card-body p-4 sm:p-6">
|
href="/files/Atridad_Lahiji_Resume.pdf"
|
||||||
<h2 class="card-title text-xl sm:text-2xl">{data.sections.volunteer.name || "Volunteering"}</h2>
|
download="Atridad_Lahiji_Resume.pdf"
|
||||||
<div class="space-y-3 sm:space-y-4">
|
class="btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base"
|
||||||
{data.sections.volunteer.items.map((vol, index) => (
|
>
|
||||||
<div>
|
<Icon name="mdi:download" /> Download Resume (PDF)
|
||||||
<h3 class="text-base sm:text-lg font-semibold">{vol.organization}</h3>
|
</a>
|
||||||
<p class="text-sm sm:text-base">{vol.position} ({vol.date})</p>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
{data.sections.summary && (
|
||||||
</div>
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
|
<div class="card-body p-4 sm:p-6">
|
||||||
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
|
{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 && (
|
||||||
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
|
<div class="card-body p-4 sm:p-6">
|
||||||
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
|
{data.sections.profiles.name || "Profiles"}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-wrap gap-3 sm:gap-4">
|
||||||
|
{data.sections.profiles.items.map(
|
||||||
|
(profile) => {
|
||||||
|
let iconName = "mdi:web";
|
||||||
|
const networkLower =
|
||||||
|
profile.network.toLowerCase();
|
||||||
|
if (networkLower === "github")
|
||||||
|
iconName =
|
||||||
|
"simple-icons:github";
|
||||||
|
else if (
|
||||||
|
networkLower === "linkedin"
|
||||||
|
)
|
||||||
|
iconName =
|
||||||
|
"simple-icons:linkedin";
|
||||||
|
else if (networkLower === "gitea")
|
||||||
|
iconName = "simple-icons:gitea";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={profile.url.href}
|
||||||
|
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} (
|
||||||
|
{profile.username})
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.sections.skills &&
|
||||||
|
data.sections.skills.items &&
|
||||||
|
data.sections.skills.items.length > 0 && (
|
||||||
|
<ResumeSkills
|
||||||
|
title={data.sections.skills.name}
|
||||||
|
skills={data.sections.skills.items}
|
||||||
|
client:visible
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.sections.experience &&
|
||||||
|
data.sections.experience.items &&
|
||||||
|
data.sections.experience.items.length > 0 && (
|
||||||
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
|
<div class="card-body p-4 sm:p-6">
|
||||||
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
|
{data.sections.experience.name ||
|
||||||
|
"Experience"}
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3 sm:space-y-4">
|
||||||
|
{data.sections.experience.items.map(
|
||||||
|
(exp, index) => (
|
||||||
|
<details
|
||||||
|
class="collapse collapse-arrow bg-base-100"
|
||||||
|
open={
|
||||||
|
index === 0
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<summary class="collapse-title text-lg sm:text-xl font-medium p-3 sm:p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2">
|
||||||
|
<span class="font-semibold">
|
||||||
|
{exp.position} at{" "}
|
||||||
|
{exp.company}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 text-sm sm:text-base font-normal">
|
||||||
|
<span>
|
||||||
|
{exp.date}
|
||||||
|
</span>
|
||||||
|
{exp.location && (
|
||||||
|
<span class="text-base-content/70">
|
||||||
|
{
|
||||||
|
exp.location
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="collapse-content p-3 sm:p-4">
|
||||||
|
{exp.url &&
|
||||||
|
exp.url.href && (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
exp.url.href
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="link link-primary block mb-2 text-sm sm:text-base break-all"
|
||||||
|
>
|
||||||
|
{exp.url.href}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div class="mt-2">
|
||||||
|
<ul class="list space-y-1">
|
||||||
|
{exp.summary
|
||||||
|
.replace(
|
||||||
|
/<\/?ul>|<\/?p>/g,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.split("<li>")
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.trim() !==
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.map((item) => (
|
||||||
|
<li class="list-row text-sm sm:text-base">
|
||||||
|
{item.replace(
|
||||||
|
"</li>",
|
||||||
|
"",
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.sections.education &&
|
||||||
|
data.sections.education.items &&
|
||||||
|
data.sections.education.items.length > 0 && (
|
||||||
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
|
<div class="card-body p-4 sm:p-6">
|
||||||
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
|
{data.sections.education.name ||
|
||||||
|
"Education"}
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3 sm:space-y-4">
|
||||||
|
{data.sections.education.items.map(
|
||||||
|
(edu, index) => (
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base sm:text-lg font-semibold">
|
||||||
|
{edu.institution}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm sm:text-base">
|
||||||
|
{edu.studyType} - {edu.area}{" "}
|
||||||
|
({edu.date})
|
||||||
|
</p>
|
||||||
|
{edu.summary && (
|
||||||
|
<div
|
||||||
|
class="ml-2 sm:ml-4 text-xs sm:text-sm mt-1"
|
||||||
|
set:html={edu.summary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.sections.volunteer &&
|
||||||
|
data.sections.volunteer.items &&
|
||||||
|
data.sections.volunteer.items.length > 0 && (
|
||||||
|
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||||
|
<div class="card-body p-4 sm:p-6">
|
||||||
|
<h2 class="card-title text-xl sm:text-2xl">
|
||||||
|
{data.sections.volunteer.name ||
|
||||||
|
"Volunteering"}
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3 sm:space-y-4">
|
||||||
|
{data.sections.volunteer.items.map(
|
||||||
|
(vol, index) => (
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base sm:text-lg font-semibold">
|
||||||
|
{vol.organization}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm sm:text-base">
|
||||||
|
{vol.position} ({vol.date})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
</Layout>
|
|
||||||
)}
|
|
||||||
|
37
src/pages/talks.astro
Normal file
37
src/pages/talks.astro
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import TalkCard from "../components/TalkCard.astro";
|
||||||
|
|
||||||
|
const talks = [
|
||||||
|
{
|
||||||
|
id: "devedmonton-hateoas",
|
||||||
|
name: "Hypermedia as the engine of application state - An Introduction",
|
||||||
|
description:
|
||||||
|
"A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.",
|
||||||
|
link: "/files/DevEdmonton_Talk_HATEOAS.pdf",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<div class="min-h-screen p-4 sm:p-8">
|
||||||
|
<h1
|
||||||
|
class="text-3xl sm:text-4xl font-bold text-secondary mb-6 sm:mb-8 text-center"
|
||||||
|
>
|
||||||
|
Talks
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
|
||||||
|
>
|
||||||
|
{talks.map((talk) => <TalkCard talk={talk} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
talks.length === 0 && (
|
||||||
|
<p class="text-center text-gray-500 mt-12">
|
||||||
|
No talks available yet. Check back soon!
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
Reference in New Issue
Block a user