feat: add picture, improve layout

This commit is contained in:
Alexander Daichendt 2025-07-04 17:05:28 +02:00
parent bf720b79ee
commit b3d158d52c
10 changed files with 361 additions and 168 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

BIN
src/assets/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

View file

@ -63,3 +63,24 @@ const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Alexander Daichendt",
"url": "https://daichendt.one/",
"image": "https://daichendt.one/files/alexdaichendt.jpg",
"sameAs": ["https://github.com/alexdaichendt"],
"jobTitle": "IT Consultant",
"worksFor": {
"@type": "Organization",
"name": "Freelance"
},
"alumniOf": {
"@type": "CollegeOrUniversity",
"name": "Technical University Munich"
},
"description": "Short description about you and your expertise."
}
</script>

View file

@ -18,5 +18,5 @@ const { title, subtitle } = Astro.props;
</p>
)
}
<div class="mt-8 w-24 h-1 bg-blue-600 mx-auto rounded-full"></div>
<div class="mt-8 w-24 h-1 bg-mytheme-400 mx-auto rounded-full"></div>
</div>

View file

@ -6,4 +6,5 @@ import { Picture as AstroPicture } from "astro:assets";
src={Astro.props.src}
alt={Astro.props.alt}
formats={["avif", "webp"]}
{...Astro.props}
/>

View file

@ -8,6 +8,70 @@
) as NodeListOf<HTMLInputElement>;
const sliders = document.querySelectorAll(".slider");
// Function to animate theme transition
function animateThemeTransition(
toggleButton: HTMLElement,
isDarkMode: boolean,
) {
// Set animation to start from top right corner
const centerX = window.innerWidth - 20; // 20px from right edge
const centerY = 20; // 20px from top edge
// Calculate radius needed to cover entire screen from top right
const maxDistance = Math.sqrt(
Math.pow(centerX, 2) + Math.pow(window.innerHeight - centerY, 2),
);
// Create a temporary pseudo-element animation using CSS custom properties
const newThemeColor = isDarkMode ? "rgb(17, 24, 39)" : "rgb(255, 255, 255)";
// Apply the animation using CSS custom properties
document.documentElement.style.setProperty(
"--theme-transition-color",
newThemeColor,
);
document.documentElement.style.setProperty(
"--theme-transition-x",
`${centerX}px`,
);
document.documentElement.style.setProperty(
"--theme-transition-y",
`${centerY}px`,
);
document.documentElement.style.setProperty(
"--theme-transition-radius",
"0px",
);
// Add animation class but don't change theme colors yet
document.body.classList.add("theme-transitioning");
// Start animation
requestAnimationFrame(() => {
document.documentElement.style.setProperty(
"--theme-transition-radius",
`${maxDistance}px`,
);
});
// Switch theme colors halfway through animation when background has covered most content
setTimeout(() => {
if (isDarkMode) {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
}
}, 300); // Half of the 600ms animation
// Clean up after full animation completes
setTimeout(() => {
// Remove animation class
document.body.classList.remove("theme-transitioning");
}, 600);
}
// Set initial state of toggle based on previous settings
if (
localStorage.getItem("color-theme") === "dark" ||
@ -23,35 +87,25 @@
// Remove no-transition class after initial load
window.addEventListener("load", () => {
setTimeout(() => {
sliders.forEach((slider) =>
slider.classList.remove("no-transition"),
);
sliders.forEach((slider) => slider.classList.remove("no-transition"));
}, 0);
});
themeToggleBtns.forEach((btn) => {
btn.addEventListener("change", function () {
// If is set in localStorage
if (localStorage.getItem("color-theme")) {
if (localStorage.getItem("color-theme") === "light") {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
themeToggleBtns.forEach((btn) => (btn.checked = true));
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
themeToggleBtns.forEach((btn) => (btn.checked = false));
}
} else {
if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark");
localStorage.setItem("color-theme", "light");
themeToggleBtns.forEach((btn) => (btn.checked = false));
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("color-theme", "dark");
themeToggleBtns.forEach((btn) => (btn.checked = true));
}
}
// Prevent default theme switching - we'll handle it after animation
const currentIsDark = document.documentElement.classList.contains("dark");
const targetIsDark = !currentIsDark;
// Update toggle states immediately for visual feedback
themeToggleBtns.forEach(
(toggleBtn) => (toggleBtn.checked = targetIsDark),
);
// Find the closest switch element to get position
const switchElement = btn.closest(".switch") as HTMLElement;
// Animate the transition
animateThemeTransition(switchElement, targetIsDark);
});
});
</script>
@ -145,4 +199,22 @@
.no-transition:before {
transition: none !important;
}
/* Theme transition animation */
body.theme-transitioning::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: var(--theme-transition-color);
clip-path: circle(
var(--theme-transition-radius) at var(--theme-transition-x)
var(--theme-transition-y)
);
transition: clip-path 0.6s ease-in-out;
pointer-events: none;
z-index: -1;
}
</style>

View file

@ -6,7 +6,7 @@ const lastUpdated = new Date(); // You can set this to a specific date or pull f
<footer
class="bg-gray-100/60 dark:bg-mytheme-900 shadow-sm text-gray-600 dark:text-gray-400 px-4 py-8"
>
<div class="max-w-full mx-auto">
<div class="max-w-full lg:px-16 md:px-8 px-2 py-4">
<!-- Main horizontal layout -->
<div
class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"

View file

