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

@@ -3,8 +3,8 @@
description = "A portable development environment for atridotdad with Nix Flakes"; description = "A portable development environment for atridotdad with Nix Flakes";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Pin to a specific branch or commit for stability nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; # Helps with system boilerplate flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils }:
@@ -22,7 +22,7 @@
curl curl
]; ];
# Common libraries needed for Playwright across platforms (e.g., for WebKit/Firefox) # Common libraries needed for Playwright
playwrightCommonLibs = with pkgs; [ playwrightCommonLibs = with pkgs; [
glib glib
nss nss
@@ -44,7 +44,7 @@
gnutls gnutls
]; ];
# Linux-specific libraries for Playwright (mostly Chromium dependencies) # Linux-specific libraries for Playwright
playwrightLinuxSpecificLibs = with pkgs; [ playwrightLinuxSpecificLibs = with pkgs; [
glibc glibc
libgcc libgcc
@@ -75,7 +75,7 @@
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = commonDevTools ++ ( packages = commonDevTools ++ (
if isDarwin if isDarwin
then playwrightCommonLibs # For macOS, Playwright will download Chromium. Provide base libs. then playwrightCommonLibs # For macOS, Playwright will download Chromium.
else [ pkgs.chromium ] ++ playwrightSelfDownloadLibs # For Linux, provide Chromium and its dependencies else [ pkgs.chromium ] ++ playwrightSelfDownloadLibs # For Linux, provide Chromium and its dependencies
); );
@@ -112,7 +112,7 @@
if [ ! -d "node_modules" ]; then if [ ! -d "node_modules" ]; then
echo "📦 Installing pnpm dependencies..." echo "📦 Installing pnpm dependencies..."
pnpm install --frozen-lockfile # Use --frozen-lockfile for more consistent builds pnpm install --frozen-lockfile
fi fi
''; '';
}; };

View File

