Inital commit of Astro Website

This commit is contained in:
2026-03-21 14:37:03 +01:00
commit bea1f0741d
60 changed files with 10838 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
---
interface Props {
prevUrl?: string;
prevTitle?: string;
nextUrl?: string;
nextTitle?: string;
}
const { prevUrl, prevTitle, nextUrl, nextTitle } = Astro.props;
---
<div class="book-nav glass-container">
<div class="nav-controls">
<div class="nav-prev">
{prevUrl ? <a href={prevUrl}>← {prevTitle || 'Zurück'}</a> : <span class="disabled">← Zurück</span>}
</div>
<div class="nav-toc-toggle">
<input type="checkbox" id="toc-toggle" class="toc-checkbox" />
<label for="toc-toggle" class="toc-label">Inhaltsverzeichnis ☰</label>
<div class="toc-dropdown glass-container">
<nav class="toc-content">
<h3 class="toc-title">Kapitel</h3>
<slot name="toc" />
</nav>
</div>
</div>
<div class="nav-next">
{nextUrl ? <a href={nextUrl}>{nextTitle || 'Weiter'} →</a> : <span class="disabled">Weiter →</span>}
</div>
</div>
</div>
<style>
.book-nav {
position: relative;
margin-top: 4rem;
padding: 1.5rem;
}
.nav-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* CSS-Only Toggle Magic */
.toc-checkbox {
display: none;
}
.toc-label {
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
padding: 0.75rem 1.25rem;
border-radius: 0.5rem;
background: rgba(var(--accent-base), 0.15);
border: 1px solid rgba(var(--accent-base), 0.3);
color: rgb(var(--accent-base));
transition: all 0.2s ease;
user-select: none;
}
.toc-label:hover {
background: rgba(var(--accent-base), 0.25);
}
.toc-dropdown {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 1.5rem;
width: 320px;
max-height: 60vh;
overflow-y: auto;
z-index: 50;
opacity: 0;
transform-origin: bottom center;
background: var(--bg-color);
backdrop-filter: blur(var(--glass-blur));
}
/* When checkbox is checked, show the dropdown */
.toc-checkbox:checked ~ .toc-dropdown {
display: block;
animation: slideUpFade 0.3s ease-out forwards;
}
.toc-title {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 0.5rem;
}
@keyframes slideUpFade {
0% {
opacity: 0;
transform: translate(-50%, 15px);
}
100% {
opacity: 1;
transform: translate(-50%, 0);
}
}
</style>

View File

@@ -0,0 +1,9 @@
---
interface Props {
class?: string;
}
const { class: className = '' } = Astro.props;
---
<div class={`glass-container ${className}`}>
<slot />
</div>

View File

