Compare commits
8 commits
cloudflare
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 46e485a668 | |||
| 7481d043f2 | |||
| a0f5b86aac | |||
| 1ef855c029 | |||
| 0515fdf9bd | |||
| 3e0875a0d7 | |||
| b3d158d52c | |||
| bf720b79ee |
BIN
public/files/alexdaichendt.jpg
Normal file
|
After Width: | Height: | Size: 779 KiB |
BIN
src/assets/me.jpg
Normal file
|
After Width: | Height: | Size: 779 KiB |
BIN
src/assets/projects/discretizeui/demo.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
src/assets/projects/discretizeui/languages.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
src/assets/projects/discretizeui/tooltip.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/projects/videovault/dashboard.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
src/assets/projects/videovault/edit.png
Normal file
|
After Width: | Height: | Size: 835 KiB |
BIN
src/assets/projects/videovault/frontpage.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/assets/projects/videovault/player.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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 "../styles/global.css";
|
||||||
import ubuntuRegularWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-400-normal.woff2?url";
|
import ubuntuRegularWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-400-normal.woff2?url";
|
||||||
import ubuntuBoldWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-700-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:title" content={title} />
|
||||||
<meta property="twitter:description" content={description} />
|
<meta property="twitter:description" content={description} />
|
||||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
<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>
|
||||||
|
|
|
||||||
233
src/components/Carousel.astro
Normal 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 screen‑reader 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>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
---
|
|
||||||
const today = new Date();
|
|
||||||
---
|
|
||||||
|
|
||||||
<footer class="bg-gray-100/60 dark:bg-mytheme-900 shadow-sm">
|
|
||||||
© {today.getFullYear()} Alexander Daichendt. All rights reserved.
|
|
||||||
</footer>
|
|
||||||
<style>
|
|
||||||
footer {
|
|
||||||
padding: 2em 1em 6em 1em;
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
275
src/components/ImpressumContent.astro
Normal 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
|
|
@ -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 |
|
|
@ -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>
|
|
||||||
22
src/components/PageHeadline.astro
Normal 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>
|
||||||
|
|
@ -3,7 +3,8 @@ import { Picture as AstroPicture } from "astro:assets";
|
||||||
---
|
---
|
||||||
|
|
||||||
<AstroPicture
|
<AstroPicture
|
||||||
src={Astro.props.src}
|
src={Astro.props.src}
|
||||||
alt={Astro.props.alt}
|
alt={Astro.props.alt}
|
||||||
formats={["avif", "webp"]}
|
formats={["avif", "webp"]}
|
||||||
|
{...Astro.props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
99
src/components/ProjectSection.astro
Normal 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>
|
||||||
|
))
|
||||||
|
}
|
||||||
139
src/components/ThreeColumnSection.astro
Normal 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>
|
||||||
220
src/components/nav/DarkModeToggle.astro
Normal 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>
|
||||||
22
src/components/nav/DesktopNav.astro
Normal 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>
|
||||||
39
src/components/nav/Footer.astro
Normal 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>
|
||||||
|
© {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>
|
||||||
40
src/components/nav/MobileNav.astro
Normal 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>
|
||||||
130
src/components/nav/MobileNavDrawer.astro
Normal 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>
|
||||||
34
src/components/nav/TopHeader.astro
Normal 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>
|
||||||
194
src/consts.ts
|
|
@ -1,6 +1,196 @@
|
||||||
// Place any global data in this file.
|
import type { ImageMetadata } from "astro";
|
||||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
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_TITLE = "Alex Daichendt";
|
||||||
export const SITE_DESCRIPTION =
|
export const SITE_DESCRIPTION =
|
||||||
"Alex Daichendt's personal website, blog, and portfolio.";
|
"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,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ description: A guide on what is needed to get Linux running on the Huawei MateBo
|
||||||
heroImage: ./images/matebook.jpg
|
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.
|
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.
|
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.
|
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:
|
Sources:
|
||||||
|
|
||||||
[^1]: https://www.kernelconfig.io/search?q=CONFIG_BOOTPARAM_HOTPLUG_CPU0&kernelversion=6.12.4&arch=x86
|
[^1]: https://www.kernelconfig.io/search?q=CONFIG_BOOTPARAM_HOTPLUG_CPU0&kernelversion=6.12.4&arch=x86
|
||||||
|
|
||||||
[^2]: https://github.com/intel/intel-lpmd
|
[^2]: https://github.com/intel/intel-lpmd
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
export const publications = [
|
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: [
|
authors: [
|
||||||
"Florian Wiedner",
|
"Florian Wiedner",
|
||||||
|
|
@ -20,25 +39,6 @@ export const publications = [
|
||||||
bibtex: "/publications/bibtex/WiedHelm24Container.bib",
|
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: [
|
authors: [
|
||||||
"Florian Wiedner",
|
"Florian Wiedner",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,26 @@
|
||||||
---
|
---
|
||||||
import BaseHead from "../components/BaseHead.astro";
|
import BaseHead from "../components/BaseHead.astro";
|
||||||
import NavMenu from "../components/NavMenu.astro";
|
import Footer from "../components/nav/Footer.astro";
|
||||||
import Footer from "../components/Footer.astro";
|
|
||||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
||||||
import DarkModeToggle from "../components/DarkModeToggle.astro";
|
|
||||||
import "@fontsource/ubuntu";
|
import "@fontsource/ubuntu";
|
||||||
import "@fontsource/ubuntu/700.css";
|
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 {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: 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>
|
<!doctype html>
|
||||||
|
|
@ -20,7 +28,7 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
|
||||||
<head>
|
<head>
|
||||||
<BaseHead title={title} description={description} />
|
<BaseHead title={title} description={description} />
|
||||||
<script is:inline>
|
<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 (
|
if (
|
||||||
localStorage.getItem("color-theme") === "dark" ||
|
localStorage.getItem("color-theme") === "dark" ||
|
||||||
(!("color-theme" in localStorage) &&
|
(!("color-theme" in localStorage) &&
|
||||||
|
|
@ -32,45 +40,37 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</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 />
|
<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>
|
</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 />
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ const posts = (await getCollection("blog")).sort(
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout title="Blog" subtitle="Thoughts and Reflections">
|
||||||
<h1 class="mb-16">Blog</h1>
|
|
||||||
<section class="max-w-4xl mx-auto">
|
<section class="max-w-4xl mx-auto">
|
||||||
<ul class="">
|
<ul class="">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Contact">
|
<BaseLayout title="Contact">
|
||||||
<h1 class="mb-16">Contact</h1>
|
|
||||||
|
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li class="flex items-center space-x-3">
|
<li class="flex items-center space-x-3">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
258
src/pages/datenschutz/index.astro
Normal 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>
|
||||||
34
src/pages/impressum/index.astro
Normal 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>
|
||||||
|
|
@ -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";
|
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>
|
<BaseLayout
|
||||||
<h1 class="">Hi, my name is Alex!</h1>
|
title="Hi, my name is Alex!"
|
||||||
<p>
|
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
|
||||||
I am a software engineer, Linux enthusiast and a friend of lightweight,
|
className="w-full py-16 flex flex-col"
|
||||||
resilient systems.
|
>
|
||||||
</p>
|
<ThreeColumnSection reverseOnMobile={true}>
|
||||||
<p>
|
<div slot="left"></div>
|
||||||
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>
|
|
||||||
|
|
||||||
<h2 class="mt-16">Skills</h2>
|
<div slot="center" class="max-w-2xl">
|
||||||
<div class="flex flex-wrap gap-4">
|
<p class="mb-4">
|
||||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
I am a privacy-first software engineer passionate about building web
|
||||||
<strong>Core stack:</strong><br />
|
applications that are efficient, user-friendly, and respectful of your
|
||||||
Typescript/Node.js, Python, Rust, SQL, vanilla HTML/css
|
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>
|
||||||
|
|
||||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
<div slot="right" class="flex justify-center md:justify-start items-start">
|
||||||
<strong>Frontend:</strong><br />
|
<Picture
|
||||||
NextJS, SvelteKit, Astro, Tailwind
|
src={me}
|
||||||
|
alt="My profile picture"
|
||||||
|
class="w-48 md:w-64 h-auto rounded-lg"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</ThreeColumnSection>
|
||||||
|
|
||||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
<ThreeColumnSection bgClass="bg-mytheme-200/70 dark:bg-mytheme-700/50">
|
||||||
<strong>Ops:</strong><br />
|
<div slot="left"></div>
|
||||||
Prometheus, Docker, Kubernetes, AWS, Github/lab actions
|
<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>
|
||||||
|
<div slot="right"></div>
|
||||||
|
</ThreeColumnSection>
|
||||||
|
|
||||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
<ThreeColumnSection>
|
||||||
<strong>Misc:</strong><br />
|
<div slot="left"></div>
|
||||||
git, agile, REST, GraphQL, web performance, Linux sysadmin, Vitest
|
<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>
|
||||||
|
<div slot="right"></div>
|
||||||
|
</ThreeColumnSection>
|
||||||
|
|
||||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
|
||||||
<strong>Languages:</strong><br />
|
<h2
|
||||||
German (native), English
|
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>
|
||||||
</div>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,240 +1,13 @@
|
||||||
---
|
---
|
||||||
|
import ProjectSection from "../../components/ProjectSection.astro";
|
||||||
|
import { projects } from "../../consts";
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
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">
|
<BaseLayout
|
||||||
<h1 class="mb-16">Projects</h1>
|
title="Projects"
|
||||||
|
subtitle="Selected Passion Projects I am proud of"
|
||||||
<p>
|
className="w-full py-16 flex flex-col"
|
||||||
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
|
<ProjectSection projects={projects} />
|
||||||
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>
|
</BaseLayout>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
95
src/pages/publications/index.astro
Normal 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>
|
||||||
|
|
@ -100,8 +100,7 @@ blockquote {
|
||||||
font-size: 1.333em;
|
font-size: 1.333em;
|
||||||
}
|
}
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
@apply mb-8 border-t-2 border-t-mytheme-500 dark:border-t-mytheme-300 rounded-sm;
|
||||||
border-top: 1px solid rgb(var(--gray-light));
|
|
||||||
}
|
}
|
||||||
ul:not(ul ul, ol ul),
|
ul:not(ul ul, ol ul),
|
||||||
ol:not(ul ol, ol ol) {
|
ol:not(ul ol, ol ol) {
|
||||||
|
|
|
||||||