upgrade to different layout
Some checks failed
renovate / renovate (push) Successful in 5m22s
release-image-gitea / release (push) Failing after 7m9s
release-image-harbor / release (push) Failing after 7m9s

This commit is contained in:
2025-06-08 16:02:39 -05:00
parent e1632629a9
commit 3e89e6cb1c
75 changed files with 8314 additions and 2884 deletions

View File

@@ -1,119 +1,500 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
import { SiTypescript, SiAstro } from 'react-icons/si';
import ContactCTA from '../components/ContactCTA.astro';
import Hero from '../components/Hero.astro';
import directus, { directus_url } from "../../lib/directus"
import { readSingleton } from "@directus/sdk";
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="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 md:py-16 theme-transition-all">
<!-- Hero Section -->
<div class="relative mb-12 sm:mb-16 md:mb-20">
<!-- Decorative elements -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob theme-transition-bg"></div>
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div>
<BaseLayout title=`About | ${global.name}` description=`About ${global.name}`>
<div class="stack gap-20">
<main class="wrapper about">
<Hero
title="About"
tagline="Thanks for stopping by. Read below to learn more about myself and my background."
>
<img
width="1553"
height="873"
src=`${directus_url}/assets/${global.about}`
alt=`${global.name} hiking in Texas`
/>
</Hero>
<div class="relative grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center">
<div class="order-2 md:order-1 text-center md:text-left">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color">
Hello, I'm <span class="text-transparent bg-clip-text bg-gradient-to-r from-zinc-500 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 theme-transition-all">{global.name}</span>
</h1>
<section>
<h2 class="section-title">Background</h2>
<div
class="content"
set:html={about.background}
/>
</section>
<section>
<h2 class="section-title">Experience</h2>
<div
class="content"
set:html={about.experience}
/>
</section>
<section>
<h2 class="section-title">Education</h2>
<div
class="content"
set:html={about.education}
/>
</section>
<section>
<h2 class="section-title">Certifications</h2>
<div
class="content"
set:html={about.certifications}
/>
</section>
</main>
<p class="text-lg sm:text-xl text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 leading-relaxed theme-transition-color">
{about.background}
</p>
<ContactCTA />
</div>
<div class="flex flex-wrap gap-4 social-links-container justify-center md:justify-start theme-transition-children">
<!-- Social links remain the same -->
</div>
</div>
<div class="order-1 md:order-2 relative">
<div class="aspect-square w-full max-w-[280px] sm:max-w-[320px] md:max-w-md mx-auto overflow-hidden rounded-3xl border-4 sm:border-8 border-white dark:border-zinc-800 shadow-xl sm:shadow-2xl theme-transition-all">
<img
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
alt={global.portrait_alt}
class="w-full h-full object-cover"
loading="eager"
/>
</div>
<!-- Decorative elements -->
<div class="absolute -bottom-4 sm:-bottom-6 -right-4 sm:-right-6 w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 bg-zinc-100 dark:bg-zinc-800 rounded-full border-2 sm:border-4 border-white dark:border-zinc-900 shadow-lg flex items-center justify-center theme-transition-all">
<span class="text-2xl sm:text-3xl">👋</span>
</div>
</div>
</div>
</div>
<!-- About Section -->
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
<div class="max-w-3xl mx-auto">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 flex items-center justify-center md:justify-start theme-transition-color">
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 mr-4 theme-transition-bg"></span>
About Me
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 ml-4 theme-transition-bg"></span>
</h2>
<div class="prose prose-zinc dark:prose-invert max-w-none theme-transition-all">
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
{about.experience}
</p>
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
{about.education}
</p>
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
{about.certifications}
</p>
</div>
</div>
</div>
<!-- Skills Section -->
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-8 sm:mb-12 text-center theme-transition-color">Tech Stack</h2>
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container -->
<div class="slider-track flex animate-slide">
{ skills.map((skill, index) => (
<div key={`${skill.title}-${index}`} class="skill-card min-w-[220px] sm:min-w-[280px] mx-2 sm:mx-4 bg-white dark:bg-zinc-800/50 rounded-xl border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition-all duration-300 hover:shadow-xl transform hover:-translate-y-2 hover:scale-105 theme-transition-element">
<div class="p-4 sm:p-6">
<div class="flex items-center justify-between mb-4 sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4">
<div class="w-8 h-8 sm:w-12 sm:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 rounded-lg text-zinc-800 dark:text-zinc-200 transform transition-transform group-hover:rotate-12 theme-transition-bg theme-transition-color">
<skill.icon size={20} className="sm:text-2xl transform transition-all hover:scale-125" />
</div>
<h3 class="text-base sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 theme-transition-color">{skill.title}</h3>
</div>
<span class="text-xs sm:text-sm font-mono bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full theme-transition-all">{skill.level}%</span>
</div>
<div class="relative h-1.5 sm:h-2 w-full bg-zinc-100 dark:bg-zinc-700 overflow-hidden rounded-full theme-transition-bg">
<div
class="absolute top-0 left-0 h-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200 rounded-full transition-all duration-1000 progress-bar-animate theme-transition-bg"
style={`width: ${skill.level}%`}
></div>
</div>
<div class="flex justify-between mt-1 sm:mt-2 text-[10px] sm:text-xs text-zinc-400 dark:text-zinc-500 font-mono theme-transition-color">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div>
</div>
))}
</div>
<!-- Gradient overlays for smooth fade effect -->
<div class="absolute top-0 bottom-0 left-0 w-12 sm:w-24 bg-gradient-to-r from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div>
<div class="absolute top-0 bottom-0 right-0 w-12 sm:w-24 bg-gradient-to-l from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div>
</div>
</div>
<!-- Contact Section -->
<div class="max-w-3xl mx-auto text-center theme-transition-all">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color">Get in Touch</h2>
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 theme-transition-color">
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="inline-flex items-center justify-center px-6 sm:px-8 py-3 sm:py-4 rounded-lg bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 transition-colors text-base sm:text-lg font-medium theme-transition-all"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 sm:h-5 sm:w-5 mr-2" 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" />
</svg>
Say Hello
</a>
</div>
</div>
</BaseLayout>
<style>
.about {
display: flex;
flex-direction: column;
gap: 3.5rem;
}
/* Blob animation */
.animate-blob {
animation: blob-bounce 8s infinite ease;
}
img {
margin-top: 1.5rem;
border-radius: 1.5rem;
box-shadow: var(--shadow-md);
}
.animation-delay-2000 {
animation-delay: 2s;
}
section {
display: flex;
flex-direction: column;
gap: 0.5rem;
color: var(--gray-200);
}
@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);
}
}
.section-title {
grid-column-start: 1;
font-size: var(--text-xl);
color: var(--gray-0);
}
/* Tech Stack Slider */
.slider-track {
width: fit-content;
animation: scroll 40s linear infinite;
}
.content {
grid-column: 2 / 4;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
}
}
.content :global(a) {
text-decoration: 1px solid underline transparent;
text-underline-offset: 0.25em;
transition: text-decoration-color var(--theme-transition);
}
@media (min-width: 640px) {
.slider-track {
animation: scroll 60s linear infinite;
}
.content :global(a:hover),
.content :global(a:focus) {
text-decoration-color: currentColor;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
}
}
}
@media (min-width: 50em) {
.about {
display: grid;
grid-template-columns: 1fr 60% 1fr;
}
.tech-stack-slider:hover .slider-track {
animation-play-state: paused;
}
.about > :global(:first-child) {
grid-column-start: 2;
}
.skill-card {
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
section {
display: contents;
font-size: var(--text-lg);
}
}
.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>