Major re-work!
Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled

This commit is contained in:
2025-06-12 11:09:24 -06:00
parent 324449dd59
commit ab2eb7eeac
13 changed files with 545 additions and 348 deletions

View File

@ -0,0 +1,41 @@
import { Icon } from 'astro-icon/components';
import type { IconType, LucideIcon, AstroIconName, CustomIconComponent } from '../types';
interface IconRendererProps {
icon: IconType;
size?: number;
class?: string;
[key: string]: any; // For additional props like client:load for custom components
}
// Type guard functions
function isLucideIcon(icon: IconType): icon is LucideIcon {
return typeof icon === 'function' && icon.length <= 1; // Lucide icons are function components
}
function isAstroIconName(icon: IconType): icon is AstroIconName {
return typeof icon === 'string';
}
function isCustomComponent(icon: IconType): icon is CustomIconComponent {
return typeof icon === 'function' && !isLucideIcon(icon);
}
export default function IconRenderer({ icon, size, class: className, ...props }: IconRendererProps) {
if (isLucideIcon(icon)) {
const LucideComponent = icon;
return <LucideComponent size={size} class={className} {...props} />;
}
if (isAstroIconName(icon)) {
return <Icon name={icon} class={className} {...props} />;
}
if (isCustomComponent(icon)) {
const CustomComponent = icon;
return <CustomComponent class={className} {...props} />;
}
// Fallback
return null;
}

View File

