feat: update landing page

This commit is contained in:
Alexander Daichendt 2025-08-24 12:50:42 +02:00
parent 7481d043f2
commit 46e485a668
21 changed files with 735 additions and 283 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,6 +1,4 @@
---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import "../styles/global.css";
import ubuntuRegularWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-400-normal.woff2?url";
import ubuntuBoldWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-700-normal.woff2?url";

View file

@ -0,0 +1,233 @@
---
import type { ImageMetadata } from "astro";
import { Icon } from "astro-icon/components";
interface Props {
images: {
src: ImageMetadata;
alt: string;
}[];
}
const { images } = Astro.props;
const carouselId = `carousel-${Math.random().toString(36).slice(2, 11)}`;
const helpId = `${carouselId}-help`;
---
<section
id={carouselId}
class="relative overflow-hidden rounded-lg"
role="region"
aria-roledescription="carousel"
aria-label="Image carousel"
data-carousel-id={carouselId}
tabindex="0"
aria-describedby={helpId}
>
<!-- Slides -->
<div class="relative h-0 pb-[56.25%]">
{
images.map((image, i) => (
<img
src={image.src.src}
alt={image.alt}
class={`
absolute inset-0 w-full h-full object-cover object-top
transition-opacity duration-500 ease-in-out
${i === 0 ? "opacity-100" : "opacity-0"}
carousel-slide
`}
data-index={i}
/>
))
}
</div>
<!-- Navigation Buttons -->
<button
type="button"
class="absolute left-4 top-1/2 -translate-y-1/2
flex h-10 w-10 items-center justify-center
rounded-full bg-white/70 backdrop-blur-sm
text-gray-800 hover:bg-white focus:outline-none
focus-visible:ring-2 focus-visible:ring-indigo-600
transition-colors"
aria-label="Previous slide"
data-dir="prev"
>
<Icon name="mdi:chevron-left" class="h-5 w-5" />
<span class="sr-only">Previous</span>
</button>
<button
type="button"
class="absolute right-4 top-1/2 -translate-y-1/2
flex h-10 w-10 items-center justify-center
rounded-full bg-white/70 backdrop-blur-sm
text-gray-800 hover:bg-white focus:outline-none
focus-visible:ring-2 focus-visible:ring-indigo-600
transition-colors"
aria-label="Next slide"
data-dir="next"
>
<Icon name="mdi:chevron-right" class="h-5 w-5" />
<span class="sr-only">Next</span>
</button>
<!-- Theater toggle button (bottom-right) -->
<button
type="button"
class="absolute bottom-4 right-4 flex h-10 w-10 items-center justify-center
rounded-full bg-white/30 backdrop-blur-sm text-gray-800
hover:bg-white focus:outline-none focus-visible:ring-2
focus-visible:ring-indigo-600 transition-colors"
aria-label="Enter wide mode"
aria-pressed="false"
data-theater-toggle
>
<Icon name="mdi:arrow-expand-horizontal" class="h-5 w-5" />
</button>
<!-- Live region for screenreader announcements -->
<div
class="sr-only"
aria-live="polite"
aria-atomic="true"
data-live-announcer
>
</div>
<!-- Hidden helper text for screen readers -->
<p id={helpId} class="sr-only">
Carousel focused. Use Left and Right Arrow keys to change slides. Press T to
toggle wide mode. Press Escape to exit wide mode.
</p>
</section>
<script define:vars={{ carouselId }}>
document.addEventListener("DOMContentLoaded", () => {
const carousel = document.getElementById(carouselId);
if (!carousel) return;
const slides = carousel.querySelectorAll(".carousel-slide");
const prevBtn = carousel.querySelector('[data-dir="prev"]');
const nextBtn = carousel.querySelector('[data-dir="next"]');
const liveAnnouncer = carousel.querySelector("[data-live-announcer]");
const theaterBtn = carousel.querySelector("[data-theater-toggle]");
let current = 0;
const total = slides.length;
let autoRotateTimer;
let theater = false;
const updateTheaterButton = () => {
if (!theaterBtn) return;
theaterBtn.setAttribute("aria-pressed", theater ? "true" : "false");
theaterBtn.setAttribute(
"aria-label",
theater ? "Exit wide mode" : "Enter wide mode",
);
const icon = theaterBtn.querySelector("svg");
if (icon) {
icon.setAttribute("data-icon-state", theater ? "collapse" : "expand");
}
};
const applyTheaterState = () => {
const section = carousel.closest("[data-threecol]");
if (!section) return;
section.classList.toggle("theater-mode", theater);
updateTheaterButton();
};
const toggleTheater = () => {
theater = !theater;
applyTheaterState();
};
const exitTheater = () => {
if (!theater) return;
theater = false;
applyTheaterState();
};
theaterBtn?.addEventListener("click", (e) => {
e.preventDefault();
toggleTheater();
});
const showSlide = (index) => {
slides[current].classList.remove("opacity-100");
slides[current].classList.add("opacity-0");
current = index;
slides[current].classList.remove("opacity-0");
slides[current].classList.add("opacity-100");
if (liveAnnouncer) {
liveAnnouncer.textContent = `Slide ${current + 1} of ${total}`;
}
};
const goPrev = () => {
const prev = (current - 1 + total) % total;
showSlide(prev);
};
const goNext = () => {
const next = (current + 1) % total;
showSlide(next);
};
prevBtn?.addEventListener("click", goPrev);
nextBtn?.addEventListener("click", goNext);
// Key handling (now works when the section itself is focused)
carousel.addEventListener("keydown", (e) => {
const target = e.target;
const isTyping =
target instanceof HTMLElement &&
(target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable);
if (isTyping) return;
if (e.key === "ArrowLeft") {
e.preventDefault();
goPrev();
} else if (e.key === "ArrowRight") {
e.preventDefault();
goNext();
} else if (
(e.key === "t" || e.key === "T") &&
!e.altKey &&
!e.metaKey &&
!e.ctrlKey
) {
e.preventDefault();
toggleTheater();
} else if (e.key === "Escape") {
exitTheater();
}
});
const startAutoRotate = () => {
stopAutoRotate();
autoRotateTimer = window.setInterval(goNext, 5000);
};
const stopAutoRotate = () => {
if (autoRotateTimer) {
clearInterval(autoRotateTimer);
autoRotateTimer = undefined;
}
};
startAutoRotate();
carousel.addEventListener("mouseenter", stopAutoRotate);
carousel.addEventListener("mouseleave", startAutoRotate);
carousel.addEventListener("focusin", stopAutoRotate);
carousel.addEventListener("focusout", startAutoRotate);
});
</script>