@@ -0,0 +1,101 @@
---
const currentPath = Astro.url.pathname;
import Logo from "../assets/logo.svg";
---
<nav class="main-nav glass-container">
<div class="nav-content">
<a href="/" class="logo">
<Logo class="logo-img" />
c0ntroller.de
</a>
<div class="links">
<a
href="/portfolio"
class={currentPath.startsWith("/portfolio") ? "active" : ""}
>Portfolio</a
>
<a href="/blog" class={currentPath.startsWith("/blog") ? "active" : ""}
>Blog</a
>
<a href="/book" class={currentPath.startsWith("/book") ? "active" : ""}
>Buch</a
>
</div>
</div>
</nav>
<style is:global>
body:has(nav a[href="/"]:hover) {
--accent-base: var(--accent-base--default);
}
body:has(nav a[href="/portfolio"]:hover) {
--accent-base: var(--accent-base--portfolio);
}
body:has(nav a[href="/blog"]:hover) {
--accent-base: var(--accent-base--blog);
}
body:has(nav a[href="/book"]:hover) {
--accent-base: var(--accent-base--book);
}
</style>
<style>
.main-nav {
position: sticky;
top: 1.5rem;
z-index: 100;
margin: 0 auto 3rem auto;
max-width: 800px;
padding: 1rem 2rem;
border-radius: 2rem;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
}
.nav-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
font-size: 1.25rem;
color: #fff;
letter-spacing: -0.02em;
}
.logo-img {
height: 1.5rem;
width: auto;
--logo-eye-border: #fff;
--logo-eye-brow: #fff;
}
.links {
display: flex;
gap: 2rem;
}
.links a {
font-weight: 500;
font-size: 0.95rem;
color: var(--text-main);
transition: color 0.2s ease;
}
.links a:hover {
color: rgb(var(--accent-base));
}
.links a[href="/portfolio"]:hover {
color: rgb(var(--accent-base--portfolio));
}
.links a[href="/blog"]:hover {
color: rgb(var(--accent-base--blog));
}
.links a[href="/book"]:hover {
color: rgb(var(--accent-base--book));
}
.links a.active {
color: rgb(var(--accent-base));
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,186 @@
---
---
<div id="welcome-typewriter-container">
<noscript>
<p class="welcome-typewriter-nostyle">Automatisierungstechnik</p>
<p class="welcome-typewriter-nostyle">Software, und mehr...</p>
</noscript>
<p id="welcome-typewriter" style="display: none;"><span id="welcome-typewriter-text"></span><span id="caret"></span></p>
</div>
<style>
p {
margin: 0;
}
#welcome-typewriter-container {
font-size: min(4vw, 1.5rem);
font-weight: bold;
max-width: 90dvw;
font-family: var(--font-cascadia-code);
letter-spacing: .38em;
text-align: center;
white-space: break-spaces;
word-wrap: break-word;
color: rgb(var(--accent-base));
text-shadow: 0 0 2px black, 0 0 5px black, 0 0 10px black;
}
#welcome-typewriter-container {
min-height: 2em;
}
#welcome-typewriter, #welcome-typewriter-text, #caret {
min-height: 1.5em;
}
#welcome-typewriter span {
display: inline-block;
margin: auto;
}
.welcome-typewriter-nostyle {
min-height: 1em;
border-right: 2px solid orange;
margin: 0 auto;
display: block;
overflow-x: hidden;
white-space: nowrap;
text-overflow: clip;
width: var(--text-length);
}
.welcome-typewriter-nostyle:nth-of-type(1) {
animation: blink-caret 1s step-end infinite,
0s 2s hide-caret forwards,
2s text-typing steps(24, end) forwards normal 1;
--text-length: 23em;
}
.welcome-typewriter-nostyle:nth-of-type(2) {
animation: hide-text 0s forwards 1,
hide-caret 0s forwards 1,
1s 2s blink-caret step-end infinite,
2s 2s text-typing steps(21, end) forwards normal 1;
--text-length: 20em;
}
#caret {
display: inline-block;
height: 1em;
width: 0px;
border-right: 2px solid orange;
animation: blink-caret 1s step-end infinite;
}
@keyframes hide-text {
to { width: 0; }
}
@keyframes blink-caret {
from, to { border-color: orange }
50% { border-color: transparent; }
}
@keyframes hide-caret {
to { border-color: transparent; }
}
@keyframes text-typing {
from { width: 0; }
to { width: var(--text-length); }
}
@media (prefers-reduced-motion: reduce) {
.welcome-typewriter-nostyle {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
#caret {
animation-play-state: paused;
}
}
</style>
<script>
const element = document.getElementById("welcome-typewriter-text");
const possibleWords: string[] = [
"Automatisierungstechnik",
"Software",
"Web Development",
"Datenbanken",
"Katzen",
"Frontend",
"Backend",
"Fullstack",
"React",
"Node.js",
"Python",
"C#",
"Projektmanagement",
"IT-Sicherheit",
"Embedded",
"Linux",
"IoT",
"Industrie 4.0",
"Software-Architektur",
"Windows",
"Arduino",
"C/C++",
"git",
"CI/CD",
"Programmierung",
"Hardware",
"Technik",
"Pinguine",
"Open Source",
"Heimautomatisierung",
"Selfhosting",
]//.map(word => word.replaceAll(" ", "\u00A0"));
if (element) {
// Initial word setup
let currentWord = possibleWords[Math.floor(Math.random() * possibleWords.length)];
let currentStep = 0;
// Show the element
element.parentElement?.style.removeProperty("display");
// Get reduced motion settings from user preferences
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
if (prefersReducedMotion) {
// If the user prefers reduced motion, just show the first word without animation
element.innerText = currentWord;
} else {
// Otherwise, start the typewriter animation}
window.setInterval(() => {
if (currentStep < currentWord.length) {
// Add the next character
element.innerText += currentWord[currentStep];
currentStep++;
} else if (currentStep < currentWord.length + 24) { // 1.2s/50ms = 1200/50 steps = 24 steps
// Just wait a bit
currentStep++;
} else if (currentStep < currentWord.length * 2 + 24) {
// Remove the last character
element.innerText = element.innerText.slice(0, -1);
currentStep++;
} else if (currentStep < currentWord.length * 2 + 24 + 10) { // 500ms
// Wait a bit before the next word
currentStep++;
} else {
// Reset the step and choose new word
currentStep = 0;
let newWord;
do {
newWord = possibleWords[Math.floor(Math.random() * possibleWords.length)];
} while (newWord === currentWord);
currentWord = newWord;
}
}, 50);
}
}
</script>