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 { 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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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
|
||||
}
|
||||
];
|
@ -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">
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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
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