Compare commits

..

8 commits

43 changed files with 2279 additions and 696 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

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";
@ -63,3 +61,59 @@ 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",
"email": "jsonld@daichendt.one",
"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"
},
"nationality": {
"@type": "Country",
"name": "Germany"
},
"description": "Privacy-first software engineer passionate about building efficient, user-friendly, and data-respectful web applications. Experienced in Rust, Node.js, and more, delivering scalable, standards-compliant solutions with a focus on usability.",
"hasCredential": {
"@context": "https://schema.org",
"@type": "EducationalOccupationalCredential",
"credentialCategory": "Master's degree",
"name": "Master of Science in Informatics",
"description": "Graduate degree awarded for completing the Master of Science program in Informatics at the Technical University of Munich.",
"educationalLevel": "Master's",
"recognizedBy": {
"@type": "CollegeOrUniversity",
"name": "Technical University of Munich",
"url": "https://www.tum.de"
},
"awardedBy": {
"@type": "CollegeOrUniversity",
"name": "Technical University of Munich",
"url": "https://www.tum.de"
},
"subjectOf": {
"@type": "EducationalProgram",
"name": "Informatics",
"url": "https://www.in.tum.de/en/for-prospective-students/masters-programs/informatics-msc/"
}
}
}
</script>
<script
src="https://rybbit.shiverpeak.xyz/api/script.js"
data-site-id="1"
data-web-vitals="true"
data-track-errors="true"
is:inline
defer></script>

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

