577 lines
19 KiB
Plaintext
577 lines
19 KiB
Plaintext
---
|
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
import DynamicIcon from '../components/DynamicIcon.tsx';
|
|
|
|
import directus from '../../lib/directus';
|
|
import { readSingleton, readItems } from '@directus/sdk';
|
|
|
|
const global = await directus.request(readSingleton('global'));
|
|
const about = await directus.request(readSingleton('about'));
|
|
|
|
const skills = await directus.request(
|
|
readItems('skills', {
|
|
fields: ['*'],
|
|
})
|
|
);
|
|
---
|
|
|
|
<BaseLayout title="About Me" description={global.description}>
|
|
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
|
<!-- Hero Section -->
|
|
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
|
<!-- Decorative elements -->
|
|
<div
|
|
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
|
>
|
|
</div>
|
|
<div
|
|
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
|
>
|
|
</div>
|
|
|
|
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
|
<div class="order-2 text-center md:order-1 md:text-left">
|
|
<h1
|
|
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
|
|
>
|
|
Hello, I'm <span
|
|
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
|
|
>{global.name}</span
|
|
>
|
|
</h1>
|
|
|
|
<p
|
|
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
|
|
>
|
|
{about.background}
|
|
</p>
|
|
|
|
<div
|
|
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
|
|
>
|
|
<!-- Social links remain the same -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative order-1 md:order-2">
|
|
<div
|
|
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md dark:border-zinc-800"
|
|
>
|
|
<img
|
|
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
|
alt={global.portrait_alt}
|
|
class="h-full w-full object-cover"
|
|
loading="eager"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Decorative elements -->
|
|
<div
|
|
class="theme-transition-all absolute -right-4 -bottom-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg sm:-right-6 sm:-bottom-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24 dark:border-zinc-900 dark:bg-zinc-800"
|
|
>
|
|
<span class="text-2xl sm:text-3xl">👋</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- About Section -->
|
|
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
<div class="mx-auto max-w-3xl">
|
|
<h2
|
|
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 sm:mb-8 sm:text-3xl md:justify-start dark:text-zinc-100"
|
|
>
|
|
<span
|
|
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
|
|
></span>
|
|
About Me
|
|
<span
|
|
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
|
|
></span>
|
|
</h2>
|
|
|
|
<div class="theme-transition-all prose prose-zinc dark:prose-invert max-w-none">
|
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
{about.experience}
|
|
</p>
|
|
|
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
{about.education}
|
|
</p>
|
|
|
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
{about.certifications}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skills Section - Improved for mobile -->
|
|
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
<h2
|
|
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 sm:mb-12 sm:text-3xl dark:text-zinc-100"
|
|
>
|
|
Tech Stack
|
|
</h2>
|
|
|
|
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
|
<!-- Main slider container -->
|
|
<div class="slider-track animate-slide flex">
|
|
{
|
|
[...skills, ...skills, ...skills].map((skill) => (
|
|
<div class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600">
|
|
<div class="p-4 sm:p-6">
|
|
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
|
<div class="flex items-center gap-2 sm:gap-4">
|
|
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
|
|
<DynamicIcon name={skill.icon} />
|
|
</div>
|
|
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
|
{skill.title}
|
|
</h3>
|
|
</div>
|
|
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-zinc-800 dark:text-zinc-400">
|
|
{skill.level}%
|
|
</span>
|
|
</div>
|
|
|
|
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 sm:h-2 dark:bg-zinc-700">
|
|
<div
|
|
class="progress-bar-animate theme-transition-bg absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
|
|
style={`width: ${skill.level}%`}
|
|
/>
|
|
</div>
|
|
|
|
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 sm:mt-2 sm:text-xs dark:text-zinc-500">
|
|
<span>Beginner</span>
|
|
<span>Advanced</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
<!-- Gradient overlays for smooth fade effect -->
|
|
<div
|
|
class="theme-transition-bg absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-white to-transparent sm:w-24 dark:from-zinc-900"
|
|
>
|
|
</div>
|
|
<div
|
|
class="theme-transition-bg absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-white to-transparent sm:w-24 dark:from-zinc-900"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Section -->
|
|
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
|
<h2
|
|
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 sm:mb-6 sm:text-3xl dark:text-zinc-100"
|
|
>
|
|
Get in Touch
|
|
</h2>
|
|
<p
|
|
class="theme-transition-color mb-6 text-base text-zinc-600 sm:mb-8 sm:text-lg dark:text-zinc-400"
|
|
>
|
|
I'm always open to new opportunities and collaborations. If you'd like to work together or
|
|
just say hello, feel free to reach out.
|
|
</p>
|
|
|
|
<a
|
|
href=`mailto:${global.email}`
|
|
class="hover theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
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"
|
|
></path>
|
|
</svg>
|
|
Say Hello
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</BaseLayout>
|
|
|
|
<style>
|
|
/* Blob animation */
|
|
.animate-blob {
|
|
animation: blob-bounce 8s infinite ease;
|
|
}
|
|
|
|
.animation-delay-2000 {
|
|
animation-delay: 2s;
|
|
}
|
|
|
|
@keyframes blob-bounce {
|
|
0%,
|
|
100% {
|
|
transform: translate(0, 0) scale(1);
|
|
}
|
|
25% {
|
|
transform: translate(5%, 5%) scale(1.05);
|
|
}
|
|
50% {
|
|
transform: translate(0, 10%) scale(1);
|
|
}
|
|
75% {
|
|
transform: translate(-5%, 5%) scale(0.95);
|
|
}
|
|
}
|
|
|
|
/* Tech Stack Slider */
|
|
.slider-track {
|
|
width: fit-content;
|
|
animation: scroll 40s linear infinite;
|
|
}
|
|
|
|
@keyframes scroll {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
|
}
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.slider-track {
|
|
animation: scroll 60s linear infinite;
|
|
}
|
|
|
|
@keyframes scroll {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
|
}
|
|
}
|
|
}
|
|
|
|
.tech-stack-slider:hover .slider-track {
|
|
animation-play-state: paused;
|
|
}
|
|
|
|
.skill-card {
|
|
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.skill-card:hover {
|
|
z-index: 10;
|
|
}
|
|
|
|
/* Reduce animation complexity on mobile for better performance */
|
|
@media (max-width: 640px) {
|
|
.skill-card {
|
|
transition:
|
|
transform 0.3s ease,
|
|
box-shadow 0.3s ease;
|
|
}
|
|
|
|
.skill-card:hover {
|
|
transform: translateY(-5px) !important;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
|
}
|
|
}
|
|
|
|
.skill-card:before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -10%;
|
|
left: -10%;
|
|
width: 120%;
|
|
height: 120%;
|
|
background: radial-gradient(
|
|
circle at center,
|
|
rgba(255, 255, 255, 0.1) 0%,
|
|
rgba(255, 255, 255, 0) 70%
|
|
);
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.skill-card:hover:before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.progress-bar-animate {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar-animate:after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
animation: progress-shine 2s infinite;
|
|
}
|
|
|
|
@keyframes progress-shine {
|
|
0% {
|
|
left: -100%;
|
|
}
|
|
100% {
|
|
left: 100%;
|
|
}
|
|
}
|
|
|
|
/* Improved touch targets for mobile */
|
|
@media (max-width: 640px) {
|
|
a,
|
|
button {
|
|
min-height: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.social-link {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
|
|
/* Theme transition effect */
|
|
:global(.theme-switching) .theme-transition-element {
|
|
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
/* Smooth card transition during theme switch */
|
|
.skill-card.theme-transition-element {
|
|
transition:
|
|
background-color var(--theme-transition),
|
|
border-color var(--theme-transition),
|
|
color var(--theme-transition),
|
|
box-shadow var(--theme-transition),
|
|
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Wait for the DOM to be fully loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const sliderTrack = document.querySelector('.slider-track');
|
|
|
|
// Create seamless infinite scrolling effect
|
|
function setupInfiniteScroll() {
|
|
const cards = document.querySelectorAll('.skill-card');
|
|
if (!cards.length) return;
|
|
|
|
// Clone the first set of cards and append to create seamless loop
|
|
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
|
|
|
|
// Set proper animation based on screen size
|
|
function updateScrollAnimation() {
|
|
if (window.innerWidth >= 640) {
|
|
sliderTrack.style.animation = 'scroll 60s linear infinite';
|
|
} else {
|
|
sliderTrack.style.animation = 'scroll 40s linear infinite';
|
|
}
|
|
}
|
|
|
|
updateScrollAnimation();
|
|
window.addEventListener('resize', updateScrollAnimation);
|
|
}
|
|
|
|
setupInfiniteScroll();
|
|
|
|
// Pause animation on hover/touch
|
|
sliderTrack?.addEventListener('mouseenter', () => {
|
|
sliderTrack.style.animationPlayState = 'paused';
|
|
});
|
|
|
|
sliderTrack?.addEventListener('touchstart', () => {
|
|
sliderTrack.style.animationPlayState = 'paused';
|
|
});
|
|
|
|
sliderTrack?.addEventListener('mouseleave', () => {
|
|
sliderTrack.style.animationPlayState = 'running';
|
|
});
|
|
|
|
sliderTrack?.addEventListener('touchend', () => {
|
|
setTimeout(() => {
|
|
sliderTrack.style.animationPlayState = 'running';
|
|
}, 1000); // Delay resuming animation after touch
|
|
});
|
|
|
|
// Add hover effects to cards - only on non-touch devices
|
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
const cards = document.querySelectorAll('.skill-card');
|
|
|
|
if (!isTouchDevice) {
|
|
cards.forEach((card) => {
|
|
card.addEventListener('mousemove', (e) => {
|
|
const rect = card.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
const centerX = rect.width / 2;
|
|
const centerY = rect.height / 2;
|
|
|
|
const angleX = (y - centerY) / 15;
|
|
const angleY = (centerX - x) / 15;
|
|
|
|
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
|
|
|
|
// Dynamic shadow based on tilt
|
|
const shadowX = (x - centerX) / 25;
|
|
const shadowY = (y - centerY) / 25;
|
|
card.style.boxShadow = `
|
|
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
|
0 10px 20px rgba(0, 0, 0, 0.05)
|
|
`;
|
|
});
|
|
|
|
card.addEventListener('mouseleave', () => {
|
|
card.style.transform = '';
|
|
card.style.boxShadow = '';
|
|
});
|
|
});
|
|
} else {
|
|
// Simpler effects for touch devices
|
|
cards.forEach((card) => {
|
|
card.addEventListener('touchstart', () => {
|
|
card.classList.add('is-touched');
|
|
});
|
|
|
|
card.addEventListener('touchend', () => {
|
|
setTimeout(() => {
|
|
card.classList.remove('is-touched');
|
|
}, 300);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle theme transition
|
|
document.addEventListener('themeChange', () => {
|
|
// Add special effects during theme transition
|
|
cards.forEach((card, index) => {
|
|
// Add staggered animation delay
|
|
setTimeout(() => {
|
|
card.classList.add('theme-changing');
|
|
setTimeout(() => {
|
|
card.classList.remove('theme-changing');
|
|
}, 600);
|
|
}, index * 50);
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
// Handle SPA transitions for about page
|
|
function setupSPATransitions() {
|
|
// Handle all internal links for SPA transitions
|
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
// Skip links that are anchor links, external links, or already processed
|
|
if (
|
|
link.getAttribute('href').includes('#') ||
|
|
link.getAttribute('target') === '_blank' ||
|
|
link.hasAttribute('data-spa-handled')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Mark as handled to avoid duplicate listeners
|
|
link.setAttribute('data-spa-handled', 'true');
|
|
|
|
link.addEventListener('click', (e) => {
|
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
const targetHref = link.getAttribute('href');
|
|
|
|
// Trigger page transition animation
|
|
const pageTransition = document.getElementById('page-transition');
|
|
if (pageTransition) {
|
|
pageTransition.classList.remove('opacity-0');
|
|
pageTransition.classList.add('opacity-100');
|
|
|
|
// Navigate after transition effect
|
|
setTimeout(() => {
|
|
window.location.href = targetHref;
|
|
}, 300);
|
|
} else {
|
|
// Fallback if transition element doesn't exist
|
|
window.location.href = targetHref;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initialize animations for about page
|
|
function animateAboutContent() {
|
|
// Animate hero section elements
|
|
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
|
heroElements.forEach((el, index) => {
|
|
setTimeout(
|
|
() => {
|
|
el.classList.add('animate-reveal');
|
|
},
|
|
100 + index * 150
|
|
);
|
|
});
|
|
|
|
// Animate profile image
|
|
const profileImage = document.querySelector('.aspect-square');
|
|
if (profileImage) {
|
|
setTimeout(() => {
|
|
profileImage.classList.add('animate-reveal');
|
|
}, 200);
|
|
}
|
|
|
|
// Animate skill bars with staggered delay
|
|
const skillBars = document.querySelectorAll('.skill-bar');
|
|
skillBars.forEach((bar, index) => {
|
|
setTimeout(
|
|
() => {
|
|
bar.classList.add('animate-skill');
|
|
},
|
|
500 + index * 100
|
|
);
|
|
});
|
|
|
|
// Animate sections with staggered delay
|
|
const sections = document.querySelectorAll('section');
|
|
sections.forEach((section, index) => {
|
|
setTimeout(
|
|
() => {
|
|
section.classList.add('animate-reveal');
|
|
},
|
|
300 + index * 200
|
|
);
|
|
});
|
|
}
|
|
|
|
// Run animations
|
|
animateAboutContent();
|
|
}
|
|
|
|
// Initialize on first load
|
|
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
|
|
// Re-initialize when content changes via Astro's view transitions
|
|
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
|
|
// For compatibility with custom transition system
|
|
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
</script>
|