Compare commits

...

7 Commits

15 changed files with 1170 additions and 150 deletions

View File

@ -11,12 +11,12 @@
"@iconify-json/logos": "^1.2.4",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/ph": "^1.2.2",
"@studio-freight/lenis": "^1.0.42",
"@tailwindcss/postcss": "^4.0.12",
"@tailwindcss/vite": "^4.0.12",
"astro": "^5.4.2",
"astro-icon": "^1.1.5",
"framer-motion": "^12.4.7",
"lenis": "^1.2.3",
"next": "15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@ -343,8 +343,6 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@studio-freight/lenis": ["@studio-freight/lenis@1.0.42", "", {}, "sha512-HJAGf2DeM+BTvKzHv752z6Z7zy6bA643nZM7W88Ft9tnw2GsJSp6iJ+3cekjyMIWH+cloL2U9X82dKXgdU8kPg=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@ -983,6 +981,8 @@
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
"lenis": ["lenis@1.2.3", "", { "peerDependencies": { "@nuxt/kit": ">=3.0.0", "react": ">=17.0.0", "vue": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "react", "vue"] }, "sha512-H3VUn62jvQPfyxGW2F0STJPhP1VzX5r05KtiZ0uxHYD9xtbfTvj2eX/Km26+x13zlaKfHacEe1DTC3ouFrxw+g=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],

View File

@ -17,12 +17,12 @@
"@iconify-json/logos": "^1.2.4",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/ph": "^1.2.2",
"@studio-freight/lenis": "^1.0.42",
"@tailwindcss/postcss": "^4.0.12",
"@tailwindcss/vite": "^4.0.12",
"astro": "^5.4.2",
"astro-icon": "^1.1.5",
"framer-motion": "^12.4.7",
"lenis": "^1.2.3",
"next": "15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",

49
public/manifest.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "Jan-Marlon Leibl | Fullstack Developer",
"short_name": "JL Portfolio",
"description": "Personal website and portfolio of Jan-Marlon Leibl, Fullstack Developer",
"start_url": "/",
"display": "standalone",
"background_color": "#0A0A0A",
"theme_color": "#0A0A0A",
"orientation": "portrait-primary",
"screenshots": [
{
"src": "/screenshots/mobile.png",
"sizes": "375x812",
"type": "image/png",
"form_factor": "narrow",
"label": "Jan-Marlon Leibl's Portfolio (Mobile)"
},
{
"src": "/screenshots/desktop.png",
"sizes": "1280x800",
"type": "image/png",
"form_factor": "wide",
"label": "Jan-Marlon Leibl's Portfolio (Desktop)"
}
],
"shortcuts": [
{
"name": "About Me",
"short_name": "About",
"description": "Learn more about Jan-Marlon Leibl",
"url": "/#about",
"icons": [{ "src": "/icons/about-icon.png", "sizes": "192x192" }]
},
{
"name": "My Work",
"short_name": "Work",
"description": "View Jan-Marlon's portfolio projects",
"url": "/#work",
"icons": [{ "src": "/icons/work-icon.png", "sizes": "192x192" }]
},
{
"name": "Contact",
"short_name": "Contact",
"description": "Get in touch with Jan-Marlon",
"url": "/#contact",
"icons": [{ "src": "/icons/contact-icon.png", "sizes": "192x192" }]
}
]
}

7
public/robots.txt Normal file
View File

@ -0,0 +1,7 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Allow: /
# Optimize crawling
Crawl-delay: 10

163
public/service-worker.js Normal file
View File

@ -0,0 +1,163 @@
const CACHE_NAME = 'jleibl-portfolio-v1';
const PRECACHE_ASSETS = [
'/',
'/index.html'
];
const AUTH_ASSETS = [
'/manifest.json'
];
const STYLE_ASSETS = [
'/styles/global.css',
'/styles/theme.css'
];
const IMAGE_ASSETS = [
'/images/profile-image.jpg'
];
const ALL_ASSETS = [
...PRECACHE_ASSETS,
...AUTH_ASSETS,
...STYLE_ASSETS,
...IMAGE_ASSETS
];
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(PRECACHE_ASSETS);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
return self.clients.claim();
});
function shouldCache(url) {
const urlObj = new URL(url);
if (urlObj.hostname.includes('cloudflareaccess.com')) {
return false;
}
if (urlObj.origin === self.location.origin) {
if (urlObj.pathname.endsWith('.html') ||
urlObj.pathname.endsWith('.css') ||
urlObj.pathname.endsWith('.js') ||
urlObj.pathname.endsWith('.jpg') ||
urlObj.pathname.endsWith('.jpeg') ||
urlObj.pathname.endsWith('.png') ||
urlObj.pathname.endsWith('.svg') ||
urlObj.pathname.endsWith('.webp') ||
urlObj.pathname.endsWith('.woff') ||
urlObj.pathname.endsWith('.woff2') ||
urlObj.pathname.endsWith('.json')) {
return true;
}
if (urlObj.pathname === '/') {
return true;
}
}
return false;
}
function isAuthProtectedAsset(url) {
const urlPath = new URL(url).pathname;
return AUTH_ASSETS.some(asset => asset === urlPath);
}
self.addEventListener('fetch', event => {
if (isAuthProtectedAsset(event.request.url)) {
event.respondWith(
fetch(event.request)
.then(response => {
if (response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(error => {
console.log('Fetch failed for auth protected asset:', error);
return caches.match(event.request);
})
);
return;
}
if (event.request.mode === 'navigate' || shouldCache(event.request.url)) {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
const fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(response => {
if (response.url.includes('cloudflareaccess.com')) {
console.log('Request redirected to authentication page:', response.url);
return response;
}
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
if (shouldCache(new URL(event.request.url))) {
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(error => {
console.log('Fetch failed:', error);
if (event.request.mode === 'navigate') {
return caches.match('/');
}
return null;
});
})
);
}
});
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

View File

@ -6,7 +6,7 @@ import MobileMenu from './MobileMenu.astro';
class="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
role="banner"
>
<div class="max-w-(--breakpoint-xl) mx-auto">
<div class="max-w-7xl mx-auto">
<nav class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']" role="navigation" aria-label="Main navigation">
<div class="flex items-center">
<a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home">
@ -22,10 +22,10 @@ import MobileMenu from './MobileMenu.astro';
<div class="hidden md:flex items-center">
<div class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl">
{["About", "Work"].map((item, index) => (
{["About", "Work", "Blog", "Contact"].map((item, index) => (
<a
href={`#${item.toLowerCase()}`}
class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl rounded-r-none" : "rounded-r-xl rounded-l-none"}`}
class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl" : ""} ${index === 3 ? "rounded-r-xl" : ""}`}
aria-label={`View my ${item.toLowerCase()}`}
>
{item}
@ -33,18 +33,34 @@ import MobileMenu from './MobileMenu.astro';
))}
</div>
<div class="ml-6">
<div class="flex items-center gap-4 ml-6">
<a
href="mailto:jleibl@proton.me"
class="flex items-center gap-2 px-5 py-2 theme-bg-05 theme-primary border theme-border rounded-xl hover:bg-opacity-100 transition-all duration-300 group shadow-sm"
aria-label="Contact me via email"
>
<svg class="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg
class="w-4 h-4 theme-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<span class="text-sm font-medium">Contact</span>
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
<svg class="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg
class="w-4 h-4 theme-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
@ -62,33 +78,71 @@ import MobileMenu from './MobileMenu.astro';
<script>
document.addEventListener('DOMContentLoaded', () => {
const header = document.querySelector('header');
if (header) {
header.style.transform = 'translateY(0)';
header.style.opacity = '1';
}
if (!header) return;
header.style.transform = 'translateY(0)';
header.style.opacity = '1';
let lastScrollY = window.scrollY;
const scrollThreshold = 100;
let isHeaderVisible = true;
const sections = document.querySelectorAll('section[id]');
const navItems = document.querySelectorAll('.nav-item');
function setActiveNavItem() {
const scrollPosition = window.scrollY + 200;
navItems.forEach(item => {
item.classList.remove('nav-item-active');
item.removeAttribute('aria-current');
});
let currentSection: string | null = null;
sections.forEach(section => {
const sectionTop = section.getBoundingClientRect().top + window.scrollY;
const sectionHeight = section.getBoundingClientRect().height;
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
currentSection = section.getAttribute('id');
}
});
if (currentSection) {
navItems.forEach(item => {
const href = item.getAttribute('href')?.substring(1);
if (href === currentSection) {
item.classList.add('nav-item-active');
item.setAttribute('aria-current', 'true');
}
});
}
}
window.addEventListener('scroll', () => {
if (!header) return;
const currentScrollY = window.scrollY;
// Apply subtle shadow and border when scrolling down
if (currentScrollY > 20) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
// Hide header when scrolling down, show when scrolling up
if (currentScrollY > lastScrollY && currentScrollY > 100) {
header.style.transform = 'translateY(-100%)';
} else {
header.style.transform = 'translateY(0)';
}
lastScrollY = currentScrollY;
setActiveNavItem();
});
setActiveNavItem();
navItems.forEach(item => {
item.addEventListener('keydown', (e) => {
if (e instanceof KeyboardEvent && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
item.dispatchEvent(new MouseEvent('click'));
}
});
});
});
</script>
@ -101,11 +155,46 @@ import MobileMenu from './MobileMenu.astro';
}
header.scrolled {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.nav-item {
border-radius: 0;
position: relative;
overflow: hidden;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-item::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.black .nav-item::after {
background-color: rgba(255, 255, 255, 0.8);
}
.red .nav-item::after {
background-color: rgba(248, 113, 113, 0.8);
}
.gold .nav-item::after {
background-color: rgba(251, 191, 36, 0.8);
}
.nav-item:hover::after {
transform: translateX(0);
}
.nav-item-active {
background-color: rgba(255, 255, 255, 0.08);
}
.nav-item-active::after {
transform: translateX(0);
}
</style>

View File

@ -0,0 +1,108 @@
---
const defaultTitle = "Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert";
const defaultDescription = "Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies.";
const defaultImage = "/images/profile-image.jpg";
const siteUrl = "https://jleibl.net";
interface Props {
title?: string;
description?: string;
image?: string;
canonicalUrl?: string;
type?: "website" | "article";
publishedDate?: string;
modifiedDate?: string;
keywords?: string[];
}
const {
title = defaultTitle,
description = defaultDescription,
image = defaultImage,
canonicalUrl = siteUrl,
type = "website",
publishedDate,
modifiedDate,
keywords = [
"Software Development",
"PHP Developer",
"TypeScript",
"Fullstack Engineer",
"Web Development",
"System Architecture",
"MySQL",
"React",
"Performance Optimization"
]
} = Astro.props;
const structuredDataPerson = {
"@context": "https://schema.org",
"@type": "Person",
"name": "Jan-Marlon Leibl",
"url": siteUrl,
"image": `${siteUrl}${image}`,
"jobTitle": "Fullstack Software Developer",
"worksFor": {
"@type": "Organization",
"name": "team neusta"
},
"description": "Fullstack Software Developer specializing in PHP and TypeScript",
"sameAs": [
"https://github.com/AtomicWasTaken",
"https://www.linkedin.com/in/janmarlonleibl/",
],
"knowsAbout": ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"]
};
const structuredDataWebsite = {
"@context": "https://schema.org",
"@type": type,
"headline": title,
"description": description,
"image": `${siteUrl}${image}`,
"url": canonicalUrl,
"author": {
"@type": "Person",
"name": "Jan-Marlon Leibl",
"url": siteUrl
},
...(publishedDate && {"datePublished": publishedDate}),
...(modifiedDate && {"dateModified": modifiedDate})
};
---
<title>{title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(", ")} />
<meta name="author" content="Jan-Marlon Leibl" />
<meta name="robots" content="index, follow" />
<meta name="color-scheme" content="dark light" />
<meta name="theme-color" content="#0A0A0A" />
<meta name="generator" content={Astro.generator} />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={`${siteUrl}${image}`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
<meta property="og:site_name" content="Jan-Marlon Leibl" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<script type="application/ld+json" set:html={JSON.stringify(structuredDataPerson)} />
<script type="application/ld+json" set:html={JSON.stringify(structuredDataWebsite)} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="manifest" href="/manifest.json" />

View File

@ -1,69 +1,68 @@
---
import GermanyFlag from '../ui/GermanyFlag.astro';
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
const navItems = [
{ label: "About", href: "#about" },
{ label: "Work", href: "#work" },
{ label: "Blog", href: "#blog" },
{ label: "Contact", href: "#contact" }
];
---
<div class="mobile-menu">
<button
id="mobile-menu-toggle"
class="p-2.5 theme-accent rounded-xl theme-border border shadow-sm hover:theme-bg-05 transition-all duration-300"
aria-label="Open menu"
<button
id="menu-btn"
class="menu-btn"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu-panel"
>
<svg class="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
<div id="mobile-menu-overlay" class="fixed inset-0 bg-black opacity-0 z-90 pointer-events-none transition-opacity duration-300"></div>
<div id="mobile-menu-panel" class="fixed top-0 bottom-0 right-0 w-full sm:w-80 z-100 h-[100dvh] translate-x-full transition-transform duration-300">
<div class="nav-glass w-full h-full flex flex-col shadow-2xl" style="backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);">
<button
id="mobile-menu-close"
class="absolute top-4 right-4 p-2.5 theme-accent rounded-xl theme-border border z-110 shadow-sm hover:theme-bg-05 transition-all duration-300"
aria-label="Close menu"
>
<svg class="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<div class="w-full h-full p-6 pt-16 flex flex-col overflow-y-auto">
<nav class="mb-8">
<div class="flex flex-col space-y-3">
{[
{ href: "#about", label: "About" },
{ href: "#work", label: "Work" }
].map((item) => (
<a
href={item.href}
class="menu-link p-4 text-xl font-medium theme-primary rounded-xl theme-bg-05 border theme-border flex items-center hover:bg-white/[0.03] transition-all duration-300 shadow-sm"
>
<span>{item.label}</span>
<svg class="ml-auto w-5 h-5 theme-primary opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
))}
</div>
</nav>
<div id="mobile-menu-panel" class="menu-panel">
<div class="menu-content">
<nav class="menu-nav">
{navItems.map((item) => (
<a
href={item.href}
class="menu-item"
>
{item.label}
</a>
))}
</nav>
<div class="mt-auto">
<div class="pt-6 border-t theme-border">
<a
href="mailto:jleibl@proton.me"
class="flex items-center justify-center gap-3 w-full py-4 px-6 theme-button-primary rounded-xl transition-all duration-300 shadow-sm hover:shadow-md"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<span class="font-medium">Contact Me</span>
</a>
</div>
<div class="menu-footer">
<a
href="mailto:jleibl@proton.me"
class="contact-button"
>
<span class="icon-wrapper">
<FaEnvelope className="w-5 h-5" />
</span>
<span>Contact Me</span>
</a>
<div class="pt-8 flex justify-center pb-6 opacity-70">
<GermanyFlag />
</div>
<div class="social-links">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub profile"
>
<FaGithub className="w-5 h-5" />
</a>
<a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn profile"
>
<FaLinkedin className="w-5 h-5" />
</a>
</div>
</div>
</div>
@ -72,37 +71,191 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
<script>
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('mobile-menu-toggle');
const close = document.getElementById('mobile-menu-close');
const overlay = document.getElementById('mobile-menu-overlay');
const panel = document.getElementById('mobile-menu-panel');
const menuLinks = document.querySelectorAll('.menu-link');
const menuBtn = document.getElementById('menu-btn');
const menuPanel = document.getElementById('mobile-menu-panel');
const menuItems = document.querySelectorAll('.menu-item');
function openMenu() {
document.body.style.overflow = 'hidden';
toggle?.setAttribute('aria-expanded', 'true');
overlay?.classList.remove('opacity-0', 'pointer-events-none');
overlay?.classList.add('opacity-90');
panel?.classList.remove('translate-x-full');
function toggleMenu() {
if (!menuBtn || !menuPanel) return;
const isOpen = menuBtn.classList.toggle('active');
menuBtn.setAttribute('aria-expanded', isOpen.toString());
menuPanel.classList.toggle('open', isOpen);
document.body.style.overflow = isOpen ? 'hidden' : '';
}
function closeMenu() {
document.body.style.overflow = '';
toggle?.setAttribute('aria-expanded', 'false');
overlay?.classList.add('opacity-0', 'pointer-events-none');
overlay?.classList.remove('opacity-90');
panel?.classList.add('translate-x-full');
}
menuBtn?.addEventListener('click', toggleMenu);
toggle?.addEventListener('click', openMenu);
close?.addEventListener('click', closeMenu);
overlay?.addEventListener('click', closeMenu);
menuLinks.forEach(link => link.addEventListener('click', closeMenu));
menuItems.forEach(item => {
item.addEventListener('click', toggleMenu);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && toggle?.getAttribute('aria-expanded') === 'true') {
closeMenu();
if (e.key === 'Escape' && menuPanel?.classList.contains('open')) {
toggleMenu();
}
});
});
</script>
</script>
<style>
.mobile-menu {
position: relative;
z-index: 100;
}
.menu-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6px;
width: 40px;
height: 40px;
padding: 8px;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
position: relative;
z-index: 101;
transition: background-color 0.3s ease;
}
.menu-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.menu-btn .bar {
display: block;
width: 20px;
height: 2px;
background-color: white;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.menu-btn.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.menu-btn.active .bar:nth-child(2) {
opacity: 0;
}
.menu-btn.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
.menu-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.97);
display: flex;
align-items: center;
justify-content: center;
visibility: hidden;
opacity: 0;
transition: all 0.3s ease;
z-index: 100;
}
.menu-panel.open {
visibility: visible;
opacity: 1;
}
.menu-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding-top: 80px;
}
.menu-nav {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
}
.menu-item {
display: block;
padding: 12px 24px;
font-size: 18px;
font-weight: 500;
color: white;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
transition: all 0.2s ease;
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-footer {
margin-top: auto;
padding: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 20px;
}
.contact-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-weight: 500;
transition: background-color 0.2s ease;
}
.contact-button:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.social-links {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 8px;
}
.social-links a {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 12px;
color: white;
transition: background-color 0.2s ease;
}
.social-links a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

View File

@ -83,8 +83,8 @@ const interests = [
</div>
<ul class="space-y-4 sm:space-y-5">
{technologies.map((tech) => (
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-colors">
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
<tech.icon className="w-5 h-5" />
</span>
{tech.name}
@ -102,8 +102,8 @@ const interests = [
</div>
<ul class="space-y-4 sm:space-y-5">
{interests.map((interest) => (
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-colors">
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
<interest.icon className="w-5 h-5" />
</span>
{interest.name}

View File

@ -0,0 +1,187 @@
---
import FadeIn from '../ui/FadeIn.astro';
const blogPosts = [
{
title: "Best Practices for Modern PHP Development",
excerpt: "Explore how to leverage PHP 8.x features and modern tools to create maintainable and performant PHP applications.",
date: "May 15, 2023",
slug: "best-practices-modern-php-development",
tags: ["PHP", "Development", "Best Practices"]
},
{
title: "Building Type-Safe APIs with TypeScript",
excerpt: "How to use TypeScript's powerful type system to create robust and maintainable API integrations in your frontend applications.",
date: "April 3, 2023",
slug: "building-type-safe-apis-typescript",
tags: ["TypeScript", "API", "Frontend"]
},
{
title: "Performance Optimization Techniques for Web Applications",
excerpt: "A comprehensive guide to identifying and solving performance bottlenecks in modern web applications.",
date: "March 12, 2023",
slug: "performance-optimization-web-applications",
tags: ["Performance", "Web", "Optimization"]
}
];
---
<section id="blog" class="py-20 sm:py-32 px-4 sm:px-8 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
<div class="max-w-7xl mx-auto relative">
<FadeIn className="space-y-16">
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary">
Blog
</h2>
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl mt-4">
Thoughts, insights, and tutorials on software development and tech.
<<<<<<< HEAD
<span class="inline-flex items-center px-3 py-1 ml-2 rounded-full text-xs font-medium bg-black/20 text-white font-['Instrument_Sans']">Coming Soon</span>
</p>
</div>
<div class="hidden sm:block">
<button
class="animate-button inline-flex items-center justify-center gap-2 px-6 py-3 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-sm tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
title="Coming soon!"
>
View All Posts
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 relative p-12">
<div class="absolute inset-0 backdrop-blur-sm z-10 flex items-center justify-center rounded-xl">
<div class="bg-black/80 px-6 py-5 rounded-xl border border-white/20 text-center">
<span class="text-xl font-semibold text-white font-['DM_Sans']">Blog Coming Soon!</span>
<p class="text-white/80 mt-2 font-['Instrument_Sans']">The blog feature is currently in development.</p>
</div>
</div>
{blogPosts.map((post) => (
<div
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden opacity-80 group"
=======
</p>
</div>
<div class="hidden sm:block">
<button
class="animate-button inline-flex items-center justify-center gap-2 px-6 py-3 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-sm tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
title="Coming soon!"
>
View All Posts
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 relative p-12">
<div class="absolute inset-0 backdrop-blur-sm z-10 flex items-center justify-center rounded-xl">
<div class="bg-black/80 px-6 py-5 rounded-xl border border-white/20 text-center">
<span class="text-xl font-semibold text-white font-['DM_Sans']">Blog Coming Soon!</span>
<p class="text-white/80 mt-2 font-['Instrument_Sans']">The blog feature is currently in development.</p>
</div>
</div>
{blogPosts.map((post) => (
<a
href={`/blog/${post.slug}`}
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden hover:shadow-lg transition-all duration-300 group"
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
>
<div class="p-6 sm:p-8 flex flex-col h-full">
<div class="flex-grow space-y-4">
<div class="space-y-1.5">
<div class="flex gap-2 flex-wrap">
{post.tags.map((tag) => (
<span class="inline-flex px-2.5 py-1 rounded-lg text-xs font-medium theme-text-70 bg-black/10 font-['Instrument_Sans']">{tag}</span>
))}
</div>
<<<<<<< HEAD
<h3 class="text-xl font-bold theme-primary font-['DM_Sans']">{post.title}</h3>
=======
<h3 class="text-xl font-bold theme-primary group-hover:underline decoration-1 underline-offset-2 font-['DM_Sans']">{post.title}</h3>
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
</div>
<p class="theme-secondary font-['Instrument_Sans'] text-sm sm:text-base leading-relaxed">{post.excerpt}</p>
</div>
<div class="mt-6 pt-6 border-t theme-border flex items-center justify-between">
<span class="text-sm theme-text-70 font-['Instrument_Sans']">{post.date}</span>
<<<<<<< HEAD
<div class="p-2 rounded-full theme-accent">
<svg
class="w-4 h-4 theme-primary"
=======
<div class="p-2 rounded-full theme-accent group-hover:bg-opacity-100 transition-all">
<svg
class="w-4 h-4 theme-primary transform group-hover:translate-x-1 transition-transform"
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</div>
</div>
<<<<<<< HEAD
</div>
))}
</div>
<div class="sm:hidden flex justify-center mt-8">
<button
class="animate-button inline-flex items-center justify-center gap-2 px-8 py-4 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-base tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
title="Coming soon!"
>
View All Posts
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
</div>
=======
</a>
))}
</div>
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
</FadeIn>
</div>
</section>

View File

@ -0,0 +1,179 @@
---
import FadeIn from '../ui/FadeIn.astro';
import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } from "react-icons/fa";
---
<section id="contact" class="py-20 sm:py-32 px-4 sm:px-8 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
<div class="max-w-7xl mx-auto relative">
<FadeIn className="space-y-16">
<div class="space-y-6">
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary">
Get In Touch
</h2>
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl">
Have a project in mind or want to chat? Feel free to reach out.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16">
<div class="space-y-8">
<form id="contact-form" class="space-y-6" onsubmit="return openEmailClient(event)">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<label for="name" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Name</label>
<input
type="text"
id="name"
name="name"
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
required
aria-required="true"
/>
</div>
<div>
<label for="email" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Email</label>
<input
type="email"
id="email"
name="email"
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
required
aria-required="true"
/>
</div>
</div>
<div>
<label for="subject" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Subject</label>
<input
type="text"
id="subject"
name="subject"
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
/>
</div>
<div>
<label for="message" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Message</label>
<textarea
id="message"
name="message"
rows="5"
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 bg-transparent font-['Instrument_Sans']"
required
aria-required="true"
></textarea>
</div>
<div>
<button
type="submit"
class="animate-button w-full sm:w-auto inline-flex items-center justify-center gap-2 px-8 py-4 theme-button-primary rounded-xl transition-all text-base tracking-tight font-medium group shadow-sm font-['Instrument_Sans']"
>
Send Message
<svg
class="w-5 h-5 transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
<div id="form-status" class="mt-4 text-sm hidden font-['Instrument_Sans']"></div>
</div>
</form>
</div>
<div class="space-y-8">
<div>
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">Contact Information</h3>
<div class="space-y-4">
<div class="flex items-start gap-4">
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
<FaEnvelope className="w-5 h-5" />
</span>
<div>
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Email</h4>
<a href="mailto:jleibl@proton.me" class="theme-primary hover:underline font-['Instrument_Sans']">jleibl@proton.me</a>
</div>
</div>
<div class="flex items-start gap-4">
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
<FaMapMarkerAlt className="w-5 h-5" />
</span>
<div>
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Location</h4>
<p class="theme-primary font-['Instrument_Sans']">Bremen, Germany</p>
</div>
</div>
</div>
</div>
<div>
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">Connect</h3>
<div class="flex flex-wrap gap-4">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub profile"
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
>
<FaGithub className="w-6 h-6" />
</a>
<a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn profile"
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
>
<FaLinkedin className="w-6 h-6" />
</a>
<a
href="https://jleibl.net"
target="_blank"
rel="noopener noreferrer"
aria-label="My website"
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
>
<FaGlobe className="w-6 h-6" />
</a>
</div>
</div>
</div>
</div>
</FadeIn>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', () => {
(window as any).openEmailClient = (e: Event) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const subject = formData.get('subject') as string || 'Contact Form Message';
const message = formData.get('message') as string;
const formStatus = document.getElementById('form-status');
if (!name || !email || !message) {
if (formStatus) {
formStatus.textContent = 'Please fill in all required fields.';
formStatus.classList.remove('hidden');
}
return false;
}
const body = `Name: ${name}\nEmail: ${email}\n\n${message}`;
window.location.href = `mailto:jleibl@proton.me?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return false;
};
});
</script>

View File

@ -6,7 +6,7 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
<section class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16" id="hero">
<div class="absolute inset-0 theme-bg-gradient opacity-80"></div>
<div class="max-w-7xl w-full relative pt-8 sm:pt-24">
<div class="max-w-7xl w-full relative pt-8 sm:pt-4">
<FadeIn className="space-y-10 sm:space-y-16">
<div class="space-y-8 sm:space-y-10">
<div class="space-y-6 sm:space-y-8">

View File

@ -0,0 +1,50 @@
<a
href="#main-content"
class="skip-to-content"
aria-label="Skip to main content"
>
<span class="font-['Instrument_Sans']">Skip to content</span>
</a>
<style>
.skip-to-content {
position: absolute;
top: -100px;
left: 0;
padding: 1rem;
margin: 1rem;
background: black;
color: rgba(255, 255, 255, 0.95);
z-index: 100;
transition: top 0.3s ease-out;
font-weight: 500;
font-family: 'Instrument Sans', sans-serif;
border-radius: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.skip-to-content:focus {
top: 0;
}
/* Theme variations */
html[data-theme="red"] .skip-to-content {
background: #170c0c;
color: #f87171;
border-color: rgba(248, 113, 113, 0.15);
}
html[data-theme="gold"] .skip-to-content {
background: #121107;
color: #fbbf24;
border-color: rgba(251, 191, 36, 0.15);
}
/* Dark mode support */
:root.dark .skip-to-content {
background: #0A0A0A;
color: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
</style>

View File

@ -1,8 +1,12 @@
---
import Header from '../components/layout/Header.astro';
import Meta from '../components/layout/Meta.astro';
import SkipToContent from '../components/ui/SkipToContent.astro';
import Hero from '../components/sections/Hero.astro';
import About from '../components/sections/About.astro';
import Work from '../components/sections/Work.astro';
import BlogPreview from '../components/sections/BlogPreview.astro';
import Contact from '../components/sections/Contact.astro';
import Footer from '../components/layout/Footer.astro';
import '../styles/global.css';
@ -18,52 +22,30 @@ import '@fontsource/instrument-sans/500.css';
<html lang="en">
<head>
<title>Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="title" content="Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert" />
<meta name="description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
<meta name="keywords" content="Software Development, PHP Developer, TypeScript, Fullstack Engineer, Web Development, System Architecture, MySQL, React, Performance Optimization" />
<meta name="author" content="Jan-Marlon Leibl" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://jleibl.net/" />
<meta property="og:title" content="Jan-Marlon Leibl • Fullstack Software Developer" />
<meta property="og:description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
<meta property="og:image" content="https://jleibl.net/profile-image.jpg" />
<meta property="og:image:width" content="400" />
<meta property="og:image:height" content="400" />
<meta property="og:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://jleibl.net/" />
<meta name="twitter:title" content="Jan-Marlon Leibl • Fullstack Software Developer" />
<meta name="twitter:description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
<meta name="twitter:image" content="https://jleibl.net/profile-image.jpg" />
<meta name="twitter:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
<link rel="icon" href="/profile-image.jpg" type="image/jpeg" />
<link rel="canonical" href="https://jleibl.net/" />
<Meta />
</head>
<body>
<SkipToContent />
<div id="app-root" class="min-h-screen theme-transition black overflow-hidden">
<Header />
<Hero />
<About />
<Work />
<main id="main-content">
<Hero />
<About />
<Work />
<BlogPreview />
<Contact />
</main>
<Footer />
</div>
</body>
</html>
<script>
// Theme Provider functionality
document.addEventListener('DOMContentLoaded', () => {
const appRoot = document.getElementById('app-root');
const themeButtons = document.querySelectorAll('.theme-button');
const currentTheme = localStorage.getItem('theme') || 'black';
const currentTheme = localStorage.getItem('colorTheme') || 'black';
if (appRoot) {
appRoot.classList.remove('black', 'red', 'gold');
@ -80,13 +62,15 @@ import '@fontsource/instrument-sans/500.css';
if (appRoot && newTheme) {
appRoot.classList.remove('black', 'red', 'gold');
appRoot.classList.add(newTheme);
localStorage.setItem('colorTheme', newTheme);
}
});
});
</script>
<script>
import Lenis from '@studio-freight/lenis';
import Lenis from 'lenis';
document.addEventListener('DOMContentLoaded', () => {
const lenis = new Lenis({
@ -95,6 +79,7 @@ import '@fontsource/instrument-sans/500.css';
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 2,
infinite: false
});
const anchorLinks = document.querySelectorAll('a[href^="#"]');
@ -117,11 +102,62 @@ import '@fontsource/instrument-sans/500.css';
});
});
function raf(time) {
let animationId: number;
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
animationId = requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
animationId = requestAnimationFrame(raf);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
cancelAnimationFrame(animationId);
} else {
animationId = requestAnimationFrame(raf);
}
});
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if ('loading' in HTMLImageElement.prototype) {
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
const imgElement = img as HTMLImageElement;
imgElement.src = imgElement.dataset.src || '';
imgElement.removeAttribute('data-src');
});
} else {
const lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target as HTMLImageElement;
if (lazyImage.dataset.src) {
lazyImage.src = lazyImage.dataset.src;
lazyImage.removeAttribute('data-src');
}
lazyImageObserver.unobserve(lazyImage);
}
});
});
document.querySelectorAll('img[data-src]').forEach(image => {
lazyImageObserver.observe(image);
});
}
});
</script>
<script>
if ('serviceWorker' in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.catch(error => {
console.error('Service worker registration failed:', error);
});
});
}
</script>

View File

@ -55,7 +55,6 @@
.black .nav-item-active {
background: rgba(255, 255, 255, 0.08);
border-bottom: 2px solid rgba(255, 255, 255, 0.8);
}
.red .nav-item-active {
background: rgba(248, 113, 113, 0.08);