@ -1,148 +0,0 @@
---
---
<script>
const themeToggleBtns = document.querySelectorAll(
".theme-toggle",
) as NodeListOf<HTMLInputElement>;
const sliders = document.querySelectorAll(".slider");
// Set initial state of toggle based on previous settings
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
themeToggleBtns.forEach((btn) => (btn.checked = true));
document.documentElement.classList.add("dark");
} else {
themeToggleBtns.forEach((btn) => (btn.checked = false));
document.documentElement.classList.remove("dark");
}
// Remove no-transition class after initial load
window.addEventListener("load", () => {
setTimeout(() => {
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));
}
}
});
});
</script>
<label class="switch" for="theme-toggle">
<span class="sr-only">Toggle dark mode</span>
<input
id="theme-toggle"
class="theme-toggle"
type="checkbox"
role="switch"
aria-checked="false"
/>
<span class="slider round no-transition" aria-hidden="true"></span>
</label>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--tw-mytheme-200);
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: gold;
transition: 0.4s;
background: radial-gradient(
yellow,
orange 63%,
transparent calc(63% + 3px) 100%
);
}
input:checked + .slider {
background-color: var(--tw-mytheme-600);
}
input:checked + .slider:before {
background-color: white;
background: radial-gradient(
circle at 19% 19%,
transparent 41%,
var(--tw-mytheme-50) 43%
);
}
input:focus + .slider {
box-shadow: 0 0 5px var(--tw-mytheme-700);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.no-transition {
transition: none !important;
}
.no-transition:before {
transition: none !important;
}
</style>

View file

@ -1,14 +0,0 @@
---
const today = new Date();
---
<footer class="bg-gray-100/60 dark:bg-mytheme-900 shadow-sm">
&copy; {today.getFullYear()} Alexander Daichendt. All rights reserved.
</footer>
<style>
footer {
padding: 2em 1em 6em 1em;
color: rgb(var(--gray));
text-align: center;
}
</style>

View file

@ -0,0 +1,275 @@
---
interface Props {
companyName?: string;
ownerName?: string;
address?: {
street?: string;
city?: string;
postalCode?: string;
country?: string;
};
contact?: {
phone?: string;
email: string;
website: string;
};
business?: {
vatId?: string;
registrationOffice?: string;
};
responsiblePerson?: {
name: string;
address: {
street: string;
city: string;
postalCode: string;
country: string;
};
};
}
const {
companyName,
ownerName,
address,
contact,
business,
responsiblePerson,
} = Astro.props;
---
<div class="max-w-4xl mx-auto space-y-12">
<section>
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3"
>
Diensteanbieter
</h2>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">Firmenname:</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{companyName}
</dd>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">Inhaber:</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{ownerName}
</dd>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">Anschrift:</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{address?.street}<br />
{address?.postalCode}
{address?.city}<br />
{address?.country}
</dd>
</div>
</div>
</section>
<!-- Contact Information -->
<section>
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3"
>
Kontaktdaten
</h2>
<div class="space-y-4">
{
contact?.phone && (
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Telefon:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{contact?.phone}
</dd>
</div>
)
}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">E-Mail:</dt>
<dd class="md:col-span-2">
<a
href={`mailto:${contact?.email}`}
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
>
{contact?.email}
</a>
</dd>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">Website:</dt>
<dd class="md:col-span-2">
<a
href={contact?.website}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
>
{contact?.website}
</a>
</dd>
</div>
</div>
</section>
<!-- Business Registration -->
{
(business?.vatId || business?.registrationOffice) && (
<section>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3">
Gewerbliche Angaben
</h2>
<div class="space-y-4">
{business?.vatId && (
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Umsatzsteuer-ID:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{business.vatId}
<br />
<span class="text-sm text-gray-500 dark:text-gray-400">
gemäß § 27a Umsatzsteuergesetz
</span>
</dd>
</div>
)}
{business.registrationOffice && (
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Gewerbeanmeldung:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{business?.registrationOffice}
</dd>
</div>
)}
</div>
</section>
)
}
<!-- Responsible for Content -->
<section>
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3"
>
Verantwortlich für den Inhalt
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Verantwortlich nach § 55 Abs. 2 RStV:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
{responsiblePerson?.name}<br />
{responsiblePerson?.address.street}<br />
{responsiblePerson?.address.postalCode}
{responsiblePerson?.address.city}<br />
{responsiblePerson?.address.country}
</dd>
</div>
</section>
<!-- EU Dispute Resolution -->
<section>
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3"
>
EU-Streitschlichtung
</h2>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Online-Streitbeilegung:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
Die Europäische Kommission stellt eine Plattform zur
Online-Streitbeilegung (OS) bereit:<br />
<a
href="https://ec.europa.eu/consumers/odr/"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
>
https://ec.europa.eu/consumers/odr/
</a>
</dd>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<dt class="font-semibold text-gray-900 dark:text-white">
Verbraucherschlichtung:
</dt>
<dd class="md:col-span-2 text-gray-700 dark:text-gray-300">
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren
vor einer Verbraucherschlichtungsstelle teilzunehmen.
</dd>
</div>
</div>
</section>
<!-- Disclaimer Section -->
<section>
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-6 border-b border-gray-200 dark:border-gray-700 pb-3"
>
Haftungsausschluss
</h2>
<!-- Content Liability -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Haftung für Inhalte
</h3>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf
diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8
bis 10 TMG sind wir als Diensteanbieter jedoch nicht unter der
Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu
überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige
Tätigkeit hinweisen.
</p>
</div>
<!-- Links Liability -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Haftung für Links
</h3>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
Unser Angebot enthält Links zu externen Websites Dritter, auf deren
Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden
Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten
Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten
verantwortlich.
</p>
</div>
<!-- Copyright -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Urheberrecht
</h3>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen
Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung,
Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des
jeweiligen Autors bzw. Erstellers.
</p>
</div>
</section>
<!-- Footer Note -->
<div class="border-t border-gray-200 dark:border-gray-600 pt-8 mt-12">
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
Letzte Aktualisierung: <span class="font-medium">
{new Date().toLocaleDateString("de-DE")}
</span>
</p>
</div>
</div>

168
src/components/Logo.astro Normal file
View file

@ -0,0 +1,168 @@
<!--?xml version="1.0" encoding="UTF-8" standalone="no"?-->
<svg
version="1.0"
width="3364pt"
height="832pt"
viewBox="0 0 3364 832"
preserveAspectRatio="xMidYMid"
id="svg44"
class="logo"
xmlns="http://www.w3.org/2000/svg"
>
<g
transform="matrix(0.1,0,0,-0.1,-312.98366,2490.1118)"
stroke="none"
id="g44"
class="logo-group"
>
<path
d="m 4485,24500 c -123,-9 -208,-30 -304,-75 -324,-154 -540,-444 -581,-785 -14,-114 -14,-5531 0,-5722 17,-236 62,-375 172,-535 135,-196 338,-342 568,-406 l 75,-21 2762,-4 c 2804,-3 3115,0 3238,33 93,25 255,108 336,173 216,172 354,409 373,642 3,36 6,1363 8,2950 2,2719 1,2889 -15,2960 -35,150 -87,259 -186,389 -108,142 -225,236 -386,312 -126,59 -224,79 -430,90 -214,10 -5485,9 -5630,-1 z m 5700,-411 c 205,-25 386,-151 475,-333 82,-165 75,114 75,-3041 v -2810 l -22,-82 c -51,-190 -161,-327 -328,-408 -141,-68 45,-64 -3039,-65 -2759,-1 -2782,-1 -2859,19 -116,30 -190,74 -282,166 -93,92 -145,184 -173,305 -16,70 -17,253 -15,2898 2,3087 -2,2873 59,2994 82,165 211,282 374,340 l 65,23 1650,6 c 2136,7 3902,2 4020,-12 z"
id="path1"></path>
<path
d="m 6386,23148 c -15,-23 -124,-291 -294,-723 -125,-316 -183,-470 -407,-1065 -106,-283 -274,-724 -373,-980 -216,-559 -431,-1137 -445,-1195 -3,-14 161,-22 474,-24 l 266,-1 36,78 c 33,73 85,196 226,546 33,83 66,155 73,162 10,10 105,13 385,15 205,2 375,0 378,-3 4,-3 9,-417 12,-920 4,-649 9,-915 17,-920 17,-12 1352,-9 1516,2 352,25 581,87 860,232 397,206 682,521 817,902 67,190 88,421 79,861 -6,309 -12,356 -71,560 -34,118 -138,328 -217,437 -91,126 -231,281 -312,347 -272,221 -475,310 -771,341 -94,9 -396,28 -402,25 -1,-1 29,-84 68,-186 38,-101 94,-249 123,-329 78,-211 86,-228 104,-235 9,-4 59,-16 111,-26 352,-70 592,-366 672,-827 44,-258 8,-542 -101,-793 -49,-114 -107,-197 -201,-290 -217,-216 -542,-354 -889,-380 -204,-15 -662,-8 -683,10 -15,12 -17,66 -17,587 0,476 2,575 14,586 9,9 59,19 137,26 67,7 125,14 127,17 13,13 -205,594 -235,628 -15,16 -58,17 -634,17 -339,0 -624,3 -633,6 -14,5 -14,10 0,42 8,21 48,125 89,232 90,239 188,495 220,575 25,63 115,294 167,430 17,44 70,179 118,300 48,121 95,245 105,275 10,30 24,68 32,84 26,50 32,38 193,-394 185,-497 310,-821 457,-1190 277,-695 326,-821 462,-1200 73,-201 151,-417 174,-480 l 43,-115 39,1 c 39,1 232,21 425,45 52,6 128,14 167,18 94,8 103,17 79,79 -10,26 -29,74 -41,107 -12,33 -34,89 -47,125 -14,36 -66,175 -116,310 -222,598 -313,841 -437,1160 -70,178 -118,305 -273,715 -183,488 -403,1049 -520,1330 l -29,70 -239,6 c -131,4 -380,7 -551,8 l -313,1 z"
id="path2"></path>
<path
d="m 18208,22228 c -1,-304 -1,-588 1,-630 2,-43 -1,-78 -6,-78 -5,0 -52,37 -105,83 -166,143 -340,214 -548,224 -189,9 -349,-28 -502,-116 -175,-100 -295,-230 -388,-416 -233,-467 -142,-1104 205,-1434 122,-116 277,-202 430,-237 110,-26 380,-27 475,-1 145,38 312,144 405,256 28,33 55,59 60,57 6,-2 11,-61 13,-144 l 3,-142 h 214 c 117,0 216,3 218,8 6,9 9,822 8,2100 l -1,1022 h -240 -240 z m -433,-855 c 108,-29 184,-72 267,-154 84,-82 131,-165 165,-289 25,-92 25,-348 -1,-440 -28,-103 -83,-197 -165,-283 -60,-63 -91,-86 -155,-117 -100,-47 -160,-60 -284,-60 -177,0 -285,43 -403,159 -84,84 -122,152 -162,289 -18,65 -22,101 -22,232 0,180 13,240 76,367 89,178 240,288 429,313 71,10 179,2 255,-17 z"
id="path3"></path>
<path
d="m 24805,21214 v -1566 l 242,4 c 133,1 244,6 248,10 4,4 9,303 12,665 5,607 6,663 24,724 78,270 377,420 654,327 143,-48 241,-169 271,-337 10,-59 13,-228 14,-735 v -658 l 239,4 c 132,2 243,7 248,12 13,13 16,1294 3,1421 -29,290 -199,563 -417,672 -124,62 -203,76 -391,70 -159,-5 -193,-11 -303,-58 -77,-32 -204,-118 -272,-184 -33,-31 -61,-54 -64,-52 -2,3 -4,285 -3,626 l 1,621 h -253 -253 z"
id="path4"></path>
<path
d="m 33730,22144 c 1,-350 0,-638 -3,-640 -2,-2 -40,29 -84,69 -94,87 -143,122 -240,170 -241,122 -556,121 -798,-1 -147,-73 -244,-153 -335,-274 -101,-134 -179,-306 -216,-478 -28,-128 -26,-412 4,-536 65,-275 193,-497 368,-640 119,-98 302,-181 446,-204 110,-18 353,-8 438,18 143,42 281,132 399,258 38,41 72,74 75,74 3,0 7,-69 8,-152 l 3,-153 216,-3 216,-2 7,297 c 7,316 6,2651 -1,2766 l -5,67 h -249 -250 z m -466,-759 c 240,-51 421,-234 470,-477 20,-95 20,-276 1,-381 -21,-120 -90,-253 -172,-333 -112,-109 -252,-164 -419,-164 -248,0 -435,112 -543,326 -94,186 -113,416 -51,616 47,150 173,304 302,367 117,58 274,75 412,46 z"
id="path5"></path>
<path
d="m 21683,22716 c -90,-28 -179,-110 -201,-184 -62,-206 93,-394 313,-380 93,6 174,47 243,122 60,66 76,110 69,181 -11,110 -103,225 -207,258 -61,20 -158,21 -217,3 z"
id="path6"></path>
<path
d="m 35036,22349 c -3,-34 -6,-174 -6,-310 v -249 h -215 -215 v -149 c 0,-81 3,-176 6,-210 l 7,-61 h 96 c 53,0 146,-3 208,-7 l 111,-6 5,-611 c 5,-674 6,-688 69,-816 45,-92 159,-203 261,-253 176,-86 389,-97 599,-31 126,40 122,32 126,238 4,144 2,176 -9,176 -8,0 -48,-7 -89,-16 -44,-10 -125,-17 -195,-18 -115,-1 -122,0 -169,28 -36,21 -54,40 -70,75 -21,45 -21,54 -21,633 0,322 2,589 5,591 3,3 124,8 270,12 146,4 268,10 272,14 7,6 13,389 6,396 -2,2 -118,6 -258,10 -140,3 -265,8 -277,11 l -23,4 v 305 305 h -244 -243 z"
id="path7"></path>
<path
d="m 13699,21840 c -235,-22 -415,-107 -585,-275 -88,-87 -107,-112 -147,-195 -48,-99 -93,-248 -79,-262 5,-5 109,-11 233,-15 252,-7 232,-12 267,74 60,151 154,218 339,245 162,23 331,-19 406,-101 75,-83 108,-165 122,-300 7,-70 7,-71 -20,-81 -14,-5 -119,-17 -233,-25 -685,-50 -847,-92 -1028,-270 -153,-151 -199,-363 -124,-581 70,-202 278,-372 525,-430 125,-29 322,-25 438,10 48,14 114,39 146,56 67,33 194,128 264,195 26,25 51,44 56,41 5,-3 12,-64 16,-136 7,-122 9,-131 29,-135 11,-3 104,-4 206,-3 l 185,3 6,130 c 10,207 10,1096 -1,1255 -14,213 -52,330 -156,477 -80,113 -217,215 -359,269 -122,45 -342,69 -506,54 z m 559,-1254 c 10,-10 12,-171 3,-219 -15,-84 -38,-124 -106,-193 -69,-70 -162,-122 -270,-150 -97,-25 -291,-26 -360,-1 -124,43 -209,151 -208,265 1,70 12,99 54,146 41,47 93,74 178,91 55,12 422,54 591,68 41,4 111,-1 118,-7 z"
id="path8"></path>
<path
d="m 19935,21840 c -95,-15 -223,-58 -315,-106 -160,-84 -272,-195 -349,-343 -42,-83 -109,-276 -98,-287 9,-9 397,-18 440,-10 21,4 37,11 37,16 0,6 14,40 32,77 70,148 162,204 378,228 75,9 224,-17 288,-49 90,-46 162,-163 188,-305 19,-108 18,-120 -11,-131 -13,-5 -102,-14 -197,-20 -392,-24 -678,-66 -821,-120 -133,-50 -272,-172 -332,-291 -55,-108 -71,-269 -39,-393 60,-232 269,-419 542,-482 89,-21 275,-20 376,1 159,33 302,111 428,233 78,75 72,82 91,-93 l 12,-110 95,-3 c 52,-2 150,0 218,3 l 122,7 v 679 c 0,412 -4,718 -11,777 -39,362 -287,629 -654,707 -93,20 -332,28 -420,15 z m 608,-1388 c -1,-113 -5,-139 -23,-177 -32,-67 -119,-147 -208,-194 -99,-51 -171,-70 -297,-77 -208,-13 -339,58 -391,210 -25,72 -15,135 32,203 61,89 147,118 419,143 72,7 164,16 205,20 41,4 118,7 170,6 l 95,-1 z"
id="path9"></path>
<path
d="m 28005,21826 c -195,-38 -360,-127 -514,-279 -97,-96 -150,-171 -215,-308 -88,-184 -124,-370 -113,-584 13,-246 87,-455 226,-643 116,-156 213,-236 385,-318 156,-74 223,-87 426,-88 151,-1 186,2 260,22 172,46 298,120 441,259 102,98 217,252 204,272 -3,6 -30,23 -58,38 -62,31 -278,123 -289,123 -5,0 -43,-34 -85,-76 -91,-91 -195,-165 -272,-196 -48,-19 -75,-22 -186,-22 -124,0 -134,1 -205,32 -136,58 -230,150 -301,294 -37,73 -76,209 -65,226 3,5 342,12 778,16 495,4 777,11 783,17 17,18 9,274 -13,380 -49,239 -186,470 -372,627 -92,76 -261,163 -380,193 -108,27 -331,35 -435,15 z m 355,-422 c 30,-9 74,-27 97,-41 62,-36 174,-161 215,-240 35,-67 66,-178 53,-191 -4,-4 -247,-8 -541,-10 -613,-3 -552,-16 -511,105 30,88 99,195 168,262 63,59 171,116 249,130 71,13 201,6 270,-15 z"
id="path10"></path>
<path
d="m 23315,21821 c -206,-33 -386,-126 -541,-279 -311,-306 -413,-835 -243,-1260 149,-376 443,-620 808,-673 118,-17 343,-7 446,21 278,72 542,305 655,577 23,54 29,82 23,92 -11,17 -106,35 -258,50 -55,6 -125,13 -156,17 l -57,6 -37,-58 c -111,-177 -272,-274 -452,-274 -211,0 -387,116 -487,322 -114,234 -118,492 -13,707 104,214 267,323 482,323 111,-1 199,-26 281,-81 64,-43 109,-97 170,-204 45,-79 21,-76 259,-33 247,45 255,47 255,60 0,25 -78,181 -128,256 -131,198 -320,341 -537,406 -79,24 -110,27 -255,30 -91,1 -187,-1 -215,-5 z"
id="path11"></path>
<path
d="m 30636,21814 c -155,-34 -251,-84 -411,-213 -55,-44 -103,-77 -107,-75 -5,3 -8,60 -8,128 v 123 l -216,6 c -119,4 -220,5 -224,2 -11,-7 -12,-578 -4,-1418 l 7,-719 236,4 236,3 5,660 c 5,601 7,665 24,715 87,266 307,410 565,372 159,-23 259,-86 327,-208 71,-127 69,-102 69,-862 v -682 h 195 c 107,0 211,3 232,6 l 36,6 6,67 c 4,36 7,356 8,711 2,689 0,733 -48,872 -39,117 -90,195 -188,293 -76,76 -107,99 -186,138 -52,26 -122,54 -155,63 -86,23 -310,28 -399,8 z"
id="path12"></path>
<path
d="m 21540,20721 v -1073 l 239,4 c 132,1 243,7 248,11 9,9 9,2104 0,2113 -3,3 -114,8 -246,11 l -241,6 z"
id="path13"></path>
<path
d="m 15358,20207 c -140,-53 -208,-147 -208,-289 0,-60 5,-84 29,-133 34,-70 88,-126 151,-154 67,-30 188,-29 257,3 60,29 120,85 159,152 24,41 28,58 28,128 1,69 -3,89 -27,138 -30,62 -74,106 -140,140 -53,27 -193,36 -249,15 z"
id="path14"></path>
<path
d="m 15901,18323 c -10,-20 96,-485 148,-653 38,-120 52,-140 96,-140 32,0 34,2 69,86 56,134 136,430 136,504 0,43 10,33 16,-16 7,-55 100,-379 135,-471 35,-89 53,-113 84,-113 30,0 38,12 74,100 23,57 152,530 188,688 5,20 1,22 -30,22 -49,0 -65,-8 -82,-41 -28,-53 -134,-509 -136,-586 -2,-34 -3,-33 -11,17 -28,165 -149,553 -185,592 -10,11 -27,18 -42,16 -28,-3 -43,-45 -172,-455 -41,-130 -59,-162 -59,-104 0,16 -11,71 -24,123 -13,51 -36,145 -51,208 -35,145 -61,217 -85,230 -28,15 -58,12 -69,-7 z"
id="path15"></path>
<path
d="m 17206,18317 c -9,-12 -22,-42 -31,-66 -8,-24 -20,-49 -25,-56 -15,-18 -250,-625 -250,-646 0,-15 7,-19 38,-19 20,0 43,4 50,9 8,4 31,47 51,93 61,136 52,130 178,134 59,2 119,4 132,6 21,3 28,-4 45,-47 77,-193 85,-205 159,-205 45,0 48,-18 -25,185 -80,223 -121,329 -198,505 -54,125 -92,157 -124,107 z m 81,-284 c 28,-87 51,-158 50,-159 -1,-1 -46,-4 -101,-8 l -99,-7 7,33 c 3,18 17,60 31,93 13,34 30,94 36,133 7,40 15,72 18,72 3,0 29,-71 58,-157 z"
id="path16"></path>
<path
d="m 20902,18327 c -13,-15 56,-228 173,-542 93,-249 96,-255 146,-255 46,0 70,44 164,300 30,80 79,213 111,295 71,189 74,202 42,210 -13,3 -36,1 -51,-4 -37,-14 -72,-91 -171,-376 -43,-121 -81,-229 -85,-240 -6,-14 -26,36 -78,190 -100,294 -137,393 -156,416 -19,22 -79,26 -95,6 z"
id="path17"></path>
<path
d="m 31314,18326 c -72,-17 -112,-50 -141,-114 -63,-135 2,-242 192,-317 146,-57 185,-89 185,-149 0,-55 -73,-108 -149,-109 -53,-1 -92,17 -142,66 -45,44 -70,57 -108,57 -25,0 -31,-4 -31,-21 0,-118 221,-243 352,-200 108,36 188,127 188,216 0,106 -62,172 -221,235 -101,40 -158,76 -179,113 -13,24 -13,30 0,54 19,34 85,73 123,73 42,0 86,-22 146,-73 62,-53 88,-59 97,-23 9,37 -23,93 -78,137 -70,55 -152,74 -234,55 z"
id="path18"></path>
<path
d="m 34121,18323 c -33,-28 -308,-750 -297,-780 7,-17 67,-17 88,0 8,6 30,52 47,101 47,131 35,122 143,118 53,-2 106,1 122,7 43,17 66,-4 104,-96 51,-123 64,-140 112,-148 23,-4 46,-3 50,1 5,5 -10,56 -32,114 -22,58 -58,152 -78,210 -135,376 -187,490 -226,490 -6,0 -21,-8 -33,-17 z m 32,-175 c 3,-13 26,-79 52,-147 25,-68 44,-125 43,-126 -7,-4 -203,-24 -206,-20 -3,2 16,64 42,138 25,74 46,144 46,156 0,28 16,27 23,-1 z"
id="path19"></path>
<path
d="m 34672,18327 c -12,-14 -22,-780 -10,-792 13,-13 79,-9 89,5 5,8 9,150 9,315 1,281 2,298 17,270 39,-72 204,-302 311,-435 97,-120 124,-147 156,-158 21,-7 42,-10 45,-6 7,7 10,773 3,792 -2,7 -23,12 -54,12 h -50 l 4,-304 c 3,-282 2,-303 -13,-290 -9,8 -68,90 -130,182 -152,226 -284,391 -326,408 -39,17 -38,17 -51,1 z"
id="path20"></path>
<path
d="m 13091,18311 c -102,-41 -148,-108 -139,-202 9,-88 77,-155 212,-210 179,-72 212,-109 171,-189 -16,-31 -89,-80 -120,-80 -44,0 -102,28 -150,72 -60,54 -93,70 -118,57 -21,-12 -22,-60 -2,-99 16,-31 123,-99 189,-120 66,-21 146,-8 210,35 94,62 121,120 106,228 -8,54 -14,66 -47,95 -40,35 -86,59 -191,98 -98,37 -159,81 -175,127 -5,13 8,28 54,62 91,67 119,64 232,-27 18,-15 48,-29 68,-30 30,-3 34,0 37,24 2,15 -4,40 -12,56 -16,31 -89,89 -139,110 -40,17 -135,14 -186,-7 z"
id="path21"></path>
<path
d="m 14575,18321 c -3,-2 -5,-176 -5,-386 0,-283 3,-384 12,-393 7,-7 31,-12 55,-12 h 43 v 129 c 0,71 3,142 6,159 l 6,30 149,4 c 81,1 152,8 158,13 5,6 11,26 13,45 l 3,35 -160,5 -160,5 -5,100 c -3,55 -3,112 -1,128 l 3,27 h 174 174 v 61 61 l -230,-3 c -127,-2 -233,-5 -235,-8 z"
id="path22"></path>
<path
d="m 15179,18318 c -14,-6 -19,-17 -19,-46 0,-21 5,-43 12,-50 8,-8 50,-12 125,-12 62,0 114,-1 115,-2 1,-2 4,-154 7,-338 l 6,-335 44,-3 c 28,-2 47,1 52,10 5,7 9,153 10,323 0,171 4,319 8,330 8,18 18,20 117,20 h 109 l 8,48 c 5,26 7,51 3,54 -8,9 -573,9 -597,1 z"
id="path23"></path>
<path
d="m 17775,18323 c -11,-3 -23,-8 -27,-12 -11,-10 -22,-513 -13,-653 l 7,-128 h 53 53 l 4,138 c 4,161 10,176 67,166 20,-3 43,-8 52,-10 9,-3 49,-53 88,-113 103,-155 142,-191 208,-191 22,0 24,3 18,28 -10,39 -59,122 -121,204 -30,39 -54,77 -54,84 0,6 17,24 38,40 83,64 121,128 122,204 0,56 -36,134 -81,177 -59,56 -98,66 -254,68 -77,1 -149,0 -160,-2 z m 291,-122 c 59,-27 84,-63 84,-124 0,-46 -3,-53 -49,-98 l -48,-49 -79,6 c -130,11 -124,4 -124,138 0,91 3,118 16,130 23,23 146,21 200,-3 z"
id="path24"></path>
<path
d="m 18610,18323 c -75,-2 -117,-8 -122,-16 -11,-18 -10,-749 2,-767 8,-12 37,-14 177,-12 92,1 192,5 221,8 l 52,6 v 58 57 l -167,-1 -168,-1 -3,100 c -5,159 -16,148 146,144 l 137,-4 3,53 c 4,61 20,55 -158,56 l -125,1 -9,47 c -6,29 -6,70 0,105 l 9,58 157,-4 158,-4 15,36 c 30,72 26,77 -69,77 -46,0 -96,1 -112,3 -16,2 -81,2 -144,0 z"
id="path25"></path>
<path
d="m 19553,18323 -83,-4 v -389 -389 l 37,-7 c 62,-12 263,4 324,25 106,36 178,98 228,199 45,88 44,266 -2,362 -31,67 -131,161 -192,182 -54,19 -182,27 -312,21 z m 249,-115 c 57,-17 131,-87 158,-150 19,-45 22,-64 18,-140 -5,-72 -10,-96 -35,-140 -53,-94 -156,-143 -290,-136 l -58,3 -3,279 c -2,217 1,281 10,288 20,12 156,10 200,-4 z"
id="path26"></path>
<path
d="m 20390,18321 -75,-6 -7,-340 c -4,-187 -5,-364 -2,-392 l 5,-53 142,1 c 198,1 306,11 318,29 9,14 6,77 -5,87 -3,3 -80,6 -173,7 l -168,1 -3,80 c -2,44 0,99 3,123 l 6,42 h 150 149 v 50 49 l -152,3 -153,3 -3,107 -3,108 173,-3 173,-2 8,48 c 5,26 6,51 2,55 -9,9 -287,11 -385,3 z"
id="path27"></path>
<path
d="m 21755,18319 c -3,-8 -6,-187 -7,-399 l -3,-385 175,-3 c 96,-1 202,1 235,5 l 60,8 v 53 53 l -175,2 -175,2 -3,70 c -2,39 0,94 3,124 l 7,54 126,-5 c 70,-2 135,-3 145,0 14,3 17,14 17,59 v 56 l -140,-6 c -167,-8 -162,-11 -158,120 l 3,86 170,1 170,1 9,48 c 4,26 5,51 2,54 -4,4 -107,9 -231,11 -185,4 -226,2 -230,-9 z"
id="path28"></path>
<path
d="m 22417,18143 c -9,-236 -9,-526 -1,-576 l 7,-37 h 205 c 113,1 219,4 234,7 27,5 28,8 28,59 0,63 26,56 -207,54 l -153,-1 -2,338 -3,338 -51,3 -51,3 z"
id="path29"></path>
<path
d="m 23275,18319 c -206,-27 -350,-252 -301,-470 42,-186 160,-301 326,-316 109,-10 242,52 321,150 77,96 101,195 79,317 -18,102 -45,153 -116,219 -90,85 -187,116 -309,100 z m 153,-124 c 51,-22 120,-91 144,-145 24,-56 27,-179 5,-232 -21,-49 -92,-125 -140,-150 -51,-25 -130,-33 -185,-18 -58,15 -124,77 -152,143 -33,75 -34,214 -3,274 25,46 78,93 138,123 49,24 140,26 193,5 z"
id="path30"></path>
<path
d="m 23939,18321 c -8,-2 -17,-9 -21,-15 -4,-6 -8,-180 -8,-387 0,-420 -6,-389 71,-389 h 39 v 134 c 0,73 3,147 6,164 l 6,30 122,4 c 108,3 127,7 173,31 107,55 146,184 89,295 -56,110 -112,134 -317,136 -81,1 -153,0 -160,-3 z m 302,-111 c 55,-15 82,-50 87,-115 6,-70 -11,-97 -74,-119 -58,-20 -209,-22 -221,-3 -9,15 -18,211 -9,232 4,12 24,15 93,15 48,0 104,-5 124,-10 z"
id="path31"></path>
<path
d="m 24710,18320 -95,-5 -3,-384 c -2,-301 1,-386 10,-393 16,-9 216,-10 351,-1 l 97,6 v 56 56 h -172 -173 l -3,100 c -5,157 -15,146 141,145 73,-1 142,-1 155,-1 20,1 22,6 22,57 v 57 l -147,-7 c -82,-3 -154,-2 -161,2 -9,6 -12,35 -10,108 l 3,99 h 155 c 212,1 197,-2 202,45 2,23 1,45 -2,49 -7,11 -250,18 -370,11 z"
id="path32"></path>
<path
d="m 25286,18323 c -3,-4 -6,-183 -6,-400 v -393 h 65 65 v 128 c 0,70 3,137 6,149 6,23 29,30 82,24 29,-3 40,-15 106,-115 41,-61 89,-126 106,-143 39,-40 130,-70 130,-43 0,12 -86,152 -146,237 -24,34 -44,66 -44,71 0,6 21,22 46,37 62,36 124,131 124,190 0,100 -78,218 -162,245 -43,14 -360,25 -372,13 z m 317,-114 c 21,-5 52,-23 69,-38 26,-23 32,-37 36,-85 l 5,-57 -47,-43 c -50,-47 -77,-56 -165,-56 -83,0 -84,2 -85,122 -1,117 6,155 32,161 33,9 117,7 155,-4 z"
id="path33"></path>
<path
d="m 27150,17929 v -402 l 50,5 c 28,3 54,8 58,12 9,10 15,676 6,739 l -7,47 h -53 -54 z"
id="path34"></path>
<path
d="m 27428,18324 c -5,-4 -8,-29 -8,-56 0,-46 1,-48 28,-49 77,-2 209,-10 220,-14 9,-3 12,-80 12,-340 v -335 h 54 54 l 7,318 c 3,174 10,328 14,342 7,24 10,25 113,28 101,3 105,4 117,28 6,14 11,36 11,49 v 25 h -257 c -142,0 -280,3 -308,6 -27,3 -53,3 -57,-2 z"
id="path35"></path>
<path
d="m 28780,18315 c -78,-22 -134,-59 -179,-116 -160,-202 -96,-521 127,-631 58,-29 76,-33 147,-33 72,0 86,3 142,34 101,55 179,144 153,176 -22,27 -47,22 -104,-21 -104,-77 -166,-96 -254,-74 -132,31 -213,189 -183,355 14,76 52,130 121,172 57,35 120,49 173,39 18,-3 69,-29 112,-56 44,-28 87,-50 95,-50 16,0 40,38 40,63 0,25 -76,91 -138,120 -78,36 -170,44 -252,22 z"
id="path36"></path>
<path
d="m 29632,18319 c -146,-29 -258,-136 -296,-284 -61,-232 88,-463 323,-501 90,-14 214,38 303,127 83,82 109,143 109,257 1,147 -29,222 -126,310 -84,77 -207,113 -313,91 z m 194,-142 c 118,-74 160,-177 129,-314 -18,-78 -51,-125 -120,-173 -124,-85 -283,-47 -361,86 -35,60 -46,200 -20,267 28,74 118,149 201,167 48,10 129,-6 171,-33 z"
id="path37"></path>
<path
d="m 30287,18323 c -4,-3 -7,-181 -7,-394 v -387 l 23,-6 c 12,-3 37,-6 55,-6 h 32 l 1,203 c 0,111 0,249 -1,306 -1,63 2,101 8,97 5,-3 28,-36 50,-73 22,-38 78,-120 124,-184 249,-349 244,-343 305,-355 22,-4 32,-2 36,9 6,15 9,290 8,619 l -1,167 -34,6 c -19,4 -44,4 -56,0 -21,-7 -21,-8 -18,-310 2,-167 2,-303 2,-302 -31,37 -76,104 -97,142 -47,86 -328,459 -353,469 -21,8 -69,8 -77,-1 z"
id="path38"></path>
<path
d="m 31861,18316 c -10,-11 -12,-81 -9,-288 l 3,-273 33,-68 c 27,-55 44,-74 85,-102 157,-102 376,-51 459,109 21,39 22,53 23,336 v 295 h -55 -55 v -275 c -1,-297 -4,-320 -56,-366 -84,-74 -248,-51 -294,41 -18,37 -20,61 -21,320 l -1,280 -50,3 c -35,2 -53,-2 -62,-12 z"
id="path39"></path>
<path
d="m 32712,18323 -22,-4 2,-392 3,-392 219,1 c 245,2 244,2 253,73 l 6,41 h -181 c -155,0 -181,2 -186,16 -3,9 -6,154 -6,324 0,359 2,350 -88,333 z"
id="path40"></path>
<path
d="m 13868,18301 c -71,-28 -113,-57 -162,-112 -100,-111 -117,-299 -41,-449 35,-70 106,-142 172,-175 50,-26 69,-30 138,-30 69,0 89,4 148,32 81,39 166,116 210,192 31,54 32,60 32,171 0,106 -2,120 -28,173 -38,76 -113,151 -184,185 -77,36 -213,42 -285,13 z m 240,-117 c 93,-46 152,-145 152,-253 0,-106 -69,-208 -180,-267 -49,-26 -164,-25 -209,1 -113,67 -167,224 -125,366 42,144 225,221 362,153 z"
id="path41"></path>
<path
d="m 33171,18311 c -17,-11 -19,-67 -3,-83 7,-7 51,-12 116,-12 57,-1 108,-5 113,-9 4,-5 10,-159 12,-343 l 3,-335 57,3 56,3 5,330 c 3,182 9,336 13,343 6,8 40,12 112,12 h 103 l 11,31 c 26,74 45,69 -286,69 -164,0 -304,-4 -312,-9 z"
id="path42"></path>
<path
d="m 35462,18273 3,-48 118,-6 c 65,-3 122,-10 126,-15 5,-5 11,-155 15,-333 5,-242 9,-327 19,-333 19,-12 72,-9 85,4 9,9 12,97 12,333 0,176 4,325 8,332 5,8 45,13 117,15 l 110,3 11,35 c 6,19 9,40 7,47 -4,10 -76,13 -320,13 h -314 z"
id="path43"></path>
<path
d="m 26404,17976 c -37,-37 -45,-83 -23,-145 14,-40 35,-51 99,-51 44,0 56,4 82,31 28,27 30,34 25,78 -8,67 -25,98 -60,111 -55,19 -87,12 -123,-24 z"
id="path44"></path>
</g>
</svg>
<style>
.logo {
width: 100%;
height: 64px;
max-width: 300px; /* Adjust as needed */
}
.logo-group path {
fill: #061e45; /* Dark blue for light mode */
transition: fill 0.3s ease;
}
/* If you're using a class-based dark mode approach */
:global(.dark) .logo-group path {
fill: #e5e7eb;
}
</style>

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,85 +0,0 @@
---
import { Icon } from "astro-icon/components";
import HeaderLink from "./HeaderLink.astro";
---
<header
class="mb-8 w-full lg:w-[768px] max-w-[calc(100%-2em)] lg:mx-auto hidden lg:block"
>
<nav>
<div class="flex gap-4">
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/projects">Projects</HeaderLink>
<HeaderLink href="/publications">Publications</HeaderLink>
<HeaderLink href="/contact">Contact</HeaderLink>
</div>
</nav>
</header>
<button id="nav-menu" class="lg:hidden relative w-[30px] h-[30px]">
<Icon
name="mdi:menu"
id="iconMenu"
class="absolute inset-0 transition-all duration-300 ease-in-out"
size={30}
/>
<Icon
name="mdi:close"
id="iconClose"
class="absolute inset-0 opacity-0 rotate-90 transition-all duration-300 ease-in-out"
size={30}
/>
</button>
<header
id="drawer"
class="fixed z-50 top-14 right-0 h-full bg-neutral-50 shadow dark:bg-gray-700 p-6 rounded w-54 transform translate-x-full transition-transform duration-300 ease-in-out"
>
<div class="flex flex-col gap-4">
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/projects">Projects</HeaderLink>
<HeaderLink href="/publications">Publications</HeaderLink>
<HeaderLink href="/contact">Contact</HeaderLink>
</div>
</header>
<script>
const menu = document.getElementById("nav-menu")!;
const drawer = document.getElementById("drawer")!;
const iconMenu = document.getElementById("iconMenu")!;
const iconClose = document.getElementById("iconClose")!;
function toggle() {
drawer.classList.toggle("translate-x-full");
document.body.classList.toggle("overflow-hidden");
if (iconMenu.classList.contains("opacity-0")) {
iconMenu.classList.remove("opacity-0", "rotate-90");
iconClose.classList.add("opacity-0", "rotate-90");
} else {
iconMenu.classList.add("opacity-0", "rotate-90");
iconClose.classList.remove("opacity-0", "rotate-90");
}
}
menu.addEventListener("click", () => {
toggle();
});
// click-away listener
document.addEventListener("click", (event: MouseEvent) => {
const target = event.target as Node;
if (
!drawer.classList.contains("translate-x-full") &&
!drawer.contains(target) &&
!menu.contains(target)
) {
console.log(
!drawer.classList.contains("translate-x-full"),
!drawer.contains(target),
!menu.contains(target),
);
toggle();
}
});
</script>

View file

@ -0,0 +1,22 @@
---
interface Props {
title: string;
subtitle?: string;
}
const { title, subtitle } = Astro.props;
---
<div class="text-center mb-16">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h1>
{
subtitle && (
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{subtitle}
</p>
)
}
<div class="mt-8 w-24 h-1 bg-mytheme-400 mx-auto rounded-full"></div>
</div>

View file

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

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

@ -0,0 +1,220 @@
---
---
<script>
const themeToggleBtns = document.querySelectorAll(
".theme-toggle",
) 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" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
themeToggleBtns.forEach((btn) => (btn.checked = true));
document.documentElement.classList.add("dark");
} else {
themeToggleBtns.forEach((btn) => (btn.checked = false));
document.documentElement.classList.remove("dark");
}
// Remove no-transition class after initial load
window.addEventListener("load", () => {
setTimeout(() => {
sliders.forEach((slider) => slider.classList.remove("no-transition"));
}, 0);
});
themeToggleBtns.forEach((btn) => {
btn.addEventListener("change", function () {
// 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>
<label class="switch" for="theme-toggle">
<span class="sr-only">Toggle dark mode</span>
<input
id="theme-toggle"
class="theme-toggle"
type="checkbox"
role="switch"
aria-checked="false"
/>
<span class="slider round no-transition" aria-hidden="true"></span>
</label>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--tw-mytheme-200);
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: gold;
transition: 0.4s;
background: radial-gradient(
yellow,
orange 63%,
transparent calc(63% + 3px) 100%
);
}
input:checked + .slider {
background-color: var(--tw-mytheme-600);
}
input:checked + .slider:before {
background-color: white;
background: radial-gradient(
circle at 19% 19%,
transparent 41%,
var(--tw-mytheme-50) 43%
);
}
input:focus + .slider {
box-shadow: 0 0 5px var(--tw-mytheme-700);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.no-transition {
transition: none !important;
}
.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

@ -0,0 +1,22 @@
---
import HeaderLink from "../HeaderLink.astro";
export const navItems = [
{ href: "/", label: "Home" },
{ href: "/blog", label: "Blog" },
{ href: "/projects", label: "Projects" },
{ href: "/publications", label: "Publications" },
{ href: "/contact", label: "Contact" },
];
---
<!-- Desktop Navigation -->
<nav class="hidden lg:block">
<div class="flex gap-4">
{
navItems.map((item) => (
<HeaderLink href={item.href}>{item.label}</HeaderLink>
))
}
</div>
</nav>

View file

@ -0,0 +1,39 @@
---
const today = new Date();
const lastUpdated = new Date(); // You can set this to a specific date or pull from your build process
---
<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 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"
>
<!-- Copyright -->
<div class="text-center sm:text-left">
<p>
&copy; {today.getFullYear()} Alexander Daichendt. All rights reserved.
</p>
</div>
<!-- Legal links -->
<div class="flex items-center justify-center gap-4">
<a
href="/impressum"
class="text-gray-600 dark:text-gray-400 hover:text-accent hover:underline transition-colors duration-200"
>
Impressum
</a>
<span class="opacity-60">•</span>
<a
href="/datenschutz"
class="text-gray-600 dark:text-gray-400 hover:text-accent hover:underline transition-colors duration-200"
>
Datenschutz
</a>
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,40 @@
---
---
<!-- Mobile Navigation Button Only -->
<div class="lg:hidden">
<button
id="mobile-nav-toggle"
class="relative w-8 h-8 flex items-center justify-center flex-col z-50"
aria-label="Toggle navigation"
type="button"
>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</div>
<style>
.hamburger-line {
@apply block w-5 h-0.5 bg-gray-600 dark:bg-gray-300 transition-all duration-300;
margin-bottom: 3px;
}
.hamburger-line:last-child {
margin-bottom: 0;
}
#mobile-nav-toggle.nav-open .hamburger-line:nth-child(1) {
transform: rotate(45deg) translate(2px, 2px);
}
#mobile-nav-toggle.nav-open .hamburger-line:nth-child(2) {
opacity: 0;
}
#mobile-nav-toggle.nav-open .hamburger-line:nth-child(3) {
transform: rotate(-45deg) translate(2px, -2px);
}
</style>

