diff --git a/public/homelabber.png b/public/homelabber.png
new file mode 100644
index 0000000..892151f
Binary files /dev/null and b/public/homelabber.png differ
diff --git a/src/components/RichTextRenderer.tsx b/src/components/RichTextRenderer.tsx
index fbdae71..13727ab 100644
--- a/src/components/RichTextRenderer.tsx
+++ b/src/components/RichTextRenderer.tsx
@@ -5,6 +5,60 @@ interface RichTextRendererProps {
content: string;
}
+const parseInline = (text: string) => {
+ const parts = text.split(/(\*\*.*?\*\*)/g);
+ return parts.map((part, index) => {
+ if (part.startsWith('**') && part.endsWith('**')) {
+ return {part.slice(2, -2)};
+ }
+ return part;
+ });
+};
+
+const parseMarkdown = (text: string) => {
+ const lines = text.split('\n');
+ const elements: React.ReactNode[] = [];
+ let currentListItems: React.ReactNode[] = [];
+
+ lines.forEach((line, i) => {
+ const trimmed = line.trim();
+ if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) {
+ const content = trimmed.substring(2);
+ currentListItems.push(
+
+ {parseInline(content)}
+
+ );
+ } else {
+ if (currentListItems.length > 0) {
+ elements.push(
+
+ );
+ currentListItems = [];
+ }
+ if (trimmed.length > 0) {
+ elements.push(
+
+ {parseInline(line)}
+
+ );
+ }
+ }
+ });
+
+ if (currentListItems.length > 0) {
+ elements.push(
+
+ );
+ }
+
+ return elements;
+};
+
const RichTextRenderer: React.FC = ({ content }) => {
if (!content) return null;
@@ -22,11 +76,11 @@ const RichTextRenderer: React.FC = ({ content }) => {
return ;
}
- // Regular text (split by newlines for paragraph handling)
+ // Regular text with markdown parsing
return (
-
- {part}
-
+
+ {parseMarkdown(part)}
+
);
})}
diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx
index 36f3ce1..bbcd1f0 100644
--- a/src/pages/Projects.tsx
+++ b/src/pages/Projects.tsx
@@ -103,6 +103,100 @@ jobs:
content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull"
},
]
+ },
+ {
+ id: 102,
+ title: "Home Lab Architecture",
+ description: "A comprehensive self-hosted homelab running 20+ services. Orchestrated with Docker Compose for media automation, personal cloud storage, and secure remote access.",
+ techStack: ["Docker", "Nginx", "Tailscale", "Authentik", "PostgreSQL"],
+ image: "/homelabber.png", // Placeholder
+ links: {},
+ hasDetails: true,
+ sections: [
+ {
+ type: 'text',
+ title: 'Overview',
+ content: "My homelab acts as my personal cloud, giving me control over my data and providing a playground for learning system administration. It runs on a dedicated machine using Docker Compose to manage a suite of interconnected microservices. Access is secured via Tailscale VPN and Authentik SSO."
+ },
+ {
+ type: 'text',
+ title: 'Media & Automation',
+ content: "The core of the lab is the media stack. **Jellyfin** serves as the streaming frontend. Behind the scenes, the *Arr stack (**Sonarr**, **Radarr**, **Lidarr**) automates content acquisition, managed by **Prowlarr** for indexers and **qBittorrent** for downloads. **Jellyseerr** provides a clean interface for requesting content, and **Bazarr** handles subtitles."
+ },
+ {
+ type: 'text',
+ title: 'Personal Cloud & Productivity',
+ content: "I've replaced several commercial services with self-hosted alternatives:\n\n* **Immich**: A powerful photo and video backup solution, replacing Google Photos.\n* **Gitea**: My self-hosted Git server for code management (including CI/CD runners).\n* **Mealie**: A recipe manager to organize and plan meals."
+ },
+ {
+ type: 'text',
+ title: 'Infrastructure & Security',
+ content: "Reliability and security are key. **Nginx** handles reverse proxying with **Certbot** managing SSL certificates. **Fail2ban** prevents intrusion attempts. **Authentik** provides a unified identity layer. **Portainer** offers a GUI for Docker management, and **Tailscale** creates a secure mesh network for remote access without exposing ports."
+ },
+ {
+ type: 'text',
+ title: 'Monitoring',
+ content: "**Homepage** serves as the main dashboard. **Beszel** and **Uptime Kuma** monitor system resources and service availability, while **Speedtest Tracker** keeps a historic log of network performance."
+ },
+ {
+ type: 'code',
+ language: 'yaml',
+ title: 'Docker Compose Configuration',
+ code: `services:
+ nginx:
+ image: nginx:stable
+ restart: unless-stopped
+ ports: ["80:80", "443:443"]
+ # ... configurations
+
+ immich-server:
+ image: ghcr.io/immich-app/immich-server:release
+ environment:
+ - DB_HOSTNAME=immich-postgres
+ - REDIS_HOSTNAME=immich-redis
+ depends_on: [immich-redis, immich-postgres]
+
+ gitea:
+ image: gitea/gitea:latest
+ ports: ["3000:3000", "2222:22"]
+ environment:
+ - GITEA__actions__ENABLED=true
+
+ jellyfin:
+ image: jellyfin/jellyfin:latest
+ ports: ["8096:8096"]
+ volumes:
+ - ./media:/media
+
+ # The *Arr Stack
+ sonarr:
+ image: lscr.io/linuxserver/sonarr:latest
+ volumes: [./media:/tv, ./downloads:/downloads]
+ radarr:
+ image: lscr.io/linuxserver/radarr:latest
+ volumes: [./media:/movies, ./downloads:/downloads]
+
+ # Monitoring
+ beszel:
+ image: henrygd/beszel:latest
+ ports: ["8090:8090"]
+
+ uptime-kuma:
+ image: louislam/uptime-kuma:latest
+ ports: ["3002:3001"]
+
+ # Security & Networking
+ tailscale:
+ image: tailscale/tailscale:latest
+ network_mode: host
+
+ authentik-server:
+ image: ghcr.io/goauthentik/server:latest
+ ports: ["9001:9000", "9444:9443"]
+
+# ... full configuration includes 20+ services`
+ }
+ ]
}
];