Compare commits

..

31 Commits

Author SHA1 Message Date
e6f6be20ce This time for sure
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m25s
2026-03-08 13:40:53 -06:00
972abaf3af Ugh
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m58s
2026-03-08 13:33:47 -06:00
5b656ebb03 Pls
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m57s
2026-03-08 13:21:57 -06:00
0060d309cb Delete favourite-tools.md 2026-03-08 00:20:23 -07:00
7922b2da18 Update bun.lock
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m35s
2026-03-07 23:58:24 -07:00
4c8105d263 Fixed sameOrigin. Turns out I just had it as text and not json being
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m50s
passed :/
2026-03-07 00:35:39 -07:00
fa9d2700f2 Update astro.config.mjs
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m55s
2026-03-06 23:47:37 -07:00
79f5b5c9e9 oops
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m39s
2026-03-06 23:31:22 -07:00
3e43e73abf Deps
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m25s
2026-03-03 13:31:43 -07:00
f03318f7dc Fixed some description text
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m4s
2026-03-03 13:23:13 -07:00
1db15c64e1 Deps 2026-03-01 01:16:31 -07:00
a98bf7c7c6 Update config.ts
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m41s
2026-02-25 23:45:49 -07:00
c3c8867a37 Update 2026-infra-setup.md
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m39s
2026-02-25 23:33:23 -07:00
2430f89737 Update 2026-infra-setup.md
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m46s
2026-02-25 19:54:16 -07:00
7925fab524 Updated infra post
All checks were successful
Docker Deploy / build-and-push (push) Successful in 2m50s
2026-02-25 18:20:29 -07:00
271dad89a1 Bun-ify
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
2026-02-24 23:25:47 -07:00
47946c0703 deps
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m17s
2026-02-23 11:32:53 -07:00
4b78414562 Updated resume
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m8s
2026-02-16 23:52:02 -07:00
0cf1cfa2b0 Added Dart
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m51s
2026-02-16 14:13:28 -07:00
399cff82b0 Theme change
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m14s
2026-02-15 00:42:13 -07:00
cf163bb0b2 Add RSS links to each post
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m35s
2026-02-14 18:00:49 -07:00
cc3a408050 Added Matrix and Mastodon for fediiiiii
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m53s
2026-02-14 16:52:17 -07:00
46c42cd765 Optimized docker
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m1s
2026-02-12 15:04:10 -07:00
89c1c739c1 Re-worked icons
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m48s
2026-02-12 14:22:59 -07:00
33dfea1802 Added Haschel
Some checks failed
Docker Deploy / build-and-push (push) Failing after 4m8s
2026-02-11 23:11:13 -07:00
0efb72fffd Fixed opengraph again
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m4s
2026-02-07 13:18:18 -07:00
dce37681af Opengraph
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m39s
2026-02-07 00:28:33 -07:00
6b77ce091d Upgraded the projects view. Looks and acts MUCH nicer
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m21s
2026-02-03 11:16:07 -07:00
ba1193896f Improved posts UX
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m32s
2026-02-03 11:00:13 -07:00
63282cf34d New year, new post!
All checks were successful
Docker Deploy / build-and-push (push) Successful in 5m59s
2026-02-03 10:29:03 -07:00
3eac226630 Deps
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m17s
2026-01-31 23:25:49 -07:00
40 changed files with 2103 additions and 7251 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
.git
.gitignore
dist
.env*
*.md
.vscode
.idea
.DS_Store

View File

@@ -12,26 +12,27 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Container Registry - name: Login to Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ${{ secrets.REPO_HOST }} registry: ${{ secrets.REPO_HOST }}
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.DEPLOY_TOKEN }} password: ${{ secrets.DEPLOY_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
build-args: |
PUBLIC_RSS_TIMEZONE=${{ vars.PUBLIC_RSS_TIMEZONE }}
tags: | tags: |
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }} ${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }}
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest ${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest
provenance: false
cache-from: type=registry,ref=${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:buildcache
cache-to: type=registry,ref=${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:buildcache,mode=max

View File

@@ -1,74 +1,28 @@
FROM node:24-alpine AS base FROM oven/bun:1.3.9-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
python3 \
make \
g++ \
libc6-compat \
vips-dev \
curl
# Install pnpm globally
RUN npm install -g pnpm
# Configure pnpm
RUN pnpm config set store-dir /.pnpm-store
RUN pnpm config set network-timeout 300000
RUN pnpm config set fetch-retries 10
RUN pnpm config set fetch-retry-factor 2
FROM base AS deps
WORKDIR /app WORKDIR /app
# Copy package files FROM base AS prod-deps
COPY package.json pnpm-lock.yaml ./ COPY package.json bun.lock ./
RUN --mount=type=cache,id=bun,target=/root/.bun/install/cache \
bun install --production --frozen-lockfile || bun install --production
# Install dependencies with retry logic FROM base AS builder
RUN --mount=type=cache,id=pnpm,target=/.pnpm-store \ COPY package.json bun.lock ./
pnpm install --frozen-lockfile || \ RUN --mount=type=cache,id=bun,target=/root/.bun/install/cache \
(sleep 5 && pnpm install --frozen-lockfile) || \ bun install --frozen-lockfile || bun install
(sleep 10 && pnpm install --frozen-lockfile --no-frozen-lockfile)
FROM base AS build-deps
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install all dependencies including dev dependencies
RUN --mount=type=cache,id=pnpm,target=/.pnpm-store \
pnpm install --frozen-lockfile || \
(sleep 5 && pnpm install --frozen-lockfile) || \
(sleep 10 && pnpm install --frozen-lockfile --no-frozen-lockfile)
FROM build-deps AS builder
ARG PUBLIC_RSS_TIMEZONE
ENV PUBLIC_RSS_TIMEZONE=${PUBLIC_RSS_TIMEZONE}
COPY . . COPY . .
RUN bun run build
RUN pnpm run build FROM base AS runtime
FROM node:24-alpine AS runtime
RUN npm install -g pnpm
WORKDIR /app WORKDIR /app
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./ COPY package.json ./
ENV HOST=0.0.0.0 \ ENV HOST=0.0.0.0
PORT=4321 \ ENV PORT=4321
NODE_ENV=production
EXPOSE 4321 EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"] CMD ["bun", "run", "./dist/server/entry.mjs"]

View File

@@ -3,10 +3,8 @@ import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import vue from "@astrojs/vue"; import vue from "@astrojs/vue";
import node from "@astrojs/node"; import node from "@astrojs/node";
import icon from "astro-icon";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
// https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://atri.dad", site: "https://atri.dad",
redirects: { redirects: {
@@ -27,51 +25,7 @@ export default defineConfig({
objectPosition: "center", objectPosition: "center",
}, },
integrations: [ integrations: [vue(), mdx()],
vue(),
mdx(),
icon({
include: {
mdi: [
"clock",
"tag",
"arrow-right",
"link",
"email",
"rss",
"download",
"web",
"arrow-left",
"source-commit",
"code-tags",
"tag-multiple",
"clock-outline",
"apple",
"google-play",
"code-braces",
"circle",
"open-in-new",
],
"simple-icons": [
"gitea",
"bluesky",
"react",
"vuedotjs",
"typescript",
"astro",
"go",
"postgresql",
"dotnet",
"docker",
"github",
"kotlin",
"swift",
"flutter",
"nixos",
],
},
}),
],
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone",

1373
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
{
description = "atridotdad dev shell";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
};
outputs = { self, nixpkgs }:
let
allSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
nodejs_24
nodePackages.pnpm
];
shellHook = ''
echo "<atridotdad dev shell>"
echo "Node version: $(node --version)"
echo "pnpm version: $(pnpm --version)"
'';
};
});
};
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "atridotdad", "name": "atridotdad",
"type": "module", "type": "module",
"version": "4.0.0", "version": "4.2.0",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
@@ -10,29 +10,22 @@
"nix": "nix develop" "nix": "nix develop"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "5.0.0-beta.2", "@astrojs/mdx": "4.3.13",
"@astrojs/node": "10.0.0-beta.0", "@astrojs/node": "9.5.4",
"@astrojs/rss": "4.0.15", "@astrojs/rss": "4.0.15",
"@astrojs/vue": "6.0.0-beta.0", "@astrojs/vue": "5.1.4",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@mdi/js": "^7.4.47",
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.2.1",
"astro": "6.0.0-beta.3", "astro": "5.18.0",
"astro-icon": "^1.1.5", "react": "^19.2.4",
"lucide-vue-next": "^0.563.0",
"react": "^19.2.3",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"simple-icons": "^16.6.0", "tailwindcss": "^4.2.1",
"tailwindcss": "^4.1.18", "vue": "^3.5.29"
"vue": "^3.5.27"
}, },
"devDependencies": { "devDependencies": {
"@catppuccin/daisyui": "^2.1.1", "@types/react": "^19.2.14",
"@iconify-json/mdi": "^1.2.3", "daisyui": "^5.5.19"
"@iconify-json/simple-icons": "^1.2.67",
"@types/react": "^19.2.9",
"daisyui": "^5.5.14"
} }
} }

6465
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -4,8 +4,8 @@ email = "me@atri.dad"
website = "https://atri.dad" website = "https://atri.dad"
[layout] [layout]
left_column = ["experience", "volunteer"] left_column = ["experience"]
right_column = ["skills", "education", "awards"] right_column = ["skills", "education", "awards", "volunteer"]
[[basics.profiles]] [[basics.profiles]]
@@ -13,6 +13,19 @@ network = "Gitea"
username = "atridad" username = "atridad"
url = "https://git.atri.dad/atridad" url = "https://git.atri.dad/atridad"
[[experience]]
company = "Atash Consulting"
position = "Owner/Developer"
location = "Edmonton, Alberta"
date = "June 2019 - Present"
description = [
"Runs an independent software consultancy delivering web, mobile, and DevOps solutions for clients across various industries",
"Builds functional, accessible websites and cross-platform mobile applications for iOS and Android",
"Implements CI/CD pipelines, containerized infrastructure, and end-to-end testing to streamline development",
"Provides ongoing IT support, systems architecture guidance, and technical strategy backed by over a decade of experience",
]
url = "https://atash.dev"
[[experience]] [[experience]]
company = "University of Saskatchewan CEPHIL Lab" company = "University of Saskatchewan CEPHIL Lab"
position = "Technical Lead" position = "Technical Lead"
@@ -30,24 +43,13 @@ url = "https://cephil.ca/"
company = "University of Saskatchewan, Department of Computer Science" company = "University of Saskatchewan, Department of Computer Science"
position = "Teaching Assistant" position = "Teaching Assistant"
location = "Saskatoon, Saskatchewan" location = "Saskatoon, Saskatchewan"
date = "2024 - 2025" date = "2024 - Present"
description = [ description = [
"Marker for CMPT 141 (Introduction to Computer Organization and Architecture), grading assignments and providing feedback to help students develop an intuition for low level architecture",
"Lab instructor for CMPT 370 (Intermediate Software Engineering), leading weekly labs, guiding project teams, and supporting design and implementation exercises", "Lab instructor for CMPT 370 (Intermediate Software Engineering), leading weekly labs, guiding project teams, and supporting design and implementation exercises",
"Marker for CMPT 141 (Introduction to Computer Science), grading assignments and providing feedback to help students build foundational programming skills" "Marker for CMPT 141 (Introduction to Computer Science), grading assignments and providing feedback to help students build foundational programming skills"
] ]
[[experience]]
company = "Atash Consulting"
position = "Owner/Developer"
location = "Edmonton, Alberta"
date = "June 2019 - Present"
description = [
"Provide consulting services for application development, systems architecture, and DevOps on Linux and cloud platforms",
"Develop web applications, administrative interfaces, and REST APIs that run on containerized and virtualized infrastructure",
"Design schemas and SQL queries to support reporting, monitoring, and analytics for client systems"
]
url = "https://atash.dev"
[[experience]] [[experience]]
company = "Alberta Motor Association" company = "Alberta Motor Association"
position = "Software Developer II" position = "Software Developer II"
@@ -122,7 +124,7 @@ name = "Modern Front-end Libraries (React, Vue, Svelte)"
level = 5 level = 5
[[skills]] [[skills]]
name = "Project Magagement" name = "Project Management"
level = 4 level = 4
[[skills]] [[skills]]