View file

@ -0,0 +1,99 @@
---
import { Icon } from "astro-icon/components";
import { type Project } from "../consts";
import Carousel from "./Carousel.astro";
import ThreeColumnSection from "./ThreeColumnSection.astro";
interface Props {
projects: Project[];
}
const { projects } = Astro.props;
---
{
projects.map((project) => (
<ThreeColumnSection maxWidth="8xl" py="py-8">
{/* ---------- LEFT: Project details ---------- */}
<div slot="left" class="max-w-xs justify-self-center lg:justify-self-end">
<article class="space-y-4">
{/* Title + optional company */}
<h3 class="text-2xl font-semibold text-slate-900 dark:text-slate-100">
{project.title}
{/* Duration */}
<p class="text-sm text-slate-500 dark:text-slate-400 flex items-center mt-1">
<Icon name="mdi:calendar" class="mr-2" />
{project.duration}
</p>
</h3>
{/* Description */}
<p class="text-base text-slate-800 dark:text-slate-200">
{project.description}
</p>
{/* Tech stack */}
<div class="flex flex-wrap gap-2 mt-2">
{project.tech_stack.map((tech) => (
<span class="px-2 py-0.5 text-xs font-medium bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-slate-100 rounded">
{tech}
</span>
))}
</div>
{/* Deliverables (optional) */}
{project.deliverables?.length && (
<ul class="list-disc list-inside text-sm text-slate-700 dark:text-slate-300 mt-3">
{project.deliverables.map((item) => (
<li class="mb-1">{item}</li>
))}
</ul>
)}
{/* Links */}
<div class="flex space-x-4 mt-4">
{project.live_url && (
<a
href={project.live_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium
transition-colors duration-150
bg-slate-100 hover:bg-slate-200 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-slate-500
dark:bg-slate-800 dark:hover:bg-slate-700 dark:focus-visible:outline-slate-400"
>
<Icon name="mdi:aspect-ratio" /> Live
</a>
)}
{project.repo_url && (
<a
href={project.repo_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium
transition-colors duration-150
bg-slate-100 hover:bg-slate-200 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-slate-500
dark:bg-slate-800 dark:hover:bg-slate-700 dark:focus-visible:outline-slate-400"
>
<Icon name="mdi:github" /> Repo
</a>
)}
</div>
</article>
</div>
{/* ---------- CENTER: Carousel ---------- */}
<div slot="center" class="w-full">
<div class="rounded-lg overflow-hidden shadow-lg">
<Carousel images={project.images} />
</div>
</div>
{/* ---------- RIGHT: Empty for now ---------- */}
<div slot="right" class="hidden lg:block" />
</ThreeColumnSection>
))
}

View file

@ -0,0 +1,139 @@
---
export interface Props {
bgClass?: string;
containerClass?: string;
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "6xl" | "8xl";
py?: string;
reverseOnMobile?: boolean;
}
const {
bgClass = "",
containerClass = "",
maxWidth = "8xl",
py = "py-8 md:py-12",
reverseOnMobile = false,
} = Astro.props;
const maxWidthClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
"2xl": "max-w-2xl",
"4xl": "max-w-4xl",
"6xl": "max-w-6xl",
"8xl": "max-w-8xl",
};
---
<section class={`${bgClass} ${py}`} data-threecol>
<div
class={`threecol-row ${maxWidthClasses[maxWidth]} mx-auto flex flex-col lg:flex-row gap-8 px-4 ${containerClass}`}
>
<!-- Left Column -->
<div
class={`${reverseOnMobile ? "order-3 lg:order-1" : "order-1"} flex-col`}
data-left
>
<slot name="left" />
</div>
<!-- Center Column -->
<div class={`order-1 lg:order-2 flex-col`} data-center>
<slot name="center" />
</div>
<!-- Right Column -->
<div
class={`${reverseOnMobile ? "order-1 lg:order-3" : "order-3"} flex-col`}
data-right
>
<slot name="right" />
</div>
</div>
</section>
<style>
/* Base layout variables */
[data-threecol] .threecol-row {
--center-initial: 640px; /* initial center width at lg+ */
--transition-ease: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Make sure columns can shrink properly */
[data-threecol] [data-left],
[data-threecol] [data-center],
[data-threecol] [data-right] {
min-width: 0;
}
/* Large-screen flex behavior (mobile stays stacked naturally) */
@media (min-width: 1024px) {
[data-threecol] .threecol-row {
align-items: stretch;
}
[data-threecol] [data-left] {
max-width: 33.33%;
flex: 1 1 33.33%;
/* optional: transition flex changes if you expect subtle movement */
transition: flex 0.55s var(--transition-ease);
}
[data-threecol] [data-center] {
flex: 0 0 var(--center-initial);
transition:
flex-basis 0.55s var(--transition-ease),
flex-grow 0.55s var(--transition-ease),
max-width 0.55s var(--transition-ease);
display: flex;
flex-direction: column;
}
[data-threecol] [data-right] {
flex: 1 1 0;
transition:
flex-basis 0.55s var(--transition-ease),
flex-grow 0.55s var(--transition-ease),
opacity 0.35s ease;
display: flex;
flex-direction: column;
}
/* THEATER MODE (expanded center) */
[data-threecol].theater-mode [data-center] {
flex: 1 1 0; /* allow it to fill remaining space */
max-width: 100%;
}
[data-threecol].theater-mode [data-right] {
flex: 0 0 0;
flex-grow: 0;
opacity: 0;
pointer-events: none;
}
/* Optional: subtle emphasis on the center content while expanding */
[data-threecol] .threecol-row :is([data-center]) .carousel-theater-target,
[data-threecol] .threecol-row :is([data-center]) > * {
transition:
box-shadow 0.55s var(--transition-ease),
transform 0.55s var(--transition-ease);
}
[data-threecol].theater-mode [data-center] .carousel-theater-target,
[data-threecol].theater-mode [data-center] > * {
/* Example visual polish (comment out if not desired) */
/* box-shadow: 0 6px 24px -4px rgba(0,0,0,.25); */
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
[data-threecol] [data-left],
[data-threecol] [data-center],
[data-threecol] [data-right] {
transition: none !important;
}
}
</style>

View file

@ -1,6 +1,196 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.
import type { ImageMetadata } from "astro";
import discretizeui_demo from "./assets/projects/discretizeui/demo.png";
import discretizeui_languages from "./assets/projects/discretizeui/languages.png";
import discretizeui_tooltip from "./assets/projects/discretizeui/tooltip.png";
import optimizer1 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_52_AM.png";
import optimizer2 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_53_AM.png";
import optimizer3 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_54_AM.png";
import optimizer4 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_55_AM.png";
import videovault_dashboard from "./assets/projects/videovault/dashboard.png";
import videovault_edit from "./assets/projects/videovault/edit.png";
import videovault_frontpage from "./assets/projects/videovault/frontpage.png";
import videovault_player from "./assets/projects/videovault/player.png";
export const SITE_TITLE = "Alex Daichendt";
export const SITE_DESCRIPTION =
"Alex Daichendt's personal website, blog, and portfolio.";
export interface Project {
title: string;
featured: boolean;
live_url?: string;
repo_url?: string;
description: string;
tech_stack: string[];
duration: string;
deliverables: string[];
company?: string;
images: {
src: ImageMetadata;
alt: string;
}[];
}
export const projects: Project[] = [
{
title: "VideoVault",
featured: true,
live_url: "https://videovault.shiverpeak.xyz/",
repo_url: "https://github.com/AlexDaichendt/VideoVault",
description:
"A private, self-hosted video vault for your personal use. Supports multiple video transcodes, search, and is blazingly fast thanks to a backend written in Rust and a zero-js (vanilla) frontend.",
tech_stack: [
"Rust",
"Axum",
"sqlx",
"Tera Templates",
"Tailwind CSS",
"Node.js",
"pnpm",
"cargo",
"ffmpeg",
],
duration: "2025 - Present",
deliverables: [
"HLS Streaming of videos",
"Video Scrubbing",
"Aspect Ratio Awareness",
"Multiple Video Transcodes",
"External Transcoding Workers",
"Responsive, lightweight design",
"User Accounts via local, LDAP or OIDC",
"Sharing with timestamps",
"Quick and simple deployment (Docker)",
],
images: [
{
alt: "Frontpage",
src: videovault_frontpage,
},
{
alt: "VideoVault Dashboard",
src: videovault_dashboard,
},
{
alt: "VideoVault Video Player",
src: videovault_player,
},
{
alt: "VideoVault Edit",
src: videovault_edit,
},
],
},
{
title: "Discretize: Gear Optimizer",
featured: true,
live_url: "https://optimizer.discretize.eu/",
repo_url: "https://github.com/discretize/discretize-gear-optimizer",
description:
"A gear optimizer for the popular MMORPG Guild Wars 2. The optimizer is used by thousands of players daily to find the best gear combinations for their characters.",
tech_stack: ["React", "Redux", "Rust", "Vite", "MaterialUI"],
duration: "2021 - Present",
deliverables: [
"User-friendly interface",
"Rust/WASM calculation core",
"Internationalization",
"Keyboard Navigation",
],
images: [
{
src: optimizer1,
alt: "Discretize Gear Optimizer Screenshot",
},
{
src: optimizer2,
alt: "Discretize Gear Optimizer Screenshot",
},
{
src: optimizer3,
alt: "Discretize Gear Optimizer Screenshot",
},
{
src: optimizer4,
alt: "Discretize Gear Optimizer Screenshot",
},
],
},
{
title: "@discretize/gw2-ui-new",
featured: false,
live_url: "https://discretize.github.io/discretize-ui/gw2-ui",
repo_url: "https://github.com/discretize/discretize-ui",
description: `A modern, lightweight React component library for Guild Wars 2 UI elements. Used by all Discretize applications.`,
tech_stack: ["React", "TypeScript", "CSS Modules", "Storybook"],
duration: "2023 Present",
deliverables: [
"Refactored all components to TypeScript",
"Replaced CSS-in-JS with CSS Modules",
"Better performance by caching, batching",
],
images: [
{
src: discretizeui_demo,
alt: "Production Demo of the component library",
},
{
src: discretizeui_tooltip,
alt: "Tooltip component",
},
{
src: discretizeui_languages,
alt: "Supports multiple languages",
},
],
},
// {
// title: "Discretize -- Rewritten Website",
// description:
// "Rewritten website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Awaiting last few changes and content updates by players before deployment.",
// live_url: "https://next.discretize.eu/",
// repo_url: "https://github.com/discretize/discretize.eu-rewrite",
// tech_stack: ["Astro", "React", "TypeScript", "Tailwind CSS"],
// duration: "2022 - Present",
// complexity: 5,
// },
// {
// title: "Discretize -- CC Tool",
// description:
// "Allows players to create skill schedules with drag and drop. Used by high-end players to optimize and coordinate their gameplay.",
// live_url: "https://cc-tool.pages.dev/",
// repo_url: "https://github.com/discretize/cc-tool",
// tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
// duration: "2024 - Present",
// complexity: 3,
// },
// {
// title: "Discretize -- Random Builds",
// description:
// "Generates random builds for the popular MMORPG Guild Wars 2. Meant as a way to force players out of their comfort zone and try new things.",
// live_url: "https://random-builds.discretize.eu/",
// tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
// duration: "2022",
// complexity: 3,
// },
// {
// title: "Discretize -- Old Website",
// description:
// "Currently deployed website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Inherited project from previous maintainer. Several hundred thousand monthly users.",
// live_url: "https://discretize.eu/",
// tech_stack: ["React", "Gatsby", "Material UI"],
// duration: "2019 - Present",
// complexity: 3,
// },
// {
// title: "Minecraft LandLord Spigot Plugin",
// live_url: "https://www.spigotmc.org/resources/landlord-2.44398/",
// repo_url: "https://github.com/LandlordPlugin/LandLord",
// description:
// "Landlord aims to keep the Minecraft experience simple and fluid for players while also protecting their land. The idea for this plugin is to protect player builds with minimal game-play interference, while also allowing them to tweak the protection details in a simple and user-friendly way. Handed over the project to a new group of maintainers in 2019.",
// tech_stack: ["Java"],
// duration: "2017 - 2019",
// complexity: 2,
// },
];

View file

@ -5,6 +5,11 @@ description: A guide on what is needed to get Linux running on the Huawei MateBo
heroImage: ./images/matebook.jpg
---
**UPDATE 24/08/2024**:
Having used this machine for a year soon, I can not recommend it. None of my issues have been resolved, some even gotten worse. The laptop's battery life is poor, often barely reaching 3 hours while having a text editor open. I pretty much depend on plugging the laptop into the wall, all the time to not degrade the battery more than necessary. The webcam, fingerprint, S3 and Xe drivers are still all pretty much broken. Bluetooth also behaves a bit strangely where it sometimes requires multiple attempts to connect.
---
I recently bought a Huawei MateBook X Pro 2024. It is a beautiful laptop with a 3:2 aspect ratio display and a touchscreen. The laptop comes with Windows 11 preinstalled. However, I wanted to run Linux on it. Here is a guide on what is needed to get Linux running on the Huawei MateBook X Pro 2024.
Overall, the experience was okay, but not something I would recommend to an average user. There are a fair bit of quirks that need to be ironed out. Especially distros running older kernels will have a hard time. I am running CachyOS with the latest 6.13-rc1 kernel, more on that later.
@ -59,10 +64,10 @@ The webcam is an ipu6-based camera. Support has been trickling in over the years
The fingerprint sensor is not supported at the moment. It does not even show up anywhere. One of those ingenious Goodix sensors that are not supported by the fprintd library.
---
Sources:
[^1]: https://www.kernelconfig.io/search?q=CONFIG_BOOTPARAM_HOTPLUG_CPU0&kernelversion=6.12.4&arch=x86
[^2]: https://github.com/intel/intel-lpmd

View file

@ -1,4 +1,23 @@
export const publications = [
{
authors: [
"Alexander Daichendt",
"Florian Wiedner",
"Jonas Andre",
"Georg Carle",
],
title:
"Applicability of Hardware-Supported Containers in Low-Latency Networking",
conference:
"20th International Conference on Network and Service Management (CNSM 2024)",
location: "Prague, Czech Republic",
date: "Oct. 2024",
links: {
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_2024_cnsm.pdf",
homepage: "https://tumi8.github.io/applicability-hwsupported-containers",
bibtex: "https://www.net.in.tum.de/publications/bibtex/Wied24CNSM.bib",
},
},
{
authors: [
"Florian Wiedner",
@ -20,25 +39,6 @@ export const publications = [
bibtex: "/publications/bibtex/WiedHelm24Container.bib",
},
},
{
authors: [
"Alexander Daichendt",
"Florian Wiedner",
"Jonas Andre",
"Georg Carle",
],
title:
"Applicability of Hardware-Supported Containers in Low-Latency Networking",
conference:
"20th International Conference on Network and Service Management (CNSM 2024)",
location: "Prague, Czech Republic",
date: "Oct. 2024",
links: {
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_2024_cnsm.pdf",
homepage: "https://tumi8.github.io/applicability-hwsupported-containers",
bibtex: "https://www.net.in.tum.de/publications/bibtex/Wied24CNSM.bib",
},
},
{
authors: [
"Florian Wiedner",

View file

@ -1,10 +1,11 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import me from "../assets/me.jpg";
import { Image } from "astro:assets";
import Picture from "../components/Picture.astro";
import Link from "../components/Link.astro";
import Picture from "../components/Picture.astro";
import ProjectSection from "../components/ProjectSection.astro";
import ThreeColumnSection from "../components/ThreeColumnSection.astro";
import { projects } from "../consts";
import BaseLayout from "../layouts/BaseLayout.astro";
const love = [
{
emoji: "🦀",
@ -57,18 +58,10 @@ const skills = {
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
className="w-full py-16 flex flex-col"
>
<section
class="max-w-8xl mx-auto
grid grid-cols-1 lg:grid-cols-[auto,1fr,auto]
items-center md:items-start
gap-8
py-8 md:py-12"
>
<!-- Left Spacer: Already responsive, no changes needed -->
<div class="w-72 hidden md:block"></div>
<ThreeColumnSection reverseOnMobile={true}>
<div slot="left"></div>
<!-- Main Content: Add order to place it below the image on mobile -->
<div class="max-w-2xl px-4 order-2 md:order-none">
<div slot="center" class="max-w-2xl">
<p class="mb-4">
I am a privacy-first software engineer passionate about building web
applications that are efficient, user-friendly, and respectful of your
@ -86,14 +79,18 @@ const skills = {
</p>
</div>
<!-- Picture: Place it first on mobile using order -->
<div class="px-4 order-1 md:order-none">
<Picture src={me} alt="me" class="w-48 md:w-64 h-auto mx-auto" />
<div slot="right" class="flex justify-center md:justify-start items-start">
<Picture
src={me}
alt="My profile picture"
class="w-48 md:w-64 h-auto rounded-lg"
/>
</div>
</section>
</ThreeColumnSection>
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
<div class="max-w-2xl mx-auto px-4">
<ThreeColumnSection bgClass="bg-mytheme-200/70 dark:bg-mytheme-700/50">
<div slot="left"></div>
<div slot="center" class="max-w-2xl">
<h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
>
@ -112,10 +109,12 @@ const skills = {
}
</div>
</div>
</section>
<div slot="right"></div>
</ThreeColumnSection>
<section class="py-16">
<div class="max-w-2xl mx-auto px-4">
<ThreeColumnSection>
<div slot="left"></div>
<div slot="center">
<h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
>
@ -140,5 +139,20 @@ const skills = {
}
</div>
</div>
<div slot="right"></div>
</ThreeColumnSection>
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
<h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
>
Software I developed
</h2>
<div class="space-y-16">
<ProjectSection
projects={projects.filter((project) => project.featured)}
/>
</div>
</section>
</BaseLayout>

View file

@ -1,238 +1,13 @@
---
import ProjectSection from "../../components/ProjectSection.astro";
import { projects } from "../../consts";
import BaseLayout from "../../layouts/BaseLayout.astro";
import { Icon } from "astro-icon/components";
const projects = [
{
title: "daichendt.one (this site)",
live_url: "https://www.youtube.com/watch?v=XfELJU1mRMg",
repo_url: "https://github.com/AlexDaichendt/site",
description: "Personal website and blog.",
tech_stack: ["Astro", "Tailwind CSS"],
duration: "2022 - Present",
complexity: 4,
},
// {
// title: "Radio Player",
// live_url: "https://video.taxi/en/functions/simultaneous-interpreting/",
// description: "A radio player for the VIDEO.TAXI website. Allows users to listen to radio stations with dubbed audio with the voice of the original speaker.",
// tech_stack: ["React", "TypeScript", "Vite", "Go", "Docker"],
// duration: "2024 - Present",
// complexity: 4,
// company: "TV1 GmbH",
// },
{
title: "VIDEO.TAXI Meetings",
live_url: "https://meetings.video.taxi/",
description:
"Records, transcribes, and translates meetings (Webex, Teams, Zoom). Interfaces with the GraphQL API of VIDEO.TAXI. Architecure, development, and deployment all done by me. Greenfield project.",
tech_stack: [
"Svelte",
"TypeScript",
"Tailwind CSS",
"Express",
"GraphQL",
"PostgreSQL",
"Docker",
"OpenAPI",
],
complexity: 5,
duration: "2024 - Present",
company: "TV1 GmbH",
},
{
title: "Netbox PDU Plugin",
repo_url: "https://github.com/AlexDaichendt/axians-netbox-plugin-pdu",
description:
"Netbox plugin to read out power usage of PDUs. Forked and maintained from the original plugin by Axians. Used in production by multiple companies.",
tech_stack: ["Python", "Django", "Netbox"],
duration: "2023 - Present",
complexity: 4,
company: "TV1 GmbH",
},
{
title: "Latency IRQ Analyzer",
repo_url: "https://github.com/AlexDaichendt/latency-irq-analyzer",
description:
"Quick uni project for overlaying latency files with IRQ data.",
tech_stack: ["NodeJS", "Highcharts"],
duration: "2024",
complexity: 2,
},
{
title: "Netbox Agent",
description:
"Reads out lshw, dmidecode and other data of a server and creates Netbox devices. Forked from the original project.",
tech_stack: ["Python"],
duration: "2023 - Present",
complexity: 2,
company: "TV1 GmbH",
},
{
title: "magewell-exporter",
repo_url: "https://github.com/TV1-EU/magewell-exporter",
description:
"Prometheus exporter for Magewell AiO encoders. Allows monitoring of the capture card status and video signal. Used in production by TV1.",
tech_stack: ["NodeJs", "Typescript"],
duration: "2023",
complexity: 4,
company: "TV1 GmbH",
},
{
title: "Discretize -- Gear Optimizer",
live_url: "https://optimizer.discretize.eu/",
repo_url: "https://github.com/discretize/discretize-gear-optimizer",
description:
"A gear optimizer for the popular MMORPG Guild Wars 2. The optimizer is used by thousands of players daily to find the best gear combinations for their characters.",
tech_stack: ["React", "Redux", "Rust", "Vite", "MaterialUI"],
duration: "2021 - Present",
complexity: 5,
},
{
title: "Discretize -- UI Library",
repo_url: "https://github.com/discretize/discretize-ui",
description:
"A beautiful component library with tooltips for the popular MMORPG Guild Wars 2. Allows websites to look and feel like the game. Integral part of the Discretize ecosystem.",
live_url:
"https://discretize.github.io/discretize-ui/gw2-ui/?path=/story/components-attribute--boon-duration",
tech_stack: ["React", "TypeScript", "Storybook"],
duration: "2021 - Present",
complexity: 5,
},
{
title: "Discretize -- Rewritten Website",
description:
"Rewritten website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Awaiting last few changes and content updates by players before deployment.",
live_url: "https://next.discretize.eu/",
repo_url: "https://github.com/discretize/discretize.eu-rewrite",
tech_stack: ["Astro", "React", "TypeScript", "Tailwind CSS"],
duration: "2022 - Present",
complexity: 5,
},
{
title: "Discretize -- CC Tool",
description:
"Allows players to create skill schedules with drag and drop. Used by high-end players to optimize and coordinate their gameplay.",
live_url: "https://cc-tool.pages.dev/",
repo_url: "https://github.com/discretize/cc-tool",
tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
duration: "2024 - Present",
complexity: 3,
},
{
title: "Discretize -- Random Builds",
description:
"Generates random builds for the popular MMORPG Guild Wars 2. Meant as a way to force players out of their comfort zone and try new things.",
live_url: "https://random-builds.discretize.eu/",
tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
duration: "2022",
complexity: 3,
},
{
title: "Discretize -- Old Website",
description:
"Currently deployed website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Inherited project from previous maintainer. Several hundred thousand monthly users.",
live_url: "https://discretize.eu/",
tech_stack: ["React", "Gatsby", "Material UI"],
duration: "2019 - Present",
complexity: 3,
},
{
title: "Minecraft LandLord Spigot Plugin",
live_url: "https://www.spigotmc.org/resources/landlord-2.44398/",
repo_url: "https://github.com/LandlordPlugin/LandLord",
description:
"Landlord aims to keep the Minecraft experience simple and fluid for players while also protecting their land. The idea for this plugin is to protect player builds with minimal game-play interference, while also allowing them to tweak the protection details in a simple and user-friendly way. Handed over the project to a new group of maintainers in 2019.",
tech_stack: ["Java"],
duration: "2017 - 2019",
complexity: 2,
},
];
const getCardStyle = (company?: string) => {
const baseStyles =
"rounded-lg shadow-lg p-6 transition-colors duration-300 mb-8";
if (!company) {
return `${baseStyles} bg-white dark:bg-gray-800`;
}
const companyColors: Record<string, string> = {
Discretize: "bg-blue-50 dark:bg-blue-900/30",
"TV1 GmbH": "border-2 border-orange-500 dark:border-orange-500",
};
return `${baseStyles} ${companyColors[company] || "bg-white dark:bg-gray-800"}`;
};
---
<BaseLayout title="Projects" subtitle="Selected Passion Projects I am proud of">
<p>
Here are some of the projects I have worked on in the past. They are sorted
by my personal rating of relevancy. Projects done for a company are marked
with the company name and have a special border color.
</p>
{
projects
.sort((a, b) => b.complexity - a.complexity)
.map((project) => (
<article class={getCardStyle(project.company)}>
<div class="flex justify-between items-start mb-4">
<div>
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">
{project.title}
</h2>
{project?.company && (
<div class="flex items-center mt-1">
<Icon name="mdi:office-building" class="w-5 h-5 mr-1" />
<span class="text-sm text-gray-600 dark:text-gray-300 font-medium">
{project?.company}
</span>
</div>
)}
</div>
<span class="text-sm text-gray-500 dark:text-gray-400">
{project.duration}
</span>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-4">
{project.description}
</p>
<div class="flex flex-wrap gap-2 mb-4">
{project.tech_stack.map((tech) => (
<span class="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full">
{tech}
</span>
))}
</div>
<div class="flex gap-4">
{project.live_url && (
<a
href={project.live_url}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline flex items-center"
>
<Icon name="mdi:web" class="w-5 h-5 mr-1" />
Live Demo
</a>
)}
{project.repo_url && (
<a
href={project.repo_url}
target="_blank"
rel="noopener noreferrer"
class="text-gray-600 dark:text-gray-400 hover:underline flex items-center"
>
<Icon name="mdi:github" class="w-5 h-5 mr-1" />
Repository
</a>
)}
</div>
</article>
))
}
<BaseLayout
title="Projects"
subtitle="Selected Passion Projects I am proud of"
className="w-full py-16 flex flex-col"
>
<ProjectSection projects={projects} />
</BaseLayout>

View file

@ -100,8 +100,7 @@ blockquote {
font-size: 1.333em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
@apply mb-8 border-t-2 border-t-mytheme-500 dark:border-t-mytheme-300 rounded-sm;
}
ul:not(ul ul, ol ul),
ol:not(ul ol, ol ol) {