@@ -1,7 +1,7 @@
{ {
"name": "atridotdad", "name": "atridotdad",
"type": "module", "type": "module",
"version": "2.1.0", "version": "3.0.0",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
@@ -10,26 +10,26 @@
"nix": "nix develop" "nix": "nix develop"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.4", "@astrojs/mdx": "^4.3.6",
"@astrojs/node": "^9.4.3", "@astrojs/node": "^9.4.4",
"@astrojs/preact": "^4.1.0", "@astrojs/preact": "^4.1.1",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.12",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@preact/signals": "^2.3.1", "@preact/signals": "^2.3.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.18",
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.13",
"astro": "^5.13.5", "astro": "^5.13.10",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"lucide-preact": "^0.542.0", "lucide-preact": "^0.544.0",
"playwright": "^1.55.0", "playwright": "^1.55.0",
"preact": "^10.27.1", "preact": "^10.27.2",
"sharp": "^0.34.3", "sharp": "^0.34.4",
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/mdi": "^1.2.3", "@iconify-json/mdi": "^1.2.3",
"@iconify-json/simple-icons": "^1.2.50", "@iconify-json/simple-icons": "^1.2.52",
"daisyui": "^5.1.6" "daisyui": "^5.1.14"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [

1712
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
import { Icon } from 'astro-icon/components'; import { Icon } from "astro-icon/components";
import type { IconType, LucideIcon, AstroIconName, CustomIconComponent } from '../types'; import type {
IconType,
LucideIcon,
AstroIconName,
CustomIconComponent,
} from "../types";
interface IconRendererProps { interface IconRendererProps {
icon: IconType; icon: IconType;
@@ -10,18 +15,23 @@ interface IconRendererProps {
// Type guard functions // Type guard functions
function isLucideIcon(icon: IconType): icon is LucideIcon { 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 { function isAstroIconName(icon: IconType): icon is AstroIconName {
return typeof icon === 'string'; return typeof icon === "string";
} }
function isCustomComponent(icon: IconType): icon is CustomIconComponent { 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)) { if (isLucideIcon(icon)) {
const LucideComponent = icon; const LucideComponent = icon;
return <LucideComponent size={size} class={className} {...props} />; return <LucideComponent size={size} class={className} {...props} />;

View File

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

View File

@@ -9,7 +9,9 @@ interface Props {
const { project } = Astro.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"> <div class="card-body break-words">
<h2 <h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words" class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words"

View File

@@ -1,12 +1,11 @@
--- ---
import { Icon } from "astro-icon/components"; 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"> <div class="flex flex-row gap-3 text-3xl flex-wrap justify-center">
{ {
socialLinks.map((link) => { config.socialLinks.map((link) => (
return (
<a <a
href={link.url} href={link.url}
target={link.url.startsWith("http") ? "_blank" : undefined} target={link.url.startsWith("http") ? "_blank" : undefined}
@@ -20,7 +19,6 @@ import { socialLinks } from "../config/data";
> >
<Icon name={link.icon} /> <Icon name={link.icon} />
</a> </a>
); ))
})
} }
</div> </div>

View File

@@ -1,6 +1,6 @@
--- ---
import { Icon } from "astro-icon/components"; 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) // Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string { 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)) { if (isAstroIcon(link.icon)) {
return ( return (
<a <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,7 +2,7 @@
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"; import { config } from "../config";
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
import "../styles/global.css"; import "../styles/global.css";
@@ -14,9 +14,9 @@ export interface Props {
const { title, description } = Astro.props; const { title, description } = Astro.props;
const pageTitle = title const pageTitle = title
? `${title} | ${siteConfig.meta.title}` ? `${title} | ${config.siteConfig.meta.title}`
: siteConfig.meta.title; : config.siteConfig.meta.title;
const pageDescription = description || siteConfig.meta.description; const pageDescription = description || config.siteConfig.meta.description;
--- ---
<!doctype html> <!doctype html>
@@ -27,7 +27,7 @@ const pageDescription = description || siteConfig.meta.description;
<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} />
<meta name="description" content={pageDescription} /> <meta name="description" content={pageDescription} />
<meta name="author" content={siteConfig.meta.author} /> <meta name="author" content={config.siteConfig.meta.author} />
<title>{pageTitle}</title> <title>{pageTitle}</title>
<ClientRouter /> <ClientRouter />
</head> </head>

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ const sortedPosts = posts.sort(
--- ---
<Layout> <Layout>
<div class="min-h-screen min-w-screen p-4 sm:p-8"> <div class="w-full p-4 sm:p-8">
<h1 <h1
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center" class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
> >

View File

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

View File

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

View File

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

View File

@@ -50,41 +50,43 @@ export interface NavigationItem {
isActive?: (path: string) => boolean; isActive?: (path: string) => boolean;
} }
export type ResumeSectionKey =
| "summary"
| "experience"
| "education"
| "skills"
| "volunteer"
| "profiles"
| "awards";
export interface ResumeConfig { export interface ResumeConfig {
tomlFile: string; // Can be a file path or raw TOML content tomlFile: string; // Can be a file path or raw TOML content
layout?: { layout?: {
leftColumn?: string[]; leftColumn?: ResumeSectionKey[];
rightColumn?: string[]; rightColumn?: ResumeSectionKey[];
}; };
sections: { sections: {
enabled: string[]; enabled: ResumeSectionKey[];
summary?: { summary?: {
title?: string; title?: string;
enabled?: boolean;
}; };
experience?: { experience?: {
title?: string; title?: string;
enabled?: boolean;
}; };
education?: { education?: {
title?: string; title?: string;
enabled?: boolean;
}; };
skills?: { skills?: {
title?: string; title?: string;
enabled?: boolean;
}; };
volunteer?: { volunteer?: {
title?: string; title?: string;
enabled?: boolean;
}; };
profiles?: { profiles?: {
title?: string; title?: string;
enabled?: boolean;
}; };
awards?: { awards?: {
title?: string; title?: string;
enabled?: boolean;
}; };
}; };
} }
@@ -123,3 +125,37 @@ export interface SiteConfig {
author: string; 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[];
}