View file

@ -0,0 +1,130 @@
---
import { navItems } from "./DesktopNav.astro";
import HeaderLink from "../HeaderLink.astro";
import DarkModeToggle from "./DarkModeToggle.astro";
---
<!-- Mobile Navigation Drawer and Backdrop -->
<div class="lg:hidden">
<!-- Backdrop -->
<div
id="mobile-backdrop"
class="fixed inset-0 bg-black/25 z-40 opacity-0 pointer-events-none transition-opacity duration-300"
>
</div>
<!-- Mobile Drawer -->
<nav
id="mobile-drawer"
class="fixed top-0 right-0 h-full w-48 bg-white dark:bg-gray-800 shadow-xl transform translate-x-full transition-transform duration-300 ease-in-out z-50"
>
<!-- Close button in drawer -->
<div
class="flex justify-between p-4 border-b border-gray-200 dark:border-gray-700"
>
<DarkModeToggle />
<button
id="mobile-nav-close"
class="w-8 h-8 flex items-center justify-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label="Close navigation"
>
</button>
</div>
<!-- Navigation Links -->
<div class="p-6">
<div class="flex flex-col space-y-4">
{
navItems.map((item) => (
<HeaderLink href={item.href}>{item.label}</HeaderLink>
))
}
</div>
</div>
</nav>
</div>
<script>
function initMobileNav() {
const toggle = document.getElementById("mobile-nav-toggle")!;
const closeBtn = document.getElementById("mobile-nav-close")!;
const drawer = document.getElementById("mobile-drawer")!;
const backdrop = document.getElementById("mobile-backdrop")!;
if (!toggle || !drawer || !backdrop) {
console.error("Missing elements, retrying in 100ms...");
setTimeout(initMobileNav, 100);
return;
}
let isOpen = false;
function openNav() {
console.log("Opening nav");
isOpen = true;
toggle.classList.add("nav-open");
backdrop.classList.remove("opacity-0", "pointer-events-none");
drawer.classList.remove("translate-x-full");
document.body.style.overflow = "hidden";
}
function closeNav() {
console.log("Closing nav");
isOpen = false;
toggle.classList.remove("nav-open");
backdrop.classList.add("opacity-0", "pointer-events-none");
drawer.classList.add("translate-x-full");
document.body.style.overflow = "";
}
// Toggle button click
toggle.addEventListener("click", (e) => {
e.preventDefault();
console.log("Toggle clicked, isOpen:", isOpen);
isOpen ? closeNav() : openNav();
});
// Close button click
if (closeBtn) {
closeBtn.addEventListener("click", (e) => {
e.preventDefault();
console.log("Close button clicked");
closeNav();
});
}
// Backdrop click
backdrop.addEventListener("click", () => {
console.log("Backdrop clicked");
closeNav();
});
// Navigation links
drawer.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => {
console.log("Nav link clicked");
closeNav();
});
});
// Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen) {
console.log("Escape pressed");
closeNav();
}
});
console.log("Mobile nav initialized successfully");
}
// Try multiple initialization strategies
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initMobileNav);
} else {
initMobileNav();
}
// Also try with astro:page-load for Astro's client-side navigation
document.addEventListener("astro:page-load", initMobileNav);
</script>