@ -8,13 +8,13 @@ import MobileNav from "./MobileNav.astro";
<header
class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-700 z-20 relative"
>
<div class="max-w-full px-4 py-4">
<div class="max-w-full lg:px-16 md:px-8 px-2 py-4">
<div class="flex items-center justify-between">
<!-- Site Title - stays on far left -->
<h2 class="font-bold text-xl mb-0 font-mono flex">
<a
href="/"
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
class="hover:text-mytheme-600 dark:hover:text-mytheme-400 transition-colors"
>
<Logo />
</a>

View file

@ -12,12 +12,14 @@ interface Props {
title?: string;
description?: string;
subtitle?: string;
className?: string;
}
const {
title = SITE_TITLE,
description = SITE_DESCRIPTION,
subtitle,
className = "max-w-2xl px-4 py-8",
} = Astro.props;
---
@ -42,6 +44,8 @@ const {
<body
class="bg-white dark:bg-gray-900 text-black dark:text-white min-h-screen flex flex-col"
>
<div id="theme-overlay" class="theme-overlay"></div>
<!-- Mobile Navigation Drawer (at body level for proper positioning) -->
<MobileNavDrawer />
@ -50,7 +54,7 @@ const {
<!-- Main Content -->
<main class="flex-1">
<div class="max-w-2xl mx-auto px-4 py-8">
<div class={`${className} mx-auto`}>
<PageHeadline title={title} subtitle={subtitle} />
<slot />
</div>
@ -58,5 +62,19 @@ const {
<!-- Footer -->
<Footer />
<style>
.theme-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
display: none;
transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
</body>
</html>

View file

@ -1,56 +1,137 @@
---
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";
const love = [
{
emoji: "🦀",
text: "Rust",
},
{
emoji: "🐧",
text: "Linux",
},
{
emoji: "📚",
text: "Books",
},
{
emoji: "🎨",
text: "Clean UI",
},
{
emoji: "🤝",
text: "OpenSource",
},
];
const skills = {
"Core Stack": ["Rust", "TypeScript", "Node.js", "SQL", "HTML", "CSS"],
Frontend: ["Next.js", "SvelteKit", "Astro", "Tailwind"],
DevOps: [
"Prometheus",
"Docker",
"Kubernetes",
"AWS",
"GitHub Actions",
"GitLab Actions",
],
"Tools & Methods": [
"Git",
"Agile",
"REST",
"GraphQL",
"Web Performance",
"Linux Sysadmin",
"Vitest",
],
Languages: ["German (native)", "English"],
};
---
<BaseLayout
title="Hi, my name is Alex!"
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
className="w-full py-16 gap-16 flex flex-col"
>
<section
class="max-w-8xl mx-auto grid grid-cols-[auto,1fr,auto] items-start gap-8"
>
<div class="w-64 hidden md:block"></div>
<div class="max-w-2xl px-4">
<p>
My journey in the tech world has been a dynamic one. I've immersed myself in
countless projects spanning various video games and, for the past few years,
have been maintaining a small homelab, which ignited a passion for
automating infrastructure. I am a privacy enthusiast and advocate for
non-invasive software. Occasionally, I channel my creativity into building
sleek web applications that prioritize efficiency and usability over visual
clutter, and adhere to web standards and best practices.
I am a privacy-first software engineer passionate about building web
applications that are efficient, user-friendly, and respectful of your
data.
</p>
<p>
Currently, I am available for hire as a freelance Software Engineer,
advising clients in optimizing and creating Web Applications. With a strong
background in various programming languages and frameworks, I specialize in
developing scalable and efficient web solutions tailored to meet the unique
needs of each client.
I help clients design and develop sleek, standards-compliant web
solutions, always prioritizing usability over visual clutter. With
hands-on experience in Rust, Node.js and many more, I believe in using
the best tools and technologies for each project, ensuring scalable and
long-term maintainable results.
</p>
<p>
Contact me <a href="/contact">here</a>!
Ready to collaborate? <Link href="/contact">Get in touch here</Link>!
</p>
<h2 class="mt-16">Skills</h2>
<div class="flex flex-wrap gap-4">
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Core stack:</strong><br />
Typescript/Node.js, Python, Rust, SQL, vanilla HTML/css
</div>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Frontend:</strong><br />
NextJS, SvelteKit, Astro, Tailwind
<div>
<Picture src={me} alt="me" class="w-64 h-auto" />
</div>
</section>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Ops:</strong><br />
Prometheus, Docker, Kubernetes, AWS, Github/lab actions
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
<div class="max-w-2xl mx-auto px-4">
<h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
>
What I Love
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{
love.map((item) => (
<div class="flex items-center justify-center p-6 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-sm hover:shadow-md transition-all duration-200 border border-slate-200/50 dark:border-slate-700/50 hover:scale-105 cursor-default">
<span class="text-3xl mr-3">{item.emoji}</span>
<span class="text-lg font-medium text-slate-700 dark:text-slate-200">
{item.text}
</span>
</div>
))
}
</div>
</div>
</section>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Misc:</strong><br />
git, agile, REST, GraphQL, web performance, Linux sysadmin, Vitest
</div>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Languages:</strong><br />
German (native), English
<section class="py-16">
<div class="max-w-2xl mx-auto px-4">
<h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
>
What I Can Do
</h2>
<div class="space-y-8">
{
Object.entries(skills).map(([category, skillList]) => (
<div class="space-y-4">
<h3 class="text-xl font-semibold text-slate-700 dark:text-slate-200 border-b border-slate-200 dark:border-slate-700 pb-2">
{category}
</h3>
<div class="flex flex-wrap gap-2">
{skillList.map((skill, index) => (
<span class="inline-flex items-center px-3 py-1.5 text-sm font-medium bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 transition-colors cursor-default rounded-full">
{skill}
</span>
))}
</div>
</div>
))
}
</div>
</div>
</section>
</BaseLayout>