upgrade to different layout
This commit is contained in:
		@@ -1,8 +1,417 @@
 | 
			
		||||
---
 | 
			
		||||
import Hero from '../components/Hero.astro';
 | 
			
		||||
import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
import Layout from '../layouts/Layout.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Not Found" description="404 Error — this page was not found">
 | 
			
		||||
	<Hero title="Page Not Found" tagline="Not found" />
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
<Layout title="404 - Page Not Found">
 | 
			
		||||
  <div class="relative flex flex-col items-center justify-center min-h-[80vh] py-20 text-center px-4 overflow-hidden">
 | 
			
		||||
    <!-- Animated background elements -->
 | 
			
		||||
    <div class="absolute inset-0 overflow-hidden">
 | 
			
		||||
      <div class="absolute -top-20 -left-20 w-64 h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob"></div>
 | 
			
		||||
      <div class="absolute top-1/2 right-1/4 w-96 h-96 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
 | 
			
		||||
      <div class="absolute bottom-20 left-1/3 w-72 h-72 bg-zinc-100 dark:bg-zinc-800/40 rounded-full blur-3xl opacity-40 animate-blob animation-delay-4000"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Main content with animation -->
 | 
			
		||||
    <div class="relative z-10 max-w-xl mx-auto">
 | 
			
		||||
      <div class="glitch-wrapper">
 | 
			
		||||
        <h1 class="glitch text-9xl sm:text-[12rem] font-bold text-zinc-900 dark:text-zinc-100 leading-none" data-text="404">404</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <h2 class="mt-6 text-2xl sm:text-3xl font-bold text-zinc-800 dark:text-zinc-200">Page Not Found</h2>
 | 
			
		||||
      
 | 
			
		||||
      <p class="mt-6 text-zinc-600 dark:text-zinc-400 max-w-md mx-auto text-lg">
 | 
			
		||||
        The page you're looking for does not exist.
 | 
			
		||||
      </p>
 | 
			
		||||
      
 | 
			
		||||
      <div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
 | 
			
		||||
        <a
 | 
			
		||||
          href="/"
 | 
			
		||||
          class="group relative inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-xl"
 | 
			
		||||
        >
 | 
			
		||||
          <span class="absolute inset-0 bg-gradient-to-r from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-0"></span>
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 relative z-10">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
 | 
			
		||||
          </svg>
 | 
			
		||||
          <span class="font-medium relative z-10">Return Home</span>
 | 
			
		||||
        </a>
 | 
			
		||||
        
 | 
			
		||||
        <button 
 | 
			
		||||
          id="back-button"
 | 
			
		||||
          class="group inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 shadow-sm hover:shadow-md"
 | 
			
		||||
        >
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
 | 
			
		||||
          </svg>
 | 
			
		||||
          <span class="font-medium">Go Back</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- Random fun fact -->
 | 
			
		||||
      <div class="mt-16 p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl shadow-sm max-w-md mx-auto backdrop-blur-sm border border-zinc-100 dark:border-zinc-700/50">
 | 
			
		||||
        <h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Did you know?</h3>
 | 
			
		||||
        <p class="mt-2 text-zinc-700 dark:text-zinc-300 text-sm" id="fun-fact">
 | 
			
		||||
          The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</Layout>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Go back functionality
 | 
			
		||||
  document.getElementById('back-button')?.addEventListener('click', () => {
 | 
			
		||||
    window.history.back();
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // Array of fun 404 facts
 | 
			
		||||
  const funFacts = [
 | 
			
		||||
    "The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
 | 
			
		||||
    "In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
 | 
			
		||||
    "Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
 | 
			
		||||
    "The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.",
 | 
			
		||||
    "Many companies use creative 404 pages as a way to showcase their brand personality and humor.",
 | 
			
		||||
    "The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
 | 
			
		||||
    "Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.",
 | 
			
		||||
    "The most common cause of 404 errors is mistyped URLs."
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  // Display a random fun fact
 | 
			
		||||
  const funFactElement = document.getElementById('fun-fact');
 | 
			
		||||
  if (funFactElement) {
 | 
			
		||||
    const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
 | 
			
		||||
    funFactElement.textContent = randomFact;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Handle SPA transitions for 404 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;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Re-initialize back button after SPA navigation
 | 
			
		||||
    const backButton = document.getElementById('back-button');
 | 
			
		||||
    if (backButton) {
 | 
			
		||||
      backButton.addEventListener('click', () => {
 | 
			
		||||
        window.history.back();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 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>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Animation for floating blobs */
 | 
			
		||||
  @keyframes blob {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: translate(0px, 0px) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
      transform: translate(30px, -50px) scale(1.1);
 | 
			
		||||
    }
 | 
			
		||||
    66% {
 | 
			
		||||
      transform: translate(-20px, 20px) scale(0.9);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translate(0px, 0px) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob 7s infinite;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animation-delay-2000 {
 | 
			
		||||
    animation-delay: 2s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animation-delay-4000 {
 | 
			
		||||
    animation-delay: 4s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Glitch effect for 404 text */
 | 
			
		||||
  .glitch-wrapper {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .glitch {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    animation: glitch-skew 1s infinite linear alternate-reverse;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .glitch::before,
 | 
			
		||||
  .glitch::after {
 | 
			
		||||
    content: attr(data-text);
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .glitch::before {
 | 
			
		||||
    left: 2px;
 | 
			
		||||
    text-shadow: -2px 0 #ff00c1;
 | 
			
		||||
    clip: rect(44px, 450px, 56px, 0);
 | 
			
		||||
    animation: glitch-anim 5s infinite linear alternate-reverse;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .glitch::after {
 | 
			
		||||
    left: -2px;
 | 
			
		||||
    text-shadow: -2px 0 #00fff9, 2px 2px #ff00c1;
 | 
			
		||||
    animation: glitch-anim2 1s infinite linear alternate-reverse;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes glitch-anim {
 | 
			
		||||
    0% {
 | 
			
		||||
      clip: rect(31px, 9999px, 94px, 0);
 | 
			
		||||
      transform: skew(0.85deg);
 | 
			
		||||
    }
 | 
			
		||||
    5% {
 | 
			
		||||
      clip: rect(70px, 9999px, 71px, 0);
 | 
			
		||||
      transform: skew(0.17deg);
 | 
			
		||||
    }
 | 
			
		||||
    10% {
 | 
			
		||||
      clip: rect(9px, 9999px, 85px, 0);
 | 
			
		||||
      transform: skew(0.4deg);
 | 
			
		||||
    }
 | 
			
		||||
    15% {
 | 
			
		||||
      clip: rect(47px, 9999px, 18px, 0);
 | 
			
		||||
      transform: skew(0.22deg);
 | 
			
		||||
    }
 | 
			
		||||
    20% {
 | 
			
		||||
      clip: rect(7px, 9999px, 78px, 0);
 | 
			
		||||
      transform: skew(0.96deg);
 | 
			
		||||
    }
 | 
			
		||||
    25% {
 | 
			
		||||
      clip: rect(53px, 9999px, 54px, 0);
 | 
			
		||||
      transform: skew(0.05deg);
 | 
			
		||||
    }
 | 
			
		||||
    30% {
 | 
			
		||||
      clip: rect(84px, 9999px, 52px, 0);
 | 
			
		||||
      transform: skew(0.94deg);
 | 
			
		||||
    }
 | 
			
		||||
    35% {
 | 
			
		||||
      clip: rect(46px, 9999px, 7px, 0);
 | 
			
		||||
      transform: skew(0.01deg);
 | 
			
		||||
    }
 | 
			
		||||
    40% {
 | 
			
		||||
      clip: rect(2px, 9999px, 66px, 0);
 | 
			
		||||
      transform: skew(0.66deg);
 | 
			
		||||
    }
 | 
			
		||||
    45% {
 | 
			
		||||
      clip: rect(34px, 9999px, 33px, 0);
 | 
			
		||||
      transform: skew(0.52deg);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      clip: rect(80px, 9999px, 73px, 0);
 | 
			
		||||
      transform: skew(0.9deg);
 | 
			
		||||
    }
 | 
			
		||||
    55% {
 | 
			
		||||
      clip: rect(8px, 9999px, 81px, 0);
 | 
			
		||||
      transform: skew(0.3deg);
 | 
			
		||||
    }
 | 
			
		||||
    60% {
 | 
			
		||||
      clip: rect(10px, 9999px, 86px, 0);
 | 
			
		||||
      transform: skew(0.85deg);
 | 
			
		||||
    }
 | 
			
		||||
    65% {
 | 
			
		||||
      clip: rect(36px, 9999px, 25px, 0);
 | 
			
		||||
      transform: skew(0.28deg);
 | 
			
		||||
    }
 | 
			
		||||
    70% {
 | 
			
		||||
      clip: rect(75px, 9999px, 31px, 0);
 | 
			
		||||
      transform: skew(0.46deg);
 | 
			
		||||
    }
 | 
			
		||||
    75% {
 | 
			
		||||
      clip: rect(46px, 9999px, 87px, 0);
 | 
			
		||||
      transform: skew(0.44deg);
 | 
			
		||||
    }
 | 
			
		||||
    80% {
 | 
			
		||||
      clip: rect(19px, 9999px, 40px, 0);
 | 
			
		||||
      transform: skew(0.07deg);
 | 
			
		||||
    }
 | 
			
		||||
    85% {
 | 
			
		||||
      clip: rect(85px, 9999px, 88px, 0);
 | 
			
		||||
      transform: skew(0.71deg);
 | 
			
		||||
    }
 | 
			
		||||
    90% {
 | 
			
		||||
      clip: rect(1px, 9999px, 89px, 0);
 | 
			
		||||
      transform: skew(0.76deg);
 | 
			
		||||
    }
 | 
			
		||||
    95% {
 | 
			
		||||
      clip: rect(44px, 9999px, 25px, 0);
 | 
			
		||||
      transform: skew(0.58deg);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      clip: rect(31px, 9999px, 26px, 0);
 | 
			
		||||
      transform: skew(0.6deg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes glitch-anim2 {
 | 
			
		||||
    0% {
 | 
			
		||||
      clip: rect(65px, 9999px, 65px, 0);
 | 
			
		||||
      transform: skew(0.16deg);
 | 
			
		||||
    }
 | 
			
		||||
    5% {
 | 
			
		||||
      clip: rect(8px, 9999px, 42px, 0);
 | 
			
		||||
      transform: skew(0.65deg);
 | 
			
		||||
    }
 | 
			
		||||
    10% {
 | 
			
		||||
      clip: rect(64px, 9999px, 30px, 0);
 | 
			
		||||
      transform: skew(0.42deg);
 | 
			
		||||
    }
 | 
			
		||||
    15% {
 | 
			
		||||
      clip: rect(29px, 9999px, 49px, 0);
 | 
			
		||||
      transform: skew(0.05deg);
 | 
			
		||||
    }
 | 
			
		||||
    20% {
 | 
			
		||||
      clip: rect(25px, 9999px, 56px, 0);
 | 
			
		||||
      transform: skew(0.09deg);
 | 
			
		||||
    }
 | 
			
		||||
    25% {
 | 
			
		||||
      clip: rect(76px, 9999px, 98px, 0);
 | 
			
		||||
      transform: skew(0.79deg);
 | 
			
		||||
    }
 | 
			
		||||
    30% {
 | 
			
		||||
      clip: rect(72px, 9999px, 3px, 0);
 | 
			
		||||
      transform: skew(0.12deg);
 | 
			
		||||
    }
 | 
			
		||||
    35% {
 | 
			
		||||
      clip: rect(20px, 9999px, 60px, 0);
 | 
			
		||||
      transform: skew(0.09deg);
 | 
			
		||||
    }
 | 
			
		||||
    40% {
 | 
			
		||||
      clip: rect(61px, 9999px, 47px, 0);
 | 
			
		||||
      transform: skew(0.45deg);
 | 
			
		||||
    }
 | 
			
		||||
    45% {
 | 
			
		||||
      clip: rect(29px, 9999px, 69px, 0);
 | 
			
		||||
      transform: skew(0.09deg);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      clip: rect(82px, 9999px, 96px, 0);
 | 
			
		||||
      transform: skew(0.05deg);
 | 
			
		||||
    }
 | 
			
		||||
    55% {
 | 
			
		||||
      clip: rect(33px, 9999px, 91px, 0);
 | 
			
		||||
      transform: skew(0.16deg);
 | 
			
		||||
    }
 | 
			
		||||
    60% {
 | 
			
		||||
      clip: rect(56px, 9999px, 23px, 0);
 | 
			
		||||
      transform: skew(0.01deg);
 | 
			
		||||
    }
 | 
			
		||||
    65% {
 | 
			
		||||
      clip: rect(46px, 9999px, 21px, 0);
 | 
			
		||||
      transform: skew(0.89deg);
 | 
			
		||||
    }
 | 
			
		||||
    70% {
 | 
			
		||||
      clip: rect(50px, 9999px, 1px, 0);
 | 
			
		||||
      transform: skew(0.85deg);
 | 
			
		||||
    }
 | 
			
		||||
    75% {
 | 
			
		||||
      clip: rect(82px, 9999px, 33px, 0);
 | 
			
		||||
      transform: skew(0.87deg);
 | 
			
		||||
    }
 | 
			
		||||
    80% {
 | 
			
		||||
      clip: rect(94px, 9999px, 46px, 0);
 | 
			
		||||
      transform: skew(0.64deg);
 | 
			
		||||
    }
 | 
			
		||||
    85% {
 | 
			
		||||
      clip: rect(48px, 9999px, 95px, 0);
 | 
			
		||||
      transform: skew(0.43deg);
 | 
			
		||||
    }
 | 
			
		||||
    90% {
 | 
			
		||||
      clip: rect(60px, 9999px, 10px, 0);
 | 
			
		||||
      transform: skew(0.29deg);
 | 
			
		||||
    }
 | 
			
		||||
    95% {
 | 
			
		||||
      clip: rect(85px, 9999px, 62px, 0);
 | 
			
		||||
      transform: skew(0.66deg);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      clip: rect(61px, 9999px, 58px, 0);
 | 
			
		||||
      transform: skew(0.74deg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes glitch-skew {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: skew(-1deg);
 | 
			
		||||
    }
 | 
			
		||||
    10% {
 | 
			
		||||
      transform: skew(0deg);
 | 
			
		||||
    }
 | 
			
		||||
    20% {
 | 
			
		||||
      transform: skew(0.5deg);
 | 
			
		||||
    }
 | 
			
		||||
    30% {
 | 
			
		||||
      transform: skew(-0.5deg);
 | 
			
		||||
    }
 | 
			
		||||
    40% {
 | 
			
		||||
      transform: skew(0.2deg);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      transform: skew(0deg);
 | 
			
		||||
    }
 | 
			
		||||
    60% {
 | 
			
		||||
      transform: skew(-0.5deg);
 | 
			
		||||
    }
 | 
			
		||||
    70% {
 | 
			
		||||
      transform: skew(0.8deg);
 | 
			
		||||
    }
 | 
			
		||||
    80% {
 | 
			
		||||
      transform: skew(-0.2deg);
 | 
			
		||||
    }
 | 
			
		||||
    90% {
 | 
			
		||||
      transform: skew(0.5deg);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: skew(0deg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										294
									
								
								src/pages/blog/[...slug].astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								src/pages/blog/[...slug].astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
---
 | 
			
		||||
import BlogPost from '../../layouts/BlogPost.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
export async function getStaticPaths() {
 | 
			
		||||
  const posts = await directus.request(readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
  }));
 | 
			
		||||
  
 | 
			
		||||
  const sortedEntries = [...posts].sort(
 | 
			
		||||
    (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  return sortedEntries.map((post, index) => {
 | 
			
		||||
    return {
 | 
			
		||||
      params: { slug: post.slug },
 | 
			
		||||
      props: { 
 | 
			
		||||
        post,
 | 
			
		||||
        nextPost: index > 0 ? sortedEntries[index - 1] : null,
 | 
			
		||||
        prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}>
 | 
			
		||||
    <!-- Main Content - Enhanced with better typography and spacing -->
 | 
			
		||||
    <div class="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:text-zinc-800 dark:prose-a:text-zinc-300 prose-a:font-medium prose-a:underline-offset-4 hover:prose-a:text-zinc-600 dark:hover:prose-a:text-zinc-100 prose-img:rounded-xl sm:prose-base prose-sm">
 | 
			
		||||
      <div set:html={post.content} />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Next/Previous Navigation - Improved responsive design -->
 | 
			
		||||
    <div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12">
 | 
			
		||||
      {prevPost && (
 | 
			
		||||
        <a 
 | 
			
		||||
          href={`/blog/${prevPost.slug}`} 
 | 
			
		||||
          class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
 | 
			
		||||
          <span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1">
 | 
			
		||||
              <path d="m15 18-6-6 6-6"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
            Previous Article
 | 
			
		||||
          </span>
 | 
			
		||||
          <h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
 | 
			
		||||
            {prevPost.title}
 | 
			
		||||
          </h3>
 | 
			
		||||
        </a>
 | 
			
		||||
      )}
 | 
			
		||||
      {nextPost && (
 | 
			
		||||
        <a 
 | 
			
		||||
          href={`/blog/${nextPost.slug}`} 
 | 
			
		||||
          class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 md:text-right overflow-hidden"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
 | 
			
		||||
          <span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end">
 | 
			
		||||
            Next Article
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
              <path d="m9 18 6-6-6-6"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </span>
 | 
			
		||||
          <h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
 | 
			
		||||
            {nextPost.title}
 | 
			
		||||
          </h3>
 | 
			
		||||
        </a>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
</BlogPost>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Removing TOC-related functions
 | 
			
		||||
  
 | 
			
		||||
  // Add copy buttons to code blocks
 | 
			
		||||
  function initializeCodeCopyButtons() {
 | 
			
		||||
    const codeBlocks = document.querySelectorAll('pre');
 | 
			
		||||
    
 | 
			
		||||
    codeBlocks.forEach(block => {
 | 
			
		||||
      // Skip if already processed by either method
 | 
			
		||||
      if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      block.classList.add('code-block-processed');
 | 
			
		||||
      
 | 
			
		||||
      // Create wrapper if not already wrapped
 | 
			
		||||
      let wrapper;
 | 
			
		||||
      if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) {
 | 
			
		||||
        wrapper = block.parentNode;
 | 
			
		||||
      } else {
 | 
			
		||||
        wrapper = document.createElement('div');
 | 
			
		||||
        wrapper.className = 'relative group';
 | 
			
		||||
        block.parentNode.insertBefore(wrapper, block);
 | 
			
		||||
        wrapper.appendChild(block);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Add copy button if not already present
 | 
			
		||||
      if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
 | 
			
		||||
        const copyButton = document.createElement('button');
 | 
			
		||||
        copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
 | 
			
		||||
        copyButton.innerHTML = `
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        `;
 | 
			
		||||
        
 | 
			
		||||
        copyButton.addEventListener('click', () => {
 | 
			
		||||
          const code = block.querySelector('code').innerText;
 | 
			
		||||
          navigator.clipboard.writeText(code);
 | 
			
		||||
          
 | 
			
		||||
          // Show copied feedback
 | 
			
		||||
          copyButton.innerHTML = `
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          `;
 | 
			
		||||
          
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            copyButton.innerHTML = `
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
                <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
 | 
			
		||||
              </svg>
 | 
			
		||||
            `;
 | 
			
		||||
          }, 2000);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        wrapper.appendChild(copyButton);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Handle SPA transitions for blog post navigation
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle prev/next navigation links
 | 
			
		||||
    document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
 | 
			
		||||
      // Skip links that are anchor links or already processed
 | 
			
		||||
      if (link.getAttribute('href').includes('#') || 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;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Main initialization function
 | 
			
		||||
  function initializeBlogPost() {
 | 
			
		||||
    // Initialize remaining components
 | 
			
		||||
    initializeCodeCopyButtons();
 | 
			
		||||
    setupSPATransitions();
 | 
			
		||||
    
 | 
			
		||||
    // Scroll to hash if present in URL
 | 
			
		||||
    if (window.location.hash) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const element = document.querySelector(window.location.hash);
 | 
			
		||||
        if (element) {
 | 
			
		||||
          element.scrollIntoView({ behavior: 'smooth', block: 'start' });
 | 
			
		||||
        }
 | 
			
		||||
      }, 100);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', initializeBlogPost);
 | 
			
		||||
  
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', initializeBlogPost);
 | 
			
		||||
  
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', initializeBlogPost);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Removing TOC-related styles */
 | 
			
		||||
  
 | 
			
		||||
  /* Language badge styling */
 | 
			
		||||
  .language-badge {
 | 
			
		||||
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
 | 
			
		||||
    text-transform: lowercase;
 | 
			
		||||
    letter-spacing: 0.05em;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Extra small screens */
 | 
			
		||||
  @media (min-width: 480px) {
 | 
			
		||||
    .xs\:inline {
 | 
			
		||||
      display: inline;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:hidden {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Enhanced typography for blog content - Responsive adjustments */
 | 
			
		||||
  .prose {
 | 
			
		||||
    @apply text-zinc-800 dark:text-zinc-200;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h1, .prose h2, .prose h3, .prose h4 {
 | 
			
		||||
    @apply text-zinc-900 dark:text-zinc-100 font-semibold;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h1 {
 | 
			
		||||
    @apply text-2xl sm:text-3xl md:text-4xl;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h2 {
 | 
			
		||||
    @apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h3 {
 | 
			
		||||
    @apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose p {
 | 
			
		||||
    @apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose a {
 | 
			
		||||
    @apply text-zinc-800 dark:text-zinc-300 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-100 hover:decoration-zinc-600 dark:hover:decoration-zinc-400 transition-colors;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose blockquote {
 | 
			
		||||
    @apply border-l-4 border-zinc-300 dark:border-zinc-700 pl-4 italic text-zinc-700 dark:text-zinc-300 my-4 sm:my-6;
 | 
			
		||||
  }
 | 
			
		||||
    
 | 
			
		||||
  .prose code {
 | 
			
		||||
    @apply bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-800 dark:text-zinc-200 text-sm font-medium;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose pre {
 | 
			
		||||
    @apply bg-[#1e293b] dark:bg-[#1e293b] text-zinc-200 p-3 sm:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm my-4 sm:my-6 shadow-md !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose pre code {
 | 
			
		||||
    @apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
  .prose img {
 | 
			
		||||
    @apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose ul, .prose ol {
 | 
			
		||||
    @apply my-4 sm:my-6 pl-5 sm:pl-6;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose li {
 | 
			
		||||
    @apply mb-1 sm:mb-2 text-sm sm:text-base;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose hr {
 | 
			
		||||
    @apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Line clamp for truncating text */
 | 
			
		||||
  .line-clamp-2 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 2;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										407
									
								
								src/pages/blog/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										407
									
								
								src/pages/blog/index.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,407 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
const posts = await directus.request(
 | 
			
		||||
  readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
    sort: ["-published_date"],
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const sortedPosts = posts.sort(
 | 
			
		||||
  (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// Group posts by year for timeline effect
 | 
			
		||||
const postsByYear = sortedPosts.reduce((acc, post) => {
 | 
			
		||||
  const year = new Date(post.published_date).getFullYear();
 | 
			
		||||
  if (!acc[year]) acc[year] = [];
 | 
			
		||||
  acc[year].push(post);
 | 
			
		||||
  return acc;
 | 
			
		||||
}, {});
 | 
			
		||||
 | 
			
		||||
const years = Object.keys(postsByYear).sort((a, b) => b - a);
 | 
			
		||||
 | 
			
		||||
// Get total post count
 | 
			
		||||
const totalPosts = sortedPosts.length;
 | 
			
		||||
 | 
			
		||||
// Get unique tags for search suggestions
 | 
			
		||||
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Blog">
 | 
			
		||||
  <div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16">
 | 
			
		||||
    <!-- Header with search  -->
 | 
			
		||||
    <div class="relative mb-12 sm:mb-20">
 | 
			
		||||
      <!-- Decorative elements -->
 | 
			
		||||
      <div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
 | 
			
		||||
      <div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="relative text-center">
 | 
			
		||||
        <h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4">
 | 
			
		||||
          Blog
 | 
			
		||||
        </h1>
 | 
			
		||||
        
 | 
			
		||||
        <p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto">
 | 
			
		||||
          Thoughts, ideas, and explorations on technology and selfhosting.
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Grid layout for mobile experience -->
 | 
			
		||||
    <div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8">
 | 
			
		||||
      <!-- Featured post (if exists) -->
 | 
			
		||||
      {sortedPosts.length > 0 && (
 | 
			
		||||
        <div class="md:col-span-12 mb-8 sm:mb-12">
 | 
			
		||||
          <article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8">            
 | 
			
		||||
            <div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8">
 | 
			
		||||
              {sortedPosts[0].image && (
 | 
			
		||||
                <div class="w-full md:w-1/2 h-60 sm:h-80 md:h-96 overflow-hidden mx-auto md:mx-0 max-w-full sm:max-w-md">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`}
 | 
			
		||||
                    alt={sortedPosts[0].title}
 | 
			
		||||
                    class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
 | 
			
		||||
                    loading="eager"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
              
 | 
			
		||||
              <div class="flex-1 flex flex-col justify-center">
 | 
			
		||||
                <div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-2 mb-3 justify-center md:justify-start">
 | 
			
		||||
                  <span class="font-medium uppercase tracking-wider">Featured</span>
 | 
			
		||||
                  <span class="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span>
 | 
			
		||||
                  {sortedPosts[0].published_date && (
 | 
			
		||||
                    <time datetime={sortedPosts[0].published_date.toLocaleString()}>
 | 
			
		||||
                      {sortedPosts[0].published_date.toLocaleString('en-US', { 
 | 
			
		||||
                        year: 'numeric', 
 | 
			
		||||
                        month: 'long', 
 | 
			
		||||
                        day: 'numeric' 
 | 
			
		||||
                      })}
 | 
			
		||||
                    </time>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
 | 
			
		||||
                  <a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0">
 | 
			
		||||
                    {sortedPosts[0].title}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </h2>
 | 
			
		||||
                
 | 
			
		||||
                <p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 sm:mb-6 line-clamp-3 text-center md:text-left">
 | 
			
		||||
                  {sortedPosts[0].description}
 | 
			
		||||
                </p>
 | 
			
		||||
                
 | 
			
		||||
                <!-- Improved mobile layout for featured post metadata -->
 | 
			
		||||
                <div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">                  
 | 
			
		||||
                  {sortedPosts[0].tags && (
 | 
			
		||||
                    <div class="flex flex-wrap gap-2 justify-center md:justify-start">
 | 
			
		||||
                      {sortedPosts[0].tags.slice(0, 2).map((tag) => (
 | 
			
		||||
                        <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                          {tag}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </article>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      
 | 
			
		||||
      <!-- Improved sidebar for mobile -->
 | 
			
		||||
      <div class="md:col-span-3 relative">
 | 
			
		||||
        <div class="md:sticky md:top-24 space-y-4 mb-8 md:mb-0">
 | 
			
		||||
          <h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-4 uppercase tracking-wider text-center md:text-left">Archive</h3>
 | 
			
		||||
          
 | 
			
		||||
          <!-- Horizontal scrollable archive on mobile, vertical on desktop -->
 | 
			
		||||
          <div class="flex md:flex-col overflow-x-auto md:overflow-visible pb-4 md:pb-0 hide-scrollbar">
 | 
			
		||||
            {years.map((year, index) => (
 | 
			
		||||
              <a 
 | 
			
		||||
                href={`#year-${year}`}
 | 
			
		||||
                class={`flex items-center py-2 md:py-3 px-4 md:px-0 mr-3 md:mr-0 border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors md:w-full whitespace-nowrap md:whitespace-normal rounded-full md:rounded-none ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
 | 
			
		||||
              >
 | 
			
		||||
                <span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span>
 | 
			
		||||
                <span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
                  {postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
 | 
			
		||||
                </span>
 | 
			
		||||
              </a>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- Improved post grid for mobile -->
 | 
			
		||||
      <div class="md:col-span-9">
 | 
			
		||||
        {years.map((year) => (
 | 
			
		||||
          <div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16">
 | 
			
		||||
            <h2 class="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 border-b border-zinc-200 dark:border-zinc-800 pb-3 sm:pb-4 text-center md:text-left">
 | 
			
		||||
              {year}
 | 
			
		||||
            </h2>
 | 
			
		||||
            
 | 
			
		||||
            <div class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}>
 | 
			
		||||
              {postsByYear[year].map((post, index) => (
 | 
			
		||||
                <article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md">
 | 
			
		||||
                  {post.image && (
 | 
			
		||||
                    <div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg">
 | 
			
		||||
                      <img
 | 
			
		||||
                        src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
 | 
			
		||||
                        alt={post.title}
 | 
			
		||||
                        class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
 | 
			
		||||
                        loading="lazy"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                  
 | 
			
		||||
                  <div class="flex-1 flex flex-col">
 | 
			
		||||
                    <div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center md:justify-start flex-wrap">
 | 
			
		||||
                      {post.pubDate && (
 | 
			
		||||
                        <time datetime={post.published_date.toLocaleString()} class="flex items-center">
 | 
			
		||||
                          {post.published_date.toLocaleString('en-US', { 
 | 
			
		||||
                            month: 'short', 
 | 
			
		||||
                            day: 'numeric' 
 | 
			
		||||
                          })}
 | 
			
		||||
                        </time>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <h3 class="text-lg sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
 | 
			
		||||
                      <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
 | 
			
		||||
                        {post.title}
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    
 | 
			
		||||
                    <p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left">
 | 
			
		||||
                      {post.description}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    
 | 
			
		||||
                    {post.tags && (
 | 
			
		||||
                      <div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start">
 | 
			
		||||
                        {post.tags.slice(0, 2).map((tag) => (
 | 
			
		||||
                          <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                            {tag}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {post.tags.length > 2 && (
 | 
			
		||||
                          <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                            +{post.tags.length - 2}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </article>
 | 
			
		||||
              ))}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Search container hover effect */
 | 
			
		||||
  .search-container:hover .search-pulse {
 | 
			
		||||
    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes pulse {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Input focus animation */
 | 
			
		||||
  input:focus + div .search-pulse {
 | 
			
		||||
    animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Hide scrollbar but keep functionality */
 | 
			
		||||
  .hide-scrollbar {
 | 
			
		||||
    -ms-overflow-style: none;
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .hide-scrollbar::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Line clamp for descriptions */
 | 
			
		||||
  .line-clamp-2 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 2;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .line-clamp-3 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 3;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Improved touch targets for mobile */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    a, button {
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Script không thay đổi - giữ nguyên chức năng
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const backToTopButton = document.getElementById('back-to-top');
 | 
			
		||||
    
 | 
			
		||||
    if (backToTopButton) {
 | 
			
		||||
      // Show button when scrolled down
 | 
			
		||||
      const toggleBackToTopButton = () => {
 | 
			
		||||
        if (window.scrollY > 300) {
 | 
			
		||||
          backToTopButton.classList.remove('opacity-0', 'invisible');
 | 
			
		||||
          backToTopButton.classList.add('opacity-100', 'visible');
 | 
			
		||||
        } else {
 | 
			
		||||
          backToTopButton.classList.remove('opacity-100', 'visible');
 | 
			
		||||
          backToTopButton.classList.add('opacity-0', 'invisible');
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      // Scroll to top when clicked
 | 
			
		||||
      backToTopButton.addEventListener('click', () => {
 | 
			
		||||
        window.scrollTo({
 | 
			
		||||
          top: 0,
 | 
			
		||||
          behavior: 'smooth'
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Check scroll position
 | 
			
		||||
      window.addEventListener('scroll', toggleBackToTopButton);
 | 
			
		||||
      toggleBackToTopButton(); // Initial check
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Add smooth scrolling to year links
 | 
			
		||||
    document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
 | 
			
		||||
      anchor.addEventListener('click', function (e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetId = this.getAttribute('href');
 | 
			
		||||
        const targetElement = document.querySelector(targetId);
 | 
			
		||||
        
 | 
			
		||||
        if (targetElement) {
 | 
			
		||||
          window.scrollTo({
 | 
			
		||||
            top: targetElement.offsetTop - 100,
 | 
			
		||||
            behavior: 'smooth'
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Update URL hash without jumping
 | 
			
		||||
          history.pushState(null, null, targetId);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Add touch support for hover effects
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
    
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
      const articles = document.querySelectorAll('article');
 | 
			
		||||
      
 | 
			
		||||
      articles.forEach(article => {
 | 
			
		||||
        article.addEventListener('touchstart', () => {
 | 
			
		||||
          article.classList.add('is-touched');
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        article.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            article.classList.remove('is-touched');
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // SPA transition handling
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all blog post links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
 | 
			
		||||
      // Skip links that are anchor links or already processed
 | 
			
		||||
      if (link.getAttribute('href').includes('#') || 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;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Handle year anchor links specially
 | 
			
		||||
    document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
 | 
			
		||||
      anchor.setAttribute('data-spa-internal', 'true');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 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>
 | 
			
		||||
@@ -1,17 +1,8 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
import Layout from '../layouts/Layout.astro';
 | 
			
		||||
import FormattedDate from '../components/FormattedDate.astro';
 | 
			
		||||
 | 
			
		||||
import CallToAction from '../components/CallToAction.astro';
 | 
			
		||||
import Grid from '../components/Grid.astro';
 | 
			
		||||
import Hero from '../components/Hero.astro';
 | 
			
		||||
import Icon from '../components/Icon.astro';
 | 
			
		||||
import Pill from '../components/Pill.astro';
 | 
			
		||||
import PortfolioPreview from '../components/PortfolioPreview.astro';
 | 
			
		||||
 | 
			
		||||
import ContactCTA from '../components/ContactCTA.astro';
 | 
			
		||||
import Skills from '../components/Skills.astro';
 | 
			
		||||
 | 
			
		||||
import directus, { directus_url } from "../../lib/directus"
 | 
			
		||||
import directus from "../../lib/directus"
 | 
			
		||||
import { readItems,readSingleton } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
const global = await directus.request(readSingleton("global"));
 | 
			
		||||
@@ -22,207 +13,477 @@ const posts = await directus.request(
 | 
			
		||||
    sort: ["-published_date"],
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const recentPosts = posts
 | 
			
		||||
  .sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
 | 
			
		||||
  .slice(0, 3);
 | 
			
		||||
 | 
			
		||||
const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5);
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout
 | 
			
		||||
	title=`Home | ${global.name}`
 | 
			
		||||
	description=""
 | 
			
		||||
>
 | 
			
		||||
	<div class="stack gap-20 lg:gap-48">
 | 
			
		||||
		<div class="wrapper stack gap-8 lg:gap-20">
 | 
			
		||||
			<header class="hero">
 | 
			
		||||
				<Hero
 | 
			
		||||
					title=`Hello, my name is ${global.name}`
 | 
			
		||||
					tagline={global.tagline}
 | 
			
		||||
					align="start"
 | 
			
		||||
				>
 | 
			
		||||
					<div class="roles">
 | 
			
		||||
						<Pill><Icon icon="hard-drives" size="1.33em" /> Engineer</Pill>
 | 
			
		||||
						<Pill><Icon icon="code" size="1.33em" /> Developer</Pill>
 | 
			
		||||
						<Pill><Icon icon="pencil-line" size="1.33em" /> Writer</Pill>
 | 
			
		||||
					</div>
 | 
			
		||||
				</Hero>
 | 
			
		||||
<Layout title=`Home | ${global.name}`>
 | 
			
		||||
  <!-- Hero Section with improved mobile responsiveness -->
 | 
			
		||||
  <section class="py-10 sm:py-16 md:py-20 px-4 sm:px-6 theme-transition-all">
 | 
			
		||||
    <div class="max-w-2xl mx-auto relative">
 | 
			
		||||
      <!-- Adjusted blob positions and sizes for better mobile appearance -->
 | 
			
		||||
      <div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div>
 | 
			
		||||
      <div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="relative text-center sm:text-left">
 | 
			
		||||
        <h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color hero-text">
 | 
			
		||||
          <span class="block">Writing on technology,</span>
 | 
			
		||||
          <span class="block mt-1">development, and</span>
 | 
			
		||||
          <span class="block mt-1 relative">
 | 
			
		||||
            <span class="relative inline-block">
 | 
			
		||||
              selfhosting.
 | 
			
		||||
              <span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-800 dark:bg-zinc-200 transform origin-left theme-transition-bg"></span>
 | 
			
		||||
            </span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </h1>
 | 
			
		||||
        <p class="mt-4 sm:mt-6 md:mt-8 text-base sm:text-lg text-zinc-600 dark:text-zinc-400 leading-relaxed theme-transition-color max-w-lg mx-auto sm:mx-0">
 | 
			
		||||
          {global.about}
 | 
			
		||||
        </p>
 | 
			
		||||
        <div class="mt-6 sm:mt-8 md:mt-10 flex flex-wrap gap-3 sm:gap-4 md:gap-6 justify-center sm:justify-start">
 | 
			
		||||
          <a 
 | 
			
		||||
            href="/about" 
 | 
			
		||||
            class="group relative inline-flex items-center gap-2 text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 transition-all duration-300 theme-transition-color min-h-[44px]"
 | 
			
		||||
          >
 | 
			
		||||
            <span>More about me</span>
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
				<img
 | 
			
		||||
					alt=`${global.name} in Antarctica`
 | 
			
		||||
					width="480"
 | 
			
		||||
					height="620"
 | 
			
		||||
					src=`${directus_url}/assets/${global.portrait}`
 | 
			
		||||
				/>
 | 
			
		||||
			</header>
 | 
			
		||||
  <!-- Featured Post Section - Improved for mobile -->
 | 
			
		||||
  <section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all">
 | 
			
		||||
    <div class="max-w-3xl mx-auto">
 | 
			
		||||
      <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8 md:mb-12">
 | 
			
		||||
        <h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color text-center sm:text-left">Recent Posts</h2>
 | 
			
		||||
        <a 
 | 
			
		||||
          href="/blog" 
 | 
			
		||||
          class="group relative text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 self-center sm:self-auto theme-transition-color min-h-[44px] flex items-center justify-center"
 | 
			
		||||
        >
 | 
			
		||||
          <span class="flex items-center gap-1">
 | 
			
		||||
            View all posts
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- Improved grid for better mobile layout -->
 | 
			
		||||
      <div class="grid gap-6 sm:gap-8 md:gap-12 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
 | 
			
		||||
        {recentPosts.map((post, index) => (
 | 
			
		||||
          <article class="group relative flex flex-col items-start hover-3d theme-transition-element max-w-sm mx-auto sm:mx-0 w-full">
 | 
			
		||||
            <div class="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 dark:bg-zinc-800/50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl theme-transition-bg"></div>
 | 
			
		||||
            
 | 
			
		||||
            {post.image && (
 | 
			
		||||
              <div class="relative z-10 w-full aspect-video mb-4 overflow-hidden rounded-lg">
 | 
			
		||||
                <img 
 | 
			
		||||
                  src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
 | 
			
		||||
                  alt={post.title}
 | 
			
		||||
                  class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
 | 
			
		||||
                  loading={index === 0 ? "eager" : "lazy"}
 | 
			
		||||
                  width="400"
 | 
			
		||||
                  height="225"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            
 | 
			
		||||
            <div class="relative z-10 flex items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color justify-center sm:justify-start w-full">
 | 
			
		||||
              <time datetime={post.published_date.toLocaleString()} class="font-medium">
 | 
			
		||||
                <FormattedDate date={post.published_date} />
 | 
			
		||||
              </time>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <h3 class="relative z-10 mt-3 text-lg sm:text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color text-center sm:text-left w-full">
 | 
			
		||||
              <a href={`/blog/${post.slug}`} class="min-h-[44px] flex items-center justify-center sm:justify-start">
 | 
			
		||||
                <span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4"></span>
 | 
			
		||||
                {post.title}
 | 
			
		||||
              </a>
 | 
			
		||||
            </h3>
 | 
			
		||||
            
 | 
			
		||||
            <p class="relative z-10 mt-2 sm:mt-3 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-3 theme-transition-color text-center sm:text-left w-full">
 | 
			
		||||
              {post.description}
 | 
			
		||||
            </p>
 | 
			
		||||
            
 | 
			
		||||
            {post.tags && post.tags.length > 0 && (
 | 
			
		||||
              <div class="relative z-10 mt-3 sm:mt-4 flex flex-wrap gap-2 justify-center sm:justify-start w-full">
 | 
			
		||||
                {post.tags.slice(0, 3).map(tag => (
 | 
			
		||||
                  <a 
 | 
			
		||||
                    href={`/topics/${tag}`}
 | 
			
		||||
                    class="inline-flex items-center rounded-full bg-zinc-100 px-2 sm:px-3 py-1 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors theme-transition-all min-h-[28px]"
 | 
			
		||||
                  >
 | 
			
		||||
                    #{tag}
 | 
			
		||||
                  </a>
 | 
			
		||||
                ))}
 | 
			
		||||
                {post.tags.length > 3 && (
 | 
			
		||||
                  <span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400 theme-transition-all min-h-[28px]">
 | 
			
		||||
                    +{post.tags.length - 3} more
 | 
			
		||||
                  </span>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            
 | 
			
		||||
            <a 
 | 
			
		||||
              href={`/blog/${post.slug}`}
 | 
			
		||||
              class="relative z-10 mt-3 sm:mt-4 flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors theme-transition-color mx-auto sm:mx-0 min-h-[44px]"
 | 
			
		||||
            >
 | 
			
		||||
              <span class="relative overflow-hidden inline-block">
 | 
			
		||||
                <span class="group-hover:-translate-y-full block transition-transform duration-300">Read article</span>
 | 
			
		||||
                <span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span>
 | 
			
		||||
              </span>
 | 
			
		||||
              <svg viewBox="0 0 16 16" fill="none" aria-hidden="true" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
                <path d="M6.75 5.75 9.25 8l-2.5 2.25" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
            </a>
 | 
			
		||||
          </article>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
			<Skills />
 | 
			
		||||
		</div>
 | 
			
		||||
  <!-- Topics/Tags Section - Improved for mobile -->
 | 
			
		||||
  {allTags.length > 0 && (
 | 
			
		||||
    <section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all">
 | 
			
		||||
      <div class="max-w-3xl mx-auto">
 | 
			
		||||
        <h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 theme-transition-color text-center sm:text-left">Explore Topics</h2>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Improved grid layout for mobile -->
 | 
			
		||||
        <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4 max-w-xs sm:max-w-none mx-auto">
 | 
			
		||||
          {allTags.map(tag => {
 | 
			
		||||
            const tagCount = posts.filter(post => post.tags && post.tags.includes(tag)).length;
 | 
			
		||||
            return (
 | 
			
		||||
              <a 
 | 
			
		||||
                href={`/topics/${tag}`}
 | 
			
		||||
                class="group flex flex-col p-3 sm:p-4 md:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/70 transition-all duration-300 theme-transition-all min-h-[80px] sm:min-h-[90px]"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="flex items-start justify-between mb-2">
 | 
			
		||||
                  <span class="text-sm font-medium text-zinc-900 dark:text-zinc-100 theme-transition-color mr-2">#{tag}</span>
 | 
			
		||||
                  <span class="text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 px-2 py-0.5 rounded-full flex-shrink-0 theme-transition-all">
 | 
			
		||||
                    {tagCount} {tagCount === 1 ? 'post' : 'posts'}
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1 theme-transition-color">
 | 
			
		||||
                  Explore articles about {tag}
 | 
			
		||||
                </p>
 | 
			
		||||
              </a>
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="mt-6 sm:mt-8 text-center">
 | 
			
		||||
          <a 
 | 
			
		||||
            href="/tags" 
 | 
			
		||||
            class="inline-flex items-center text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 theme-transition-color min-h-[44px]"
 | 
			
		||||
          >
 | 
			
		||||
            <span>View all topics</span>
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  )}
 | 
			
		||||
</Layout>
 | 
			
		||||
 | 
			
		||||
		<main class="wrapper stack gap-20 lg:gap-48">
 | 
			
		||||
			<section class="section with-background with-cta">
 | 
			
		||||
				<header class="section-header stack gap-2 lg:gap-4">
 | 
			
		||||
					<h3>Selected Projects</h3>
 | 
			
		||||
					<p>Take a look below at some of my projects from the past few years.</p>
 | 
			
		||||
				</header>
 | 
			
		||||
 | 
			
		||||
				<div class="gallery">
 | 
			
		||||
					<Grid variant="offset">
 | 
			
		||||
                        {
 | 
			
		||||
                            posts.map((post) => (
 | 
			
		||||
                              <li>
 | 
			
		||||
                                <PortfolioPreview posts={post} />
 | 
			
		||||
                              </li>
 | 
			
		||||
                            ))
 | 
			
		||||
                          }
 | 
			
		||||
					</Grid>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="cta">
 | 
			
		||||
					<CallToAction href="/projects/">
 | 
			
		||||
						View All
 | 
			
		||||
						<Icon icon="arrow-right" size="1.2em" />
 | 
			
		||||
					</CallToAction>
 | 
			
		||||
				</div>
 | 
			
		||||
			</section>
 | 
			
		||||
 | 
			
		||||
		</main>
 | 
			
		||||
		<ContactCTA />
 | 
			
		||||
	</div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
<script>
 | 
			
		||||
  // Add hover effect for cards on touch devices
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Check if it's a touch device
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
    
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
      const cards = document.querySelectorAll('.hover-3d');
 | 
			
		||||
      
 | 
			
		||||
      cards.forEach(card => {
 | 
			
		||||
        card.addEventListener('touchstart', () => {
 | 
			
		||||
          card.classList.add('is-touched');
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        card.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            card.classList.remove('is-touched');
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Disable hover animations on touch devices for better performance
 | 
			
		||||
      document.documentElement.classList.add('touch-device');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Improved viewport height fix for mobile browsers
 | 
			
		||||
    const setVh = () => {
 | 
			
		||||
      const vh = window.innerHeight * 0.01;
 | 
			
		||||
      document.documentElement.style.setProperty('--vh', `${vh}px`);
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Set initial value
 | 
			
		||||
    setVh();
 | 
			
		||||
    
 | 
			
		||||
    // Update on resize and scroll to prevent content shifting
 | 
			
		||||
    window.addEventListener('resize', setVh);
 | 
			
		||||
    
 | 
			
		||||
    // Use a debounced scroll handler to prevent performance issues
 | 
			
		||||
    let scrollTimeout;
 | 
			
		||||
    window.addEventListener('scroll', () => {
 | 
			
		||||
      if (scrollTimeout) {
 | 
			
		||||
        window.cancelAnimationFrame(scrollTimeout);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      scrollTimeout = window.requestAnimationFrame(() => {
 | 
			
		||||
        // Lock width during scroll
 | 
			
		||||
        document.body.style.width = '100%';
 | 
			
		||||
        document.body.style.overflowX = 'hidden';
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Fix for iOS Safari address bar height changes
 | 
			
		||||
    if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
 | 
			
		||||
      // Force the layout to use the initial viewport size
 | 
			
		||||
      document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
 | 
			
		||||
      
 | 
			
		||||
      // Apply fixed height to sections to prevent resizing
 | 
			
		||||
      const sections = document.querySelectorAll('section');
 | 
			
		||||
      sections.forEach(section => {
 | 
			
		||||
        section.style.width = '100%';
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Improved theme change handler that preserves scroll position and provides smoother transitions
 | 
			
		||||
    document.addEventListener('themeChanged', () => {
 | 
			
		||||
      // Store current scroll position
 | 
			
		||||
      const scrollPosition = window.scrollY;
 | 
			
		||||
      
 | 
			
		||||
      // Create a temporary overlay for smoother transition
 | 
			
		||||
      const overlay = document.createElement('div');
 | 
			
		||||
      overlay.style.cssText = `
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        inset: 0;
 | 
			
		||||
        background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'};
 | 
			
		||||
        z-index: 9999;
 | 
			
		||||
        pointer-events: none;
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        transition: opacity 0.3s ease;
 | 
			
		||||
      `;
 | 
			
		||||
      document.body.appendChild(overlay);
 | 
			
		||||
      
 | 
			
		||||
      // Fade in overlay
 | 
			
		||||
      requestAnimationFrame(() => {
 | 
			
		||||
        overlay.style.opacity = '0.5';
 | 
			
		||||
        
 | 
			
		||||
        // Update theme-transition elements without forcing reflow of entire page
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
          document.querySelectorAll('.theme-transition-all, .theme-transition-bg, .theme-transition-color')
 | 
			
		||||
            .forEach(el => {
 | 
			
		||||
              // Apply a subtle animation instead of a hard reset
 | 
			
		||||
              el.style.transition = 'all 0.5s ease';
 | 
			
		||||
            });
 | 
			
		||||
          
 | 
			
		||||
          // Fade out overlay after transition completes
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            overlay.style.opacity = '0';
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              overlay.remove();
 | 
			
		||||
            }, 300);
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Restore scroll position (prevents jumping to top)
 | 
			
		||||
      if (scrollPosition > 0) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          window.scrollTo({
 | 
			
		||||
            top: scrollPosition,
 | 
			
		||||
            behavior: 'auto' // Use 'auto' to prevent animation
 | 
			
		||||
          });
 | 
			
		||||
        }, 10);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Fix theme inconsistency issues by checking theme on visibility change
 | 
			
		||||
    document.addEventListener('visibilitychange', () => {
 | 
			
		||||
      if (document.visibilityState === 'visible') {
 | 
			
		||||
        const storedTheme = localStorage.getItem('theme');
 | 
			
		||||
        const currentThemeIsDark = document.documentElement.classList.contains('dark');
 | 
			
		||||
        
 | 
			
		||||
        if (storedTheme === 'dark' && !currentThemeIsDark) {
 | 
			
		||||
          document.documentElement.classList.add('dark');
 | 
			
		||||
        } else if (storedTheme === 'light' && currentThemeIsDark) {
 | 
			
		||||
          document.documentElement.classList.remove('dark');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Add smooth reveal animations for content after loading
 | 
			
		||||
    const animateContent = () => {
 | 
			
		||||
      // Animate hero section
 | 
			
		||||
      const heroElements = document.querySelectorAll('.hero-text span, .hero-text + p, .hero-text ~ div');
 | 
			
		||||
      heroElements.forEach((el, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          el.classList.add('animate-reveal');
 | 
			
		||||
        }, 100 + (index * 150));
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Animate posts with staggered delay
 | 
			
		||||
      const articles = document.querySelectorAll('article.group');
 | 
			
		||||
      articles.forEach((article, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          article.classList.add('animate-reveal');
 | 
			
		||||
        }, 500 + (index * 150));
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Animate topic cards with staggered delay
 | 
			
		||||
      const topicCards = document.querySelectorAll('a.group.flex.flex-col');
 | 
			
		||||
      topicCards.forEach((card, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          card.classList.add('animate-reveal');
 | 
			
		||||
        }, 800 + (index * 100));
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Run animations after the loading screen is hidden
 | 
			
		||||
    const loadingScreen = document.getElementById('loading-screen');
 | 
			
		||||
    if (loadingScreen) {
 | 
			
		||||
      // Check if loading screen is already hidden (page refresh)
 | 
			
		||||
      if (loadingScreen.style.display === 'none') {
 | 
			
		||||
        animateContent();
 | 
			
		||||
      } else {
 | 
			
		||||
        // Wait for loading screen to hide
 | 
			
		||||
        const observer = new MutationObserver((mutations) => {
 | 
			
		||||
          mutations.forEach((mutation) => {
 | 
			
		||||
            if (mutation.target === loadingScreen && 
 | 
			
		||||
                mutation.type === 'attributes' && 
 | 
			
		||||
                mutation.attributeName === 'style' &&
 | 
			
		||||
                loadingScreen.style.display === 'none') {
 | 
			
		||||
              animateContent();
 | 
			
		||||
              observer.disconnect();
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        observer.observe(loadingScreen, { attributes: true });
 | 
			
		||||
        
 | 
			
		||||
        // Fallback
 | 
			
		||||
        setTimeout(animateContent, 3500);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // If loading screen doesn't exist for some reason
 | 
			
		||||
      animateContent();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // SPA transition handling for homepage
 | 
			
		||||
  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 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>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.hero {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 2rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.roles {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.hero img {
 | 
			
		||||
		aspect-ratio: 5 / 4;
 | 
			
		||||
		object-fit: cover;
 | 
			
		||||
		object-position: top;
 | 
			
		||||
		border-radius: 1.5rem;
 | 
			
		||||
		box-shadow: var(--shadow-md);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.hero {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 6fr 4fr;
 | 
			
		||||
			padding-inline: 2.5rem;
 | 
			
		||||
			gap: 3.75rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.roles {
 | 
			
		||||
			margin-top: 0.5rem;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			gap: 0.5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.hero img {
 | 
			
		||||
			aspect-ratio: 3 / 4;
 | 
			
		||||
			border-radius: 4.5rem;
 | 
			
		||||
			object-fit: cover;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.section {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		gap: 2rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.with-background {
 | 
			
		||||
		position: relative;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.with-background::before {
 | 
			
		||||
		--hero-bg: var(--bg-image-subtle-2);
 | 
			
		||||
 | 
			
		||||
		content: '';
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		pointer-events: none;
 | 
			
		||||
		left: 50%;
 | 
			
		||||
		width: 100vw;
 | 
			
		||||
		aspect-ratio: calc(2.25 / var(--bg-scale));
 | 
			
		||||
		top: 0;
 | 
			
		||||
		transform: translateY(-75%) translateX(-50%);
 | 
			
		||||
		background:
 | 
			
		||||
			url('/assets/backgrounds/noise.png') top center/220px repeat,
 | 
			
		||||
			var(--hero-bg) center center / var(--bg-gradient-size) no-repeat,
 | 
			
		||||
			var(--gray-999);
 | 
			
		||||
		background-blend-mode: overlay, normal, normal, normal;
 | 
			
		||||
		mix-blend-mode: var(--bg-blend-mode);
 | 
			
		||||
		z-index: -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.with-background.bg-variant::before {
 | 
			
		||||
		--hero-bg: var(--bg-image-subtle-1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.section-header {
 | 
			
		||||
		justify-self: center;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		max-width: 50ch;
 | 
			
		||||
		font-size: var(--text-md);
 | 
			
		||||
		color: var(--gray-300);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.section-header h3 {
 | 
			
		||||
		font-size: var(--text-2xl);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.section {
 | 
			
		||||
			grid-template-columns: repeat(4, 1fr);
 | 
			
		||||
			grid-template-areas: 'header header header header' 'gallery gallery gallery gallery';
 | 
			
		||||
			gap: 5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.section.with-cta {
 | 
			
		||||
			grid-template-areas: 'header header header cta' 'gallery gallery gallery gallery';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.section-header {
 | 
			
		||||
			grid-area: header;
 | 
			
		||||
			font-size: var(--text-lg);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.section-header h3 {
 | 
			
		||||
			font-size: var(--text-4xl);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.with-cta .section-header {
 | 
			
		||||
			justify-self: flex-start;
 | 
			
		||||
			text-align: left;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.gallery {
 | 
			
		||||
			grid-area: gallery;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.cta {
 | 
			
		||||
			grid-area: cta;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.mention-card {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		height: 7rem;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		border: 1px solid var(--gray-800);
 | 
			
		||||
		border-radius: 1.5rem;
 | 
			
		||||
		color: var(--gray-300);
 | 
			
		||||
		background: var(--gradient-subtle);
 | 
			
		||||
		box-shadow: var(--shadow-sm);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.mention-card {
 | 
			
		||||
			border-radius: 1.5rem;
 | 
			
		||||
			height: 9.5rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
  /* Fix for theme transition issues */
 | 
			
		||||
  :global(:root) {
 | 
			
		||||
    --theme-transition-duration: 0.5s;
 | 
			
		||||
    --theme-transition-timing: ease;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :global(html), :global(body) {
 | 
			
		||||
    transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :global(.theme-transition-all) {
 | 
			
		||||
    transition: all var(--theme-transition-duration) var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :global(.theme-transition-bg) {
 | 
			
		||||
    transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :global(.theme-transition-color) {
 | 
			
		||||
    transition: color var(--theme-transition-duration) var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Ensure transitions apply to all theme-related properties */
 | 
			
		||||
  :global(*) {
 | 
			
		||||
    transition-property: background-color, border-color, color, fill, stroke, opacity;
 | 
			
		||||
    transition-duration: var(--theme-transition-duration);
 | 
			
		||||
    transition-timing-function: var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Remove the forced transition disabling which causes flickering */
 | 
			
		||||
  :global(.theme-switching), :global(.theme-switching *) {
 | 
			
		||||
    /* Use a subtle transition instead of none */
 | 
			
		||||
    transition-duration: 0.3s !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Content reveal animations */
 | 
			
		||||
  .hero-text span, 
 | 
			
		||||
  .hero-text + p, 
 | 
			
		||||
  .hero-text ~ div,
 | 
			
		||||
  article.group,
 | 
			
		||||
  a.group.flex.flex-col {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transform: translateY(20px);
 | 
			
		||||
    transition: opacity 0.8s ease, transform 0.8s ease;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animate-reveal {
 | 
			
		||||
    opacity: 1 !important;
 | 
			
		||||
    transform: translateY(0) !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Rest of your existing styles... */
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
import ContactCTA from '../components/ContactCTA.astro';
 | 
			
		||||
import PortfolioPreview from '../components/PortfolioPreview.astro';
 | 
			
		||||
import Hero from '../components/Hero.astro';
 | 
			
		||||
import Grid from '../components/Grid.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../lib/directus"
 | 
			
		||||
import { readItems,readSingleton } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
const global = await directus.request(readSingleton("global"));
 | 
			
		||||
 | 
			
		||||
const posts = await directus.request(
 | 
			
		||||
  readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
    sort: ["-published_date"],
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout
 | 
			
		||||
	title=`My Projects | ${global.name}`
 | 
			
		||||
	description=`Learn about ${global.name}'s most recent projects`
 | 
			
		||||
>
 | 
			
		||||
	<div class="stack gap-20">
 | 
			
		||||
		<main class="wrapper stack gap-8">
 | 
			
		||||
			<Hero
 | 
			
		||||
				title="My Projects"
 | 
			
		||||
				tagline="See my most recent projects below to get an idea of my past experience."
 | 
			
		||||
				align="start"
 | 
			
		||||
			/>
 | 
			
		||||
			<Grid variant="offset">
 | 
			
		||||
                {
 | 
			
		||||
                    posts.map((post) => (
 | 
			
		||||
                      <li>
 | 
			
		||||
                        <PortfolioPreview posts={post} />
 | 
			
		||||
                      </li>
 | 
			
		||||
                    ))
 | 
			
		||||
                  }               
 | 
			
		||||
			</Grid>
 | 
			
		||||
		</main>
 | 
			
		||||
		<ContactCTA />
 | 
			
		||||
	</div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
@@ -1,147 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
import ContactCTA from '../../components/ContactCTA.astro';
 | 
			
		||||
import Hero from '../../components/Hero.astro';
 | 
			
		||||
import Icon from '../../components/Icon.astro';
 | 
			
		||||
import Pill from '../../components/Pill.astro';
 | 
			
		||||
 | 
			
		||||
import directus, { directus_url } from "../../../lib/directus";
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
export async function getStaticPaths() {
 | 
			
		||||
  const posts = await directus.request(readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
  }));
 | 
			
		||||
  return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const post = Astro.props;
 | 
			
		||||
const published_date: string = new Date(post.published_date).toLocaleDateString();
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={post.title}>
 | 
			
		||||
	<div class="stack gap-20">
 | 
			
		||||
		<div class="stack gap-15">
 | 
			
		||||
			<header>
 | 
			
		||||
				<div class="wrapper stack gap-2">
 | 
			
		||||
					<a class="back-link" href="/projects/"><Icon icon="arrow-left" /> Projects</a>
 | 
			
		||||
					<Hero
 | 
			
		||||
                        title={post.title}
 | 
			
		||||
                        tagline=`Published on ${published_date}`
 | 
			
		||||
                        align="start"
 | 
			
		||||
                    >
 | 
			
		||||
						<div class="details">
 | 
			
		||||
							<div class="tags">
 | 
			
		||||
								{post.tags.map((t) => <Pill>{t}</Pill>)}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</Hero>
 | 
			
		||||
				</div>
 | 
			
		||||
			</header>
 | 
			
		||||
			<main class="wrapper">
 | 
			
		||||
				<div class="stack gap-10 content">
 | 
			
		||||
					{post.image && <img src={`${directus_url}/assets/${post.image}?width=500`} alt={post.image_alt || ''} />}
 | 
			
		||||
					<div class="content">
 | 
			
		||||
						<div set:html={post.content} />
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</main>
 | 
			
		||||
		</div>
 | 
			
		||||
		<ContactCTA />
 | 
			
		||||
	</div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	header {
 | 
			
		||||
		padding-bottom: 2.5rem;
 | 
			
		||||
		border-bottom: 1px solid var(--gray-800);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.back-link {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.details {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		padding: 0.5rem;
 | 
			
		||||
		gap: 1.5rem;
 | 
			
		||||
		justify-content: space-between;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.tags {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.description {
 | 
			
		||||
		font-size: var(--text-lg);
 | 
			
		||||
		max-width: 54ch;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content {
 | 
			
		||||
		max-width: 65ch;
 | 
			
		||||
		margin-inline: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content > :global(* + *) {
 | 
			
		||||
		margin-top: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content :global(h1),
 | 
			
		||||
	.content :global(h2),
 | 
			
		||||
	.content :global(h3),
 | 
			
		||||
	.content :global(h4),
 | 
			
		||||
	.content :global(h5) {
 | 
			
		||||
		margin: 1.5rem 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content :global(img) {
 | 
			
		||||
		border-radius: 1.5rem;
 | 
			
		||||
		box-shadow: var(--shadow-sm);
 | 
			
		||||
		background: var(--gradient-subtle);
 | 
			
		||||
		border: 1px solid var(--gray-800);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content :global(blockquote) {
 | 
			
		||||
		font-size: var(--text-lg);
 | 
			
		||||
		font-family: var(--font-brand);
 | 
			
		||||
		font-weight: 600;
 | 
			
		||||
		line-height: 1.1;
 | 
			
		||||
		padding-inline-start: 1.5rem;
 | 
			
		||||
		border-inline-start: 0.25rem solid var(--accent-dark);
 | 
			
		||||
		color: var(--gray-0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.back-link,
 | 
			
		||||
	.content :global(a) {
 | 
			
		||||
		text-decoration: 1px solid underline transparent;
 | 
			
		||||
		text-underline-offset: 0.25em;
 | 
			
		||||
		transition: text-decoration-color var(--theme-transition);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.back-link:hover,
 | 
			
		||||
	.back-link:focus,
 | 
			
		||||
	.content :global(a:hover),
 | 
			
		||||
	.content :global(a:focus) {
 | 
			
		||||
		text-decoration-color: currentColor;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 50em) {
 | 
			
		||||
		.back-link {
 | 
			
		||||
			display: block;
 | 
			
		||||
			align-self: flex-start;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.details {
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			gap: 2.5rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.content :global(blockquote) {
 | 
			
		||||
			font-size: var(--text-2xl);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										27
									
								
								src/pages/rss.xml.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/pages/rss.xml.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import rss from '@astrojs/rss';
 | 
			
		||||
 | 
			
		||||
import directus from "../../lib/directus"
 | 
			
		||||
import { readItems,readSingleton } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
export async function GET(context: any) {
 | 
			
		||||
  const global = await directus.request(readSingleton("global"));
 | 
			
		||||
  const posts = await directus.request(
 | 
			
		||||
    readItems("posts", {
 | 
			
		||||
      fields: ['*'],
 | 
			
		||||
      sort: ["-published_date"],
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  return rss({
 | 
			
		||||
    title: `${global.name}`,
 | 
			
		||||
    description: `${global.description}`,
 | 
			
		||||
    site: context.site,
 | 
			
		||||
    items: posts.map((post) => ({
 | 
			
		||||
      title: post.title,
 | 
			
		||||
      pubDate: post.published_date,
 | 
			
		||||
      description: post.slug,
 | 
			
		||||
      link: `/blog/${post.slug}/`,
 | 
			
		||||
      categories: post.tags || [],
 | 
			
		||||
    })),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										390
									
								
								src/pages/topics/[tag].astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										390
									
								
								src/pages/topics/[tag].astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,390 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
import FormattedDate from '../../components/FormattedDate.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
export const prerender = true;
 | 
			
		||||
 | 
			
		||||
export async function getStaticPaths() {
 | 
			
		||||
  const posts = await directus.request(readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // Get all unique tags
 | 
			
		||||
  const uniqueTags = [...new Set(posts.flatMap(post => post.tags || []))];
 | 
			
		||||
 | 
			
		||||
  // Create a path for each tag
 | 
			
		||||
  return uniqueTags.map(tag => {
 | 
			
		||||
    // Make tag matching case-insensitive
 | 
			
		||||
    const filteredPosts = posts.filter(post =>
 | 
			
		||||
      post.tags?.some(t => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
 | 
			
		||||
    );
 | 
			
		||||
    return {
 | 
			
		||||
      params: { tag },
 | 
			
		||||
      props: { posts: filteredPosts },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { tag } = Astro.params as { tag: string };
 | 
			
		||||
const { posts = [] } = Astro.props;
 | 
			
		||||
 | 
			
		||||
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
 | 
			
		||||
 | 
			
		||||
const sortedPosts = posts && posts.length > 0
 | 
			
		||||
  ? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
 | 
			
		||||
  : [];
 | 
			
		||||
console.log(`Sorted posts length: ${sortedPosts.length}`);
 | 
			
		||||
 | 
			
		||||
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
 | 
			
		||||
const relatedTags = [...new Set(
 | 
			
		||||
  sortedPosts.flatMap(post => post.tags || [])
 | 
			
		||||
    .filter(t => t !== tag)
 | 
			
		||||
)].slice(0, 5);
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={`Posts tagged with "${tag}"`}>
 | 
			
		||||
  <div class="max-w-5xl mx-auto px-4 py-10 sm:py-16">
 | 
			
		||||
    <!-- Header section -->
 | 
			
		||||
    <div class="relative mb-10 sm:mb-16">
 | 
			
		||||
      <div class="absolute -top-20 -left-20 w-48 sm:w-64 h-48 sm:h-64 bg-zinc-100 dark:bg-zinc-900/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
 | 
			
		||||
      <div class="absolute -bottom-10 -right-10 w-36 sm:w-48 h-36 sm:h-48 bg-zinc-200 dark:bg-zinc-900/20 rounded-full blur-2xl opacity-20 animate-blob animation-delay-2000"></div>
 | 
			
		||||
 | 
			
		||||
      <div class="relative text-center sm:text-left">
 | 
			
		||||
        <a href="/tags" class="inline-flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-4 group">
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:-translate-x-1">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
 | 
			
		||||
          </svg>
 | 
			
		||||
          <span>Back to all topics</span>
 | 
			
		||||
          <span class="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-zinc-300 dark:bg-zinc-700"></span>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <div class="flex flex-col sm:flex-row sm:items-center gap-4 mb-2 justify-center sm:justify-start">
 | 
			
		||||
          <div class="tag-icon flex items-center justify-center w-12 h-12 rounded-xl bg-zinc-100 dark:bg-zinc-800 shadow-sm mx-auto sm:mx-0">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-zinc-700 dark:text-zinc-300">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">
 | 
			
		||||
            <span class="relative">
 | 
			
		||||
              #{tag}
 | 
			
		||||
              <span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-200 dark:bg-zinc-700"></span>
 | 
			
		||||
              <span class="absolute -bottom-1 left-0 w-1/2 h-1 bg-zinc-900 dark:bg-zinc-100 opacity-70 animate-expand"></span>
 | 
			
		||||
            </span>
 | 
			
		||||
          </h1>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mt-4 max-w-2xl mx-auto sm:mx-0">
 | 
			
		||||
          Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100">{sortedPosts.length}</span> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100">"{tag}"</span>
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Related tags section -->
 | 
			
		||||
    {relatedTags.length > 0 && (
 | 
			
		||||
      <div class="mb-8 sm:mb-12 overflow-x-auto pb-4 hide-scrollbar">
 | 
			
		||||
        <h2 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-3 text-center sm:text-left">Related topics</h2>
 | 
			
		||||
        <div class="flex gap-2 flex-nowrap justify-center sm:justify-start">
 | 
			
		||||
          {relatedTags.map(relatedTag => (
 | 
			
		||||
            <a
 | 
			
		||||
              href={`/topics/${relatedTag}`}
 | 
			
		||||
              class="flex-shrink-0 inline-flex items-center rounded-full px-3 py-1.5 text-sm font-medium bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors"
 | 
			
		||||
            >
 | 
			
		||||
              #{relatedTag}
 | 
			
		||||
            </a>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )}
 | 
			
		||||
 | 
			
		||||
    <!-- Posts list -->
 | 
			
		||||
    <div class="relative">
 | 
			
		||||
      <div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 pointer-events-none"></div>
 | 
			
		||||
 | 
			
		||||
      <div class="relative space-y-6 sm:space-y-8">
 | 
			
		||||
        {sortedPosts.map((post) => (
 | 
			
		||||
          <article class="group relative flex flex-col p-5 sm:p-8 rounded-2xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50/80 dark:hover:bg-zinc-900/50 transition-all duration-300 hover:shadow-md hover-card max-w-2xl mx-auto sm:mx-0">
 | 
			
		||||
            <div class="absolute inset-0 bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 dark:from-zinc-900/0 dark:to-zinc-800/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl"></div>
 | 
			
		||||
 | 
			
		||||
            <div class="flex flex-col sm:flex-row gap-5 sm:gap-6">
 | 
			
		||||
              {post.image && (
 | 
			
		||||
                <div class="flex-shrink-0 w-full sm:w-56 h-40 rounded-xl overflow-hidden shadow-sm group-hover:shadow-md transition-all duration-300 mx-auto sm:mx-0">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`}
 | 
			
		||||
                    alt={post.image_alt}
 | 
			
		||||
                    class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
 | 
			
		||||
                    loading="lazy"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="flex flex-wrap items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center sm:justify-start">
 | 
			
		||||
                  {post.published_date && (
 | 
			
		||||
                    <time datetime={post.published_date.toLocaleString()} class="flex items-center gap-1.5">
 | 
			
		||||
                      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 sm:w-4 sm:h-4">
 | 
			
		||||
                        <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
 | 
			
		||||
                        A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
 | 
			
		||||
                      </svg>
 | 
			
		||||
                      <FormattedDate date={post.published_date} />
 | 
			
		||||
                    </time>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center sm:text-left">
 | 
			
		||||
                  <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
 | 
			
		||||
                    {post.title}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </h2>
 | 
			
		||||
 | 
			
		||||
                <p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 sm:line-clamp-3 text-center sm:text-left">
 | 
			
		||||
                  {post.description}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="flex flex-wrap justify-center sm:justify-between items-end mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800">
 | 
			
		||||
              {post.tags && post.tags.length > 0 && (
 | 
			
		||||
                <div class="flex flex-wrap gap-2 mb-3 sm:mb-0 justify-center sm:justify-start">
 | 
			
		||||
                  {post.tags.slice(0, 3).map(postTag => (
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={`/topics/${postTag}`}
 | 
			
		||||
                      class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
 | 
			
		||||
                        postTag === tag
 | 
			
		||||
                          ? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
 | 
			
		||||
                          : 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
 | 
			
		||||
                      }`}
 | 
			
		||||
                    >
 | 
			
		||||
                      #{postTag}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  {post.tags.length > 3 && (
 | 
			
		||||
                    <span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
 | 
			
		||||
                      +{post.tags.length - 3}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <div class="mx-auto sm:ml-auto sm:mr-0">
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`/blog/${post.slug}/`}
 | 
			
		||||
                  class="inline-flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors"
 | 
			
		||||
                  aria-hidden="true"
 | 
			
		||||
                  tabindex="-1"
 | 
			
		||||
                >
 | 
			
		||||
                  <span class="relative overflow-hidden inline-block">
 | 
			
		||||
                    <span class="block transition-transform duration-300 group-hover:-translate-y-full">Read article</span>
 | 
			
		||||
                    <span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span>
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </a>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </article>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Empty state với màu zinc -->
 | 
			
		||||
    {sortedPosts.length === 0 && (
 | 
			
		||||
      <div class="text-center py-12 sm:py-20">
 | 
			
		||||
        <div class="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-4 sm:mb-6">
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 sm:w-10 sm:h-10 text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">No posts found</h2>
 | 
			
		||||
        <p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
 | 
			
		||||
        <a href="/blog" class="inline-flex items-center gap-2 mt-6 px-4 py-2 rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-all duration-300 text-sm font-medium">
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
 | 
			
		||||
          </svg>
 | 
			
		||||
          <span>Browse all articles</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    )}
 | 
			
		||||
  </div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Grid pattern background */
 | 
			
		||||
  .bg-grid-pattern {
 | 
			
		||||
    background-size: 30px 30px;
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :global(.dark) .bg-grid-pattern {
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Hide scrollbar but keep functionality */
 | 
			
		||||
  .hide-scrollbar {
 | 
			
		||||
    -ms-overflow-style: none;
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .hide-scrollbar::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Animated underline */
 | 
			
		||||
  @keyframes expand {
 | 
			
		||||
    from { width: 0; }
 | 
			
		||||
    to { width: 50%; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animate-expand {
 | 
			
		||||
    animation: expand 1s ease-out forwards;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Blob animation */
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob 7s infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animation-delay-2000 {
 | 
			
		||||
    animation-delay: 2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes blob {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: translate(0px, 0px) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
      transform: translate(20px, -20px) scale(1.1);
 | 
			
		||||
    }
 | 
			
		||||
    66% {
 | 
			
		||||
      transform: translate(-20px, 20px) scale(0.9);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translate(0px, 0px) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Hover card effect */
 | 
			
		||||
  .hover-card {
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
    transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (hover: hover) {
 | 
			
		||||
    .hover-card:hover {
 | 
			
		||||
      transform: translateY(-2px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Line clamp for descriptions */
 | 
			
		||||
  .line-clamp-2 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 2;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .line-clamp-3 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 3;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Responsive adjustments */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    .animate-blob {
 | 
			
		||||
      animation-duration: 10s;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Handle SPA transitions for tag pages
 | 
			
		||||
  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 tag page
 | 
			
		||||
    function animateTagContent() {
 | 
			
		||||
      // Animate header elements
 | 
			
		||||
      const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
 | 
			
		||||
      headerElements.forEach((el, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          el.classList.add('animate-reveal');
 | 
			
		||||
        }, 100 + (index * 150));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate posts with staggered delay
 | 
			
		||||
      const articles = document.querySelectorAll('article');
 | 
			
		||||
      articles.forEach((article, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          article.classList.add('animate-reveal');
 | 
			
		||||
        }, 400 + (index * 100));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate related tags
 | 
			
		||||
      const relatedTags = document.querySelectorAll('.related-tags a');
 | 
			
		||||
      relatedTags.forEach((tag, index) => {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          tag.classList.add('animate-reveal');
 | 
			
		||||
        }, 600 + (index * 50));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run animations
 | 
			
		||||
    animateTagContent();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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>
 | 
			
		||||
 | 
			
		||||
<!-- Add this at the end of your page -->
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
							
								
								
									
										648
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										648
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,648 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
const posts = await directus.request(
 | 
			
		||||
  readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
    sort: ["-published_date"],
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const tags = [...new Set(posts.flatMap(post => post.tags || []))].sort();
 | 
			
		||||
 | 
			
		||||
// Count posts for each tag and create tag objects with additional data
 | 
			
		||||
const tagObjects = tags.map(tag => {
 | 
			
		||||
  const count = posts.filter(post => post.tags?.includes(tag)).length;
 | 
			
		||||
  // Generate a consistent but random-looking hue for each tag
 | 
			
		||||
  const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
 | 
			
		||||
  return { 
 | 
			
		||||
    name: tag, 
 | 
			
		||||
    count,
 | 
			
		||||
    size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
 | 
			
		||||
    hue
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Explore Tags">
 | 
			
		||||
  <div class="w-full mx-auto px-3 sm:px-6 py-6 sm:py-12 md:py-16 theme-transition-all">
 | 
			
		||||
    <!-- Enhanced header section with animated elements - improved for mobile -->
 | 
			
		||||
    <div class="relative mb-8 sm:mb-12 md:mb-16 text-center theme-transition-element">
 | 
			
		||||
      <div class="absolute -top-16 -left-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div>
 | 
			
		||||
      <div class="absolute -bottom-16 -right-16 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>
 | 
			
		||||
      <div class="absolute top-8 right-8 w-24 sm:w-32 md:w-40 h-24 sm:h-32 md:h-40 bg-zinc-100/30 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-40 animate-blob animation-delay-4000 theme-transition-bg"></div>
 | 
			
		||||
      
 | 
			
		||||
      <h1 class="relative text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 theme-transition-color">
 | 
			
		||||
        <span class="inline-block relative">
 | 
			
		||||
          <span class="relative inline-block">
 | 
			
		||||
            <span class="absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 dark:from-zinc-800/50 dark:to-zinc-700/50 blur-sm theme-transition-bg"></span>
 | 
			
		||||
            <span class="relative">Explore</span>
 | 
			
		||||
          </span>
 | 
			
		||||
          {" "}
 | 
			
		||||
          <span class="relative inline-block">
 | 
			
		||||
            Topics
 | 
			
		||||
            <span class="absolute -bottom-1 sm:-bottom-2 left-0 w-full h-0.5 sm:h-1 bg-gradient-to-r from-zinc-400 to-zinc-600 dark:from-zinc-600 dark:to-zinc-400 transform origin-left animate-underline theme-transition-bg"></span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </span>
 | 
			
		||||
      </h1>
 | 
			
		||||
      <p class="relative text-sm sm:text-base md:text-lg lg:text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto theme-transition-color">
 | 
			
		||||
        Discover content organized by your interests
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    {tags.length === 0 ? (
 | 
			
		||||
      <div class="text-center py-8 sm:py-12 md:py-16 theme-transition-element">
 | 
			
		||||
        <div class="inline-flex items-center justify-center w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-3 sm:mb-4 md:mb-6 shadow-inner theme-transition-bg">
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 sm:w-10 md:w-12 h-8 sm:h-10 md:h-12 text-zinc-500 dark:text-zinc-400 theme-transition-color">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p class="text-lg sm:text-xl md:text-2xl font-medium text-zinc-800 dark:text-zinc-200 theme-transition-color">No tags found yet.</p>
 | 
			
		||||
        <p class="mt-2 text-xs sm:text-sm md:text-base text-zinc-500 dark:text-zinc-500 theme-transition-color">Check back later for categorized content.</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    ) : (
 | 
			
		||||
      <div class="flex justify-center w-full">
 | 
			
		||||
        <!-- Featured Tags Section - ultra-responsive design -->
 | 
			
		||||
        <div class="tag-cloud relative p-3 sm:p-4 md:p-6 lg:p-8 rounded-lg sm:rounded-xl md:rounded-2xl lg:rounded-3xl border border-zinc-100 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm hover-3d glass theme-transition-all w-full">
 | 
			
		||||
          <div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 theme-transition-bg"></div>
 | 
			
		||||
          <div class="absolute -top-8 -right-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div>
 | 
			
		||||
          <div class="absolute -bottom-8 -left-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div>
 | 
			
		||||
          
 | 
			
		||||
          <h2 class="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 lg:mb-8 text-center theme-transition-color">Popular Topics</h2>
 | 
			
		||||
          
 | 
			
		||||
          <!-- Ultra-responsive grid layout with fallbacks -->
 | 
			
		||||
          <div class="grid grid-cols-2 xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-1.5 xxxs:gap-2 xxs:gap-2 xs:gap-2 sm:gap-3 md:gap-4 w-full">
 | 
			
		||||
            {sortedTags.map((tag) => (
 | 
			
		||||
              <a 
 | 
			
		||||
                href={`/topics/${tag.name}`}
 | 
			
		||||
                class="group relative overflow-hidden rounded-md sm:rounded-lg md:rounded-xl border border-zinc-200 dark:border-zinc-800 transition-all duration-300 hover:shadow-md sm:hover:shadow-lg hover:scale-[1.03] hover:border-zinc-300 dark:hover:border-zinc-700 active:scale-95 theme-transition-element theme-ripple flex-grow min-w-0"
 | 
			
		||||
                style={`--tag-hue: ${tag.hue};`}
 | 
			
		||||
              >
 | 
			
		||||
                <div class="absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 dark:from-zinc-800/90 dark:to-zinc-900/90 opacity-100 group-hover:opacity-95 transition-opacity theme-transition-bg"></div>
 | 
			
		||||
                
 | 
			
		||||
                <div class="relative px-1.5 xxxs:px-2 xxs:px-2 xs:px-2 sm:px-3 md:px-4 py-1.5 xxxs:py-2 xxs:py-2 xs:py-2 sm:py-3 md:py-4 flex items-center gap-1.5 xxs:gap-2 w-full">
 | 
			
		||||
                  <div class="flex-shrink-0 flex items-center justify-center w-5 h-5 xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 sm:w-8 sm:h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light transition-all duration-300 shadow-sm theme-transition-all">
 | 
			
		||||
                    <span class="text-xs xxxs:text-xs xxs:text-xs xs:text-sm sm:text-base md:text-lg font-semibold">#</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  
 | 
			
		||||
                  <div class="flex-1 min-w-0 overflow-hidden">
 | 
			
		||||
                    <h3 class="text-[10px] xxxs:text-xs xxs:text-xs xs:text-xs sm:text-sm md:text-base font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color break-words hyphens-auto truncate">
 | 
			
		||||
                      {tag.name}
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    <p class="text-[8px] xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] sm:text-xs md:text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color truncate">{tag.count} article{tag.count !== 1 ? 's' : ''}</p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </a>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )}
 | 
			
		||||
  </div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Ultra-reliable responsiveness handling
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Fix viewport width issues on mobile
 | 
			
		||||
    const fixViewportWidth = () => {
 | 
			
		||||
      // Force the viewport to be exactly the width of the device
 | 
			
		||||
      const viewport = document.querySelector('meta[name="viewport"]');
 | 
			
		||||
      if (!viewport) {
 | 
			
		||||
        const meta = document.createElement('meta');
 | 
			
		||||
        meta.name = 'viewport';
 | 
			
		||||
        meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
 | 
			
		||||
        document.getElementsByTagName('head')[0].appendChild(meta);
 | 
			
		||||
      } else {
 | 
			
		||||
        viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Fix for horizontal overflow
 | 
			
		||||
      document.body.style.overflowX = 'hidden';
 | 
			
		||||
      document.documentElement.style.overflowX = 'hidden';
 | 
			
		||||
      document.documentElement.style.width = '100%';
 | 
			
		||||
      document.body.style.width = '100%';
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    fixViewportWidth();
 | 
			
		||||
    
 | 
			
		||||
    // Adjust tag items based on screen size with extreme precision
 | 
			
		||||
    const adjustTagItems = () => {
 | 
			
		||||
      const tagItems = document.querySelectorAll('.theme-ripple');
 | 
			
		||||
      const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
 | 
			
		||||
      const isVerySmall = width < 360;
 | 
			
		||||
      const isExtremelySmall = width < 280;
 | 
			
		||||
      const isMicroScreen = width < 240;
 | 
			
		||||
      
 | 
			
		||||
      // Fix container width to match viewport exactly
 | 
			
		||||
      const container = document.querySelector('.tag-cloud');
 | 
			
		||||
      if (container) {
 | 
			
		||||
        container.style.maxWidth = '100vw';
 | 
			
		||||
        container.style.width = '100%';
 | 
			
		||||
        container.style.boxSizing = 'border-box';
 | 
			
		||||
        
 | 
			
		||||
        // Remove any margins that might cause overflow
 | 
			
		||||
        container.style.marginLeft = '0';
 | 
			
		||||
        container.style.marginRight = '0';
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Fix grid width
 | 
			
		||||
      const grid = document.querySelector('.grid');
 | 
			
		||||
      if (grid) {
 | 
			
		||||
        grid.style.width = '100%';
 | 
			
		||||
        grid.style.maxWidth = '100%';
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      tagItems.forEach(item => {
 | 
			
		||||
        // Set appropriate classes based on screen size
 | 
			
		||||
        if (isMicroScreen) {
 | 
			
		||||
          item.classList.add('micro-screen');
 | 
			
		||||
          item.classList.remove('extremely-small-screen', 'very-small-screen');
 | 
			
		||||
        } else if (isExtremelySmall) {
 | 
			
		||||
          item.classList.add('extremely-small-screen');
 | 
			
		||||
          item.classList.remove('very-small-screen', 'micro-screen');
 | 
			
		||||
        } else if (isVerySmall) {
 | 
			
		||||
          item.classList.add('very-small-screen');
 | 
			
		||||
          item.classList.remove('extremely-small-screen', 'micro-screen');
 | 
			
		||||
        } else {
 | 
			
		||||
          item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Ensure text doesn't overflow on small screens
 | 
			
		||||
        const tagName = item.querySelector('h3');
 | 
			
		||||
        const tagCount = item.querySelector('p');
 | 
			
		||||
        
 | 
			
		||||
        if (tagName) {
 | 
			
		||||
          // Set title for accessibility
 | 
			
		||||
          tagName.title = tagName.textContent.trim();
 | 
			
		||||
          
 | 
			
		||||
          // Adjust text length based on screen size
 | 
			
		||||
          if (isMicroScreen && tagName.textContent.length > 6) {
 | 
			
		||||
            tagName.dataset.fullText = tagName.textContent;
 | 
			
		||||
            tagName.textContent = tagName.textContent.substring(0, 6) + '...';
 | 
			
		||||
          } else if (isExtremelySmall && tagName.textContent.length > 8) {
 | 
			
		||||
            tagName.dataset.fullText = tagName.textContent;
 | 
			
		||||
            tagName.textContent = tagName.textContent.substring(0, 8) + '...';
 | 
			
		||||
          } else if (isVerySmall && tagName.textContent.length > 12) {
 | 
			
		||||
            tagName.dataset.fullText = tagName.textContent;
 | 
			
		||||
            tagName.textContent = tagName.textContent.substring(0, 12) + '...';
 | 
			
		||||
          } else if (tagName.dataset.fullText) {
 | 
			
		||||
            tagName.textContent = tagName.dataset.fullText;
 | 
			
		||||
            delete tagName.dataset.fullText;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Set the tag hue for hover effects
 | 
			
		||||
        const hue = item.style.getPropertyValue('--tag-hue');
 | 
			
		||||
        item.style.setProperty('--hover-hue', hue);
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Run on load
 | 
			
		||||
    adjustTagItems();
 | 
			
		||||
    
 | 
			
		||||
    // Run on resize with optimized debounce
 | 
			
		||||
    let resizeTimer;
 | 
			
		||||
    const handleResize = () => {
 | 
			
		||||
      if (resizeTimer) {
 | 
			
		||||
        window.cancelAnimationFrame(resizeTimer);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      resizeTimer = window.requestAnimationFrame(() => {
 | 
			
		||||
        fixViewportWidth();
 | 
			
		||||
        adjustTagItems();
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    window.addEventListener('resize', handleResize);
 | 
			
		||||
    window.addEventListener('orientationchange', handleResize);
 | 
			
		||||
    
 | 
			
		||||
    // Ensure layout is recalculated after page is fully loaded
 | 
			
		||||
    window.addEventListener('load', () => {
 | 
			
		||||
      fixViewportWidth();
 | 
			
		||||
      adjustTagItems();
 | 
			
		||||
      // Force recalculation after images and fonts are loaded
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        fixViewportWidth();
 | 
			
		||||
        adjustTagItems();
 | 
			
		||||
      }, 500);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Fix for iOS Safari and other mobile browsers
 | 
			
		||||
    if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
 | 
			
		||||
      document.documentElement.style.setProperty('--safe-area-inset-bottom', 'env(safe-area-inset-bottom)');
 | 
			
		||||
      
 | 
			
		||||
      // Fix for mobile viewport height issues
 | 
			
		||||
      const setVh = () => {
 | 
			
		||||
        const vh = window.innerHeight * 0.01;
 | 
			
		||||
        document.documentElement.style.setProperty('--vh', `${vh}px`);
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      setVh();
 | 
			
		||||
      window.addEventListener('resize', setVh);
 | 
			
		||||
      window.addEventListener('orientationchange', () => {
 | 
			
		||||
        // Wait for orientation change to complete
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          setVh();
 | 
			
		||||
          fixViewportWidth();
 | 
			
		||||
        }, 100);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Add touch support for mobile devices
 | 
			
		||||
    const addTouchSupport = () => {
 | 
			
		||||
      const tagItems = document.querySelectorAll('.theme-ripple');
 | 
			
		||||
      
 | 
			
		||||
      tagItems.forEach(item => {
 | 
			
		||||
        item.addEventListener('touchstart', () => {
 | 
			
		||||
          item.classList.add('touch-active');
 | 
			
		||||
        }, { passive: true });
 | 
			
		||||
        
 | 
			
		||||
        item.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            item.classList.remove('touch-active');
 | 
			
		||||
          }, 150);
 | 
			
		||||
        }, { passive: true });
 | 
			
		||||
        
 | 
			
		||||
        // Cancel active state if touch moves away
 | 
			
		||||
        item.addEventListener('touchmove', (e) => {
 | 
			
		||||
          const touch = e.touches[0];
 | 
			
		||||
          const rect = item.getBoundingClientRect();
 | 
			
		||||
          
 | 
			
		||||
          if (
 | 
			
		||||
            touch.clientX < rect.left || 
 | 
			
		||||
            touch.clientX > rect.right || 
 | 
			
		||||
            touch.clientY < rect.top || 
 | 
			
		||||
            touch.clientY > rect.bottom
 | 
			
		||||
          ) {
 | 
			
		||||
            item.classList.remove('touch-active');
 | 
			
		||||
          }
 | 
			
		||||
        }, { passive: true });
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    addTouchSupport();
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Base styles */
 | 
			
		||||
  .tag-cloud {
 | 
			
		||||
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), 
 | 
			
		||||
                0 2px 4px rgba(0, 0, 0, 0.03), 
 | 
			
		||||
                0 4px 8px rgba(0, 0, 0, 0.05);
 | 
			
		||||
    transform-style: preserve-3d;
 | 
			
		||||
    perspective: 1000px;
 | 
			
		||||
    transition: all var(--theme-transition);
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
    max-width: 100% !important;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    margin-left: 0 !important;
 | 
			
		||||
    margin-right: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Fix for horizontal overflow */
 | 
			
		||||
  :global(html), :global(body) {
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :global(.max-w-6xl) {
 | 
			
		||||
    max-width: 100% !important;
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Ultra-responsive breakpoints for extreme reliability */
 | 
			
		||||
  /* Micro screens (below 240px) */
 | 
			
		||||
  @media (max-width: 239px) {
 | 
			
		||||
    .tag-cloud {
 | 
			
		||||
      padding: 0.5rem !important;
 | 
			
		||||
      margin: 0 !important;
 | 
			
		||||
      border-radius: 0.25rem !important;
 | 
			
		||||
      width: 100% !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .tag-cloud h2 {
 | 
			
		||||
      font-size: 0.875rem !important;
 | 
			
		||||
      margin-bottom: 0.375rem !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .grid {
 | 
			
		||||
      grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
 | 
			
		||||
      gap: 0.375rem !important;
 | 
			
		||||
      width: 100% !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .micro-screen .flex {
 | 
			
		||||
      padding: 0.25rem !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .micro-screen h3 {
 | 
			
		||||
      font-size: 0.625rem !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .micro-screen p {
 | 
			
		||||
      font-size: 0.5rem !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Extra extra extra small screens (240px-279px) */
 | 
			
		||||
  @media (min-width: 240px) and (max-width: 279px) {
 | 
			
		||||
    .xxxs\:grid-cols-2 {
 | 
			
		||||
      grid-template-columns: repeat(2, minmax(0, 1fr));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:px-2 {
 | 
			
		||||
      padding-left: 0.5rem;
 | 
			
		||||
      padding-right: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:py-2 {
 | 
			
		||||
      padding-top: 0.5rem;
 | 
			
		||||
      padding-bottom: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:w-6 {
 | 
			
		||||
      width: 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:h-6 {
 | 
			
		||||
      height: 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:text-xs {
 | 
			
		||||
      font-size: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:gap-2 {
 | 
			
		||||
      gap: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxxs\:text-\[9px\] {
 | 
			
		||||
      font-size: 9px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Extra extra small screens (280px-359px) */
 | 
			
		||||
  @media (min-width: 280px) {
 | 
			
		||||
    .xxs\:grid-cols-2 {
 | 
			
		||||
      grid-template-columns: repeat(2, minmax(0, 1fr));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:px-2 {
 | 
			
		||||
      padding-left: 0.5rem;
 | 
			
		||||
      padding-right: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:py-2 {
 | 
			
		||||
      padding-top: 0.5rem;
 | 
			
		||||
      padding-bottom: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:w-6 {
 | 
			
		||||
      width: 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:h-6 {
 | 
			
		||||
      height: 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:text-xs {
 | 
			
		||||
      font-size: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:gap-2 {
 | 
			
		||||
      gap: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xxs\:text-\[9px\] {
 | 
			
		||||
      font-size: 9px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Extra small screens (360px-639px) */
 | 
			
		||||
  @media (min-width: 360px) {
 | 
			
		||||
    .xs\:grid-cols-3 {
 | 
			
		||||
      grid-template-columns: repeat(3, minmax(0, 1fr));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:px-2 {
 | 
			
		||||
      padding-left: 0.5rem;
 | 
			
		||||
      padding-right: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:py-2 {
 | 
			
		||||
      padding-top: 0.5rem;
 | 
			
		||||
      padding-bottom: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:w-7 {
 | 
			
		||||
      width: 1.75rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:h-7 {
 | 
			
		||||
      height: 1.75rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:text-xs {
 | 
			
		||||
      font-size: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:text-sm {
 | 
			
		||||
      font-size: 0.875rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:gap-2 {
 | 
			
		||||
      gap: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:text-\[10px\] {
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Ensure text doesn't overflow on small screens */
 | 
			
		||||
  .truncate {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Ensure proper word breaking for long tag names */
 | 
			
		||||
  .break-words {
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
    overflow-wrap: break-word;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .hyphens-auto {
 | 
			
		||||
    hyphens: auto;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Improved shadow for dark mode */
 | 
			
		||||
  :global(.dark) .tag-cloud {
 | 
			
		||||
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05),
 | 
			
		||||
                0 2px 4px rgba(0, 0, 0, 0.1),
 | 
			
		||||
                0 4px 8px rgba(0, 0, 0, 0.15);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Prevent layout shifts */
 | 
			
		||||
  .flex-grow {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .min-w-0 {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Ensure container doesn't overflow */
 | 
			
		||||
  .overflow-hidden {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Touch support for mobile */
 | 
			
		||||
  .touch-active {
 | 
			
		||||
    transform: scale(0.97) !important;
 | 
			
		||||
    opacity: 0.9;
 | 
			
		||||
    transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Animation for blob */
 | 
			
		||||
  @keyframes blob {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      transform: translate(0, 0) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
    25% {
 | 
			
		||||
      transform: translate(10px, -10px) scale(1.05);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      transform: translate(0, 20px) scale(0.95);
 | 
			
		||||
    }
 | 
			
		||||
    75% {
 | 
			
		||||
      transform: translate(-10px, -10px) scale(1.05);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob 20s infinite ease-in-out;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animation-delay-2000 {
 | 
			
		||||
    animation-delay: 2s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animation-delay-4000 {
 | 
			
		||||
    animation-delay: 4s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Animation for underline */
 | 
			
		||||
  @keyframes underline {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: scaleX(0);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: scaleX(1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animate-underline {
 | 
			
		||||
    animation: underline 1.5s ease-out forwards;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Fix for iOS Safari notch */
 | 
			
		||||
  @supports (padding: max(0px)) {
 | 
			
		||||
    .tag-cloud {
 | 
			
		||||
      padding-left: max(0.75rem, env(safe-area-inset-left));
 | 
			
		||||
      padding-right: max(0.75rem, env(safe-area-inset-right));
 | 
			
		||||
      padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Handle SPA transitions for tags index 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;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Add hover effect for tag cards on touch devices
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
    
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
      const tagCards = document.querySelectorAll('.tag-cloud a');
 | 
			
		||||
      
 | 
			
		||||
      tagCards.forEach(card => {
 | 
			
		||||
        card.addEventListener('touchstart', () => {
 | 
			
		||||
          card.classList.add('is-touched');
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        card.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            card.classList.remove('is-touched');
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Animate tag cards with staggered delay
 | 
			
		||||
    const tagCards = document.querySelectorAll('.tag-cloud a');
 | 
			
		||||
    tagCards.forEach((card, index) => {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        card.classList.add('animate-reveal');
 | 
			
		||||
      }, 100 + (index * 50));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 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>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user