View file

@ -0,0 +1,34 @@
---
import DarkModeToggle from "./DarkModeToggle.astro";
import DesktopNav from "./DesktopNav.astro";
import Logo from "../Logo.astro";
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 lg:px-16 md:px-8 px-2 py-4">
<div class="flex items-center justify-between">
<h2 class="font-bold text-xl mb-0 font-mono flex">
<a
href="/"
class="hover:text-mytheme-600 dark:hover:text-mytheme-400 transition-colors"
>
<Logo />
</a>
</h2>
<div class="flex-1 flex justify-center">
<div class="max-w-2xl w-full flex justify-end lg:justify-start">
<DesktopNav />
</div>
</div>
<div class="flex items-center gap-4">
<div class="hidden lg:block"><DarkModeToggle /></div>
<MobileNav />
</div>
</div>
</div>
</header>

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,18 +1,26 @@
---
import BaseHead from "../components/BaseHead.astro";
import NavMenu from "../components/NavMenu.astro";
import Footer from "../components/Footer.astro";
import Footer from "../components/nav/Footer.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import DarkModeToggle from "../components/DarkModeToggle.astro";
import "@fontsource/ubuntu";
import "@fontsource/ubuntu/700.css";
import TopHeader from "../components/nav/TopHeader.astro";
import PageHeadline from "../components/PageHeadline.astro";
import MobileNavDrawer from "../components/nav/MobileNavDrawer.astro";
interface Props {
title?: string;
description?: string;
subtitle?: string;
className?: string;
}
const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
const {
title = SITE_TITLE,
description = SITE_DESCRIPTION,
subtitle,
className = "max-w-2xl px-4 py-8",
} = Astro.props;
---
<!doctype html>
@ -20,7 +28,7 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
<head>
<BaseHead title={title} description={description} />
<script is:inline>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
// Prevent FOUC for dark mode
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
@ -32,45 +40,37 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
}
</script>
</head>
<body class="bg-white dark:bg-gray-900 text-black dark:text-white">
<!-- Mobile layout -->
<div class="lg:hidden flex flex-col min-h-screen p-2 sm:p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-lg mb-0">
<a href="/">{SITE_TITLE}</a>
</h2>
<div class="flex items-center gap-4">
<DarkModeToggle />
<NavMenu />
</div>
</div>
<main class="flex-grow">
<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>
<MobileNavDrawer />
<TopHeader />
<main class="flex-1">
<div class={`${className} mx-auto`}>
<PageHeadline title={title} subtitle={subtitle} />
<slot />
</main>
</div>
<!-- Desktop layout -->
<div
class="hidden lg:grid grid-cols-[150px_1fr_60px] gap-4 min-h-screen p-4"
>
<div class="flex items-start">
<h2 class="font-bold text-xl mt-3">
<a href="/">{SITE_TITLE}</a>
</h2>
</div>
</main>
<div>
<NavMenu />
<main class="w-full lg:w-[768px] max-w-[calc(100%-2em)] mx-auto p-2">
<slot />
</main>
</div>
<div class="flex justify-end">
<DarkModeToggle />
</div>
</div>
<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

