feat: add initial Astro project structure and components

This commit is contained in:
2025-03-09 13:38:05 +01:00
parent 0ab11c240c
commit 6cc0e06918
41 changed files with 11299 additions and 1023 deletions

View File

@ -0,0 +1,70 @@
---
import FadeIn from "../ui/FadeIn.astro";
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
---
<footer
role="contentinfo"
class="mt-20 sm:mt-32 theme-bg-gradient border-t theme-border"
>
<div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32">
<div class="grid gap-16 sm:gap-24">
<FadeIn>
<div class="text-center space-y-4 sm:space-y-5">
<h2
class="font-['DM_Sans'] text-4xl sm:text-7xl font-semibold tracking-tight theme-primary"
>
Let&apos;s Connect
</h2>
<p
class="font-['Instrument_Sans'] theme-text-70 text-lg sm:text-2xl max-w-2xl mx-auto"
>
Always interested in new opportunities and collaborations.
</p>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div class="flex flex-col items-center gap-8 sm:gap-12 font-['Instrument_Sans']">
<a
href="mailto:jleibl@proton.me"
class="group flex items-center gap-3 text-xl sm:text-4xl theme-text-90 hover:theme-primary transition-colors"
>
<FaEnvelope className="w-6 h-6 sm:w-8 sm:h-8" />
jleibl@proton.me
</a>
<div class="flex gap-6 sm:gap-10">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
class="group theme-text-70 hover:theme-primary transition-colors"
aria-label="GitHub Profile"
>
<FaGithub className="w-8 h-8 sm:w-10 sm:h-10" />
</a>
<a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
class="group theme-text-70 hover:theme-primary transition-colors"
aria-label="LinkedIn Profile"
>
<FaLinkedin className="w-8 h-8 sm:w-10 sm:h-10" />
</a>
</div>
</div>
</FadeIn>
<FadeIn delay={0.2}>
<div class="pt-6 sm:pt-12 text-center">
<p class="font-['Instrument_Sans'] theme-text-40 text-xs sm:text-sm tracking-[0.1em]">
© {new Date().getFullYear()} Jan-Marlon Leibl. All rights reserved.
</p>
</div>
</FadeIn>
</div>
</div>
</footer>

View File

@ -1,39 +0,0 @@
import { FadeIn } from '../ui/FadeIn';
export const Footer = () => {
return (
<footer role="contentinfo" className="mt-20 sm:mt-32 theme-bg-gradient border-t theme-border">
<div className="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32">
<div className="grid gap-16 sm:gap-24">
<FadeIn>
<div className="text-center space-y-4 sm:space-y-5">
<h2 className="font-['DM_Sans'] text-4xl sm:text-7xl font-semibold tracking-tight theme-primary">Let&apos;s Connect</h2>
<p className="font-['Instrument_Sans'] theme-text-70 text-lg sm:text-2xl max-w-2xl mx-auto">
Always interested in new opportunities and collaborations.
</p>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div className="flex flex-col items-center gap-8 sm:gap-12">
<a
href="mailto:jleibl@proton.me"
className="group flex items-center gap-2 sm:gap-3 text-xl sm:text-4xl theme-text-90 hover:theme-primary transition-colors"
>
jleibl@proton.me
</a>
</div>
</FadeIn>
<FadeIn delay={0.2}>
<div className="pt-6 sm:pt-12 text-center">
<p className="theme-text-40 text-xs sm:text-sm tracking-[0.1em]">
© {new Date().getFullYear()} Jan-Marlon Leibl. All rights reserved.
</p>
</div>
</FadeIn>
</div>
</div>
</footer>
);
};

View File

