Inital commit of Astro Website
This commit is contained in:
111
src/components/BookNavigation.astro
Normal file
111
src/components/BookNavigation.astro
Normal 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>
|
||||
9
src/components/GlassContainer.astro
Normal file
9
src/components/GlassContainer.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = '' } = Astro.props;
|
||||
---
|
||||
<div class={`glass-container ${className}`}>
|
||||
<slot />
|
||||
</div>
|
||||
101
src/components/Navigation.astro
Normal file
101
src/components/Navigation.astro
Normal 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>
|
||||
186
src/components/WelcomeTypewriter.astro
Normal file
186
src/components/WelcomeTypewriter.astro
Normal 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>
|
||||
Reference in New Issue
Block a user