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

View File

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

View File

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

View File

@ -2,22 +2,37 @@
import { Icon } from 'astro-icon/components';
import SpotifyIcon from './SpotifyIcon';
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">
{socialLinks.map((link) => (
link.id === 'spotify' ? (
<SpotifyIcon profileUrl={link.url} client:load />
) : (
<a
href={link.url}
target={link.url.startsWith('http') ? '_blank' : undefined}
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
)
))}
{socialLinks.map((link) => {
if (isSpotifyIcon(link.icon)) {
return (
<SpotifyIcon profileUrl={link.url} client:load />
);
} else if (isAstroIcon(link.icon)) {
return (
<a
href={link.url}
target={link.url.startsWith('http') ? '_blank' : undefined}
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
);
}
return null;
})}
</div>

View File

@ -1,8 +1,8 @@
---
import { Icon } from "astro-icon/components";
import type { Talk } from '../config/data';
import type { Talk } from '../types';
export interface Props {
interface Props {
talk: Talk;
}
@ -16,13 +16,29 @@ const { talk } = Astro.props;
<h2
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>
<p class="text-center break-words my-4 text-base-100">
{talk.description}
</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">
<a
href={talk.link}

View File

@ -1,17 +1,28 @@
---
import { Icon } from 'astro-icon/components';
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">
{techLinks.map((link) => (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
))}
{techLinks.map((link) => {
if (isAstroIcon(link.icon)) {
return (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
);
}
return null;
})}
</div>

View File

@ -1,45 +1,114 @@
export interface Talk {
id: string;
name: string;
description: string;
link: string;
date?: string;
venue?: string;
}
import type {
Talk,
Project,
SocialLink,
TechLink,
NavigationItem,
PersonalInfo,
HomepageSections,
SiteConfig,
ResumeConfig
} from '../types';
export interface Project {
id: string;
name: string;
description: string;
link: string;
technologies?: string[];
status?: string;
}
// Import Lucide Icons
import {
Home,
NotebookPen,
BriefcaseBusiness,
CodeXml,
Terminal as TerminalIcon,
Megaphone,
} from "lucide-preact";
export interface SocialLink {
id: string;
name: string;
url: string;
icon: string;
ariaLabel: string;
}
import SpotifyIcon from '../components/SpotifyIcon';
export interface TechLink {
id: string;
name: string;
url: string;
icon: string;
ariaLabel: string;
}
// Astro Icon references
const EMAIL_ICON = "mdi:email";
const RSS_ICON = "mdi:rss";
const GITEA_ICON = "simple-icons:gitea";
const BLUESKY_ICON = "simple-icons:bluesky";
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 {
id: string;
name: string;
path: string;
tooltip: string;
icon: string;
isActive?: (path: string) => boolean;
}
// Personal Information Configuration
export const personalInfo: PersonalInfo = {
name: "Atridad Lahiji",
profileImage: {
src: "/logo_real.webp",
alt: "A drawing of Atridad Lahiji by Shelze!",
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[] = [
{
@ -47,7 +116,6 @@ export const talks: Talk[] = [
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",
venue: "Dev Edmonton Society",
},
];
@ -112,35 +180,35 @@ export const socialLinks: SocialLink[] = [
id: "email",
name: "Email",
url: "mailto:me@atri.dad",
icon: "mdi:email",
icon: EMAIL_ICON,
ariaLabel: "Email me"
},
{
id: "rss",
name: "RSS Feed",
url: "/feed",
icon: "mdi:rss",
icon: RSS_ICON,
ariaLabel: "RSS Feed"
},
{
id: "gitea",
name: "Forgejo (Git)",
url: "https://git.atri.dad/atridad",
icon: "simple-icons:gitea",
icon: GITEA_ICON,
ariaLabel: "Forgejo (Git)"
},
{
id: "bluesky",
name: "Bluesky",
url: "https://bsky.app/profile/atri.dad",
icon: "simple-icons:bluesky",
icon: BLUESKY_ICON,
ariaLabel: "Bluesky Profile"
},
{
id: "spotify",
name: "Spotify",
url: "https://open.spotify.com/user/31pjwuuqwnn5zr7fnhfjjmi7c4bi?si=1be2bfdc844c4d85",
icon: "spotify", // Special component
icon: SpotifyIcon,
ariaLabel: "Spotify Profile"
}
];
@ -150,49 +218,49 @@ export const techLinks: TechLink[] = [
id: "react",
name: "React",
url: "https://react.dev/",
icon: "simple-icons:react",
icon: REACT_ICON,
ariaLabel: "React"
},
{
id: "typescript",
name: "TypeScript",
url: "https://www.typescriptlang.org/",
icon: "simple-icons:typescript",
icon: TYPESCRIPT_ICON,
ariaLabel: "TypeScript"
},
{
id: "astro",
name: "Astro",
url: "https://astro.build/",
icon: "simple-icons:astro",
icon: ASTRO_ICON,
ariaLabel: "Astro"
},
{
id: "go",
name: "Go",
url: "https://go.dev/",
icon: "simple-icons:go",
icon: GO_ICON,
ariaLabel: "Go"
},
{
id: "postgresql",
name: "PostgreSQL",
url: "https://www.postgresql.org/",
icon: "simple-icons:postgresql",
icon: POSTGRESQL_ICON,
ariaLabel: "PostgreSQL"
},
{
id: "redis",
name: "Redis",
url: "https://redis.io/",
icon: "simple-icons:redis",
icon: REDIS_ICON,
ariaLabel: "Redis"
},
{
id: "docker",
name: "Docker",
url: "https://www.docker.com/",
icon: "simple-icons:docker",
icon: DOCKER_ICON,
ariaLabel: "Docker"
}
];
@ -203,14 +271,16 @@ export const navigationItems: NavigationItem[] = [
name: "Home",
path: "/",
tooltip: "Home",
icon: "Home"
icon: Home,
enabled: true
},
{
id: "posts",
name: "Posts",
path: "/posts",
tooltip: "Posts",
icon: "NotebookPen",
icon: NotebookPen,
enabled: true,
isActive: (path: string) => path.startsWith("/posts") || path.startsWith("/post/")
},
{
@ -218,14 +288,16 @@ export const navigationItems: NavigationItem[] = [
name: "Resume",
path: "/resume",
tooltip: "Resume",
icon: "BriefcaseBusiness"
icon: BriefcaseBusiness,
enabled: true
},
{
id: "projects",
name: "Projects",
path: "/projects",
tooltip: "Projects",
icon: "CodeXml",
icon: CodeXml,
enabled: true,
isActive: (path: string) => path.startsWith("/projects")
},
{
@ -233,7 +305,8 @@ export const navigationItems: NavigationItem[] = [
name: "Talks",
path: "/talks",
tooltip: "Talks",
icon: "Megaphone",
icon: Megaphone,
enabled: true,
isActive: (path: string) => path.startsWith("/talks")
},
{
@ -241,6 +314,7 @@ export const navigationItems: NavigationItem[] = [
name: "Terminal",
path: "/terminal",
tooltip: "Terminal",
icon: "TerminalIcon"
icon: TerminalIcon,
enabled: true
}
];

View File

@ -2,8 +2,19 @@
import { ClientRouter } from "astro:transitions";
import NavigationBar from "../components/NavigationBar";
import ScrollUpButton from "../components/ScrollUpButton";
import { siteConfig } from "../config/data";
const currentPath = Astro.url.pathname;
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>
@ -13,7 +24,9 @@ import '../styles/global.css';
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<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 />
</head>
<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 } {
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')
}
};
const postsFiles: { [key: string]: FileSystemNode } = {};
postsData.forEach((post: any) => {
const fileName = `${post.slug}.md`;
@ -173,33 +166,15 @@ ${post.content}`;
}
function buildTalksFiles(): { [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')
}
};
const talksFiles: { [key: string]: FileSystemNode } = {};
talks.forEach(talk => {
const fileName = `${talk.id}.md`;
let content = `---
title: "${talk.name}"
description: "${talk.description}"
${talk.venue ? `venue: "${talk.venue}"` : ''}
${talk.date ? `date: "${talk.date}"` : ''}
link: "${talk.link}"
---
# ${talk.name}
const fileName = `${talk.id}.txt`;
let content = `${talk.name}
${talk.description}
${talk.venue ? `**Venue:** ${talk.venue}` : ''}
${talk.date ? `**Date:** ${talk.date}` : ''}
**Download:** [${talk.link}](${talk.link})`;
${talk.venue || ''}
${talk.date || ''}
${talk.link}`;
talksFiles[fileName] = {
type: 'file',
@ -212,32 +187,15 @@ ${talk.date ? `**Date:** ${talk.date}` : ''}
}
function buildProjectsFiles(): { [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')
}
};
const projectsFiles: { [key: string]: FileSystemNode } = {};
projects.forEach(project => {
const fileName = `${project.id}.md`;
let content = `---
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}
const fileName = `${project.id}.txt`;
let content = `${project.name}
${project.description}
${project.status ? `**Status:** ${project.status}` : ''}
${project.technologies ? `**Technologies:** ${project.technologies.join(', ')}` : ''}
${project.link ? `**Link:** [${project.link}](${project.link})` : ''}`;
${project.status || ''}
${project.technologies ? project.technologies.join(', ') : ''}
${project.link}`;
projectsFiles[fileName] = {
type: 'file',
@ -250,23 +208,12 @@ ${project.link ? `**Link:** [${project.link}](${project.link})` : ''}`;
}
function buildSocialFiles(): { [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')
}
};
const socialFiles: { [key: string]: FileSystemNode } = {};
socialLinks.forEach(link => {
const fileName = `${link.id}.txt`;
let content = `${link.name}
${link.ariaLabel}
URL: ${link.url}
Icon: ${link.icon}`;
${link.url}`;
socialFiles[fileName] = {
type: 'file',
@ -279,23 +226,12 @@ Icon: ${link.icon}`;
}
function buildTechFiles(): { [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')
}
};
const techFiles: { [key: string]: FileSystemNode } = {};
techLinks.forEach(link => {
const fileName = `${link.id}.txt`;
let content = `${link.name}
${link.ariaLabel}
URL: ${link.url}
Icon: ${link.icon}`;
${link.url}`;
techFiles[fileName] = {
type: 'file',

View File

@ -3,31 +3,32 @@ import { Image } from "astro:assets";
import SocialLinks from "../components/SocialLinks.astro";
import TechLinks from "../components/TechLinks.astro";
import Layout from "../layouts/Layout.astro";
import { personalInfo, homepageSections } from "../config/data";
---
<Layout>
<Image
src="/logo_real.webp"
alt="A drawing of Atridad Lahiji by Shelze!"
height={150}
width={150}
src={personalInfo.profileImage.src}
alt={personalInfo.profileImage.alt}
height={personalInfo.profileImage.height}
width={personalInfo.profileImage.width}
/>
<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"
>
Atridad Lahiji
{personalInfo.name}
</h1>
<h2 class="text-xl sm:text-3xl font-bold text-center">
Researcher, Full-Stack Developer, and IT Professional.
{personalInfo.tagline}
</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 />
<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 />
</Layout>

View File

@ -2,6 +2,7 @@
import { Icon } from "astro-icon/components";
import Layout from "../layouts/Layout.astro";
import ResumeSkills from "../components/ResumeSkills";
import { siteConfig } from "../config/data";
import "../styles/global.css";
interface ResumeData {
@ -66,8 +67,8 @@ try {
// Get the base URL for the current request
const baseUrl = Astro.url.origin;
// Fetch the JSON file from the public directory
const response = await fetch(`${baseUrl}/files/resume.json`);
// Fetch the JSON file from the public directory using config
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`);
if (!response.ok) {
throw new Error(
@ -96,11 +97,12 @@ try {
}
const data = resumeData;
const resumeConfig = siteConfig.resume;
---
{
(!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">
<h1 class="text-2xl font-bold text-red-600">
Error loading resume data.
@ -113,7 +115,7 @@ const data = resumeData;
{
data && !fetchError && (
<Layout>
<Layout title="Resume">
<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}
@ -164,19 +166,19 @@ const data = resumeData;
<div class="text-center mb-6 sm:mb-8">
<a
href="/files/Atridad_Lahiji_Resume.pdf"
download="Atridad_Lahiji_Resume.pdf"
href={resumeConfig.pdfFile.path}
download={resumeConfig.pdfFile.filename}
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>
</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-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">
{data.sections.summary.name || "Summary"}
{resumeConfig.sections.summary.title || data.sections.summary.name || "Summary"}
</h2>
<div set:html={data.sections.summary.content} />
</div>
@ -185,11 +187,12 @@ const data = resumeData;
{data.sections.profiles &&
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-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">
{data.sections.profiles.name || "Profiles"}
{resumeConfig.sections.profiles.title || data.sections.profiles.name || "Profiles"}
</h2>
<div class="flex flex-wrap gap-3 sm:gap-4">
{data.sections.profiles.items.map(
@ -197,16 +200,23 @@ const data = resumeData;
let iconName = "mdi:web";
const networkLower =
profile.network.toLowerCase();
if (networkLower === "github")
iconName =
"simple-icons:github";
else if (
if (networkLower === "github") {
iconName = "simple-icons:github";
} else if (
networkLower === "linkedin"
)
) {
iconName =
"simple-icons:linkedin";
else if (networkLower === "gitea")
iconName = "simple-icons:gitea";
} else if (
networkLower === "twitter"
) {
iconName = "simple-icons:x";
} else if (
networkLower === "youtube"
) {
iconName =
"simple-icons:youtube";
}
return (
<a
@ -215,9 +225,8 @@ const data = resumeData;
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})
<Icon name={iconName} />
{profile.network}
</a>
);
},
@ -229,93 +238,62 @@ const data = resumeData;
{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.skills.items.length > 0 &&
resumeConfig.sections.skills?.enabled && (
<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">
{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.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-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">
{data.sections.experience.name ||
"Experience"}
{resumeConfig.sections.experience.title || data.sections.experience.name || "Experience"}
</h2>
<div class="space-y-3 sm:space-y-4">
<div class="space-y-4 sm:space-y-6">
{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>
(experience) => (
<div class="border-l-2 border-primary pl-4 sm:pl-6">
<h3 class="text-lg sm:text-xl font-semibold">
{experience.position}
</h3>
<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">
<span class="font-medium">
{experience.company}
</span>
<span>{experience.date}</span>
<span>
{experience.location}
</span>
</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>
@ -325,28 +303,33 @@ const data = resumeData;
{data.sections.education &&
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-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">
{data.sections.education.name ||
"Education"}
{resumeConfig.sections.education.title || data.sections.education.name || "Education"}
</h2>
<div class="space-y-3 sm:space-y-4">
<div class="space-y-4">
{data.sections.education.items.map(
(edu, index) => (
<div>
<h3 class="text-base sm:text-lg font-semibold">
{edu.institution}
(education) => (
<div class="border-l-2 border-secondary pl-4 sm:pl-6">
<h3 class="text-lg sm:text-xl font-semibold">
{education.institution}
</h3>
<p class="text-sm sm:text-base">
{edu.studyType} - {edu.area}{" "}
({edu.date})
</p>
{edu.summary && (
<div class="text-sm sm:text-base text-base-content/70 mb-2">
<span class="font-medium">
{education.studyType} in{" "}
{education.area}
</span>
<span class="block sm:inline sm:ml-4">
{education.date}
</span>
</div>
{education.summary && (
<div
class="ml-2 sm:ml-4 text-xs sm:text-sm mt-1"
set:html={edu.summary}
class="prose prose-sm sm:prose-base max-w-none"
set:html={education.summary}
/>
)}
</div>
@ -359,23 +342,28 @@ const data = resumeData;
{data.sections.volunteer &&
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-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">
{data.sections.volunteer.name ||
"Volunteering"}
{resumeConfig.sections.volunteer.title || data.sections.volunteer.name || "Volunteer Work"}
</h2>
<div class="space-y-3 sm:space-y-4">
<div class="space-y-4">
{data.sections.volunteer.items.map(
(vol, index) => (
<div>
<h3 class="text-base sm:text-lg font-semibold">
{vol.organization}
(volunteer) => (
<div class="border-l-2 border-accent pl-4 sm:pl-6">
<h3 class="text-lg sm:text-xl font-semibold">
{volunteer.organization}
</h3>
<p class="text-sm sm:text-base">
{vol.position} ({vol.date})
</p>
<div class="text-sm sm:text-base text-base-content/70 mb-2">
<span class="font-medium">
{volunteer.position}
</span>
<span class="block sm:inline sm:ml-4">
{volunteer.date}
</span>
</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;
};
}