@ -0,0 +1,80 @@
---
import MobileMenu from './MobileMenu.astro';
---
<header
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">
<nav class="py-5 px-6 sm:px-8 flex justify-between items-center font-['DM_Sans']" role="navigation" aria-label="Main navigation">
<div class="flex items-center">
<a href="#hero" class="flex items-center gap-2 group" aria-label="Home">
<div class="relative w-10 h-10 rounded-full theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300">
<span class="text-lg font-bold tracking-tight theme-primary theme-transition">JL</span>
<div class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
<span class="text-lg font-medium tracking-tight theme-primary theme-transition hidden sm:block">
Jan-Marlon Leibl
</span>
</a>
</div>
<div class="hidden md:flex items-center">
<div class="flex bg-black/10 backdrop-blur-md rounded-full overflow-hidden theme-border border divide-x divide-white/5">
{["About", "Work"].map((item) => (
<a
href={`#${item.toLowerCase()}`}
class="nav-item px-6 py-2 theme-secondary hover:theme-primary theme-transition"
aria-label={`View my ${item.toLowerCase()}`}
>
{item}
</a>
))}
</div>
<div class="ml-6">
<a
href="mailto:jleibl@proton.me"
class="flex items-center gap-2 px-6 py-2 theme-bg-05 theme-primary border theme-border rounded-full hover:bg-opacity-100 transition-all duration-300 group"
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">
<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">Contact</span>
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-500">
<svg class="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</a>
</div>
</div>
<div class="flex md:hidden">
<MobileMenu />
</div>
</nav>
</div>
<div class="h-px w-full theme-border opacity-50"></div>
</header>
<script>
document.addEventListener('DOMContentLoaded', () => {
const header = document.querySelector('header');
if (header) {
header.style.transform = 'translateY(0)';
header.style.opacity = '1';
}
});
</script>
<style>
header {
transform: translateY(-100px);
opacity: 0;
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
}
</style>

View File

@ -1,89 +0,0 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import dynamic from 'next/dynamic';
const MobileMenu = dynamic(() => import('./MobileMenu').then(mod => ({ default: mod.MobileMenu })), {
ssr: false,
loading: () => (
<div className="p-2 theme-accent rounded-lg theme-border">
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</div>
)
});
export const Header = () => {
return (
<motion.header
className="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
role="banner"
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
type: "spring",
damping: 25,
stiffness: 100,
mass: 1,
delay: 0.2
}}
>
<div className="max-w-(--breakpoint-xl) mx-auto">
<nav className="py-5 px-6 sm:px-8 flex justify-between items-center font-['DM_Sans']" role="navigation" aria-label="Main navigation">
<div className="flex items-center">
<Link href="/" className="flex items-center gap-2 group" aria-label="Home">
<div className="relative w-10 h-10 rounded-full theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300">
<span className="text-lg font-bold tracking-tight theme-primary theme-transition">JL</span>
<div className="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"/>
</div>
<span className="text-lg font-medium tracking-tight theme-primary theme-transition hidden sm:block">
Jan-Marlon Leibl
</span>
</Link>
</div>
<div className="hidden md:flex items-center">
<div className="flex bg-black/10 backdrop-blur-md rounded-full overflow-hidden theme-border border divide-x divide-white/5">
{["Work", "About"].map((item) => (
<a
key={item}
href={`#${item.toLowerCase()}`}
className="nav-item px-6 py-2 theme-secondary hover:theme-primary theme-transition"
aria-label={`View my ${item.toLowerCase()}`}
>
{item}
</a>
))}
</div>
<div className="ml-6">
<motion.a
href="mailto:jleibl@proton.me"
className="flex items-center gap-2 px-6 py-2 theme-bg-05 theme-primary border theme-border rounded-full hover:bg-opacity-100 transition-all duration-300 group"
aria-label="Contact me via email"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<svg className="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="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 className="text-sm">Contact</span>
<div className="w-0 overflow-hidden group-hover:w-4 transition-all duration-500">
<svg className="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</motion.a>
</div>
</div>
<div className="flex md:hidden">
<MobileMenu />
</div>
</nav>
</div>
<div className="h-px w-full theme-border opacity-50"></div>
</motion.header>
);
};

View File