27
src/components/Icon.astro Normal file
View File

@@ -0,0 +1,27 @@
---
import { icons, type IconName } from "../config/icons";
interface Props {
name: IconName;
class?: string;
"class:list"?: any;
}
const { name, class: className, "class:list": classList } = Astro.props;
const svg = icons[name];
if (!svg) {
throw new Error(`Icon "${name}" not found in icon registry`);
}
---
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
class:list={[className, classList]}
aria-hidden="true"
set:html={svg}
/>

30
src/components/Icon.vue Normal file
View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from "vue";
import { icons, type IconName } from "../config/icons";
const props = defineProps<{
name: IconName;
class?: string;
}>();
const svg = computed(() => {
const content = icons[props.name];
if (!content) {
console.error(`Icon "${props.name}" not found in icon registry`);
}
return content;
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
:class="props.class"
aria-hidden="true"
v-html="svg"
/>
</template>

16
src/components/Logo.astro Normal file
View File

@@ -0,0 +1,16 @@
---
import { Image } from "astro:assets";
import { config } from "../config";
---
<Image
src={config.personalInfo.profileImage.src}
alt={config.personalInfo.profileImage.alt}
widths={[192, 384]}
sizes="12rem"
layout="constrained"
loading="eager"
fetchpriority="high"
class="rounded-full mx-auto"
style="max-width: 12rem; width: 100%;"
/>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue"; import { ref, computed, onMounted, onUnmounted } from "vue";
import { config } from "../config"; import { config } from "../config";
import type { Component } from "vue"; import Icon from "./Icon.vue";
const props = defineProps<{ const props = defineProps<{
currentPath: string; currentPath: string;
@@ -109,10 +109,9 @@ onUnmounted(() => {
:data-tip="item.tooltip" :data-tip="item.tooltip"
data-astro-prefetch="hover" data-astro-prefetch="hover"
> >
<component <Icon
:is="item.icon as Component" :name="item.icon"
:size="18" class="w-[18px] h-[18px] sm:w-5 sm:h-5"
class="sm:w-5 sm:h-5"
/> />
<span class="sr-only">{{ item.name }}</span> <span class="sr-only">{{ item.name }}</span>
</a> </a>

View File

@@ -1,144 +0,0 @@
---
import { Icon } from "astro-icon/components";
import { config } from "../config";
import { fetchGiteaInfoFromUrl, formatRelativeTime } from "../utils/gitea";
import type { Project } from "../types";
Astro.response.headers.set(
"Cache-Control",
"public, max-age=300, s-maxage=300, stale-while-revalidate=60",
);
function isGiteaDomain(url: string): boolean {
if (!config.siteConfig.giteaDomains) return true;
try {
const urlObj = new URL(url);
return config.siteConfig.giteaDomains.some(
(domain) => urlObj.origin === new URL(domain).origin,
);
} catch {
return false;
}
}
const projectsWithGiteaInfo = await Promise.all(
config.projects.map(async (project) => {
if (
project.gitLink &&
!project.giteaInfo &&
isGiteaDomain(project.gitLink)
) {
const giteaInfo = await fetchGiteaInfoFromUrl(project.gitLink);
if (giteaInfo) {
return { ...project, giteaInfo } as Project;
}
}
return project;
}),
);
const sortedProjects = projectsWithGiteaInfo.sort((a, b) => {
const aTime = a.giteaInfo?.updatedAt
? new Date(a.giteaInfo.updatedAt).getTime()
: 0;
const bTime = b.giteaInfo?.updatedAt
? new Date(b.giteaInfo.updatedAt).getTime()
: 0;
return bTime - aTime;
});
---
<ul class="list bg-base-200 rounded-box max-w-4xl mx-auto">
{
sortedProjects.map((project) => (
<li class="list-row hover:bg-base-300 transition-colors">
{/* Project icon/avatar */}
<div class="list-col-grow-0">
<div class="w-12 h-12 rounded-lg bg-accent flex items-center justify-center">
<Icon name="mdi:code-braces" class="w-6 h-6 text-accent-content" />
</div>
</div>
{/* Main content */}
<div class="list-col-grow">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<h3 class="font-bold text-lg">{project.name}</h3>
{project.giteaInfo?.updatedAt && (
<span class="text-xs opacity-60">
{formatRelativeTime(project.giteaInfo.updatedAt)}
</span>
)}
</div>
<p class="text-sm opacity-80 mt-1">{project.description}</p>
{/* Languages & Topics */}
<div class="flex flex-wrap gap-1 mt-2">
{project.giteaInfo?.languages?.slice(0, 3).map((lang: string) => (
<span class="badge badge-sm badge-primary">{lang}</span>
))}
{project.giteaInfo?.topics?.slice(0, 4).map((topic: string) => (
<span class="badge badge-sm badge-outline">{topic}</span>
))}
</div>
</div>
{/* Action buttons */}
<div class="list-col-grow-0 flex flex-wrap gap-1 justify-end">
{project.webLink && (
<a
href={project.webLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-primary hover:bg-primary hover:text-primary-content transition-all"
aria-label={`Visit ${project.name} website`}
>
<Icon name="mdi:web" class="w-5 h-5" />
</a>
)}
{project.gitLink && (
<a
href={project.gitLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-secondary hover:bg-secondary hover:text-secondary-content transition-all"
aria-label={`View ${project.name} source`}
>
<Icon name="simple-icons:gitea" class="w-5 h-5" />
</a>
)}
{project.iosLink && (
<a
href={project.iosLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-accent hover:bg-accent hover:text-accent-content transition-all"
aria-label={`${project.name} on iOS`}
>
<Icon name="mdi:apple" class="w-5 h-5" />
</a>
)}
{project.androidLink && (
<a
href={project.androidLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-success hover:bg-success hover:text-success-content transition-all"
aria-label={`${project.name} on Android`}
>
<Icon name="mdi:google-play" class="w-5 h-5" />
</a>
)}
</div>
</li>
))
}
</ul>
{
sortedProjects.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No projects available yet. Check back soon!
</p>
)
}

View File

@@ -1,7 +0,0 @@
---
---
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { Settings, X } from "lucide-vue-next"; import Icon from "./Icon.vue";
const tomlContent = ref(""); const tomlContent = ref("");
const isGenerating = ref(false); const isGenerating = ref(false);
@@ -105,9 +105,9 @@ const generatePDF = async () => {
const response = await fetch("/api/resume/generate", { const response = await fetch("/api/resume/generate", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "text/plain", "Content-Type": "application/json",
}, },
body: tomlContent.value, body: JSON.stringify({ toml: tomlContent.value }),
}); });
if (!response.ok) { if (!response.ok) {
@@ -162,7 +162,7 @@ const loadTemplate = async () => {
:class="$attrs.class" :class="$attrs.class"
aria-label="Resume Settings" aria-label="Resume Settings"
> >
<Settings class="text-lg" /> <Icon name="settings" class="text-lg" />
</button> </button>
<!-- Modal --> <!-- Modal -->
@@ -176,7 +176,7 @@ const loadTemplate = async () => {
@click="closeModal" @click="closeModal"
class="btn btn-circle btn-secondary hover:btn-primary" class="btn btn-circle btn-secondary hover:btn-primary"
> >
<X class="text-lg" /> <Icon name="x" class="text-lg" />
</button> </button>
</div> </div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import { ArrowUp } from "lucide-vue-next"; import Icon from "./Icon.vue";
const isVisible = ref(false); const isVisible = ref(false);
let ticking = false; let ticking = false;
@@ -45,6 +45,6 @@ onUnmounted(() => {
" "
aria-label="Scroll to top" aria-label="Scroll to top"
> >
<ArrowUp class="text-lg" /> <Icon name="arrow-up" class="text-lg" />
</button> </button>
</template> </template>

View File

@@ -1,5 +1,5 @@
--- ---
import { Icon } from "astro-icon/components"; import Icon from "./Icon.astro";
import { config } from "../config"; import { config } from "../config";
--- ---

View File

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

View File

@@ -1,27 +1,8 @@
import type { Config } from "./types"; import type { Config } from "./types";
import { Home, Newspaper, FileUser, CodeXml, Megaphone } from "lucide-vue-next";
import logo from "./assets/logo.webp"; import logo from "./assets/logo.webp";
import resumeToml from "./assets/resume.toml?raw"; 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 VUEJS_ICON = "simple-icons:vuedotjs" 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 DOTNET_ICON = "simple-icons:dotnet" 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 NIX_ICON = "simple-icons:nixos" as const;
export const config: Config = { export const config: Config = {
personalInfo: { personalInfo: {
name: "Atridad Lahiji", name: "Atridad Lahiji",
@@ -142,6 +123,41 @@ export const config: Config = {
url: "https://atri.dad", url: "https://atri.dad",
author: "Atridad Lahiji", author: "Atridad Lahiji",
}, },
openGraph: {
image: {
url: "/logo.webp",
width: 1024,
height: 1024,
type: "image/webp",
alt: "Atridad Lahiji",
},
type: "website",
locale: "en_US",
siteName: "Atridad Lahiji",
},
pageOpenGraph: {
home: {
title: "Atridad Lahiji",
description:
"Personal website of Atridad Lahiji - Researcher, Full-Stack Developer, and IT Professional",
},
posts: {
title: "Blog Posts",
description: "Thoughts and ramblings.",
},
projects: {
title: "Projects",
description: "Projects I'm working on.",
},
talks: {
title: "Talks",
description: "Conference talks and presentations by Atridad Lahiji",
},
resume: {
title: "Resume",
description: "Resume + TOML Resume Generator",
},
},
giteaDomains: ["https://git.atri.dad"], giteaDomains: ["https://git.atri.dad"],
}, },
@@ -202,10 +218,25 @@ export const config: Config = {
"My NixOS desktop configuration, named after a character in Legend of Dragoon for the PS1: Lavitz.", "My NixOS desktop configuration, named after a character in Legend of Dragoon for the PS1: Lavitz.",
gitLink: "https://git.atri.dad/atridad/lavitz", gitLink: "https://git.atri.dad/atridad/lavitz",
}, },
{
id: "haschel",
name: "Haschel",
description:
"My NixOS proxy server configuration, named after a character in Legend of Dragoon for the PS1: Haschel",
gitLink: "https://git.atri.dad/atridad/haschel",
},
{
id: "dart",
name: "Dart",
description:
"My Nix macOS configuration, named after a character in Legend of Dragoon for the PS1: Dart",
gitLink: "https://git.atri.dad/atridad/dart",
},
{ {
id: "atrodotdad", id: "atrodotdad",
name: "Personal Site", name: "Personal Site",
description: "My personal website built with Astro.", description: "My personal website built with Astro.",
webLink: "https://atri.dad",
gitLink: "https://git.atri.dad/atridad/atridotdad", gitLink: "https://git.atri.dad/atridad/atridotdad",
}, },
], ],
@@ -238,30 +269,44 @@ export const config: Config = {
id: "email", id: "email",
name: "Email", name: "Email",
url: "mailto:me@atri.dad", url: "mailto:me@atri.dad",
icon: EMAIL_ICON, icon: "email",
ariaLabel: "Email me", ariaLabel: "Email me",
}, },
{ {
id: "rss", id: "rss",
name: "RSS Feed", name: "RSS Feed",
url: "/feed", url: "/feed",
icon: RSS_ICON, icon: "rss",
ariaLabel: "RSS Feed", ariaLabel: "RSS Feed",
}, },
{ {
id: "gitea", id: "gitea",
name: "Forgejo (Git)", name: "Forgejo (Git)",
url: "https://git.atri.dad/atridad", url: "https://git.atri.dad/atridad",
icon: GITEA_ICON, icon: "gitea",
ariaLabel: "Forgejo (Git)", ariaLabel: "Forgejo (Git)",
}, },
{ {
id: "bluesky", id: "bluesky",
name: "Bluesky", name: "Bluesky",
url: "https://bsky.app/profile/atri.dad", url: "https://bsky.app/profile/atri.dad",
icon: BLUESKY_ICON, icon: "bluesky",
ariaLabel: "Bluesky Profile", ariaLabel: "Bluesky Profile",
}, },
{
id: "matrix",
name: "Matrix",
url: "https://matrix.to/#/@atridad:atri.dad",
icon: "matrix",
ariaLabel: "Matrix Profile",
},
{
id: "mastodon",
name: "Mastodon",
url: "https://fedi.atri.dad/@atridad",
icon: "mastodon",
ariaLabel: "Mastodon Profile",
},
], ],
techLinks: [ techLinks: [
@@ -269,77 +314,77 @@ export const config: Config = {
id: "react", id: "react",
name: "React", name: "React",
url: "https://react.dev/", url: "https://react.dev/",
icon: REACT_ICON, icon: "react",
ariaLabel: "React", ariaLabel: "React",
}, },
{ {
id: "vuejs", id: "vuejs",
name: "Vue.js", name: "Vue.js",
url: "https://vuejs.org//", url: "https://vuejs.org//",
icon: VUEJS_ICON, icon: "vuedotjs",
ariaLabel: "Vue.js", ariaLabel: "Vue.js",
}, },
{ {
id: "typescript", id: "typescript",
name: "TypeScript", name: "TypeScript",
url: "https://www.typescriptlang.org/", url: "https://www.typescriptlang.org/",
icon: TYPESCRIPT_ICON, icon: "typescript",
ariaLabel: "TypeScript", ariaLabel: "TypeScript",
}, },
{ {
id: "astro", id: "astro",
name: "Astro", name: "Astro",
url: "https://astro.build/", url: "https://astro.build/",
icon: ASTRO_ICON, icon: "astro",
ariaLabel: "Astro", ariaLabel: "Astro",
}, },
{ {
id: "go", id: "go",
name: "Go", name: "Go",
url: "https://go.dev/", url: "https://go.dev/",
icon: GO_ICON, icon: "go",
ariaLabel: "Go", ariaLabel: "Go",
}, },
{ {
id: "postgresql", id: "postgresql",
name: "PostgreSQL", name: "PostgreSQL",
url: "https://www.postgresql.org/", url: "https://www.postgresql.org/",
icon: POSTGRESQL_ICON, icon: "postgresql",
ariaLabel: "PostgreSQL", ariaLabel: "PostgreSQL",
}, },
{ {
id: "dotnet", id: "dotnet",
name: "DotNet", name: "DotNet",
url: "https://dot.net/", url: "https://dot.net/",
icon: DOTNET_ICON, icon: "dotnet",
ariaLabel: "DotNet", ariaLabel: "DotNet",
}, },
{ {
id: "docker", id: "docker",
name: "Docker", name: "Docker",
url: "https://www.docker.com/", url: "https://www.docker.com/",
icon: DOCKER_ICON, icon: "docker",
ariaLabel: "Docker", ariaLabel: "Docker",
}, },
{ {
id: "kotlin", id: "kotlin",
name: "Kotlin", name: "Kotlin",
url: "https://kotlinlang.org/", url: "https://kotlinlang.org/",
icon: KOTLIN_ICON, icon: "kotlin",
ariaLabel: "Kotlin", ariaLabel: "Kotlin",
}, },
{ {
id: "swift", id: "swift",
name: "Swift", name: "Swift",
url: "https://www.swift.org/", url: "https://www.swift.org/",
icon: SWIFT_ICON, icon: "swift",
ariaLabel: "Swift", ariaLabel: "Swift",
}, },
{ {
id: "nix", id: "nix",
name: "Nix", name: "Nix",
url: "https://nix.org", url: "https://nixos.org",
icon: NIX_ICON, icon: "nixos",
ariaLabel: "Nix", ariaLabel: "Nix",
}, },
], ],
@@ -350,7 +395,7 @@ export const config: Config = {
name: "Home", name: "Home",
path: "/", path: "/",
tooltip: "Home", tooltip: "Home",
icon: Home, icon: "house",
enabled: true, enabled: true,
}, },
{ {
@@ -358,7 +403,7 @@ export const config: Config = {
name: "Posts", name: "Posts",
path: "/posts", path: "/posts",
tooltip: "Posts", tooltip: "Posts",
icon: Newspaper, icon: "newspaper",
enabled: true, enabled: true,
isActive: (path: string) => isActive: (path: string) =>
path.startsWith("/posts") || path.startsWith("/post/"), path.startsWith("/posts") || path.startsWith("/post/"),
@@ -368,7 +413,7 @@ export const config: Config = {
name: "Resume", name: "Resume",
path: "/resume", path: "/resume",
tooltip: "Resume", tooltip: "Resume",
icon: FileUser, icon: "file-user",
enabled: !!(resumeToml && resumeToml.trim()), enabled: !!(resumeToml && resumeToml.trim()),
}, },
{ {
@@ -376,7 +421,7 @@ export const config: Config = {
name: "Projects", name: "Projects",
path: "/projects", path: "/projects",
tooltip: "Projects", tooltip: "Projects",
icon: CodeXml, icon: "code-xml",
enabled: true, enabled: true,
isActive: (path: string) => path.startsWith("/projects"), isActive: (path: string) => path.startsWith("/projects"),
}, },
@@ -385,7 +430,7 @@ export const config: Config = {
name: "Talks", name: "Talks",
path: "/talks", path: "/talks",
tooltip: "Talks", tooltip: "Talks",
icon: Megaphone, icon: "megaphone",
enabled: true, enabled: true,
isActive: (path: string) => path.startsWith("/talks"), isActive: (path: string) => path.startsWith("/talks"),
}, },

59
src/config/icons.ts Normal file
View File

@@ -0,0 +1,59 @@
export const icons = {
clock: `<path fill="currentColor" d="M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m4.2 14.2L11 13V7h1.5v5.2l4.5 2.7z"/>`,
tag: `<path fill="currentColor" d="M5.5 7A1.5 1.5 0 0 1 4 5.5A1.5 1.5 0 0 1 5.5 4A1.5 1.5 0 0 1 7 5.5A1.5 1.5 0 0 1 5.5 7m15.91 4.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.11 0-2 .89-2 2v7c0 .55.22 1.05.59 1.41l8.99 9c.37.36.87.59 1.42.59s1.05-.23 1.41-.59l7-7c.37-.36.59-.86.59-1.41c0-.56-.23-1.06-.59-1.42"/>`,
"arrow-right": `<path fill="currentColor" d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11z"/>`,
link: `<path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7a5 5 0 0 0-5 5a5 5 0 0 0 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1M8 13h8v-2H8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4a5 5 0 0 0 5-5a5 5 0 0 0-5-5"/>`,
email: `<path fill="currentColor" d="m20 8l-8 5l-8-5V6l8 5l8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2"/>`,
rss: `<path fill="currentColor" d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27zm0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93z"/>`,
download: `<path fill="currentColor" d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/>`,
web: `<path fill="currentColor" d="M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2s.06-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.92 7.92 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8 8 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.7 15.7 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"/>`,
"arrow-left": `<path fill="currentColor" d="M20 11v2H8l5.5 5.5l-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5L8 11z"/>`,
"source-commit": `<path fill="currentColor" d="M17 12a5 5 0 0 1-4 4.9V21h-2v-4.1a5 5 0 0 1 0-9.8V3h2v4.1a5 5 0 0 1 4 4.9m-5-3a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3"/>`,
"code-tags": `<path fill="currentColor" d="m14.6 16.6l4.6-4.6l-4.6-4.6L16 6l6 6l-6 6zm-5.2 0L4.8 12l4.6-4.6L8 6l-6 6l6 6z"/>`,
"tag-multiple": `<path fill="currentColor" d="M5.5 9A1.5 1.5 0 0 0 7 7.5A1.5 1.5 0 0 0 5.5 6A1.5 1.5 0 0 0 4 7.5A1.5 1.5 0 0 0 5.5 9m11.91 2.58c.36.36.59.86.59 1.42c0 .55-.22 1.05-.59 1.41l-5 5a1.996 1.996 0 0 1-2.83 0l-6.99-6.99C2.22 12.05 2 11.55 2 11V6c0-1.11.89-2 2-2h5c.55 0 1.05.22 1.41.58zm-3.87-5.87l1-1l6.87 6.87c.37.36.59.87.59 1.42s-.22 1.05-.58 1.41l-5.38 5.38l-1-1L20.75 13z"/>`,
"clock-outline": `<path fill="currentColor" d="M12 20a8 8 0 0 0 8-8a8 8 0 0 0-8-8a8 8 0 0 0-8 8a8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10a10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67l-.75 1.23L11 13V7z"/>`,
apple: `<path fill="currentColor" d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47c-1.34.03-1.77-.79-3.29-.79c-1.53 0-2 .77-3.27.82c-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51c1.28-.02 2.5.87 3.29.87c.78 0 2.26-1.07 3.81-.91c.65.03 2.47.26 3.64 1.98c-.09.06-2.17 1.28-2.15 3.81c.03 3.02 2.65 4.03 2.68 4.04c-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5c.13 1.17-.34 2.35-1.04 3.19c-.69.85-1.83 1.51-2.95 1.42c-.15-1.15.41-2.35 1.05-3.11"/>`,
"google-play": `<path fill="currentColor" d="M3 20.5v-17c0-.59.34-1.11.84-1.35L13.69 12l-9.85 9.85c-.5-.25-.84-.76-.84-1.35m13.81-5.38L6.05 21.34l8.49-8.49zm3.35-4.31c.34.27.59.69.59 1.19s-.22.9-.57 1.18l-2.29 1.32l-2.5-2.5l2.5-2.5zM6.05 2.66l10.76 6.22l-2.27 2.27z"/>`,
"code-braces": `<path fill="currentColor" d="M8 3a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2H3v2h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h2v-2H8v-5a2 2 0 0 0-2-2a2 2 0 0 0 2-2V5h2V3m6 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2a2 2 0 0 1-2-2V5h-2V3z"/>`,
circle: `<path fill="currentColor" d="M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"/>`,
"open-in-new": `<path fill="currentColor" d="M14 3v2h3.59l-9.83 9.83l1.41 1.41L19 6.41V10h2V3m-2 16H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2z"/>`,
phone: `<path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.15 6.59 6.59l2.2-2.2c.28-.28.67-.36 1.02-.25c1.12.37 2.32.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57c.11.35.03.74-.25 1.02z"/>`,
gitea: `<path fill="currentColor" d="M4.209 4.603c-.247 0-.525.02-.84.088c-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768c1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367c2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068c0 0 2.107-4.471 2.107-8.823c-.042-1.318-.367-1.55-.443-1.627c-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17c-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12m8.33 2.554c.26.003.509.127.509.127l.868.422l-.529 1.075a.69.69 0 0 0-.614.359a.69.69 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527a.69.69 0 0 0 .347.763a.686.686 0 0 0 .867-.206a.69.69 0 0 0-.069-.882l.916-1.874a.7.7 0 0 0 .237-.02a.66.66 0 0 0 .271-.137a9 9 0 0 1 1.016.512a.76.76 0 0 1 .286.282c.073.21-.073.569-.073.569c-.087.29-.702 1.55-.702 1.55a.69.69 0 0 0-.676.477a.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431c.19-.397.515-1.16.515-1.16c.035-.066.218-.394.103-.814c-.095-.435-.48-.638-.48-.638c-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.7.7 0 0 0-.148-.241l.516-1.062l2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657c-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.1 1.1 0 0 1-.393-.045l-.202-.08l-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.9.9 0 0 1 .35-.077"/>`,
bluesky: `<path fill="currentColor" d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037c-.856 3.061-3.978 3.842-6.755 3.37c4.854.826 6.089 3.562 3.422 6.299c-5.065 5.196-7.28-1.304-7.847-2.97c-.104-.305-.152-.448-.153-.327c0-.121-.05.022-.153.327c-.568 1.666-2.782 8.166-7.847 2.97c-2.667-2.737-1.432-5.473 3.422-6.3c-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"/>`,
react: `<path fill="currentColor" d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236a2.236 2.236 0 0 1-2.236-2.236a2.236 2.236 0 0 1 2.235-2.236a2.236 2.236 0 0 1 2.236 2.236m2.648-10.69c-1.346 0-3.107.96-4.888 2.622c-1.78-1.653-3.542-2.602-4.887-2.602c-.41 0-.783.093-1.106.278c-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03c-.704 3.113-.39 5.588.988 6.38c.32.187.69.275 1.102.275c1.345 0 3.107-.96 4.888-2.624c1.78 1.654 3.542 2.603 4.887 2.603c.41 0 .783-.09 1.106-.275c1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032c.704-3.11.39-5.587-.988-6.38a2.17 2.17 0 0 0-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127c.666.382.955 1.835.73 3.704c-.054.46-.142.945-.25 1.44a23.5 23.5 0 0 0-3.107-.534A24 24 0 0 0 12.769 4.7c1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28c-.686.72-1.37 1.537-2.02 2.442a23 23 0 0 0-3.113.538a15 15 0 0 1-.254-1.42c-.23-1.868.054-3.32.714-3.707c.19-.09.4-.127.563-.132zm4.882 3.05q.684.704 1.36 1.564c-.44-.02-.89-.034-1.345-.034q-.691-.001-1.36.034c.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093q.61.874 1.183 1.86q.557.961 1.018 1.946c-.308.655-.646 1.31-1.013 1.95c-.38.66-.773 1.288-1.18 1.87a25.6 25.6 0 0 1-4.412.005a27 27 0 0 1-1.183-1.86q-.557-.961-1.018-1.946a25 25 0 0 1 1.013-1.954c.38-.66.773-1.286 1.18-1.868A25 25 0 0 1 12 8.098zm-3.635.254c-.24.377-.48.763-.704 1.16q-.336.585-.635 1.174c-.265-.656-.49-1.31-.676-1.947c.64-.15 1.315-.283 2.015-.386zm7.26 0q1.044.153 2.006.387c-.18.632-.405 1.282-.66 1.933a26 26 0 0 0-1.345-2.32zm3.063.675q.727.226 1.375.498c1.732.74 2.852 1.708 2.852 2.476c-.005.768-1.125 1.74-2.857 2.475c-.42.18-.88.342-1.355.493a24 24 0 0 0-1.1-2.98c.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98a23 23 0 0 0-1.086 2.964c-.484-.15-.944-.318-1.37-.5c-1.732-.737-2.852-1.706-2.852-2.474s1.12-1.742 2.852-2.476c.42-.18.88-.342 1.356-.494m11.678 4.28c.265.657.49 1.312.676 1.948c-.64.157-1.316.29-2.016.39a26 26 0 0 0 1.341-2.338zm-9.945.02c.2.392.41.783.64 1.175q.345.586.705 1.143a22 22 0 0 1-2.006-.386c.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423c.23 1.868-.054 3.32-.714 3.708c-.147.09-.338.128-.563.128c-1.012 0-2.514-.807-4.11-2.28c.686-.72 1.37-1.536 2.02-2.44c1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532c.66.905 1.345 1.727 2.035 2.446c-1.595 1.483-3.092 2.295-4.11 2.295a1.2 1.2 0 0 1-.553-.132c-.666-.38-.955-1.834-.73-3.703c.054-.46.142-.944.25-1.438zm4.56.64q.661.032 1.345.034q.691.001 1.36-.034c-.44.572-.895 1.095-1.345 1.565q-.684-.706-1.36-1.565"/>`,
vuedotjs: `<path fill="currentColor" d="M24 1.61h-9.94L12 5.16L9.94 1.61H0l12 20.78ZM12 14.08L5.16 2.23h4.43L12 6.41l2.41-4.18h4.43Z"/>`,
typescript: `<path fill="currentColor" d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75q.918 0 1.627.111a6.4 6.4 0 0 1 1.306.34v2.458a4 4 0 0 0-.643-.361a5 5 0 0 0-.717-.26a5.5 5.5 0 0 0-1.426-.2q-.45 0-.819.086a2.1 2.1 0 0 0-.623.242q-.254.156-.393.374a.9.9 0 0 0-.14.49q0 .294.156.529q.156.234.443.444c.287.21.423.276.696.41q.41.203.926.416q.705.296 1.266.628q.561.333.963.753q.402.418.614.957q.213.538.214 1.253q0 .986-.373 1.656a3 3 0 0 1-1.012 1.085a4.4 4.4 0 0 1-1.487.596q-.85.18-1.79.18a10 10 0 0 1-1.84-.164a5.5 5.5 0 0 1-1.512-.493v-2.63a5.03 5.03 0 0 0 3.237 1.2q.5 0 .872-.09q.373-.09.623-.25q.249-.162.373-.38a1.02 1.02 0 0 0-.074-1.089a2.1 2.1 0 0 0-.537-.5a5.6 5.6 0 0 0-.807-.444a28 28 0 0 0-1.007-.436q-1.377-.575-2.053-1.405t-.676-2.005q0-.92.369-1.582q.368-.662 1.004-1.089a4.5 4.5 0 0 1 1.47-.629a7.5 7.5 0 0 1 1.77-.201m-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"/>`,
astro: `<path fill="currentColor" d="M8.358 20.162c-1.186-1.07-1.532-3.316-1.038-4.944c.856 1.026 2.043 1.352 3.272 1.535c1.897.283 3.76.177 5.522-.678c.202-.098.388-.229.608-.36c.166.473.209.95.151 1.437c-.14 1.185-.738 2.1-1.688 2.794c-.38.277-.782.525-1.175.787c-1.205.804-1.531 1.747-1.078 3.119l.044.148a3.16 3.16 0 0 1-1.407-1.188a3.3 3.3 0 0 1-.544-1.815c-.004-.32-.004-.642-.048-.958c-.106-.769-.472-1.113-1.161-1.133c-.707-.02-1.267.411-1.415 1.09c-.012.053-.028.104-.045.165zm-5.961-4.445s3.24-1.575 6.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614s.57.248.662.614l2.45 7.565c3.85 0 6.491 1.575 6.491 1.575L16.088.727C15.93.285 15.663 0 15.303 0H8.697c-.36 0-.615.285-.784.727z"/>`,
go: `<path fill="currentColor" d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514c-.176.046-.187.058-.34-.117c-.174-.199-.303-.327-.548-.444c-.737-.362-1.45-.257-2.115.175c-.795.514-1.204 1.274-1.192 2.22c.011.935.654 1.706 1.577 1.835c.795.105 1.46-.175 1.987-.77c.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35c.152-.362.432-.97.596-1.274a.32.32 0 0 1 .292-.187h4.253c-.023.316-.023.631-.07.947a5 5 0 0 1-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986c-1.145.152-2.209-.07-3.143-.77c-.865-.655-1.356-1.52-1.484-2.595c-.152-1.274.222-2.419.993-3.424c.83-1.086 1.928-1.776 3.272-2.02c1.098-.2 2.15-.07 3.096.571c.62.41 1.063.97 1.356 1.648c.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.67 3.67 0 0 1-1.262-2.255c-.21-1.32.152-2.489.947-3.529c.853-1.122 1.881-1.706 3.272-1.95c1.192-.21 2.314-.095 3.33.595c.923.63 1.496 1.484 1.648 2.605c.198 1.578-.257 2.863-1.344 3.962c-.771.783-1.718 1.273-2.805 1.495c-.315.06-.63.07-.934.106m2.78-4.72c-.011-.153-.011-.27-.034-.387c-.21-1.157-1.274-1.81-2.384-1.554c-1.087.245-1.788.935-2.045 2.033c-.21.912.234 1.835 1.075 2.21c.643.28 1.285.244 1.905-.07c.923-.48 1.425-1.228 1.484-2.233z"/>`,
postgresql: `<path fill="currentColor" d="M23.56 14.723a.5.5 0 0 0-.057-.12q-.21-.395-1.007-.231c-1.654.34-2.294.13-2.526-.02c1.342-2.048 2.445-4.522 3.041-6.83c.272-1.05.798-3.523.122-4.73a1.6 1.6 0 0 0-.15-.236C21.693.91 19.8.025 17.51.001c-1.495-.016-2.77.346-3.116.479a10 10 0 0 0-.516-.082a8 8 0 0 0-1.312-.127c-1.182-.019-2.203.264-3.05.84C8.66.79 4.729-.534 2.296 1.19C.935 2.153.309 3.873.43 6.304c.041.818.507 3.334 1.243 5.744q.69 2.26 1.433 3.582q.83 1.493 1.714 1.79c.448.148 1.133.143 1.858-.729a56 56 0 0 1 1.945-2.206c.435.235.906.362 1.39.377v.004a11 11 0 0 0-.247.305c-.339.43-.41.52-1.5.745c-.31.064-1.134.233-1.146.811a.6.6 0 0 0 .091.327c.227.423.922.61 1.015.633c1.335.333 2.505.092 3.372-.679c-.017 2.231.077 4.418.345 5.088c.221.553.762 1.904 2.47 1.904q.375.001.829-.094c1.782-.382 2.556-1.17 2.855-2.906c.15-.87.402-2.875.539-4.101c.017-.07.036-.12.057-.136c0 0 .07-.048.427.03l.044.007l.254.022l.015.001c.847.039 1.911-.142 2.531-.43c.644-.3 1.806-1.033 1.595-1.67M2.37 11.876c-.744-2.435-1.178-4.885-1.212-5.571c-.109-2.172.417-3.683 1.562-4.493c1.837-1.299 4.84-.54 6.108-.13l-.01.01C6.795 3.734 6.843 7.226 6.85 7.44c0 .082.006.199.016.36c.034.586.1 1.68-.074 2.918c-.16 1.15.194 2.276.973 3.089q.12.126.252.237c-.347.371-1.1 1.193-1.903 2.158c-.568.682-.96.551-1.088.508c-.392-.13-.813-.587-1.239-1.322c-.48-.839-.963-2.032-1.415-3.512m6.007 5.088a1.6 1.6 0 0 1-.432-.178c.089-.039.237-.09.483-.14c1.284-.265 1.482-.451 1.915-1a8 8 0 0 1 .367-.443a.4.4 0 0 0 .074-.13c.17-.151.272-.11.436-.042c.156.065.308.26.37.475c.03.102.062.295-.045.445c-.904 1.266-2.222 1.25-3.168 1.013m2.094-3.988l-.052.14c-.133.357-.257.689-.334 1.004c-.667-.002-1.317-.288-1.81-.803c-.628-.655-.913-1.566-.783-2.5c.183-1.308.116-2.447.08-3.059l-.013-.22c.296-.262 1.666-.996 2.643-.772c.446.102.718.406.83.928c.585 2.704.078 3.83-.33 4.736a9 9 0 0 0-.23.546m7.364 4.572q-.024.266-.062.596l-.146.438a.4.4 0 0 0-.018.108c-.006.475-.054.649-.115.87a4.8 4.8 0 0 0-.18 1.057c-.11 1.414-.878 2.227-2.417 2.556c-1.515.325-1.784-.496-2.02-1.221a7 7 0 0 0-.078-.227c-.215-.586-.19-1.412-.157-2.555c.016-.561-.025-1.901-.33-2.646q.006-.44.019-.892a.4.4 0 0 0-.016-.113a2 2 0 0 0-.044-.208c-.122-.428-.42-.786-.78-.935c-.142-.059-.403-.167-.717-.087c.067-.276.183-.587.309-.925l.053-.142c.06-.16.134-.325.213-.5c.426-.948 1.01-2.246.376-5.178c-.237-1.098-1.03-1.634-2.232-1.51c-.72.075-1.38.366-1.709.532a6 6 0 0 0-.196.104c.092-1.106.439-3.174 1.736-4.482a4 4 0 0 1 .303-.276a.35.35 0 0 0 .145-.064c.752-.57 1.695-.85 2.802-.833q.616.01 1.174.081c1.94.355 3.244 1.447 4.036 2.383c.814.962 1.255 1.931 1.431 2.454c-1.323-.134-2.223.127-2.68.78c-.992 1.418.544 4.172 1.282 5.496c.135.242.252.452.289.54c.24.583.551.972.778 1.256c.07.087.138.171.189.245c-.4.116-1.12.383-1.055 1.717a35 35 0 0 1-.084.815c-.046.208-.07.46-.1.766m.89-1.621c-.04-.832.27-.919.597-1.01l.135-.041a1 1 0 0 0 .134.103c.57.376 1.583.421 3.007.134c-.202.177-.519.4-.953.601c-.41.19-1.096.333-1.747.364c-.72.034-1.086-.08-1.173-.151m.57-9.271a7 7 0 0 1-.105 1.001c-.055.358-.112.728-.127 1.177c-.014.436.04.89.093 1.33c.107.887.216 1.8-.207 2.701a4 4 0 0 1-.188-.385a8 8 0 0 0-.325-.617c-.616-1.104-2.057-3.69-1.32-4.744c.38-.543 1.342-.566 2.179-.463m.228 7.013l-.085-.107l-.035-.044c.726-1.2.584-2.387.457-3.439c-.052-.432-.1-.84-.088-1.222c.013-.407.066-.755.118-1.092c.064-.415.13-.844.111-1.35a.6.6 0 0 0 .012-.19c-.046-.486-.6-1.938-1.73-3.253a7.8 7.8 0 0 0-2.688-2.04A9.3 9.3 0 0 1 17.62.746c2.052.046 3.675.814 4.824 2.283a1 1 0 0 1 .067.1c.723 1.356-.276 6.275-2.987 10.54m-8.816-6.116c-.025.18-.31.423-.621.423l-.081-.006a.8.8 0 0 1-.506-.315c-.046-.06-.12-.178-.106-.285a.22.22 0 0 1 .093-.149c.118-.089.352-.122.61-.086c.316.044.642.193.61.418m7.93-.411c.011.08-.049.2-.153.31a.72.72 0 0 1-.408.223l-.075.005c-.293 0-.541-.234-.56-.371c-.024-.177.264-.31.56-.352c.298-.042.612.009.636.185"/>`,
dotnet: `<path fill="currentColor" d="M24 8.77h-2.468v7.565h-1.425V8.77h-2.462V7.53H24zm-6.852 7.565h-4.821V7.53h4.63v1.24h-3.205v2.494h2.953v1.234h-2.953v2.604h3.396zm-6.708 0H8.882L4.78 9.863a3 3 0 0 1-.258-.51h-.036q.048.283.048 1.21v5.772H3.157V7.53h1.659l3.965 6.32q.25.392.323.54h.024q-.06-.35-.06-1.185V7.529h1.372zm-8.703-.693a.868.829 0 0 1-.869.829a.868.829 0 0 1-.868-.83a.868.829 0 0 1 .868-.828a.868.829 0 0 1 .869.829"/>`,
docker: `<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.119a.185.185 0 0 0-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 0 0 .186-.186V3.574a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 0 0 .186-.186V6.29a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 0 0 .184-.186V6.29a.185.185 0 0 0-.185-.185H8.1a.185.185 0 0 0-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 0 0 .185-.186V6.29a.185.185 0 0 0-.185-.185H5.136a.186.186 0 0 0-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 0 0 .185-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.186.186 0 0 0-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51q-.508.001-1.01.087c-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199l-.226.327c-.284.438-.49.922-.612 1.43c-.23.97-.09 1.882.403 2.661c-.595.332-1.55.413-1.744.42H.751a.75.75 0 0 0-.75.748a11.4 11.4 0 0 0 .692 4.062c.545 1.428 1.355 2.48 2.41 3.124c1.18.723 3.1 1.137 5.275 1.137a15.7 15.7 0 0 0 2.93-.266a12.3 12.3 0 0 0 3.823-1.389a10.5 10.5 0 0 0 2.61-2.136c1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009c.309-.293.55-.65.707-1.046l.098-.288Z"/>`,
github: `<path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>`,
kotlin: `<path fill="currentColor" d="M24 24H0V0h24L12 12Z"/>`,
swift: `<path fill="currentColor" d="m7.508 0l-.86.002q-.362.001-.724.01q-.198.005-.395.015A9 9 0 0 0 4.348.15A5.5 5.5 0 0 0 2.85.645A5.04 5.04 0 0 0 .645 2.848c-.245.48-.4.972-.495 1.5c-.093.52-.122 1.05-.136 1.576a35 35 0 0 0-.012.724L0 7.508v8.984l.002.862q.002.36.012.722c.014.526.043 1.057.136 1.576c.095.528.25 1.02.495 1.5a5.03 5.03 0 0 0 2.205 2.203c.48.244.97.4 1.498.495c.52.093 1.05.124 1.576.138q.362.01.724.01q.43.003.86.002h8.984l.86-.002q.362 0 .724-.01a10.5 10.5 0 0 0 1.578-.138a5.3 5.3 0 0 0 1.498-.495a5.04 5.04 0 0 0 2.203-2.203c.245-.48.4-.972.495-1.5c.093-.52.124-1.05.138-1.576q.01-.361.01-.722q.003-.431.002-.862V7.508l-.002-.86a34 34 0 0 0-.01-.724a10.5 10.5 0 0 0-.138-1.576a5.3 5.3 0 0 0-.495-1.5A5.04 5.04 0 0 0 21.152.645A5.3 5.3 0 0 0 19.654.15a10.5 10.5 0 0 0-1.578-.138a35 35 0 0 0-.722-.01L16.492 0zm6.035 3.41c4.114 2.47 6.545 7.162 5.549 11.131c-.024.093-.05.181-.076.272l.002.001c2.062 2.538 1.5 5.258 1.236 4.745c-1.072-2.086-3.066-1.568-4.088-1.043a7 7 0 0 1-.281.158l-.02.012l-.002.002c-2.115 1.123-4.957 1.205-7.812-.022a12.57 12.57 0 0 1-5.64-4.838c.649.48 1.35.902 2.097 1.252c3.019 1.414 6.051 1.311 8.197-.002C9.651 12.73 7.101 9.67 5.146 7.191a10.6 10.6 0 0 1-1.005-1.384c2.34 2.142 6.038 4.83 7.365 5.576C8.69 8.408 6.208 4.743 6.324 4.86c4.436 4.47 8.528 6.996 8.528 6.996c.154.085.27.154.36.213q.128-.322.224-.668c.708-2.588-.09-5.548-1.893-7.992z"/>`,
flutter: `<path fill="currentColor" d="M14.314 0L2.3 12L6 15.7L21.684.013h-7.357zm.014 11.072L7.857 17.53l6.47 6.47H21.7l-6.46-6.468l6.46-6.46h-7.37z"/>`,
nixos: `<path fill="currentColor" d="m7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713l-3.137-.008l-1.32 2.34h11.69l-1.353-2.332l-3.192-.006l-2.214-3.865zm6.175 0l-2.687.025l5.846 10.127l1.341-2.34l-1.59-2.765l2.24-3.85l-.683-1.182h-1.336l-1.57 2.705l-1.56-2.72zm6.887 4.195l-5.846 10.125l2.696-.008l1.601-2.76l4.453.016l.682-1.183l-.666-1.157l-3.13-.008L21.778 8.1l-1.365-2.313zM9.432 8.086l-2.696.008l-1.601 2.76l-4.453-.016L0 12.02l.666 1.157l3.13.008l-1.575 2.71l1.365 2.315zM7.33 12.25l-.006.01l-.002-.004l-1.342 2.34l1.59 2.765l-2.24 3.85l.684 1.182H7.35l.004-.006h.001l1.567-2.698l1.558 2.72l2.688-.026l-.004-.006h.01zm2.55 3.93l1.354 2.332l3.192.006l2.215 3.865l1.363-.002l.668-1.156l-1.557-2.713l3.137.008l1.32-2.34z"/>`,
house: `<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
newspaper: `<path d="M15 18h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M18 14h-8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><rect width="8" height="4" x="10" y="6" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
"file-user": `<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M16 22a4 4 0 0 0-8 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="15" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
"code-xml": `<path d="m18 16 4-4-4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m6 8-4 4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m14.5 4-5 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
megaphone: `<path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 6v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
settings: `<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
x: `<path d="M18 6 6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m6 6 12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
"arrow-up": `<path d="m5 12 7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 19V5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
mastodon: `<path fill="currentColor" d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>`,
matrix: `<path fill="currentColor" d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/>`,
} as const;
export type IconName = keyof typeof icons;
export const pdfIconPaths: Record<string, string> = {
email:
"m20 8l-8 5l-8-5V6l8 5l8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2",
phone:
"M6.62 10.79c1.44 2.83 3.76 5.15 6.59 6.59l2.2-2.2c.28-.28.67-.36 1.02-.25c1.12.37 2.32.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57c.11.35.03.74-.25 1.02z",
gitea:
"M4.209 4.603c-.247 0-.525.02-.84.088c-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768c1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367c2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068c0 0 2.107-4.471 2.107-8.823c-.042-1.318-.367-1.55-.443-1.627c-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17c-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12m8.33 2.554c.26.003.509.127.509.127l.868.422l-.529 1.075a.69.69 0 0 0-.614.359a.69.69 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527a.69.69 0 0 0 .347.763a.686.686 0 0 0 .867-.206a.69.69 0 0 0-.069-.882l.916-1.874a.7.7 0 0 0 .237-.02a.66.66 0 0 0 .271-.137a9 9 0 0 1 1.016.512a.76.76 0 0 1 .286.282c.073.21-.073.569-.073.569c-.087.29-.702 1.55-.702 1.55a.69.69 0 0 0-.676.477a.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431c.19-.397.515-1.16.515-1.16c.035-.066.218-.394.103-.814c-.095-.435-.48-.638-.48-.638c-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.7.7 0 0 0-.148-.241l.516-1.062l2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657c-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.1 1.1 0 0 1-.393-.045l-.202-.08l-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.9.9 0 0 1 .35-.077",
};

View File

@@ -0,0 +1,30 @@
---
title: "My 2026 Infrastructure"
description: "Building a Homelab that scales."
pubDate: "2026-02-25"
tags: ["devops", "infra", "nas", "nix"]
---
In the year of our lord, 2026, I figured I'd go over how I have set up this website, along with the other services I host for myself and for my business. Hopefully this proves useful to perspective self-hosters out there!
## The Network
One word: Tailscale.
Tailscale is a wireguard based mesh network where your devices connect to eachother on a "tailnet". Each device can access eachother directly via a name and a tailnet domain you are assigned. For instance, if your device is called ```megatron```, you might connect to it through another machine on the network using ```megatron.stinky-panda.ts.net```. It's that easy! This is the glue of the network, which allows me to expose services from my home without exposing my home IP.
## Lloyd
Lloyd is the main machine here. It runs TrueNAS Scale as its hypervisor, and runs a combination of applications from their "apps" section and docker containers I deploy manually. It, along with everything else on my network, is named after characters from the 1999 PS1 game ```The Legend of Dragoon```. Lloyd is not ever directly exposed from my network, but is instead connected to Tailscale where it connects to the next piece of the puzzle.
## Haschel
Following with my theme, my proxy server hosted on OVHCloud is called Haschel. Haschel is responsible for proxying using NGINX. Now, typically you would use this to point to local services. This is why Tailscale is so useful, however. Say I need to point to a web server on port ```6969``` on Lloyd. All I would do is point to port ```6969``` and hostname ```lloyd.stinky-panda.ts.net```. Tailscale routes the request to Lloyd, and the only IP address ever exposed in the process belongs to Haschel hosted on OVHCloud. One quirk of Haschel is it is running NixOS, which is a Nix based operating system that can be declaratively configured. This means that everything that runs on Haschel can be defined in [this](https://git.atri.dad/atridad/haschel) git repo, and my continuous integration takes care of connecting to the actual server and re-building with the new configuration. Due to the flexibility of Nix, I can swap VPS providers at any time and be up and running in under an hour.
## Putting it all together
I realize that not everyone is familiar with multi-cloud setups or mesh networking, so I made a diagram which I hope will make it clear:
![Diagram of Tailnet](https://msrc.atri.dad/img/pako:eNqNVetv2jAQ_1csT4hWCgjyIuTDpLWV1k4rQwtC02AfTOKAhbGRbVYo4n_f5UHCo9VqpHC-u9_5Hr7zHscyoTjEjcaeCWZCtJ8KhJpmQVe0GaLmjGjatGremChGZpzqZqmKMp14OVdyI5IM8SnNVwEC6VqxFVG7e8mlyuW-3bOJeykf0a2pddIAfvalzp1UCVW1luu6PY9UWpwJ-u4xmsZSJGeO2IFNHL_SMFQZdqZwYX9FmLhbzt_CCshi4d1bwJhvNFgvsUIKeiWqsRd-p1KYiL3mxei6620z4x-m4nBoNKZirsh6gb7_LJT1ZlYwRoRxHRNO0WSK682AmhepllP8p9DPVsIUjQ2TorKSrUei4wXlgC6pWvRj_IjGw6hmDL4-DX6hmwHbooSmUIPk9uyEmvrO5S4Bm_l_zR6pDR18iSoQFSAsyCcByRHU3NwcqdvbQnIRcUTVXxZTnQVc0VlQ-VFvBzy6q7lRdzKkSktBOIqYoSeAyJ7cS6E33DAxvxI6k2-U8x3Efcp1J2MCgBcCZT0TeJNnyFCsSGpyn-HMU7E_abfb76cBtVrocTQaRkB8RmelKTeZxlDJ7Q4Nida52km2i2_OAFELor7i2Fcc54rjXnG8K45_9L3RQA-KxBtOUD5BEqTNDgZIWcCMPrmvKeM8zDrE0kbJJQ3Lfii3rYToBVGK7ELkIc-Ks14Ny248tVjdgdxg2ZCVzVnSd9L-EV0Mm1P0MZkFOJjRfppcOvTu0UUSCmiazgI__jC0qnOBLt38P7r4xhxK_kBTpIvgBzCUjn50UxJ8xI_cBtwLK7KtyLEi14o8K_JPTWILzxVLcGigby28ogoGI2xx_h5McV7lKQ6BhHmQtcEUw7wC2JqI31Kujkh4MuYLHKaEa9ht1gkx9IERaOhVxVXQBdnI3wiDw66T28DhHm9xaHttt-85Hbfb6fm-3-n6Ft7hsBU47U7gd1zX6XY923Hdg4Vf82M77aDndWA5geMHbr8XWJgmzEj1XLyC-WN4-Ac64hx8?type=png)
Feel free to reach out if you have any questions about how I got everything working. I can be reached by email at [me@atri.dad](mailto:me@atri.dad).

View File

@@ -1,20 +0,0 @@
---
title: "Current List of Favourite Tools"
description: "A running list of my favourite tools to use day-to-day."
pubDate: "2025-01-28"
tags: ["list"]
---
I change what I use _constantly_ in order to find something that feels just
right. I wanted to share them here and update them here so when someone asks, I
can just point them to this article.
1. Zed - Zed is a performany open-source code editor that allows you to configure away all of the AI and signin features. Performs well and has its own extensive extension ecosystem. My config to clean it up can be found [here](https://git.atri.dad/atridad/zed-config).
3. Ghostty - A Zig based terminal emulator by one of the founders of Hashicorp. Runs great on MacOS and Linux. No windows for those who are into that.
4. Bitwarden - An open-source password manager. Easy to self host with Vaultwarden and with the recent updates, it has SSH Agent support!
5. iA Writer - A minimalist Markdown editor. For MacOS and Windows only, but really the MacOS version is the most mature. Awesome for focus.
6. Dataflare - A simple but powerful cross-platform database client. Supports most common databases, including LibSQL which is rare!
7. Bruno - A simple and powerful API client, similar to Postman. An critical tool to debug API endpoints.
I hope you found this helpful! This will be periodically updated to avoid
outdated recommendations.

View File

@@ -3,20 +3,30 @@ import { ClientRouter } from "astro:transitions";
import NavigationBar from "../components/NavigationBar.vue"; import NavigationBar from "../components/NavigationBar.vue";
import ScrollUpButton from "../components/ScrollUpButton.vue"; import ScrollUpButton from "../components/ScrollUpButton.vue";
import { config } from "../config"; import { config } from "../config";
import type { OpenGraphImage } from "../types";
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
import "../styles/global.css"; import "../styles/global.css";
export interface Props { export interface Props {
title?: string; title?: string;
description?: string; description?: string;
ogImage?: OpenGraphImage;
ogType?: "website" | "article";
} }
const { title, description } = Astro.props; const { title, description, ogImage, ogType } = Astro.props;
const pageTitle = title const pageTitle = title
? `${title} | ${config.siteConfig.meta.title}` ? `${title} | ${config.siteConfig.meta.title}`
: config.siteConfig.meta.title; : config.siteConfig.meta.title;
const pageDescription = description || config.siteConfig.meta.description; const pageDescription = description || config.siteConfig.meta.description;
const og = config.siteConfig.openGraph;
const canonicalUrl = new URL(Astro.url.pathname, config.siteConfig.meta.url)
.href;
const resolvedOgImage = ogImage || og.image;
const resolvedOgImageUrl = new URL(resolvedOgImage.url, config.siteConfig.meta.url).href;
const resolvedOgType = ogType || og.type || "website";
--- ---
<!doctype html> <!doctype html>
@@ -25,10 +35,24 @@ const pageDescription = description || config.siteConfig.meta.description;
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="canonical" href={canonicalUrl} />
<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={config.siteConfig.meta.author} /> <meta name="author" content={config.siteConfig.meta.author} />
<title>{pageTitle}</title> <title>{pageTitle}</title>
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content={resolvedOgType} />
<meta property="og:url" content={canonicalUrl} />
{og.siteName && <meta property="og:site_name" content={og.siteName} />}
{og.locale && <meta property="og:locale" content={og.locale} />}
<meta property="og:image" content={resolvedOgImageUrl} />
<meta property="og:image:width" content={String(resolvedOgImage.width)} />
<meta property="og:image:height" content={String(resolvedOgImage.height)} />
<meta property="og:image:type" content={resolvedOgImage.type} />
<meta property="og:image:alt" content={resolvedOgImage.alt} />
<ClientRouter /> <ClientRouter />
</head> </head>
<body class="flex flex-col min-h-screen overflow-x-hidden"> <body class="flex flex-col min-h-screen overflow-x-hidden">

View File

@@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
import FuzzyText from "../components/FuzzyText.vue"; import FuzzyText from "../components/FuzzyText.vue";
--- ---
<Layout title="404 - Not Found"> <Layout title="404 - Not Found" description="Page not found">
<div class="flex flex-col items-center justify-center w-full"> <div class="flex flex-col items-center justify-center w-full">
<FuzzyText <FuzzyText
text="404" text="404"

View File

@@ -11,9 +11,7 @@ export const GET: APIRoute = async ({ request }) => {
let tomlContent: string; let tomlContent: string;
// Check if tomlFile is a path (starts with /) or raw content
if (config.resumeConfig.tomlFile.startsWith("/")) { if (config.resumeConfig.tomlFile.startsWith("/")) {
// 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}`;
@@ -27,7 +25,6 @@ export const GET: APIRoute = async ({ request }) => {
tomlContent = await response.text(); tomlContent = await response.text();
} else { } else {
// It's raw TOML content
tomlContent = config.resumeConfig.tomlFile; tomlContent = config.resumeConfig.tomlFile;
} }
const resumeData = TOML.parse(tomlContent); const resumeData = TOML.parse(tomlContent);
@@ -35,7 +32,7 @@ export const GET: APIRoute = async ({ request }) => {
return new Response(JSON.stringify(resumeData), { return new Response(JSON.stringify(resumeData), {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "public, max-age=300", // Cache for 5 minutes "Cache-Control": "public, max-age=300",
}, },
}); });
} catch (error) { } catch (error) {

View File

@@ -4,18 +4,25 @@ import type { ResumeData } from "../../../types";
import * as TOML from "@iarna/toml"; import * as TOML from "@iarna/toml";
import { renderToStream } from "@react-pdf/renderer"; import { renderToStream } from "@react-pdf/renderer";
import { ResumeDocument } from "../../../pdf/ResumeDocument"; import { ResumeDocument } from "../../../pdf/ResumeDocument";
import { getMdiIconPath, fetchProfileIcons } from "../../../utils/icons"; import { pdfIconPaths } from "../../../config/icons";
const generatePDF = async (data: ResumeData) => { const generatePDF = async (data: ResumeData) => {
const resumeConfig = config.resumeConfig; const resumeConfig = config.resumeConfig;
const profileIcons = fetchProfileIcons(data.basics.profiles);
const icons = { const icons: { [key: string]: string } = {
...profileIcons, email: pdfIconPaths.email,
email: getMdiIconPath("mdi:email"), phone: pdfIconPaths.phone,
phone: getMdiIconPath("mdi:phone"),
}; };
if (data.basics.profiles) {
for (const profile of data.basics.profiles) {
const key = profile.network.toLowerCase();
if (pdfIconPaths[key]) {
icons[key] = pdfIconPaths[key];
}
}
}
return await renderToStream(ResumeDocument({ data, resumeConfig, icons })); return await renderToStream(ResumeDocument({ data, resumeConfig, icons }));
}; };
@@ -63,7 +70,7 @@ export const GET: APIRoute = async ({ request }) => {
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
const tomlContent = await request.text(); const { toml: tomlContent } = await request.json();
if (!tomlContent.trim()) { if (!tomlContent.trim()) {
return new Response("TOML content is required", { status: 400 }); return new Response("TOML content is required", { status: 400 });

View File

@@ -4,20 +4,16 @@ 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 { config } from "../config"; import { config } from "../config";
import Logo from "../components/Logo.astro";
--- ---
<Layout> <Layout
<Image title={config.siteConfig.pageOpenGraph.home.title}
src={config.personalInfo.profileImage.src} description={config.siteConfig.pageOpenGraph.home.description}
alt={config.personalInfo.profileImage.alt} ogImage={config.siteConfig.pageOpenGraph.home.image}
widths={[192, 384]} ogType={config.siteConfig.pageOpenGraph.home.type}
sizes="12rem" >
layout="constrained" <Logo client:idle />
loading="eager"
fetchpriority="high"
class="rounded-full mx-auto"
style="max-width: 12rem; width: 100%;"
/>
<h1 class="text-primary text-4xl sm:text-6xl font-bold text-center"> <h1 class="text-primary text-4xl sm:text-6xl font-bold text-center">
{config.personalInfo.name} {config.personalInfo.name}

View File

@@ -1,6 +1,6 @@
--- ---
import { getCollection, render, type CollectionEntry } from "astro:content"; import { getCollection, render, type CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components"; import Icon from "../../components/Icon.astro";
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
export const prerender = true; export const prerender = true;
@@ -17,7 +17,11 @@ const { post }: { post: CollectionEntry<"posts"> } = Astro.props;
const { Content } = await render(post); const { Content } = await render(post);
--- ---
<Layout> <Layout
title={post.data.title}
description={post.data.description}
ogType="article"
>
<div class="w-full 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">
@@ -29,7 +33,7 @@ const { Content } = await render(post);
<div <div
class="flex items-center flex-row gap-2 text-base-content opacity-75" class="flex items-center flex-row gap-2 text-base-content opacity-75"
> >
<Icon name="mdi:clock" class="text-xl" /> <Icon name="clock" class="text-xl" />
<time datetime={post.data.pubDate.toISOString()}> <time datetime={post.data.pubDate.toISOString()}>
{ {
new Date(post.data.pubDate).toLocaleDateString( new Date(post.data.pubDate).toLocaleDateString(
@@ -49,9 +53,19 @@ const { Content } = await render(post);
href="/posts" href="/posts"
class="btn btn-outline btn-primary btn-sm font-bold" class="btn btn-outline btn-primary btn-sm font-bold"
> >
<Icon name="mdi:arrow-left" class="text-lg" /> <Icon name="arrow-left" class="text-lg" />
Back Back
</a> </a>
{/* RSS feed button */}
<a
href="/feed"
class="btn btn-outline btn-primary btn-sm font-bold"
aria-label="RSS Feed"
>
<Icon name="rss" class="text-lg" />
RSS
</a>
</div> </div>
{ {
@@ -59,7 +73,7 @@ const { Content } = await render(post);
<div class="flex gap-2 flex-wrap mb-6"> <div class="flex gap-2 flex-wrap mb-6">
{post.data.tags.map((tag: string) => ( {post.data.tags.map((tag: string) => (
<div class="badge badge-primary font-bold"> <div class="badge badge-primary font-bold">
<Icon name="mdi:tag" class="text-lg" /> <Icon name="tag" class="text-lg" />
{tag} {tag}
</div> </div>
))} ))}

View File

@@ -1,7 +1,7 @@
--- ---
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components"; import { config } from "../config";
// Get all posts from the content collection // Get all posts from the content collection
const posts = await getCollection("posts"); const posts = await getCollection("posts");
@@ -21,141 +21,61 @@ function formatDate(date: Date): string {
} }
--- ---
<Layout> <Layout
<div class="w-full p-4 sm:p-8"> title={config.siteConfig.pageOpenGraph.posts.title}
description={config.siteConfig.pageOpenGraph.posts.description}
ogImage={config.siteConfig.pageOpenGraph.posts.image}
ogType={config.siteConfig.pageOpenGraph.posts.type}
>
<div class="w-full max-w-3xl mx-auto 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"
> >
Posts Posts
</h1> </h1>
{/* Mobile: One-sided compact timeline */}
<ul
class="timeline timeline-vertical timeline-compact timeline-snap-icon max-w-3xl mx-auto px-4 md:hidden"
>
{
sortedPosts.map((post, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon
name="mdi:circle"
class="w-4 h-4 text-primary"
/>
</div>
<div class="timeline-end mb-8 ml-4">
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
<time class="font-mono text-sm opacity-60">
{formatDate(new Date(post.data.pubDate))}
</time>
<a
href={`/post/${post.id}`}
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{post.data.title}
</h3>
<p class="text-sm opacity-80 mt-1">
{post.data.description ||
"No description available."}
</p>
</a>
{post.data.tags &&
post.data.tags.length > 0 && (
<div class="flex gap-1 flex-wrap mt-2">
{post.data.tags
.slice(0, 3)
.map((tag: string) => (
<span class="badge badge-sm badge-outline">
{tag}
</span>
))}
</div>
)}
</div>
</div>
{index < sortedPosts.length - 1 && (
<hr class="bg-primary" />
)}
</li>
))
}
</ul>
{/* Desktop: Dual-sided alternating timeline */}
<ul
class="timeline timeline-vertical timeline-snap-icon max-w-3xl mx-auto px-4 hidden md:block"
>
{
sortedPosts.map((post, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon
name="mdi:circle"
class="w-4 h-4 text-primary"
/>
</div>
<div
class={`timeline-${index % 2 === 0 ? "start" : "end"} text-${index % 2 === 0 ? "end" : "start"} mb-8 mx-4`}
>
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
<time class="font-mono text-sm opacity-60">
{formatDate(new Date(post.data.pubDate))}
</time>
<a
href={`/post/${post.id}`}
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{post.data.title}
</h3>
<p class="text-sm opacity-80 mt-1">
{post.data.description ||
"No description available."}
</p>
</a>
{post.data.tags &&
post.data.tags.length > 0 && (
<div
class={`flex gap-1 flex-wrap mt-2 ${index % 2 === 0 ? "justify-end" : "justify-start"}`}
>
{post.data.tags
.slice(0, 3)
.map((tag: string) => (
<span class="badge badge-sm badge-outline">
{tag}
</span>
))}
</div>
)}
</div>
</div>
{index < sortedPosts.length - 1 && (
<hr class="bg-primary" />
)}
</li>
))
}
</ul>
{ {
sortedPosts.length === 0 && ( sortedPosts.length === 0 ? (
<p class="text-center text-gray-500 mt-12"> <p class="text-center text-gray-500 mt-12">
No posts available yet. Check back soon! No posts available yet. Check back soon!
</p> </p>
) : (
<ul class="flex flex-col bg-base-100 rounded-box shadow-md border border-base-content/20 divide-y divide-base-content/20">
{sortedPosts.map((post) => (
<li class="flex items-center hover:bg-base-200/50 transition-colors p-4 group relative rounded-none first:rounded-t-box last:rounded-b-box">
<a href={`/post/${post.id}`} class="absolute inset-0 z-0" aria-label={`Read ${post.data.title}`}></a>
<div class="w-24 sm:w-32 flex-none opacity-80 flex flex-col justify-center text-right pr-4 border-r border-base-content/20 z-10 pointer-events-none">
<span class="font-mono text-sm sm:text-base font-bold">
{post.data.pubDate.toLocaleDateString("en-us", { month: "short", day: "numeric" })}
</span>
<span class="text-xs opacity-70">
{post.data.pubDate.getFullYear()}
</span>
</div>
<div class="flex-grow flex flex-col gap-1 justify-center pl-4 z-10 pointer-events-none">
<h2
class="font-bold text-lg sm:text-xl text-primary group-hover:text-accent transition-colors"
>
{post.data.title}
</h2>
<p class="text-sm opacity-80 line-clamp-2 leading-relaxed">
{post.data.description || "No description available."}
</p>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex gap-2 mt-1">
{post.data.tags.slice(0, 3).map((tag: string) => (
<span class="badge badge-xs badge-outline opacity-70">
{tag}
</span>
))}
</div>
)}
</div>
</li>
))}
</ul>
) )
} }
</div> </div>

View File

@@ -1,21 +1,194 @@
--- ---
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import ProjectsIsland from "../components/ProjectsIsland.astro"; import Icon from "../components/Icon.astro";
import ProjectsLoader from "../components/ProjectsLoader.astro"; import { config } from "../config";
import { fetchGiteaInfoFromUrl, formatRelativeTime } from "../utils/gitea";
import type { Project } from "../types";
export const prerender = false; export const prerender = false;
Astro.response.headers.set(
"Cache-Control",
"public, max-age=300, s-maxage=300, stale-while-revalidate=60",
);
function isGiteaDomain(url: string): boolean {
if (!config.siteConfig.giteaDomains) return true;
try {
const urlObj = new URL(url);
return config.siteConfig.giteaDomains.some(
(domain) => urlObj.origin === new URL(domain).origin,
);
} catch {
return false;
}
}
const projectsWithGiteaInfo = await Promise.all(
config.projects.map(async (project) => {
if (
project.gitLink &&
!project.giteaInfo &&
isGiteaDomain(project.gitLink)
) {
const giteaInfo = await fetchGiteaInfoFromUrl(project.gitLink);
if (giteaInfo) {
return { ...project, giteaInfo } as Project;
}
}
return project;
}),
);
const sortedProjects = projectsWithGiteaInfo.sort((a, b) => {
const aTime = a.giteaInfo?.updatedAt
? new Date(a.giteaInfo.updatedAt).getTime()
: 0;
const bTime = b.giteaInfo?.updatedAt
? new Date(b.giteaInfo.updatedAt).getTime()
: 0;
return bTime - aTime;
});
--- ---
<Layout> <Layout
<div class="w-full p-4 sm:p-8"> title={config.siteConfig.pageOpenGraph.projects.title}
description={config.siteConfig.pageOpenGraph.projects.description}
ogImage={config.siteConfig.pageOpenGraph.projects.image}
ogType={config.siteConfig.pageOpenGraph.projects.type}
>
<div class="w-full max-w-3xl mx-auto 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"
> >
Projects Projects
</h1> </h1>
<ProjectsIsland server:defer> <ul
<ProjectsLoader slot="fallback" /> class="flex flex-col bg-base-100 rounded-box shadow-md border border-base-content/20 divide-y divide-base-content/20"
</ProjectsIsland> >
{
sortedProjects.map((project) => (
<li class="flex items-center hover:bg-base-200/50 transition-colors p-4 group relative rounded-none first:rounded-t-box last:rounded-b-box">
{/* Project icon/avatar */}
<div class="flex-none z-10 mr-4">
{project.giteaInfo?.avatarUrl ? (
<img
src={project.giteaInfo.avatarUrl}
alt={`${project.name} avatar`}
class="w-12 h-12 rounded-lg object-cover"
/>
) : (
<div class="w-12 h-12 rounded-lg bg-accent flex items-center justify-center">
<Icon
name="code-braces"
class="w-6 h-6 text-accent-content"
/>
</div>
)}
</div>
{/* Main content */}
<div class="flex-grow flex flex-col justify-center gap-1 z-10 pointer-events-none">
<div class="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-2">
<h3 class="font-bold text-lg sm:text-xl text-primary group-hover:text-accent transition-colors">
{project.name}
</h3>
{project.giteaInfo?.updatedAt && (
<span class="text-xs opacity-60">
{formatRelativeTime(
project.giteaInfo.updatedAt,
)}
</span>
)}
</div>
<p class="text-sm opacity-80 leading-relaxed">
{project.description}
</p>
{/* Languages & Topics */}
<div class="flex flex-wrap gap-1 mt-1">
{project.giteaInfo?.languages
?.slice(0, 3)
.map((lang: string) => (
<span class="badge badge-xs badge-primary">
{lang}
</span>
))}
{project.giteaInfo?.topics
?.slice(0, 4)
.map((topic: string) => (
<span class="badge badge-xs badge-outline opacity-70">
{topic}
</span>
))}
</div>
</div>
{/* Action buttons */}
<div class="flex-none flex flex-wrap gap-1 justify-end ml-4 z-20">
{project.webLink && (
<a
href={project.webLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-primary hover:bg-primary hover:text-primary-content transition-all"
aria-label={`Visit ${project.name} website`}
>
<Icon name="web" class="w-5 h-5" />
</a>
)}
{project.gitLink && (
<a
href={project.gitLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-success hover:bg-success hover:text-success-content transition-all"
aria-label={`View ${project.name} source`}
>
<Icon
name="gitea"
class="w-5 h-5"
/>
</a>
)}
{project.iosLink && (
<a
href={project.iosLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-accent hover:bg-accent hover:text-accent-content transition-all"
aria-label={`${project.name} on iOS`}
>
<Icon name="apple" class="w-5 h-5" />
</a>
)}
{project.androidLink && (
<a
href={project.androidLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-square btn-ghost text-success hover:bg-success hover:text-success-content transition-all"
aria-label={`${project.name} on Android`}
>
<Icon
name="google-play"
class="w-5 h-5"
/>
</a>
)}
</div>
</li>
))
}
</ul>
{
sortedProjects.length === 0 && (
<p class="text-center text-gray-500 mt-12">
No projects available yet. Check back soon!
</p>
)
}
</div> </div>
</Layout> </Layout>

View File

@@ -1,5 +1,5 @@
--- ---
import { Icon } from "astro-icon/components"; import Icon from "../components/Icon.astro";
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import ResumeSkills from "../components/ResumeSkills.vue"; import ResumeSkills from "../components/ResumeSkills.vue";
import ResumeDownloadButton from "../components/ResumeDownloadButton.vue"; import ResumeDownloadButton from "../components/ResumeDownloadButton.vue";
@@ -95,7 +95,12 @@ if (!data) {
} }
--- ---
<Layout title="Resume"> <Layout
title={config.siteConfig.pageOpenGraph.resume.title}
description={config.siteConfig.pageOpenGraph.resume.description}
ogImage={config.siteConfig.pageOpenGraph.resume.image}
ogType={config.siteConfig.pageOpenGraph.resume.type}
>
<ResumeSettingsModal client:load /> <ResumeSettingsModal client:load />
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full"> <div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-4xl w-full">
<h1 <h1
@@ -113,13 +118,13 @@ if (!data) {
href={`mailto:${data.basics.email}`} href={`mailto:${data.basics.email}`}
class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base" class="link link-hover inline-flex items-center gap-1 text-sm sm:text-base"
> >
<Icon name="mdi:email" /> {data.basics.email} <Icon name="email" /> {data.basics.email}
</a> </a>
) )
} }
{ {
data.basics.profiles?.map((profile) => { data.basics.profiles?.map((profile) => {
const iconName = `simple-icons:${profile.network.toLowerCase()}`; const iconName = profile.network.toLowerCase();
return ( return (
<a <a
href={profile.url} href={profile.url}
@@ -212,7 +217,7 @@ if (!data) {
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2" class="inline-flex items-center gap-1 text-primary hover:text-primary-focus text-sm mt-2"
> >
<Icon name="mdi:link" /> <Icon name="link" />
Website Website
</a> </a>
)} )}

View File

@@ -1,6 +1,6 @@
--- ---
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import { Icon } from "astro-icon/components"; import Icon from "../components/Icon.astro";
import { config } from "../config"; import { config } from "../config";
// Sort talks by date, newest first // Sort talks by date, newest first
@@ -18,140 +18,66 @@ function formatDate(dateStr: string): string {
} }
--- ---
<Layout> <Layout
<div class="w-full p-4 sm:p-8"> title={config.siteConfig.pageOpenGraph.talks.title}
description={config.siteConfig.pageOpenGraph.talks.description}
ogImage={config.siteConfig.pageOpenGraph.talks.image}
ogType={config.siteConfig.pageOpenGraph.talks.type}
>
<div class="w-full max-w-3xl mx-auto 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"
> >
Talks Talks
</h1> </h1>
{/* Single talk: Simple centered card without timeline */}
{sortedTalks.length === 1 && (
<div class="max-w-xl mx-auto px-4">
<div class="border border-base-content/20 rounded-box p-6 bg-base-200 hover:border-primary transition-colors">
{sortedTalks[0].date && (
<time class="font-mono text-sm opacity-60">
{formatDate(sortedTalks[0].date)}
</time>
)}
<a
href={sortedTalks[0].link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-xl font-bold text-primary group-hover:text-accent transition-colors">
{sortedTalks[0].name}
</h3>
<p class="opacity-80 mt-2">
{sortedTalks[0].description}
</p>
<span class="inline-flex items-center gap-1 text-sm text-primary mt-3 group-hover:text-accent transition-colors">
<Icon name="mdi:open-in-new" class="w-4 h-4" />
View talk
</span>
</a>
</div>
</div>
)}
{/* Multiple talks: Mobile one-sided compact timeline */}
{sortedTalks.length > 1 && (
<ul class="timeline timeline-vertical timeline-compact timeline-snap-icon max-w-3xl mx-auto px-4 md:hidden">
{
sortedTalks.map((talk, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon name="mdi:circle" class="w-4 h-4 text-primary" />
</div>
<div class="timeline-end mb-8 ml-4">
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
{talk.date && (
<time class="font-mono text-sm opacity-60">
{formatDate(talk.date)}
</time>
)}
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{talk.name}
</h3>
<p class="text-sm opacity-80 mt-1">
{talk.description}
</p>
</a>
</div>
</div>
{index < sortedTalks.length - 1 && <hr class="bg-primary" />}
</li>
))
}
</ul>
)}
{/* Multiple talks: Desktop dual-sided alternating timeline */}
{sortedTalks.length > 1 && (
<ul class="timeline timeline-vertical timeline-snap-icon max-w-3xl mx-auto px-4 hidden md:block">
{
sortedTalks.map((talk, index) => (
<li>
{index > 0 && <hr class="bg-primary" />}
<div class="timeline-middle">
<Icon name="mdi:circle" class="w-4 h-4 text-primary" />
</div>
<div class={`timeline-${index % 2 === 0 ? 'start' : 'end'} text-${index % 2 === 0 ? 'end' : 'start'} mb-8 mx-4`}>
<div class="border border-base-content/20 rounded-box p-4 bg-base-200 hover:border-primary transition-colors">
{talk.date && (
<time class="font-mono text-sm opacity-60">
{formatDate(talk.date)}
</time>
)}
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<h3 class="text-lg font-bold text-primary group-hover:text-accent transition-colors">
{talk.name}
</h3>
<p class="text-sm opacity-80 mt-1">
{talk.description}
</p>
</a>
</div>
</div>
{index < sortedTalks.length - 1 && <hr class="bg-primary" />}
</li>
))
}
</ul>
)}
{ {
sortedTalks.length === 0 && ( sortedTalks.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>
) : (
<ul class="flex flex-col bg-base-100 rounded-box shadow-md border border-base-content/20 divide-y divide-base-content/20">
{sortedTalks.map((talk) => {
const talkDate = talk.date ? new Date(talk.date) : null;
return (
<li class="flex items-center hover:bg-base-200/50 transition-colors p-4 group relative rounded-none first:rounded-t-box last:rounded-b-box">
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="absolute inset-0 z-0"
aria-label={`View ${talk.name}`}
></a>
<div class="w-24 sm:w-32 flex-none opacity-80 flex flex-col justify-center text-right pr-4 border-r border-base-content/20 z-10 pointer-events-none">
{talkDate ? (
<>
<span class="font-mono text-sm sm:text-base font-bold">
{talkDate.toLocaleDateString("en-us", { month: "short", day: "numeric" })}
</span>
<span class="text-xs opacity-70">
{talkDate.getFullYear()}
</span>
</>
) : (
<span class="text-xs opacity-50 italic">Undated</span>
)}
</div>
<div class="flex-grow flex flex-col gap-1 justify-center pl-4 z-10 pointer-events-none">
<h2 class="font-bold text-lg sm:text-xl text-primary group-hover:text-accent transition-colors flex items-center gap-2">
{talk.name}
<Icon name="open-in-new" class="w-4 h-4 opacity-50 group-hover:opacity-100 transition-opacity" />
</h2>
<p class="text-sm opacity-80 line-clamp-2 leading-relaxed">
{talk.description}
</p>
</div>
</li>
);
})}
</ul>
) )
} }
</div> </div>

View File

@@ -118,12 +118,12 @@ const styles = StyleSheet.create({
}, },
progressBarBg: { progressBarBg: {
height: 4, height: 4,
backgroundColor: "#E5E7EB", // bg-gray-200 backgroundColor: "#E5E7EB",
borderRadius: 2, borderRadius: 2,
}, },
progressBarFill: { progressBarFill: {
height: 4, height: 4,
backgroundColor: "#2563EB", // bg-blue-600 backgroundColor: "#2563EB",
borderRadius: 2, borderRadius: 2,
}, },
// Education // Education
@@ -272,7 +272,7 @@ const AwardsSection = ({ awards }: { awards: ResumeData["awards"] }) => (
interface ResumeDocumentProps { interface ResumeDocumentProps {
data: ResumeData; data: ResumeData;
resumeConfig: any; resumeConfig: any;
icons: { [key: string]: string }; // Map of icon name to SVG path icons: { [key: string]: string };
} }
export const ResumeDocument = ({ export const ResumeDocument = ({
@@ -353,8 +353,8 @@ export const ResumeDocument = ({
)} )}
{data.basics.profiles?.map((profile: any, i: number) => ( {data.basics.profiles?.map((profile: any, i: number) => (
<View key={i} style={styles.contactItem}> <View key={i} style={styles.contactItem}>
{icons[profile.network] && ( {icons[profile.network.toLowerCase()] && (
<Icon path={icons[profile.network]} /> <Icon path={icons[profile.network.toLowerCase()]} />
)} )}
<Link <Link
src={profile.url} src={profile.url}

View File

@@ -2,6 +2,5 @@
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@plugin "daisyui" { @plugin "daisyui" {
themes: false; themes: dracula --default;
} }
@plugin "./macchiato.ts";

View File

@@ -1,10 +0,0 @@
import { createCatppuccinPlugin } from "@catppuccin/daisyui";
export default createCatppuccinPlugin(
"macchiato",
{},
{
default: true,
prefersdark: true,
},
);

View File

@@ -1,12 +1,6 @@
import type { ImageMetadata } from "astro"; import type { ImageMetadata } from "astro";
import type { Component } from "vue";
import type { GiteaRepoInfo } from "./utils/gitea"; import type { GiteaRepoInfo } from "./utils/gitea";
import type { IconName } from "./config/icons";
// Icon Types
export type LucideIcon = Component;
export type AstroIconName = string;
export type CustomIconComponent = Component;
export type IconType = LucideIcon | AstroIconName | CustomIconComponent;
export interface Talk { export interface Talk {
id: string; id: string;
@@ -32,7 +26,7 @@ export interface SocialLink {
id: string; id: string;
name: string; name: string;
url: string; url: string;
icon: IconType; icon: IconName;
ariaLabel: string; ariaLabel: string;
} }
@@ -40,7 +34,7 @@ export interface TechLink {
id: string; id: string;
name: string; name: string;
url: string; url: string;
icon: IconType; icon: IconName;
ariaLabel: string; ariaLabel: string;
} }
@@ -49,7 +43,7 @@ export interface NavigationItem {
name: string; name: string;
path: string; path: string;
tooltip: string; tooltip: string;
icon: IconType; icon: IconName;
enabled?: boolean; enabled?: boolean;
isActive?: (path: string) => boolean; isActive?: (path: string) => boolean;
} }
@@ -64,7 +58,7 @@ export type ResumeSectionKey =
| "awards"; | "awards";
export interface ResumeConfig { export interface ResumeConfig {
tomlFile: string; // Can be a file path or raw TOML content tomlFile: string;
layout?: { layout?: {
leftColumn?: ResumeSectionKey[]; leftColumn?: ResumeSectionKey[];
rightColumn?: ResumeSectionKey[]; rightColumn?: ResumeSectionKey[];
@@ -169,6 +163,28 @@ export interface HomepageSections {
}; };
} }
export interface OpenGraphImage {
url: string;
width: number;
height: number;
type: string;
alt: string;
}
export interface OpenGraphConfig {
image: OpenGraphImage;
type?: "website" | "article";
locale?: string;
siteName?: string;
}
export interface PageOpenGraph {
title?: string;
description?: string;
image?: OpenGraphImage;
type?: "website" | "article";
}
export interface SiteConfig { export interface SiteConfig {
personal: PersonalInfo; personal: PersonalInfo;
homepage: HomepageSections; homepage: HomepageSections;
@@ -179,6 +195,14 @@ export interface SiteConfig {
url: string; url: string;
author: string; author: string;
}; };
openGraph: OpenGraphConfig;
pageOpenGraph: {
home: PageOpenGraph;
posts: PageOpenGraph;
projects: PageOpenGraph;
talks: PageOpenGraph;
resume: PageOpenGraph;
};
giteaDomains?: string[]; giteaDomains?: string[];
} }

View File

@@ -4,6 +4,7 @@ export interface GiteaRepoInfo {
size: number; size: number;
defaultBranch: string; defaultBranch: string;
topics: string[]; topics: string[];
avatarUrl?: string;
} }
export interface GiteaConfig { export interface GiteaConfig {
@@ -74,6 +75,7 @@ export async function fetchGiteaRepoInfo(
size: data.size || 0, size: data.size || 0,
defaultBranch: data.default_branch || "main", defaultBranch: data.default_branch || "main",
topics: Array.isArray(data.topics) ? data.topics : [], topics: Array.isArray(data.topics) ? data.topics : [],
avatarUrl: data.avatar_url,
}; };
} catch (error) { } catch (error) {
return null; return null;

View File

@@ -1,35 +0,0 @@
import { mdiEmail, mdiPhone, mdiDownload, mdiLink } from "@mdi/js";
import * as simpleIcons from "simple-icons";
export function getSimpleIconPath(network: string): string {
try {
const slug = network.toLowerCase().normalize("NFKD").replace(/[^\w]/g, "");
const iconKey = `si${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;
const icon = (simpleIcons as any)[iconKey];
return icon ? icon.path : "";
} catch (error) {
console.warn(`Error finding icon for network: ${network}`, error);
return "";
}
}
export function getMdiIconPath(iconName: string): string {
const iconMap: { [key: string]: string } = {
"mdi:email": mdiEmail,
"mdi:phone": mdiPhone,
"mdi:download": mdiDownload,
"mdi:link": mdiLink,
};
return iconMap[iconName] || "";
}
export const fetchProfileIcons = (profiles: any[]) => {
const profileIcons: { [key: string]: string } = {};
if (profiles) {
for (const profile of profiles) {
profileIcons[profile.network] = getSimpleIconPath(profile.network);
}
}
return profileIcons;
};