3.0.0 - Dependency updates, improved typesafe config, improve typing
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m44s

This commit is contained in:
2025-09-22 15:07:03 -06:00
parent 6c9fabe770
commit 75931d4a43
20 changed files with 1353 additions and 1533 deletions

View File

@@ -1,5 +1,10 @@
import { Icon } from 'astro-icon/components';
import type { IconType, LucideIcon, AstroIconName, CustomIconComponent } from '../types';
import { Icon } from "astro-icon/components";
import type {
IconType,
LucideIcon,
AstroIconName,
CustomIconComponent,
} from "../types";
interface IconRendererProps {
icon: IconType;
@@ -10,32 +15,37 @@ interface IconRendererProps {
// Type guard functions
function isLucideIcon(icon: IconType): icon is LucideIcon {
return typeof icon === 'function' && icon.length <= 1; // Lucide icons are function components
return typeof icon === "function" && icon.length <= 1; // Lucide icons are function components
}
function isAstroIconName(icon: IconType): icon is AstroIconName {
return typeof icon === 'string';
return typeof icon === "string";
}
function isCustomComponent(icon: IconType): icon is CustomIconComponent {
return typeof icon === 'function' && !isLucideIcon(icon);
return typeof icon === "function" && !isLucideIcon(icon);
}
export default function IconRenderer({ icon, size, class: className, ...props }: IconRendererProps) {
export default function IconRenderer({
icon,
size,
class: className,
...props
}: IconRendererProps) {
if (isLucideIcon(icon)) {
const LucideComponent = icon;
return <LucideComponent size={size} class={className} {...props} />;
}
if (isAstroIconName(icon)) {
return <Icon name={icon} class={className} {...props} />;
}
if (isCustomComponent(icon)) {
const CustomComponent = icon;
return <CustomComponent class={className} {...props} />;
}
// Fallback
return null;
}
}

View File

@@ -1,6 +1,6 @@
import { useComputed, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { navigationItems } from "../config/data";
import { config } from "../config";
import type { LucideIcon } from "../types";
interface NavigationBarProps {
@@ -20,7 +20,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
});
// Filter out disabled navigation items
const enabledNavigationItems = navigationItems.filter(
const enabledNavigationItems = config.navigationItems.filter(
(item) => item.enabled !== false,
);

View File

@@ -9,7 +9,9 @@ interface Props {
const { project } = Astro.props;
---
<div class="card bg-accent text-accent-content w-full max-w-sm shrink shadow-md">
<div
class="card bg-accent text-accent-content w-full max-w-sm shrink shadow-md"
>
<div class="card-body break-words">
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words"

View File

@@ -1,26 +1,24 @@
---
import { Icon } from "astro-icon/components";
import { socialLinks } from "../config/data";
import { config } from "../config";
---
<div class="flex flex-row gap-3 text-3xl flex-wrap justify-center">
{
socialLinks.map((link) => {
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>
);
})
config.socialLinks.map((link) => (
<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>
))
}
</div>

View File

@@ -1,6 +1,6 @@
---
import { Icon } from "astro-icon/components";
import { techLinks } from "../config/data";
import { config } from "../config";
// Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string {
@@ -8,9 +8,9 @@ function isAstroIcon(icon: any): icon is string {
}
---
<div class="flex flex-row gap-4 text-3xl flex-wrap justify-center">
<div class="flex flex-row gap-3 text-3xl flex-wrap justify-center">
{
techLinks.map((link) => {
config.techLinks.map((link) => {
if (isAstroIcon(link.icon)) {
return (
<a

392
src/config.ts Normal file
View File

@@ -0,0 +1,392 @@
import type { Config } from "./types";
// Import Lucide Icons
import { Home, Newspaper, FileUser, CodeXml, Megaphone } from "lucide-preact";
import logo from "./assets/logo_real.webp";
import resumeToml from "./assets/resume.toml?raw";
// Astro Icon references
const EMAIL_ICON = "mdi:email" as const;
const RSS_ICON = "mdi:rss" as const;
const GITEA_ICON = "simple-icons:gitea" as const;
const BLUESKY_ICON = "simple-icons:bluesky" as const;
const REACT_ICON = "simple-icons:react" as const;
const TYPESCRIPT_ICON = "simple-icons:typescript" as const;
const ASTRO_ICON = "simple-icons:astro" as const;
const GO_ICON = "simple-icons:go" as const;
const POSTGRESQL_ICON = "simple-icons:postgresql" as const;
const REDIS_ICON = "simple-icons:redis" as const;
const DOCKER_ICON = "simple-icons:docker" as const;
const KOTLIN_ICON = "simple-icons:kotlin" as const;
const SWIFT_ICON = "simple-icons:swift" as const;
const FLUTTER_ICON = "simple-icons:flutter" as const;
export const config: Config = {
personalInfo: {
name: "Atridad Lahiji",
profileImage: {
src: logo,
alt: "A drawing of Atridad Lahiji by Shelze!",
},
tagline: "Researcher, Full-Stack Developer, and IT Professional",
description: "Researcher, Full-Stack Developer, and IT Professional",
},
homepageSections: {
socialLinks: {
title: "Places I Exist:",
description: "Find me across the web",
},
techStack: {
title: "Technologies I Use:",
description: "Technologies and tools I work with",
},
},
resumeConfig: {
tomlFile: resumeToml,
layout: {
leftColumn: ["experience", "volunteer"],
rightColumn: ["skills", "education", "awards"],
},
sections: {
enabled: [
"summary",
"experience",
"education",
"skills",
"volunteer",
"awards",
],
summary: {
title: "Summary",
},
experience: {
title: "Professional Experience",
},
education: {
title: "Education",
},
skills: {
title: "Skills",
},
volunteer: {
title: "Volunteer Work",
},
awards: {
title: "Awards",
},
},
},
siteConfig: {
personal: {
name: "Atridad Lahiji",
profileImage: {
src: logo,
alt: "A drawing of Atridad Lahiji by Shelze!",
},
tagline: "Researcher, Full-Stack Developer, and IT Professional",
description: "Researcher, Full-Stack Developer, and IT Professional",
},
homepage: {
socialLinks: {
title: "Places I Exist:",
description: "Find me across the web",
},
techStack: {
title: "Technologies I Use:",
description: "Technologies and tools I work with",
},
},
resume: {
tomlFile: resumeToml,
layout: {
leftColumn: ["experience", "volunteer"],
rightColumn: ["skills", "education", "awards"],
},
sections: {
enabled: [
"summary",
"experience",
"education",
"skills",
"volunteer",
"awards",
],
summary: {
title: "Summary",
},
experience: {
title: "Professional Experience",
},
education: {
title: "Education",
},
skills: {
title: "Skills",
},
volunteer: {
title: "Volunteer Work",
},
awards: {
title: "Awards",
},
},
},
meta: {
title: "Atridad Lahiji",
description:
"Personal website of Atridad Lahiji - Researcher, Full-Stack Developer, and IT Professional",
url: "https://atri.dad",
author: "Atridad Lahiji",
},
},
talks: [
{
id: "devedmonton-hateoas",
name: "Hypermedia as the engine of application state - An Introduction",
description:
"A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.",
link: "/files/DevEdmonton_Talk_HATEOAS.pdf",
},
],
projects: [
{
id: "openclimb",
name: "OpenClimb",
description: "FOSS Rock Climbing Tracker for iOS and Android",
link: "https://git.atri.dad/atridad/OpenClimb",
tags: [
"kotlin",
"jetpack compose",
"swift",
"swiftui",
"mobile",
"monorepo",
],
},
{
id: "muse",
name: "muse",
description: "Go-based music generation using TOML song definitions",
link: "https://git.atri.dad/atridad/muse",
tags: ["golang", "cli"],
},
{
id: "magiccounter",
name: "MagicCounter",
description: "Jeckpack Compose based Magic the Gathering Health Tracker",
link: "https://git.atri.dad/atridad/MagicCounter",
tags: ["kotlin", "mobile"],
},
{
id: "mealient",
name: "Mealient (Fork of project by Kirill Kamakin)",
description: "An Android client for a self-hosted recipe manager Mealie.",
link: "https://git.atri.dad/atridad/Mealient",
tags: ["kotlin", "mobile", "fork"],
},
{
id: "goth-stack",
name: "GOTH Stack",
description:
"🚀 A Web Application Template Powered by HTMX + Go + Tailwind 🚀",
link: "https://git.atri.dad/atridad/goth.stack",
tags: ["golang", "web"],
},
{
id: "himbot",
name: "Himbot",
description:
"A discord bot written in Go. Loosly named after my username online (HimbothySwaggins).",
link: "https://git.atri.dad/atridad/himbot",
tags: ["golang", "webserver"],
},
{
id: "loadr",
name: "loadr",
description:
"A lightweight REST load testing tool with robust support for different verbs, token auth, and performance reports.",
link: "https://git.atri.dad/atridad/loadr",
tags: ["golang", "cli"],
},
],
sections: {
resume: {
name: "Resume",
path: "/resume",
description: "Professional experience, skills, and background",
},
posts: {
name: "Blog Posts",
path: "/posts",
description: "Technical articles and thoughts",
},
talks: {
name: "Talks",
path: "/talks",
description: "Conference talks and presentations",
},
projects: {
name: "Projects",
path: "/projects",
description: "Personal and professional projects",
},
},
socialLinks: [
{
id: "email",
name: "Email",
url: "mailto:me@atri.dad",
icon: EMAIL_ICON,
ariaLabel: "Email me",
},
{
id: "rss",
name: "RSS Feed",
url: "/feed",
icon: RSS_ICON,
ariaLabel: "RSS Feed",
},
{
id: "gitea",
name: "Forgejo (Git)",
url: "https://git.atri.dad/atridad",
icon: GITEA_ICON,
ariaLabel: "Forgejo (Git)",
},
{
id: "bluesky",
name: "Bluesky",
url: "https://bsky.app/profile/atri.dad",
icon: BLUESKY_ICON,
ariaLabel: "Bluesky Profile",
},
],
techLinks: [
{
id: "react",
name: "React",
url: "https://react.dev/",
icon: REACT_ICON,
ariaLabel: "React",
},
{
id: "typescript",
name: "TypeScript",
url: "https://www.typescriptlang.org/",
icon: TYPESCRIPT_ICON,
ariaLabel: "TypeScript",
},
{
id: "astro",
name: "Astro",
url: "https://astro.build/",
icon: ASTRO_ICON,
ariaLabel: "Astro",
},
{
id: "go",
name: "Go",
url: "https://go.dev/",
icon: GO_ICON,
ariaLabel: "Go",
},
{
id: "postgresql",
name: "PostgreSQL",
url: "https://www.postgresql.org/",
icon: POSTGRESQL_ICON,
ariaLabel: "PostgreSQL",
},
{
id: "redis",
name: "Redis",
url: "https://redis.io/",
icon: REDIS_ICON,
ariaLabel: "Redis",
},
{
id: "docker",
name: "Docker",
url: "https://www.docker.com/",
icon: DOCKER_ICON,
ariaLabel: "Docker",
},
{
id: "kotlin",
name: "Kotlin",
url: "https://kotlinlang.org/",
icon: KOTLIN_ICON,
ariaLabel: "Kotlin",
},
{
id: "swift",
name: "Swift",
url: "https://www.swift.org/",
icon: SWIFT_ICON,
ariaLabel: "Swift",
},
{
id: "flutter",
name: "Flutter",
url: "https://flutter.dev",
icon: FLUTTER_ICON,
ariaLabel: "Flutter",
},
],
navigationItems: [
{
id: "home",
name: "Home",
path: "/",
tooltip: "Home",
icon: Home,
enabled: true,
},
{
id: "posts",
name: "Posts",
path: "/posts",
tooltip: "Posts",
icon: Newspaper,
enabled: true,
isActive: (path: string) =>
path.startsWith("/posts") || path.startsWith("/post/"),
},
{
id: "resume",
name: "Resume",
path: "/resume",
tooltip: "Resume",
icon: FileUser,
enabled: !!(resumeToml && resumeToml.trim()),
},
{
id: "projects",
name: "Projects",
path: "/projects",
tooltip: "Projects",
icon: CodeXml,
enabled: true,
isActive: (path: string) => path.startsWith("/projects"),
},
{
id: "talks",
name: "Talks",
path: "/talks",
tooltip: "Talks",
icon: Megaphone,
enabled: true,
isActive: (path: string) => path.startsWith("/talks"),
},
],
} as const;

View File

@@ -1,361 +0,0 @@
import type {
Talk,
Project,
SocialLink,
TechLink,
NavigationItem,
PersonalInfo,
HomepageSections,
SiteConfig,
ResumeConfig,
} from "../types";
// Import Lucide Icons
import {
Home,
Newspaper,
FileUser,
CodeXml,
Megaphone,
} from "lucide-preact";
import logo from "../assets/logo_real.webp";
import resumeToml from "../assets/resume.toml?raw";
// 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";
const KOTLIN_ICON = "simple-icons:kotlin";
const SWIFT_ICON = "simple-icons:swift";
const FLUTTER_ICON = "simple-icons:flutter";
// Personal Information Configuration
export const personalInfo: PersonalInfo = {
name: "Atridad Lahiji",
profileImage: {
src: logo,
alt: "A drawing of Atridad Lahiji by Shelze!",
},
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: "Technologies I Use:",
description: "Technologies and tools I work with",
},
};
// Resume Configuration
export const resumeConfig: ResumeConfig = {
tomlFile: resumeToml,
layout: {
leftColumn: ["experience", "volunteer"],
rightColumn: ["skills", "education", "awards"],
},
sections: {
enabled: [
"summary",
"experience",
"education",
"skills",
"volunteer",
"awards",
],
summary: {
title: "Summary",
enabled: true,
},
experience: {
title: "Professional Experience",
enabled: true,
},
education: {
title: "Education",
enabled: true,
},
skills: {
title: "Skills",
enabled: true,
},
volunteer: {
title: "Volunteer Work",
enabled: true,
},
awards: {
title: "Awards",
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[] = [
{
id: "devedmonton-hateoas",
name: "Hypermedia as the engine of application state - An Introduction",
description:
"A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.",
link: "/files/DevEdmonton_Talk_HATEOAS.pdf",
},
];
export const projects: Project[] = [
{
id: "muse",
name: "muse",
description: "Go-based music generation using TOML song definitions",
link: "https://git.atri.dad/atridad/muse",
tags: ["golang","cli"]
},
{
id: "openclimb",
name: "OpenClimb",
description: "Jeckpack Compose based Rock Climbing Tracker",
link: "https://git.atri.dad/atridad/OpenClimb",
tags: ["kotlin","mobile"],
},
{
id: "mealient",
name: "Mealient (Fork of project by Kirill Kamakin)",
description:
"An Android client for a self-hosted recipe manager Mealie.",
link: "https://git.atri.dad/atridad/Mealient",
tags: ["kotlin","mobile"],
},
{
id: "magiccounter",
name: "MagicCounter",
description:
"Jeckpack Compose based Magic the Gathering Health Tracker",
link: "https://git.atri.dad/atridad/MagicCounter",
tags: ["kotlin","mobile"],
},
{
id: "goth-stack",
name: "GOTH Stack",
description:
"🚀 A Web Application Template Powered by HTMX + Go + Tailwind 🚀",
link: "https://git.atri.dad/atridad/goth.stack",
tags: ["golang","web"],
},
{
id: "himbot",
name: "Himbot",
description:
"A discord bot written in Go. Loosly named after my username online (HimbothySwaggins).",
link: "https://git.atri.dad/atridad/himbot",
tags: ["golang","webserver"],
},
{
id: "loadr",
name: "loadr",
description:
"A lightweight REST load testing tool with robust support for different verbs, token auth, and performance reports.",
link: "https://git.atri.dad/atridad/loadr",
tags: ["golang","cli"],
},
];
export const sections = {
resume: {
name: "Resume",
path: "/resume",
description: "Professional experience, skills, and background",
},
posts: {
name: "Blog Posts",
path: "/posts",
description: "Technical articles and thoughts",
},
talks: {
name: "Talks",
path: "/talks",
description: "Conference talks and presentations",
},
projects: {
name: "Projects",
path: "/projects",
description: "Personal and professional projects",
},
} as const;
export const socialLinks: SocialLink[] = [
{
id: "email",
name: "Email",
url: "mailto:me@atri.dad",
icon: EMAIL_ICON,
ariaLabel: "Email me",
},
{
id: "rss",
name: "RSS Feed",
url: "/feed",
icon: RSS_ICON,
ariaLabel: "RSS Feed",
},
{
id: "gitea",
name: "Forgejo (Git)",
url: "https://git.atri.dad/atridad",
icon: GITEA_ICON,
ariaLabel: "Forgejo (Git)",
},
{
id: "bluesky",
name: "Bluesky",
url: "https://bsky.app/profile/atri.dad",
icon: BLUESKY_ICON,
ariaLabel: "Bluesky Profile",
},
];
export const techLinks: TechLink[] = [
{
id: "react",
name: "React",
url: "https://react.dev/",
icon: REACT_ICON,
ariaLabel: "React",
},
{
id: "typescript",
name: "TypeScript",
url: "https://www.typescriptlang.org/",
icon: TYPESCRIPT_ICON,
ariaLabel: "TypeScript",
},
{
id: "astro",
name: "Astro",
url: "https://astro.build/",
icon: ASTRO_ICON,
ariaLabel: "Astro",
},
{
id: "go",
name: "Go",
url: "https://go.dev/",
icon: GO_ICON,
ariaLabel: "Go",
},
{
id: "postgresql",
name: "PostgreSQL",
url: "https://www.postgresql.org/",
icon: POSTGRESQL_ICON,
ariaLabel: "PostgreSQL",
},
{
id: "redis",
name: "Redis",
url: "https://redis.io/",
icon: REDIS_ICON,
ariaLabel: "Redis",
},
{
id: "docker",
name: "Docker",
url: "https://www.docker.com/",
icon: DOCKER_ICON,
ariaLabel: "Docker",
},
{
id: "kotlin",
name: "Kotlin",
url: "https://kotlinlang.org/",
icon: KOTLIN_ICON,
ariaLabel: "Kotlin",
},
{
id: "swift",
name: "Swift",
url: "https://www.swift.org/",
icon: SWIFT_ICON,
ariaLabel: "Swift",
},
{
id: "flutter",
name: "Flutter",
url: "https://flutter.dev",
icon: FLUTTER_ICON,
ariaLabel: "Flutter",
},
];
export const navigationItems: NavigationItem[] = [
{
id: "home",
name: "Home",
path: "/",
tooltip: "Home",
icon: Home,
enabled: true,
},
{
id: "posts",
name: "Posts",
path: "/posts",
tooltip: "Posts",
icon: Newspaper,
enabled: true,
isActive: (path: string) =>
path.startsWith("/posts") || path.startsWith("/post/"),
},
{
id: "resume",
name: "Resume",
path: "/resume",
tooltip: "Resume",
icon: FileUser,
enabled: !!(resumeConfig.tomlFile && resumeConfig.tomlFile.trim()),
},
{
id: "projects",
name: "Projects",
path: "/projects",
tooltip: "Projects",
icon: CodeXml,
enabled: true,
isActive: (path: string) => path.startsWith("/projects"),
},
{
id: "talks",
name: "Talks",
path: "/talks",
tooltip: "Talks",
icon: Megaphone,
enabled: true,
isActive: (path: string) => path.startsWith("/talks"),
},
];

View File

@@ -2,42 +2,42 @@
import { ClientRouter } from "astro:transitions";
import NavigationBar from "../components/NavigationBar";
import ScrollUpButton from "../components/ScrollUpButton";
import { siteConfig } from "../config/data";
import { config } from "../config";
const currentPath = Astro.url.pathname;
import "../styles/global.css";
export interface Props {
title?: string;
description?: string;
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;
? `${title} | ${config.siteConfig.meta.title}`
: config.siteConfig.meta.title;
const pageDescription = description || config.siteConfig.meta.description;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} />
<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 overflow-x-hidden">
<main
class="flex-grow flex flex-col gap-4 items-center justify-center pb-[68px] sm:pb-[76px]"
>
<slot />
</main>
<NavigationBar client:load currentPath={currentPath} />
<ScrollUpButton client:load />
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} />
<meta name="description" content={pageDescription} />
<meta name="author" content={config.siteConfig.meta.author} />
<title>{pageTitle}</title>
<ClientRouter />
</head>
<body class="flex flex-col min-h-screen overflow-x-hidden">
<main
class="flex-grow flex flex-col gap-4 items-center justify-center pb-[68px] sm:pb-[76px]"
>
<slot />
</main>
<NavigationBar client:load currentPath={currentPath} />
<ScrollUpButton client:load />
</body>
</html>

View File

@@ -1,23 +1,23 @@
import type { APIRoute } from "astro";
import * as TOML from "@iarna/toml";
import { siteConfig } from "../../config/data";
import { config } from "../../config";
export const GET: APIRoute = async ({ request }) => {
try {
// Check if resume TOML content is configured
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
if (!config.resumeConfig.tomlFile || !config.resumeConfig.tomlFile.trim()) {
return new Response("Resume not configured", { status: 404 });
}
let tomlContent: string;
// Check if tomlFile is a path (starts with /) or raw content
if (siteConfig.resume.tomlFile.startsWith("/")) {
if (config.resumeConfig.tomlFile.startsWith("/")) {
// It's a file path - fetch it
const url = new URL(request.url);
const baseUrl = `${url.protocol}//${url.host}`;
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
const response = await fetch(`${baseUrl}${config.resumeConfig.tomlFile}`);
if (!response.ok) {
throw new Error(
@@ -28,7 +28,7 @@ export const GET: APIRoute = async ({ request }) => {
tomlContent = await response.text();
} else {
// It's raw TOML content
tomlContent = siteConfig.resume.tomlFile;
tomlContent = config.resumeConfig.tomlFile;
}
const resumeData = TOML.parse(tomlContent);

View File

@@ -1,6 +1,6 @@
import type { APIRoute } from "astro";
import { chromium } from "playwright";
import { siteConfig } from "../../../config/data";
import { config } from "../../../config";
import * as TOML from "@iarna/toml";
// Helper function to fetch and return SVG icon from Simple Icons CDN
@@ -300,7 +300,7 @@ const fetchProfileIcons = async (profiles: any[]) => {
};
const generateResumeHTML = async (data: ResumeData): Promise<string> => {
const resumeConfig = siteConfig.resume;
const resumeConfig = config.resumeConfig;
// Use layout from TOML data, fallback to site config, then to default
const layout = data.layout
? {
@@ -405,19 +405,19 @@ async function generatePDFFromToml(tomlContent: string): Promise<Uint8Array> {
export const GET: APIRoute = async ({ request }) => {
try {
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
if (!config.resumeConfig.tomlFile || !config.resumeConfig.tomlFile.trim()) {
return new Response("Resume not configured", { status: 404 });
}
let tomlContent: string;
// Check if tomlFile is a path (starts with /) or raw content
if (siteConfig.resume.tomlFile.startsWith("/")) {
if (config.resumeConfig.tomlFile.startsWith("/")) {
// It's a file path - fetch it
const url = new URL(request.url);
const baseUrl = `${url.protocol}//${url.host}`;
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
const response = await fetch(`${baseUrl}${config.resumeConfig.tomlFile}`);
if (!response.ok) {
throw new Error(
@@ -428,7 +428,7 @@ export const GET: APIRoute = async ({ request }) => {
tomlContent = await response.text();
} else {
// It's raw TOML content
tomlContent = siteConfig.resume.tomlFile;
tomlContent = config.resumeConfig.tomlFile;
}
const pdfBuffer = await generatePDFFromToml(tomlContent);

View File

@@ -3,13 +3,13 @@ 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";
import { config } from "../config";
---
<Layout>
<Image
src={personalInfo.profileImage.src}
alt={personalInfo.profileImage.alt}
src={config.personalInfo.profileImage.src}
alt={config.personalInfo.profileImage.alt}
width={300}
height={300}
layout="constrained"
@@ -18,24 +18,22 @@ import { personalInfo, homepageSections } from "../config/data";
style="max-width: 12rem; width: 100%;"
/>
<h1
class="text-primary text-4xl sm:text-6xl font-bold text-center"
>
{personalInfo.name}
<h1 class="text-primary text-4xl sm:text-6xl font-bold text-center">
{config.personalInfo.name}
</h1>
<h2 class="text-xl sm:text-3xl font-bold text-center mx-6">
{personalInfo.tagline}
{config.personalInfo.tagline}
</h2>
<h3 class="text-lg sm:text-2xl font-bold">
{homepageSections.socialLinks.title}
{config.homepageSections.socialLinks.title}
</h3>
<SocialLinks />
<h3 class="text-lg sm:text-2xl font-bold">
{homepageSections.techStack.title}
{config.homepageSections.techStack.title}
</h3>
<TechLinks />

View File

@@ -18,7 +18,7 @@ const { Content } = await post.render();
---
<Layout>
<div class="min-h-screen p-4 md:p-8">
<div class="w-full p-4 md:p-8">
<div class="max-w-3xl mx-auto">
<div class="p-4 md:p-8">
<h1 class="text-4xl md:text-5xl font-bold text-primary mb-6">
@@ -45,7 +45,10 @@ const { Content } = await post.render();
</div>
{/* Back button */}
<a href="/posts" class="btn btn-outline btn-primary btn-sm font-bold">
<a
href="/posts"
class="btn btn-outline btn-primary btn-sm font-bold"
>
<Icon name="mdi:arrow-left" class="text-lg" />
Back
</a>

View File

@@ -8,30 +8,30 @@ const posts = await getCollection("posts");
// Sort posts by date, newest first
const sortedPosts = posts.sort(
(a: CollectionEntry<"posts">, b: CollectionEntry<"posts">) =>
new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(),
(a: CollectionEntry<"posts">, b: CollectionEntry<"posts">) =>
new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf(),
);
---
<Layout>
<div class="min-h-screen min-w-screen p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Posts
</h1>
<div
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{sortedPosts.map((post) => <PostCard post={post} />)}
</div>
<div class="w-full p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Posts
</h1>
<div
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{sortedPosts.map((post) => <PostCard post={post} />)}
</div>
{
sortedPosts.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No posts available yet. Check back soon!
</p>
)
}
</div>
{
sortedPosts.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No posts available yet. Check back soon!
</p>
)
}
</div>
</Layout>

View File

@@ -1,29 +1,32 @@
---
import Layout from "../layouts/Layout.astro";
import ProjectCard from "../components/ProjectCard.astro";
import { projects } from "../config/data";
import { config } from "../config";
---
<Layout>
<div class="min-h-screen min-w-screen p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Projects
</h1>
<div
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{projects.map((project) => <ProjectCard project={project} />)}
<div class="w-full p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
Projects
</h1>
<div
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{
config.projects.map((project) => (
<ProjectCard project={project} />
))
}
</div>
{
config.projects.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No projects available yet. Check back soon!
</p>
)
}
</div>
{
projects.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No projects available yet. Check back soon!
</p>
)
}
</div>
</Layout>

View File

@@ -4,7 +4,7 @@ import Layout from "../layouts/Layout.astro";
import ResumeSkills from "../components/ResumeSkills";
import ResumeDownloadButton from "../components/ResumeDownloadButton";
import ResumeSettingsModal from "../components/ResumeSettingsModal";
import { siteConfig } from "../config/data";
import { config } from "../config";
import "../styles/global.css";
import * as TOML from "@iarna/toml";
@@ -57,16 +57,18 @@ interface ResumeData {
let resumeData: ResumeData | undefined = undefined;
let fetchError: string | null = null;
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
if (!config.resumeConfig.tomlFile || !config.resumeConfig.tomlFile.trim()) {
return Astro.redirect("/");
}
try {
let tomlContent: string;
if (siteConfig.resume.tomlFile.startsWith("/")) {
if (config.resumeConfig.tomlFile.startsWith("/")) {
const baseUrl = Astro.url.origin;
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
const response = await fetch(
`${baseUrl}${config.resumeConfig.tomlFile}`,
);
if (!response.ok) {
throw new Error(
@@ -76,7 +78,7 @@ try {
tomlContent = await response.text();
} else {
tomlContent = siteConfig.resume.tomlFile;
tomlContent = config.resumeConfig.tomlFile;
}
resumeData = TOML.parse(tomlContent) as unknown as ResumeData;
@@ -86,7 +88,7 @@ try {
}
const data = resumeData;
const resumeConfig = siteConfig.resume;
const resumeConfig = config.resumeConfig;
if (!data) {
return Astro.redirect("/");
@@ -136,26 +138,29 @@ if (!data) {
<ResumeDownloadButton client:load />
{
data.summary && resumeConfig.sections.summary?.enabled && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.summary.title || "Summary"}
</h2>
<div>{data.summary.content}</div>
data.summary &&
resumeConfig.sections.enabled.includes("summary") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.summary?.title ||
"Summary"}
</h2>
<div>{data.summary.content}</div>
</div>
</div>
</div>
)
)
}
{
data.skills &&
data.skills.length > 0 &&
resumeConfig.sections.skills?.enabled && (
resumeConfig.sections.enabled.includes("skills") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.skills.title || "Skills"}
{resumeConfig.sections.skills?.title ||
"Skills"}
</h2>
<ResumeSkills
skills={data.skills.map((skill, index) => ({
@@ -173,11 +178,11 @@ if (!data) {
{
data.experience &&
data.experience.length > 0 &&
resumeConfig.sections.experience?.enabled && (
resumeConfig.sections.enabled.includes("experience") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.experience.title ||
{resumeConfig.sections.experience?.title ||
"Experience"}
</h2>
<div class="space-y-4 sm:space-y-6">
@@ -222,11 +227,11 @@ if (!data) {
{
data.education &&
data.education.length > 0 &&
resumeConfig.sections.education?.enabled && (
resumeConfig.sections.enabled.includes("education") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.education.title ||
{resumeConfig.sections.education?.title ||
"Education"}
</h2>
<div class="space-y-4">
@@ -264,11 +269,11 @@ if (!data) {
{
data.volunteer &&
data.volunteer.length > 0 &&
resumeConfig.sections.volunteer?.enabled && (
resumeConfig.sections.enabled.includes("volunteer") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.volunteer.title ||
{resumeConfig.sections.volunteer?.title ||
"Volunteer Work"}
</h2>
<div class="space-y-4">
@@ -296,11 +301,11 @@ if (!data) {
{
data.awards &&
data.awards.length > 0 &&
resumeConfig.sections.awards?.enabled && (
resumeConfig.sections.enabled.includes("awards") && (
<div class="card bg-base-300 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.awards.title ||
{resumeConfig.sections.awards?.title ||
"Awards & Recognition"}
</h2>
<div class="space-y-4">

View File

@@ -1,11 +1,11 @@
---
import Layout from "../layouts/Layout.astro";
import TalkCard from "../components/TalkCard.astro";
import { talks } from "../config/data";
import { config } from "../config";
---
<Layout>
<div class="min-h-screen min-w-screen p-4 sm:p-8">
<div class="w-full p-4 sm:p-8">
<h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
>
@@ -14,11 +14,11 @@ import { talks } from "../config/data";
<div
class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto"
>
{talks.map((talk) => <TalkCard talk={talk} />)}
{config.talks.map((talk) => <TalkCard talk={talk} />)}
</div>
{
talks.length === 0 && (
config.talks.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No talks available yet. Check back soon!
</p>

View File

@@ -50,41 +50,43 @@ export interface NavigationItem {
isActive?: (path: string) => boolean;
}
export type ResumeSectionKey =
| "summary"
| "experience"
| "education"
| "skills"
| "volunteer"
| "profiles"
| "awards";
export interface ResumeConfig {
tomlFile: string; // Can be a file path or raw TOML content
layout?: {
leftColumn?: string[];
rightColumn?: string[];
leftColumn?: ResumeSectionKey[];
rightColumn?: ResumeSectionKey[];
};
sections: {
enabled: string[];
enabled: ResumeSectionKey[];
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;
};
awards?: {
title?: string;
enabled?: boolean;
};
};
}
@@ -123,3 +125,37 @@ export interface SiteConfig {
author: string;
};
}
export interface Config {
personalInfo: PersonalInfo;
homepageSections: HomepageSections;
resumeConfig: ResumeConfig;
siteConfig: SiteConfig;
talks: readonly Talk[];
projects: readonly Project[];
sections: {
readonly resume: {
readonly name: string;
readonly path: string;
readonly description: string;
};
readonly posts: {
readonly name: string;
readonly path: string;
readonly description: string;
};
readonly talks: {
readonly name: string;
readonly path: string;
readonly description: string;
};
readonly projects: {
readonly name: string;
readonly path: string;
readonly description: string;
};
};
socialLinks: readonly SocialLink[];
techLinks: readonly TechLink[];
navigationItems: readonly NavigationItem[];
}