@ -0,0 +1,108 @@
---
import GermanyFlag from '../ui/GermanyFlag.astro';
---
<div class="mobile-menu">
<button
id="mobile-menu-toggle"
class="p-2 theme-accent rounded-lg theme-border"
aria-label="Open menu"
aria-expanded="false"
>
<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>
</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 theme-accent rounded-lg theme-border z-110"
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-2">
{[
{ href: "#about", label: "About" },
{ href: "#work", label: "Work" }
].map((item) => (
<a
href={item.href}
class="menu-link p-4 text-xl theme-primary rounded-lg theme-accent flex items-center hover:theme-bg-05 transition-colors"
>
<span>{item.label}</span>
<svg class="ml-auto w-5 h-5 theme-primary" 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 class="mt-auto">
<div class="pt-4 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-primary border theme-border rounded-lg hover:theme-bg-05 transition-colors"
>
<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>Contact Me</span>
</a>
</div>
<div class="pt-6 flex justify-center pb-6">
<GermanyFlag />
</div>
</div>
</div>
</div>
</div>
</div>
<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');
function openMenu() {
document.body.style.overflow = 'hidden';
toggle?.setAttribute('aria-expanded', 'true');
overlay?.classList.remove('opacity-0', 'pointer-events-none');
overlay?.classList.add('opacity-95');
panel?.classList.remove('translate-x-full');
}
function closeMenu() {
document.body.style.overflow = '';
toggle?.setAttribute('aria-expanded', 'false');
overlay?.classList.add('opacity-0', 'pointer-events-none');
overlay?.classList.remove('opacity-95');
panel?.classList.add('translate-x-full');
}
toggle?.addEventListener('click', openMenu);
close?.addEventListener('click', closeMenu);
overlay?.addEventListener('click', closeMenu);
menuLinks.forEach(link => link.addEventListener('click', closeMenu));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && toggle?.getAttribute('aria-expanded') === 'true') {
closeMenu();
}
});
});
</script>

View File

@ -1,147 +0,0 @@
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GermanyFlag } from '../ui/GermanyFlag';
export const MobileMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, [isOpen]);
return (
<>
<motion.button
onClick={() => setIsOpen(true)}
className="p-2 theme-accent rounded-lg theme-border"
aria-label="Open menu"
aria-expanded={isOpen}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</motion.button>
<AnimatePresence mode="wait">
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black z-90"
initial={{ opacity: 0 }}
animate={{ opacity: 0.95 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
<motion.div
className="fixed top-0 bottom-0 right-0 w-full sm:w-80 z-100 h-[100dvh]"
ref={menuRef}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 30, stiffness: 300, mass: 1 }}
>
<div className="nav-glass w-full h-full flex flex-col shadow-2xl"
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)' }}>
<motion.button
onClick={() => setIsOpen(false)}
className="absolute top-4 right-4 p-2 theme-accent rounded-lg theme-border z-110"
aria-label="Close menu"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 20, delay: 0.2 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M6 18L18 6M6 6l12 12"></path>
</svg>
</motion.button>
<div className="w-full h-full p-6 pt-16 flex flex-col overflow-y-auto">
<nav className="mb-8">
<motion.div
className="flex flex-col space-y-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ type: "spring", stiffness: 100, damping: 20, delay: 0.1 }}
>
{[
{ href: "#work", label: "Work" },
{ href: "#about", label: "About" }
].map((item, index) => (
<motion.a
key={item.href}
href={item.href}
onClick={() => setIsOpen(false)}
className="p-4 text-xl theme-primary rounded-lg theme-accent flex items-center hover:theme-bg-05 transition-colors"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ type: "spring", stiffness: 200, damping: 20, delay: 0.2 + index * 0.1 }}
>
<span>{item.label}</span>
<svg className="ml-auto w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</motion.a>
))}
</motion.div>
</nav>
<motion.div
className="mt-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 100, damping: 20, delay: 0.4 }}
>
<div className="pt-4 border-t theme-border">
<motion.a
href="mailto:jleibl@proton.me"
onClick={() => setIsOpen(false)}
className="flex items-center justify-center gap-3 w-full py-4 px-6 theme-primary border theme-border rounded-lg hover:theme-bg-05 transition-colors"
whileHover={{ scale: 1.02 }}
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="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>Contact Me</span>
</motion.a>
</div>
<div className="pt-6 flex justify-center pb-6">
<GermanyFlag />
</div>
</motion.div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
};