@ -1,14 +1,7 @@
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,
BriefcaseBusiness,
CodeXml,
Terminal as TerminalIcon,
Megaphone,
} from "lucide-preact";
import { navigationItems } from '../config/data'; import { navigationItems } from '../config/data';
import type { LucideIcon } from '../types';
interface NavigationBarProps { interface NavigationBarProps {
currentPath: string; currentPath: string;
@ -26,6 +19,9 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
return prevScrollPos.value > currentPos; return prevScrollPos.value > currentPos;
}); });
// Filter out disabled navigation items
const enabledNavigationItems = navigationItems.filter(item => item.enabled !== false);
// Update client path when location changes // Update client path when location changes
useEffect(() => { useEffect(() => {
const updatePath = () => { const updatePath = () => {
@ -67,15 +63,6 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
? activePath.slice(0, -1) ? activePath.slice(0, -1)
: activePath; : activePath;
const iconMap = {
Home,
NotebookPen,
BriefcaseBusiness,
CodeXml,
TerminalIcon,
Megaphone,
};
useEffect(() => { useEffect(() => {
let scrollTimer: ReturnType<typeof setTimeout> | undefined; let scrollTimer: ReturnType<typeof setTimeout> | undefined;
@ -106,8 +93,8 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
> >
<div class="overflow-visible"> <div class="overflow-visible">
<ul class="menu menu-horizontal bg-base-200 rounded-box p-1.5 sm:p-2 shadow-lg flex flex-nowrap whitespace-nowrap"> <ul class="menu menu-horizontal bg-base-200 rounded-box p-1.5 sm:p-2 shadow-lg flex flex-nowrap whitespace-nowrap">
{navigationItems.map((item) => { {enabledNavigationItems.map((item) => {
const Icon = iconMap[item.icon as keyof typeof iconMap]; const Icon = item.icon as LucideIcon;
const isActive = item.isActive const isActive = item.isActive
? item.isActive(normalizedPath) ? item.isActive(normalizedPath)
: normalizedPath === item.path; : normalizedPath === item.path;

View File

@ -1,8 +1,8 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from "astro-icon/components";
import type { Project } from '../config/data'; import type { Project } from '../types';
export interface Props { interface Props {
project: Project; project: Project;
} }

View File

@ -8,11 +8,10 @@ interface Skill {
} }
interface ResumeSkillsProps { interface ResumeSkillsProps {
title: string;
skills: Skill[]; skills: Skill[];
} }
export default function ResumeSkills({ title, skills }: ResumeSkillsProps) { export default function ResumeSkills({ skills }: ResumeSkillsProps) {
const animatedLevels = useSignal<{ [key: string]: number }>({}); const animatedLevels = useSignal<{ [key: string]: number }>({});
const hasAnimated = useSignal(false); const hasAnimated = useSignal(false);
@ -65,31 +64,26 @@ export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
}; };
return ( return (
<div id="skills-section" class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div id="skills-section" class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
<div class="card-body p-4 sm:p-6"> {skills.map((skill) => {
<h2 class="card-title text-xl sm:text-2xl">{title || "Skills"}</h2> const currentLevel = animatedLevels.value[skill.id] || 0;
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4"> const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100
{skills.map((skill) => {
const currentLevel = animatedLevels.value[skill.id] || 0;
const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100
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"> <span class="label-text text-sm sm:text-base">
{skill.name} {skill.name}
</span> </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>
); );
})} })}
</div>
</div>
</div> </div>
); );
} }

View File

@ -2,22 +2,37 @@
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import SpotifyIcon from './SpotifyIcon'; import SpotifyIcon from './SpotifyIcon';
import { socialLinks } from '../config/data'; import { socialLinks } from '../config/data';
// Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string {
return typeof icon === 'string';
}
// Helper function to check if icon is SpotifyIcon component
function isSpotifyIcon(icon: any): boolean {
return icon === SpotifyIcon;
}
--- ---
<div class="flex flex-row gap-1 sm:gap-4 text-3xl"> <div class="flex flex-row gap-1 sm:gap-4 text-3xl">
{socialLinks.map((link) => ( {socialLinks.map((link) => {
link.id === 'spotify' ? ( if (isSpotifyIcon(link.icon)) {
<SpotifyIcon profileUrl={link.url} client:load /> return (
) : ( <SpotifyIcon profileUrl={link.url} client:load />
<a );
href={link.url} } else if (isAstroIcon(link.icon)) {
target={link.url.startsWith('http') ? '_blank' : undefined} return (
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined} <a
aria-label={link.ariaLabel} href={link.url}
class="hover:text-primary transition-colors" target={link.url.startsWith('http') ? '_blank' : undefined}
> rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
<Icon name={link.icon} /> aria-label={link.ariaLabel}
</a> class="hover:text-primary transition-colors"
) >
))} <Icon name={link.icon} />
</a>
);
}
return null;
})}
</div> </div>

View File

@ -1,8 +1,8 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import type { Talk } from '../config/data'; import type { Talk } from '../types';
export interface Props { interface Props {
talk: Talk; talk: Talk;
} }
@ -16,13 +16,29 @@ const { talk } = Astro.props;
<h2 <h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100" class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
> >
{talk.name} <a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary transition-colors"
>
{talk.name}
</a>
</h2> </h2>
<p class="text-center break-words my-4 text-base-100"> <p class="text-center break-words my-4 text-base-100">
{talk.description} {talk.description}
</p> </p>
<div class="flex flex-col gap-2 mb-4 text-sm">
{talk.date && (
<div class="flex items-center gap-2">
<span class="font-semibold">Date:</span>
<span>{talk.date}</span>
</div>
)}
</div>
<div class="card-actions justify-end mt-4"> <div class="card-actions justify-end mt-4">
<a <a
href={talk.link} href={talk.link}

View File

@ -1,17 +1,28 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { techLinks } from '../config/data'; import { techLinks } from '../config/data';
// Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string {
return typeof icon === 'string';
}
--- ---
<div class="flex flex-row gap-1 sm:gap-4 text-3xl"> <div class="flex flex-row gap-1 sm:gap-4 text-3xl">
{techLinks.map((link) => ( {techLinks.map((link) => {
<a if (isAstroIcon(link.icon)) {
href={link.url} return (
target="_blank" <a
rel="noopener noreferrer" href={link.url}
aria-label={link.ariaLabel} target="_blank"
class="hover:text-primary transition-colors" rel="noopener noreferrer"
> aria-label={link.ariaLabel}
<Icon name={link.icon} /> class="hover:text-primary transition-colors"
</a> >
))} <Icon name={link.icon} />
</a>
);
}
return null;
})}
</div> </div>

View File

@ -1,45 +1,114 @@
export interface Talk { import type {
id: string; Talk,
name: string; Project,
description: string; SocialLink,
link: string; TechLink,
date?: string; NavigationItem,
venue?: string; PersonalInfo,
} HomepageSections,
SiteConfig,
ResumeConfig
} from '../types';
export interface Project { // Import Lucide Icons
id: string; import {
name: string; Home,
description: string; NotebookPen,
link: string; BriefcaseBusiness,
technologies?: string[]; CodeXml,
status?: string; Terminal as TerminalIcon,
} Megaphone,
} from "lucide-preact";
export interface SocialLink { import SpotifyIcon from '../components/SpotifyIcon';
id: string;
name: string;
url: string;
icon: string;
ariaLabel: string;
}
export interface TechLink { // Astro Icon references
id: string; const EMAIL_ICON = "mdi:email";
name: string; const RSS_ICON = "mdi:rss";
url: string; const GITEA_ICON = "simple-icons:gitea";
icon: string; const BLUESKY_ICON = "simple-icons:bluesky";
ariaLabel: string; const REACT_ICON = "simple-icons:react";
} const TYPESCRIPT_ICON = "simple-icons:typescript";
const ASTRO_ICON = "simple-icons:astro";
const GO_ICON = "simple-icons:go";
const POSTGRESQL_ICON = "simple-icons:postgresql";
const REDIS_ICON = "simple-icons:redis";
const DOCKER_ICON = "simple-icons:docker";
export interface NavigationItem { // Personal Information Configuration
id: string; export const personalInfo: PersonalInfo = {
name: string; name: "Atridad Lahiji",
path: string; profileImage: {
tooltip: string; src: "/logo_real.webp",
icon: string; alt: "A drawing of Atridad Lahiji by Shelze!",
isActive?: (path: string) => boolean; width: 150,
} height: 150
},
tagline: "Researcher, Full-Stack Developer, and IT Professional.",
description: "Researcher, Full-Stack Developer, and IT Professional."
};
// Homepage Section Configuration
export const homepageSections: HomepageSections = {
socialLinks: {
title: "Places I Exist:",
description: "Find me across the web"
},
techStack: {
title: "Stuff I Use:",
description: "Technologies and tools I work with"
}
};
// Resume Configuration
export const resumeConfig: ResumeConfig = {
jsonFile: "/files/resume.json",
pdfFile: {
path: "/files/Atridad_Lahiji_Resume.pdf",
filename: "Atridad_Lahiji_Resume.pdf",
displayText: "Download Resume (PDF)"
},
sections: {
enabled: ["summary", "experience", "education", "skills", "volunteer", "profiles"],
summary: {
title: "Summary",
enabled: true
},
experience: {
title: "Professional Experience",
enabled: true
},
education: {
title: "Education",
enabled: true
},
skills: {
title: "Technical Skills",
enabled: true
},
volunteer: {
title: "Volunteer Work",
enabled: true
},
profiles: {
title: "Professional Profiles",
enabled: true
}
}
};
// Site Metadata Configuration
export const siteConfig: SiteConfig = {
personal: personalInfo,
homepage: homepageSections,
resume: resumeConfig,
meta: {
title: "Atridad Lahiji",
description: "Personal website of Atridad Lahiji - Researcher, Full-Stack Developer, and IT Professional",
url: "https://atri.dad",
author: "Atridad Lahiji"
}
};
export const talks: Talk[] = [ export const talks: Talk[] = [
{ {
@ -47,7 +116,6 @@ export const talks: Talk[] = [
name: "Hypermedia as the engine of application state - An Introduction", 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.", description: "A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.",
link: "/files/DevEdmonton_Talk_HATEOAS.pdf", link: "/files/DevEdmonton_Talk_HATEOAS.pdf",
venue: "Dev Edmonton Society",
}, },
]; ];
@ -112,35 +180,35 @@ export const socialLinks: SocialLink[] = [
id: "email", id: "email",
name: "Email", name: "Email",
url: "mailto:me@atri.dad", url: "mailto:me@atri.dad",
icon: "mdi:email", icon: EMAIL_ICON,
ariaLabel: "Email me" ariaLabel: "Email me"
}, },
{ {
id: "rss", id: "rss",
name: "RSS Feed", name: "RSS Feed",
url: "/feed", url: "/feed",
icon: "mdi:rss", icon: RSS_ICON,
ariaLabel: "RSS Feed" ariaLabel: "RSS Feed"
}, },
{ {
id: "gitea", id: "gitea",
name: "Forgejo (Git)", name: "Forgejo (Git)",
url: "https://git.atri.dad/atridad", url: "https://git.atri.dad/atridad",
icon: "simple-icons:gitea", icon: GITEA_ICON,
ariaLabel: "Forgejo (Git)" ariaLabel: "Forgejo (Git)"
}, },
{ {
id: "bluesky", id: "bluesky",
name: "Bluesky", name: "Bluesky",
url: "https://bsky.app/profile/atri.dad", url: "https://bsky.app/profile/atri.dad",
icon: "simple-icons:bluesky", icon: BLUESKY_ICON,
ariaLabel: "Bluesky Profile" ariaLabel: "Bluesky Profile"
}, },
{ {
id: "spotify", id: "spotify",
name: "Spotify", name: "Spotify",
url: "https://open.spotify.com/user/31pjwuuqwnn5zr7fnhfjjmi7c4bi?si=1be2bfdc844c4d85", url: "https://open.spotify.com/user/31pjwuuqwnn5zr7fnhfjjmi7c4bi?si=1be2bfdc844c4d85",
icon: "spotify", // Special component icon: SpotifyIcon,
ariaLabel: "Spotify Profile" ariaLabel: "Spotify Profile"
} }
]; ];
@ -150,49 +218,49 @@ export const techLinks: TechLink[] = [
id: "react", id: "react",
name: "React", name: "React",
url: "https://react.dev/", url: "https://react.dev/",
icon: "simple-icons:react", icon: REACT_ICON,
ariaLabel: "React" ariaLabel: "React"
}, },
{ {
id: "typescript", id: "typescript",
name: "TypeScript", name: "TypeScript",
url: "https://www.typescriptlang.org/", url: "https://www.typescriptlang.org/",
icon: "simple-icons:typescript", icon: TYPESCRIPT_ICON,
ariaLabel: "TypeScript" ariaLabel: "TypeScript"
}, },
{ {
id: "astro", id: "astro",
name: "Astro", name: "Astro",
url: "https://astro.build/", url: "https://astro.build/",
icon: "simple-icons:astro", icon: ASTRO_ICON,
ariaLabel: "Astro" ariaLabel: "Astro"
}, },
{ {
id: "go", id: "go",
name: "Go", name: "Go",
url: "https://go.dev/", url: "https://go.dev/",
icon: "simple-icons:go", icon: GO_ICON,
ariaLabel: "Go" ariaLabel: "Go"
}, },
{ {
id: "postgresql", id: "postgresql",
name: "PostgreSQL", name: "PostgreSQL",
url: "https://www.postgresql.org/", url: "https://www.postgresql.org/",
icon: "simple-icons:postgresql", icon: POSTGRESQL_ICON,
ariaLabel: "PostgreSQL" ariaLabel: "PostgreSQL"
}, },
{ {
id: "redis", id: "redis",
name: "Redis", name: "Redis",
url: "https://redis.io/", url: "https://redis.io/",
icon: "simple-icons:redis", icon: REDIS_ICON,
ariaLabel: "Redis" ariaLabel: "Redis"
}, },
{ {
id: "docker", id: "docker",
name: "Docker", name: "Docker",
url: "https://www.docker.com/", url: "https://www.docker.com/",
icon: "simple-icons:docker", icon: DOCKER_ICON,
ariaLabel: "Docker" ariaLabel: "Docker"
} }
]; ];
@ -203,14 +271,16 @@ export const navigationItems: NavigationItem[] = [
name: "Home", name: "Home",
path: "/", path: "/",
tooltip: "Home", tooltip: "Home",
icon: "Home" icon: Home,
enabled: true
}, },
{ {
id: "posts", id: "posts",
name: "Posts", name: "Posts",
path: "/posts", path: "/posts",
tooltip: "Posts", tooltip: "Posts",
icon: "NotebookPen", icon: NotebookPen,
enabled: true,
isActive: (path: string) => path.startsWith("/posts") || path.startsWith("/post/") isActive: (path: string) => path.startsWith("/posts") || path.startsWith("/post/")
}, },
{ {
@ -218,14 +288,16 @@ export const navigationItems: NavigationItem[] = [
name: "Resume", name: "Resume",
path: "/resume", path: "/resume",
tooltip: "Resume", tooltip: "Resume",
icon: "BriefcaseBusiness" icon: BriefcaseBusiness,
enabled: true
}, },
{ {
id: "projects", id: "projects",
name: "Projects", name: "Projects",
path: "/projects", path: "/projects",
tooltip: "Projects", tooltip: "Projects",
icon: "CodeXml", icon: CodeXml,
enabled: true,
isActive: (path: string) => path.startsWith("/projects") isActive: (path: string) => path.startsWith("/projects")
}, },
{ {
@ -233,7 +305,8 @@ export const navigationItems: NavigationItem[] = [
name: "Talks", name: "Talks",
path: "/talks", path: "/talks",
tooltip: "Talks", tooltip: "Talks",
icon: "Megaphone", icon: Megaphone,
enabled: true,
isActive: (path: string) => path.startsWith("/talks") isActive: (path: string) => path.startsWith("/talks")
}, },
{ {
@ -241,6 +314,7 @@ export const navigationItems: NavigationItem[] = [
name: "Terminal", name: "Terminal",
path: "/terminal", path: "/terminal",
tooltip: "Terminal", tooltip: "Terminal",
icon: "TerminalIcon" icon: TerminalIcon,
enabled: true
} }
]; ];

View File

@ -2,8 +2,19 @@
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
import NavigationBar from "../components/NavigationBar"; import NavigationBar from "../components/NavigationBar";
import ScrollUpButton from "../components/ScrollUpButton"; import ScrollUpButton from "../components/ScrollUpButton";
import { siteConfig } from "../config/data";
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
import '../styles/global.css'; import '../styles/global.css';
export interface Props {
title?: string;
description?: string;
}
const { title, description } = Astro.props;
const pageTitle = title ? `${title} | ${siteConfig.meta.title}` : siteConfig.meta.title;
const pageDescription = description || siteConfig.meta.description;
--- ---
<!doctype html> <!doctype html>
@ -13,7 +24,9 @@ import '../styles/global.css';
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>Atridad Lahiji</title> <meta name="description" content={pageDescription} />
<meta name="author" content={siteConfig.meta.author} />
<title>{pageTitle}</title>
<ClientRouter /> <ClientRouter />
</head> </head>
<body class="flex flex-col min-h-screen"> <body class="flex flex-col min-h-screen">

View File

@ -142,14 +142,7 @@ function buildResumeFiles(resumeData: ResumeData): { [key: string]: FileSystemNo
} }
function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } { function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } {
const postsFiles: { [key: string]: FileSystemNode } = { const postsFiles: { [key: string]: FileSystemNode } = {};
'README.txt': {
type: 'file',
name: 'README.txt',
content: 'Blog posts and articles.\n\nUse "open /posts" to see the full list on the website.\n\nAvailable posts:\n' +
postsData.map((post: any) => `- ${post.slug}.md`).join('\n')
}
};
postsData.forEach((post: any) => { postsData.forEach((post: any) => {
const fileName = `${post.slug}.md`; const fileName = `${post.slug}.md`;
@ -173,33 +166,15 @@ ${post.content}`;
} }
function buildTalksFiles(): { [key: string]: FileSystemNode } { function buildTalksFiles(): { [key: string]: FileSystemNode } {
const talksFiles: { [key: string]: FileSystemNode } = { const talksFiles: { [key: string]: FileSystemNode } = {};
'README.txt': {
type: 'file',
name: 'README.txt',
content: 'Conference talks and presentations.\n\nUse "open /talks" to see the full list on the website.\n\nAvailable talks:\n' +
talks.map(talk => `- ${talk.id}.md`).join('\n')
}
};
talks.forEach(talk => { talks.forEach(talk => {
const fileName = `${talk.id}.md`; const fileName = `${talk.id}.txt`;
let content = `--- let content = `${talk.name}
title: "${talk.name}"
description: "${talk.description}"
${talk.venue ? `venue: "${talk.venue}"` : ''}
${talk.date ? `date: "${talk.date}"` : ''}
link: "${talk.link}"
---
# ${talk.name}
${talk.description} ${talk.description}
${talk.venue || ''}
${talk.venue ? `**Venue:** ${talk.venue}` : ''} ${talk.date || ''}
${talk.date ? `**Date:** ${talk.date}` : ''} ${talk.link}`;
**Download:** [${talk.link}](${talk.link})`;
talksFiles[fileName] = { talksFiles[fileName] = {
type: 'file', type: 'file',
@ -212,32 +187,15 @@ ${talk.date ? `**Date:** ${talk.date}` : ''}
} }
function buildProjectsFiles(): { [key: string]: FileSystemNode } { function buildProjectsFiles(): { [key: string]: FileSystemNode } {
const projectsFiles: { [key: string]: FileSystemNode } = { const projectsFiles: { [key: string]: FileSystemNode } = {};
'README.txt': {
type: 'file',
name: 'README.txt',
content: 'Personal and professional projects.\n\nUse "open /projects" to see the full portfolio on the website.\n\nAvailable projects:\n' +
projects.map(project => `- ${project.id}.md`).join('\n')
}
};
projects.forEach(project => { projects.forEach(project => {
const fileName = `${project.id}.md`; const fileName = `${project.id}.txt`;
let content = `--- let content = `${project.name}
title: "${project.name}"
description: "${project.description}"
${project.status ? `status: "${project.status}"` : ''}
${project.link ? `link: "${project.link}"` : ''}
${project.technologies ? `technologies: [${project.technologies.map(tech => `"${tech}"`).join(', ')}]` : ''}
---
# ${project.name}
${project.description} ${project.description}
${project.status || ''}
${project.status ? `**Status:** ${project.status}` : ''} ${project.technologies ? project.technologies.join(', ') : ''}
${project.technologies ? `**Technologies:** ${project.technologies.join(', ')}` : ''} ${project.link}`;
${project.link ? `**Link:** [${project.link}](${project.link})` : ''}`;
projectsFiles[fileName] = { projectsFiles[fileName] = {
type: 'file', type: 'file',
@ -250,23 +208,12 @@ ${project.link ? `**Link:** [${project.link}](${project.link})` : ''}`;
} }
function buildSocialFiles(): { [key: string]: FileSystemNode } { function buildSocialFiles(): { [key: string]: FileSystemNode } {
const socialFiles: { [key: string]: FileSystemNode } = { const socialFiles: { [key: string]: FileSystemNode } = {};
'README.txt': {
type: 'file',
name: 'README.txt',
content: 'Social media profiles and contact information.\n\nAvailable social links:\n' +
socialLinks.map(link => `- ${link.id}.txt`).join('\n')
}
};
socialLinks.forEach(link => { socialLinks.forEach(link => {
const fileName = `${link.id}.txt`; const fileName = `${link.id}.txt`;
let content = `${link.name} let content = `${link.name}
${link.url}`;
${link.ariaLabel}
URL: ${link.url}
Icon: ${link.icon}`;
socialFiles[fileName] = { socialFiles[fileName] = {
type: 'file', type: 'file',
@ -279,23 +226,12 @@ Icon: ${link.icon}`;
} }
function buildTechFiles(): { [key: string]: FileSystemNode } { function buildTechFiles(): { [key: string]: FileSystemNode } {
const techFiles: { [key: string]: FileSystemNode } = { const techFiles: { [key: string]: FileSystemNode } = {};
'README.txt': {
type: 'file',
name: 'README.txt',
content: 'Technologies and tools I use.\n\nAvailable tech links:\n' +
techLinks.map(link => `- ${link.id}.txt`).join('\n')
}
};
techLinks.forEach(link => { techLinks.forEach(link => {
const fileName = `${link.id}.txt`; const fileName = `${link.id}.txt`;
let content = `${link.name} let content = `${link.name}
${link.url}`;
${link.ariaLabel}
URL: ${link.url}
Icon: ${link.icon}`;
techFiles[fileName] = { techFiles[fileName] = {
type: 'file', type: 'file',

View File

@ -3,31 +3,32 @@ import { Image } from "astro:assets";
import SocialLinks from "../components/SocialLinks.astro"; import SocialLinks from "../components/SocialLinks.astro";
import TechLinks from "../components/TechLinks.astro"; import TechLinks from "../components/TechLinks.astro";
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import { personalInfo, homepageSections } from "../config/data";
--- ---
<Layout> <Layout>
<Image <Image
src="/logo_real.webp" src={personalInfo.profileImage.src}
alt="A drawing of Atridad Lahiji by Shelze!" alt={personalInfo.profileImage.alt}
height={150} height={personalInfo.profileImage.height}
width={150} width={personalInfo.profileImage.width}
/> />
<h1 <h1
class="bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent text-4xl sm:text-6xl font-bold text-center" class="bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent text-4xl sm:text-6xl font-bold text-center"
> >
Atridad Lahiji {personalInfo.name}
</h1> </h1>
<h2 class="text-xl sm:text-3xl font-bold text-center"> <h2 class="text-xl sm:text-3xl font-bold text-center">
Researcher, Full-Stack Developer, and IT Professional. {personalInfo.tagline}
</h2> </h2>
<h3 class="text-lg sm:text-2xl font-bold">Places I Exist:</h3> <h3 class="text-lg sm:text-2xl font-bold">{homepageSections.socialLinks.title}</h3>
<SocialLinks /> <SocialLinks />
<h3 class="text-lg sm:text-2xl font-bold">Stuff I Use:</h3> <h3 class="text-lg sm:text-2xl font-bold">{homepageSections.techStack.title}</h3>
<TechLinks /> <TechLinks />
</Layout> </Layout>

View File

@ -2,6 +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 { siteConfig } from "../config/data";
import "../styles/global.css"; import "../styles/global.css";
interface ResumeData { interface ResumeData {
@ -66,8 +67,8 @@ 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 using config
const response = await fetch(`${baseUrl}/files/resume.json`); const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`);
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
@ -96,11 +97,12 @@ try {
} }
const data = resumeData; const data = resumeData;
const resumeConfig = siteConfig.resume;
--- ---
{ {
(!data || fetchError) && ( (!data || fetchError) && (
<Layout> <Layout title="Resume">
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl text-center w-full"> <div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl text-center w-full">
<h1 class="text-2xl font-bold text-red-600"> <h1 class="text-2xl font-bold text-red-600">
Error loading resume data. Error loading resume data.
@ -113,7 +115,7 @@ const data = resumeData;
{ {
data && !fetchError && ( data && !fetchError && (
<Layout> <Layout title="Resume">
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full"> <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"> <h1 class="text-3xl sm:text-4xl font-bold mb-4 sm:mb-6 text-center">
{data.basics.name} {data.basics.name}
@ -164,19 +166,19 @@ const data = resumeData;
<div class="text-center mb-6 sm:mb-8"> <div class="text-center mb-6 sm:mb-8">
<a <a
href="/files/Atridad_Lahiji_Resume.pdf" href={resumeConfig.pdfFile.path}
download="Atridad_Lahiji_Resume.pdf" download={resumeConfig.pdfFile.filename}
class="btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base" class="btn btn-primary inline-flex items-center gap-2 text-sm sm:text-base"
> >
<Icon name="mdi:download" /> Download Resume (PDF) <Icon name="mdi:download" /> {resumeConfig.pdfFile.displayText}
</a> </a>
</div> </div>
{data.sections.summary && ( {data.sections.summary && resumeConfig.sections.summary?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{data.sections.summary.name || "Summary"} {resumeConfig.sections.summary.title || data.sections.summary.name || "Summary"}
</h2> </h2>
<div set:html={data.sections.summary.content} /> <div set:html={data.sections.summary.content} />
</div> </div>
@ -185,11 +187,12 @@ const data = resumeData;
{data.sections.profiles && {data.sections.profiles &&
data.sections.profiles.items && data.sections.profiles.items &&
data.sections.profiles.items.length > 0 && ( data.sections.profiles.items.length > 0 &&
resumeConfig.sections.profiles?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{data.sections.profiles.name || "Profiles"} {resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"}
</h2> </h2>
<div class="flex flex-wrap gap-3 sm:gap-4"> <div class="flex flex-wrap gap-3 sm:gap-4">
{data.sections.profiles.items.map( {data.sections.profiles.items.map(
@ -197,16 +200,23 @@ const data = resumeData;
let iconName = "mdi:web"; let iconName = "mdi:web";
const networkLower = const networkLower =
profile.network.toLowerCase(); profile.network.toLowerCase();
if (networkLower === "github") if (networkLower === "github") {
iconName = iconName = "simple-icons:github";
"simple-icons:github"; } else if (
else if (
networkLower === "linkedin" networkLower === "linkedin"
) ) {
iconName = iconName =
"simple-icons:linkedin"; "simple-icons:linkedin";
else if (networkLower === "gitea") } else if (
iconName = "simple-icons:gitea"; networkLower === "twitter"
) {
iconName = "simple-icons:x";
} else if (
networkLower === "youtube"
) {
iconName =
"simple-icons:youtube";
}
return ( return (
<a <a
@ -215,9 +225,8 @@ const data = resumeData;
rel="noopener noreferrer" rel="noopener noreferrer"
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base" class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
> >
<Icon name={iconName} />{" "} <Icon name={iconName} />
{profile.network} ( {profile.network}
{profile.username})
</a> </a>
); );
}, },
@ -229,93 +238,62 @@ const data = resumeData;
{data.sections.skills && {data.sections.skills &&
data.sections.skills.items && data.sections.skills.items &&
data.sections.skills.items.length > 0 && ( data.sections.skills.items.length > 0 &&
<ResumeSkills resumeConfig.sections.skills?.enabled && (
title={data.sections.skills.name} <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
skills={data.sections.skills.items} <div class="card-body p-4 sm:p-6">
client:visible <h2 class="card-title text-xl sm:text-2xl">
/> {resumeConfig.sections.skills.title || data.sections.skills.name || "Skills"}
</h2>
<ResumeSkills
skills={data.sections.skills.items}
client:load
/>
</div>
</div>
)} )}
{data.sections.experience && {data.sections.experience &&
data.sections.experience.items && data.sections.experience.items &&
data.sections.experience.items.length > 0 && ( data.sections.experience.items.length > 0 &&
resumeConfig.sections.experience?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{data.sections.experience.name || {resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"}
"Experience"}
</h2> </h2>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-4 sm:space-y-6">
{data.sections.experience.items.map( {data.sections.experience.items.map(
(exp, index) => ( (experience) => (
<details <div class="border-l-2 border-primary pl-4 sm:pl-6">
class="collapse collapse-arrow bg-base-100" <h3 class="text-lg sm:text-xl font-semibold">
open={ {experience.position}
index === 0 </h3>
? true <div class="flex flex-col sm:flex-row sm:items-center sm:gap-4 text-sm sm:text-base text-base-content/70 mb-2">
: undefined <span class="font-medium">
} {experience.company}
> </span>
<summary class="collapse-title text-lg sm:text-xl font-medium p-3 sm:p-4"> <span>{experience.date}</span>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2"> <span>
<span class="font-semibold"> {experience.location}
{exp.position} at{" "} </span>
{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> </div>
</details> <div
class="prose prose-sm sm:prose-base max-w-none"
set:html={experience.summary}
/>
{experience.url && experience.url.href && (
<a
href={experience.url.href}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2"
>
<Icon name="mdi:link" />
Company Website
</a>
)}
</div>
), ),
)} )}
</div> </div>
@ -325,28 +303,33 @@ const data = resumeData;
{data.sections.education && {data.sections.education &&
data.sections.education.items && data.sections.education.items &&
data.sections.education.items.length > 0 && ( data.sections.education.items.length > 0 &&
resumeConfig.sections.education?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{data.sections.education.name || {resumeConfig.sections.education.title || data.sections.education.name || "Education"}
"Education"}
</h2> </h2>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-4">
{data.sections.education.items.map( {data.sections.education.items.map(
(edu, index) => ( (education) => (
<div> <div class="border-l-2 border-secondary pl-4 sm:pl-6">
<h3 class="text-base sm:text-lg font-semibold"> <h3 class="text-lg sm:text-xl font-semibold">
{edu.institution} {education.institution}
</h3> </h3>
<p class="text-sm sm:text-base"> <div class="text-sm sm:text-base text-base-content/70 mb-2">
{edu.studyType} - {edu.area}{" "} <span class="font-medium">
({edu.date}) {education.studyType} in{" "}
</p> {education.area}
{edu.summary && ( </span>
<span class="block sm:inline sm:ml-4">
{education.date}
</span>
</div>
{education.summary && (
<div <div
class="ml-2 sm:ml-4 text-xs sm:text-sm mt-1" class="prose prose-sm sm:prose-base max-w-none"
set:html={edu.summary} set:html={education.summary}
/> />
)} )}
</div> </div>
@ -359,23 +342,28 @@ const data = resumeData;
{data.sections.volunteer && {data.sections.volunteer &&
data.sections.volunteer.items && data.sections.volunteer.items &&
data.sections.volunteer.items.length > 0 && ( data.sections.volunteer.items.length > 0 &&
resumeConfig.sections.volunteer?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6"> <div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl"> <h2 class="card-title text-xl sm:text-2xl">
{data.sections.volunteer.name || {resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"}
"Volunteering"}
</h2> </h2>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-4">
{data.sections.volunteer.items.map( {data.sections.volunteer.items.map(
(vol, index) => ( (volunteer) => (
<div> <div class="border-l-2 border-accent pl-4 sm:pl-6">
<h3 class="text-base sm:text-lg font-semibold"> <h3 class="text-lg sm:text-xl font-semibold">
{vol.organization} {volunteer.organization}
</h3> </h3>
<p class="text-sm sm:text-base"> <div class="text-sm sm:text-base text-base-content/70 mb-2">
{vol.position} ({vol.date}) <span class="font-medium">
</p> {volunteer.position}
</span>
<span class="block sm:inline sm:ml-4">
{volunteer.date}
</span>
</div>
</div> </div>
), ),
)} )}

121
src/types/index.ts Normal file
View File

@ -0,0 +1,121 @@
import type { ComponentType } from "preact";
// Icon Types
export type LucideIcon = ComponentType<{ size?: number; class?: string }>;
export type AstroIconName = string; // For astro-icon string references like "mdi:email"
export type CustomIconComponent = ComponentType<any>; // For custom components like SpotifyIcon
export type IconType = LucideIcon | AstroIconName | CustomIconComponent;
export interface Talk {
id: string;
name: string;
description: string;
link: string;
date?: string;
}
export interface Project {
id: string;
name: string;
description: string;
link: string;
technologies?: string[];
status?: string;
}
export interface SocialLink {
id: string;
name: string;
url: string;
icon: IconType;
ariaLabel: string;
}
export interface TechLink {
id: string;
name: string;
url: string;
icon: IconType;
ariaLabel: string;
}
export interface NavigationItem {
id: string;
name: string;
path: string;
tooltip: string;
icon: IconType;
enabled?: boolean;
isActive?: (path: string) => boolean;
}
export interface ResumeConfig {
jsonFile: string;
pdfFile: {
path: string;
filename: string;
displayText: string;
};
sections: {
enabled: string[];
summary?: {
title?: string;
enabled?: boolean;
};
experience?: {
title?: string;
enabled?: boolean;
};
education?: {
title?: string;
enabled?: boolean;
};
skills?: {
title?: string;
enabled?: boolean;
};
volunteer?: {
title?: string;
enabled?: boolean;
};
profiles?: {
title?: string;
enabled?: boolean;
};
};
}
export interface PersonalInfo {
name: string;
profileImage: {
src: string;
alt: string;
width: number;
height: number;
};
tagline: string;
description?: string;
}
export interface HomepageSections {
socialLinks: {
title: string;
description?: string;
};
techStack: {
title: string;
description?: string;
};
}
export interface SiteConfig {
personal: PersonalInfo;
homepage: HomepageSections;
resume: ResumeConfig;
meta: {
title: string;
description: string;
url: string;
author: string;
};
}