This commit is contained in:
41
src/components/IconRenderer.tsx
Normal file
41
src/components/IconRenderer.tsx
Normal 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;
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
@ -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">
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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
121
src/types/index.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user