@ -8,8 +8,7 @@ const posts = (await getCollection("blog")).sort(
);
---
<BaseLayout>
<h1 class="mb-16">Blog</h1>
<BaseLayout title="Blog" subtitle="Thoughts and Reflections">
<section class="max-w-4xl mx-auto">
<ul class="">
{

View file

@ -1,11 +1,9 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import BaseLayout from "../../layouts/BaseLayout.astro";
import { Icon } from "astro-icon/components";
---
<BaseLayout title="Contact">
<h1 class="mb-16">Contact</h1>
<ul class="space-y-2">
<li class="flex items-center space-x-3">
<span class="flex items-center">

View file

@ -0,0 +1,258 @@
---
import Link from "../../components/Link.astro";
import BaseLayout from "../../layouts/BaseLayout.astro";
---
<BaseLayout title="Datenschutz (Germany)">
<h2>Datenschutzerklärung</h2>
<p>
Im Folgenden möchten wir Sie aufklären, wie Ihre Daten von uns verarbeitet
werden.
</p>
<p>
<strong>Verantwortlich im Sinne der DSGVO ist:</strong><br />
<span class="placeholder">Alexander Daichendt</span><br />
<span class="placeholder">Wiesenweg 10a, 85464 Neufinsing</span><br />
<span class="placeholder">datenschutz@daichendt.one</span>
</p>
<p>
Sollten Sie per E-Mail oder über Kontaktformular mit uns Kontakt aufnehmen,
werden die mitgeteilten Daten von uns gespeichert, um Ihr Anliegen zu
bearbeiten.
</p>
<p><strong>Zu den verarbeiteten Daten zählen:</strong></p>
<ul>
<li><span class="placeholder">[Ihr Name]</span></li>
<li><span class="placeholder">[Ihre E-Mail-Adresse]</span></li>
<li><span class="placeholder">[Ihre Telefonnummer]</span></li>
</ul>
<p>
Wir werden die Daten löschen, sobald die Speicherung nicht mehr erforderlich
ist oder die Verarbeitung einschränken, falls gesetzliche
Aufbewahrungspflichten bestehen.
</p>
<h2>Betroffenenrechte</h2>
<p>
Sie haben als betroffene Person, das Recht auf Auskunft, das Recht auf
Berichtigung oder Löschung, das Recht auf Einschränkung der Verarbeitung und
das Recht auf Widerspruch gegen die Verarbeitung Ihrer Daten. Sofern Sie uns
eine Einwilligung erteilt haben, können Sie diese jederzeit mit Wirkung für
die Zukunft widerrufen.
</p>
<p>Bitte richten Sie Ihren Widerspruch formlos an die obige Adresse.</p>
<p>
Darüber hinaus haben Sie das Recht auf Datenübertragbarkeit. Sie haben
weiter das Recht, sich bei einer Aufsichtsbehörde über die Verarbeitung zu
beschweren. Eine Liste der entsprechenden Behörden finden Sie unter: <Link
href="https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html"
>https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html</Link
>.
</p>
<div class="legal-note">
<h3>Rechtliche Hinweise</h3>
<h4>Betroffenenrechte</h4>
<p>
Sollte die Datenverarbeitung nicht auf Einwilligung des Betroffenen
beruhen (siehe Kontaktformular), muss das Widerrufsrecht nicht angegeben
werden (Art. 13 Abs. 2 lit. c DSGVO).
</p>
</div>
<h2>Cloudflare</h2>
<p>
Wir nutzen das Content Delivery Network (CDN) von Cloudflare Germany GmbH,
Rosental 7, c/o Mindspace, 80331 München Deutschland (Cloudflare), um die
Sicherheit und die Auslieferungsgeschwindigkeit unserer Website zu erhöhen.
Dies entspricht unserem berechtigten Interesse (Art. 6 Abs. 1 lit. f DSGVO).
Ein CDN ist ein Netzwerk aus weltweit verteilten Servern, das in der Lage
ist, optimiert Inhalte an den Websitenutzer auszuliefern. Für diesen Zweck
können personenbezogene Daten in Server-Logfiles von Cloudflare verarbeitet
werden. Bitte vergleichen Sie die Ausführungen unter „Hosting".
</p>
<p>
Cloudflare ist Empfänger Ihrer personenbezogenen Daten und als
Auftragsverarbeiter für uns tätig. Die entspricht unserem berechtigten
Interesse im Sinne des Art. 6 Abs. 1 S. 1 lit. f DSGVO, selbst kein Content
Delivery Network zu betreiben.
</p>
<p>
Sie haben das Recht der Verarbeitung zu widersprechen. Ob der Widerspruch
erfolgreich ist, ist im Rahmen einer Interessenabwägung zu ermitteln.
</p>
<p>
Die Verarbeitung der unter diesem Abschnitt angegebenen Daten ist weder
gesetzlich noch vertraglich vorgeschrieben. Die Funktionsfähigkeit der
Website ist ohne die Verarbeitung nicht gewährleistet.
</p>
<p>
Ihre personenbezogenen Daten werden von Cloudflare so lange gespeichert, wie
es für die beschriebenen Zwecke erforderlich ist.
</p>
<p>
Weitere Informationen zu Widerspruchs- und Beseitigungsmöglichkeiten
gegenüber Cloudflare finden Sie unter: Cloudflare DPA
</p>
<p>
Cloudflare hat Compliance-Maßnahmen für internationale Datenübermittlungen
umgesetzt. Diese gelten für alle weltweiten Aktivitäten, bei denen
Cloudflare personenbezogene Daten von natürlichen Personen in der EU
verarbeitet. Diese Maßnahmen basieren auf den EU-Standardvertragsklauseln
(SCCs). Weitere Informationen finden Sie unter: <Link
href="https://www.cloudflare.com/cloudflare_customer_SCCs-German.pdf"
>https://www.cloudflare.com/cloudflare_customer_SCCs-German.pdf</Link
>
</p>
<h2>Hosting</h2>
<p>
Sofern Sie sich als Besucher weder registrieren noch einloggen, erheben wir
in sog. Logfiles folgende Daten, die Ihr Browser übermittelt:
</p>
<p>
IP-Adresse, Datum und Uhrzeit der Anfrage, Zeitzonendifferenz zur Greenwich
Mean Time, Inhalt der Anforderung, HTTP-Statuscode, übertragene Datenmenge,
Website, von der die Anforderung kommt und Informationen zu Browser und
Betriebssystem.
</p>
<p>
Das ist erforderlich, um unsere Website anzuzeigen und die Stabilität und
Sicherheit zu gewährleisten. Dies entspricht unserem berechtigten Interesse
im Sinne des Art. 6 Abs. 1 S. 1 lit. f DSGVO.
</p>
<p>
Wir setzen für die Zurverfügungstellung unserer Website folgenden Hoster
ein: Cloudflare
</p>
<p>
Dieser ist Empfänger Ihrer personenbezogenen Daten und als
Auftragsverarbeiter für uns tätig. Dies entspricht unserem berechtigten
Interesse im Sinne des Art. 6 Abs. 1 S. 1 lit. f DSGVO, selbst keinen Server
in unseren Räumlichkeiten vorhalten zu müssen.
</p>
<p>
Sie haben das Recht der Verarbeitung zu widersprechen. Ob der Widerspruch
erfolgreich ist, ist im Rahmen einer Interessenabwägung zu ermitteln.
</p>
<p>
Die Verarbeitung der unter diesem Abschnitt angegebenen Daten ist weder
gesetzlich noch vertraglich vorgeschrieben. Die Funktionsfähigkeit der
Website ist ohne die Verarbeitung nicht gewährleistet.
</p>
<div class="technical-note">
<h3>Technische Hinweise</h3>
<p>
Neben den Server-Logfiles können auch von der verwendeten Applikation und
deren Plugins personenbezogene Daten verarbeitet werden. Darunter fallen
u.a. die Protokollierung fehlerhafter Anmeldeversuche, oder Zugriffe auf
nicht existierende Seiten (404). Dies sollte überprüft und entsprechend
ergänzt werden.
</p>
<p>
Im Falle eine Speicherung, sollte ebenfalls angegeben werden, wie lange
diese erfolgt und ob und ab wann eine Anonymisierung der erhobenen Daten
stattfindet.
</p>
</div>
<div class="legal-note">
<h3>Rechtliche Hinweise</h3>
<p>
Grundsätzlich ist ein Auftragsverarbeitungsvertrag mit dem Hoster
abzuschließen. Das bayerische Landesamt für Datenschutzaufsicht hat für
das Hosting rein statischer Websites eine Ausnahme gemacht. Für den Fall,
dass die Webseite der Selbstdarstellung dient, z.B. von Vereinen oder
Kleinunternehmen, keine personenbezogenen Daten an den Betreiber fließen
und kein Tracking stattfindet, liegt keine Auftragsverarbeitung vor.
Weiter heißt es: „Die Tatsache, dass auch beim Hosting von statischen
Webseiten zwangsläufig IP-Adressen, d.h. personenbezogene Daten,
verarbeitet werden müssen, führt nicht zur Annahme einer
Auftragsverarbeitung. Das wäre nicht sachgerecht. Die (kurzfristige)
IP-Adressenspeicherung ist vielmehr noch der TK-Zugangsvermittlung des
Website-Hosters nach dem TKG zuzurechnen und dient in erster Linie
Sicherheitszwecken des Hosters." (<Link
href="https://www.lda.bayern.de/media/veroeffentlichungen/FAQ_Hosting_keine_Auftragsverarbeitung.pdf"
>https://www.lda.bayern.de/media/veroeffentlichungen/FAQ_Hosting_keine_Auftragsverarbeitung.pdf</Link
>) Es sollte deshalb überprüft werden, ob der Hoster Tracking und
Auswertungstools zur Verfügung stellt und ob und wie lange Logfiles
aufbewahrt werden.
</p>
</div>
<h3>Weitere Zwecke der Datenverarbeitung</h3>
<ul>
<li>Gewährleistung der Stabilität und Sicherheit der Website</li>
<li>Auswertung der Systemsicherheit und -Stabilität</li>
<li>Optimierung der Website</li>
<li>Überprüfung, ob rechtswidrige Nutzung stattgefunden hat</li>
</ul>
<div class="legal-note">
<h3>Widerspruchs- und Beseitigungsmöglichkeit</h3>
<p>
Der häufig verwendete Hinweis, dass seitens des Nutzers keine
Widerspruchsmöglichkeit bestehe, entspricht nicht der gesetzlichen
Vorgabe. Wird die Verarbeitung auf das berechtigte Interesse des
Verantwortlichen gestützt (Art. 6 Abs. 1 lit.f DSGVO), so ist das Recht
auf Widerspruch nicht per se ausgeschlossen. Ob dieser jedoch Erfolg hat,
ist im Rahmen einer Interessenabwägung zu ermitteln. Auch wenn in der
Praxis das berechtigte Interesse des Websitebetreibers wohl überwiegen
wird, folgt daraus kein Ausschluss des Widerspruchrechts. Eine solche
Formulierung sollte korrigiert werden, da sie dazu führen kann, dass der
Betroffene an der Ausübung seines Widerspruchrechts gehindert wird.
</p>
<h3>Empfänger</h3>
<p>
Gemäß Art. 13. Abs. 1 lit. e DSGVO, besteht die Pflicht „die Empfänger
oder Kategorien von Empfängern der personenbezogenen Daten" anzugeben.
Häufig wird vertreten, dass vorrangig Empfänger namentlich und mit
Anschrift zu benennen sind und nur hilfsweise auf Kategorien
zurückgegriffen werden darf. Eine andere Auffassung vertritt ein Wahlrecht
zwischen der namentlichen Nennung und der Angabe von Kategorien. (Vgl.
Daum: Pflichtangaben auf Webseiten MMR 2020 643 (646) m.w.N.) Demnach wäre
es ausreichend als Kategorie „Hoster" anzugeben. Für diese Auffassung
spricht jedoch, wenn überhaupt, nur die Übersichtlichkeit. Dem Sinn und
Zweck der Vorschrift entspricht es aber vielmehr den Namen und die
Anschrift anzugeben, zumal dieser im Rahmen des Hostings bereits feststeht
(Vgl. Lorenz: Datenschutzrechtliche Informationspflichten (VuR 2019, 213
(216)).
</p>
<h3>Speicherdauer</h3>
<p>
Für die Feststellung der Speicherdauer sollten die Server- und
Applikationseinstellungen überprüft werden, auch um Widersprüche zwischen
den angebenden Zwecken zu vermeiden. So kann es beispielsweise zu
Unstimmigkeiten kommen, wenn angegeben wird, dass nach jeder Sitzung die
Daten gelöscht werden, diese aber gleichzeitig der Stabilität und
Sicherheit dienen sollen. Eine allgemeine Mitteilung, die Daten würden so
lange gespeichert werden, wie es für die angegebenen Zwecke erforderlich
ist, ist nicht ausreichend (Vgl. Simitis/Hornung/Spiecker gen. Döhmann,
Datenschutzrecht, Art. 13 Rn 15). Ausreichend ist aber gem. Art. 13 Abs. 2
lit a. DSGVO die Angabe von Kriterien für die Festlegung der
Speicherdauer.
</p>
</div>
</BaseLayout>

View file

@ -0,0 +1,34 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import ImpressumContent from "../../components/ImpressumContent.astro";
const address = {
street: "Wiesenweg 10a",
city: "Neufinsing",
postalCode: "85464",
country: "Germany",
};
---
<BaseLayout
title="Impressum"
subtitle="Rechtliche Angaben gemäß § 5 TMG (Telemediengesetz)"
>
<ImpressumContent
companyName="Daichendt IT (freiberuflich)"
ownerName="Alexander Daichendt"
contact={{
email: "inquiries@daichendt.one",
website: "https://daichendt.one",
}}
responsiblePerson={{
name: "Alexander Daichendt",
address,
}}
business={{
vatId: "beantragt",
registrationOffice: "Finanzamt Erding",
}}
address={address}
/>
</BaseLayout>

View file

@ -1,58 +1,158 @@
---
import me from "../assets/me.jpg";
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: "🦀",
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>
<h1 class="">Hi, my name is Alex!</h1>
<p>
I am a software engineer, Linux enthusiast and a friend of lightweight,
resilient systems.
</p>
<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.
</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.
</p>
<p>
Contact me <a href="/contact">here</a>!
</p>
<BaseLayout
title="Hi, my name is Alex!"
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
className="w-full py-16 flex flex-col"
>
<ThreeColumnSection reverseOnMobile={true}>
<div slot="left"></div>
<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 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
data.
</p>
<p class="mb-4">
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>
Ready to collaborate? <Link href="/contact">Get in touch here</Link>!
</p>
</div>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Frontend:</strong><br />
NextJS, SvelteKit, Astro, Tailwind
<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>
</ThreeColumnSection>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Ops:</strong><br />
Prometheus, Docker, Kubernetes, AWS, Github/lab actions
<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"
>
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>
<div slot="right"></div>
</ThreeColumnSection>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Misc:</strong><br />
git, agile, REST, GraphQL, web performance, Linux sysadmin, Vitest
<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"
>
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>
<div slot="right"></div>
</ThreeColumnSection>
<div class="flex-[0_1_calc(50%-0.5rem)]">
<strong>Languages:</strong><br />
German (native), English
<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>
</div>
</section>
</BaseLayout>

View file

@ -1,240 +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">
<h1 class="mb-16">Projects</h1>
<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

@ -1,97 +0,0 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import { publications } from "../data/publications";
---
<BaseLayout title="Publications">
<h2>Conference papers</h2>
<ul class="space-y-4">
{
publications.map((pub) => (
<li class="rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-lg transition-shadow">
<div class="space-y-2">
<p class="text-lg dark:text-gray-200">
{pub.authors.join(", ")}
</p>
<p class="text-xl font-semibold dark:text-gray-100">
"{pub.title}"
</p>
<p class="text-gray-600 dark:text-gray-400">
{pub.conference ||
pub.journal +
", Volume " +
pub.volume +
", " +
pub.date +
", " +
pub.pages}
</p>
{pub.location && (
<p class="text-gray-600 dark:text-gray-400">
{pub.location}, {pub.date}
</p>
)}
{pub.links && (
<div class="flex gap-4 mt-3">
{pub.links.pdf && (
<a
href={pub.links.pdf}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" />
<path d="M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
</svg>
PDF
</a>
)}
{pub.links.homepage && (
<a
href={pub.links.homepage}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
Homepage
</a>
)}
{pub.links.bibtex && (
<a
href={pub.links.bibtex}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
clip-rule="evenodd"
/>
</svg>
BibTeX
</a>
)}
</div>
)}
</div>
</li>
))
}
</ul>
</BaseLayout>

View file

@ -0,0 +1,95 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import { publications } from "../../data/publications";
---
<BaseLayout title="Publications">
<h2>Conference papers</h2>
<ul class="space-y-4">
{
publications.map((pub) => (
<li class="rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-lg transition-shadow">
<div class="space-y-2">
<p class="text-lg dark:text-gray-200">{pub.authors.join(", ")}</p>
<p class="text-xl font-semibold dark:text-gray-100">
"{pub.title}"
</p>
<p class="text-gray-600 dark:text-gray-400">
{pub.conference ||
pub.journal +
", Volume " +
pub.volume +
", " +
pub.date +
", " +
pub.pages}
</p>
{pub.location && (
<p class="text-gray-600 dark:text-gray-400">
{pub.location}, {pub.date}
</p>
)}
{pub.links && (
<div class="flex gap-4 mt-3">
{pub.links.pdf && (
<a
href={pub.links.pdf}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" />
<path d="M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
</svg>
PDF
</a>
)}
{pub.links.homepage && (
<a
href={pub.links.homepage}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
Homepage
</a>
)}
{pub.links.bibtex && (
<a
href={pub.links.bibtex}
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
clip-rule="evenodd"
/>
</svg>
BibTeX
</a>
)}
</div>
)}
</div>
</li>
))
}
</ul>
</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) {