251 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
---
 | 
						|
import { Icon } from 'astro-icon/components';
 | 
						|
import { readItems } from '@directus/sdk';
 | 
						|
 | 
						|
import type { Skill } from '@lib/directusTypes';
 | 
						|
 | 
						|
import directus from '@lib/directus';
 | 
						|
 | 
						|
const skills = await directus.request(
 | 
						|
  readItems('site_skills', {
 | 
						|
    fields: ['*'],
 | 
						|
    sort: ['-date_created'],
 | 
						|
  })
 | 
						|
);
 | 
						|
 | 
						|
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
 | 
						|
const borderClasses =
 | 
						|
  'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
 | 
						|
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
 | 
						|
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
 | 
						|
const shadowClasses = 'shadow-xs hover:shadow-lg';
 | 
						|
---
 | 
						|
 | 
						|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
 | 
						|
  <h3
 | 
						|
    class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
 | 
						|
  >
 | 
						|
    Skills
 | 
						|
  </h3>
 | 
						|
  <div class="">
 | 
						|
    <div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
 | 
						|
      <!-- Main slider container -->
 | 
						|
      <div class="slider-track animate-slide flex">
 | 
						|
        {
 | 
						|
          [...skills, ...skills, ...skills].map((skill: Skill) => {
 | 
						|
            return (
 | 
						|
              <div
 | 
						|
                class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
 | 
						|
              >
 | 
						|
                <div class="p-4 sm:p-6">
 | 
						|
                  <div class="mb-4 flex items-center justify-between sm:mb-6">
 | 
						|
                    <div class="flex items-center gap-2 sm:gap-4">
 | 
						|
                      <div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
 | 
						|
                        <Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
 | 
						|
                      </div>
 | 
						|
                      <h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
 | 
						|
                        {skill.title}
 | 
						|
                      </h3>
 | 
						|
                    </div>
 | 
						|
                    <span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
 | 
						|
                      {skill.level}%
 | 
						|
                    </span>
 | 
						|
                  </div>
 | 
						|
 | 
						|
                  <div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
 | 
						|
                    <div
 | 
						|
                      class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-gradient-to-r transition-all duration-1000"
 | 
						|
                      style={`width: ${skill.level}%`}
 | 
						|
                    />
 | 
						|
                  </div>
 | 
						|
 | 
						|
                  <div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
 | 
						|
                    <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 z-10 w-12 bg-gradient-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
 | 
						|
      >
 | 
						|
      </div>
 | 
						|
      <div
 | 
						|
        class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
 | 
						|
      >
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  </div>
 | 
						|
</section>
 | 
						|
 | 
						|
<script>
 | 
						|
  document.addEventListener('astro:page-load', () => {
 | 
						|
    // Create seamless infinite scrolling effect
 | 
						|
    function setupInfiniteScroll() {
 | 
						|
      const cards = document.querySelectorAll('.skill-card');
 | 
						|
      if (!cards.length) return;
 | 
						|
    }
 | 
						|
 | 
						|
    setupInfiniteScroll();
 | 
						|
 | 
						|
    // 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);
 | 
						|
        });
 | 
						|
      });
 | 
						|
    }
 | 
						|
  });
 | 
						|
</script>
 | 
						|
 | 
						|
<style>
 | 
						|
  /* Tech Stack Slider */
 | 
						|
  .slider-track {
 | 
						|
    width: fit-content;
 | 
						|
    animation: scroll 40s linear infinite;
 | 
						|
  }
 | 
						|
 | 
						|
  @keyframes scroll {
 | 
						|
    0% {
 | 
						|
      transform: translateX(0);
 | 
						|
    }
 | 
						|
    100% {
 | 
						|
      transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @media (min-width: 640px) {
 | 
						|
    .slider-track {
 | 
						|
      animation: scroll 80s linear infinite;
 | 
						|
    }
 | 
						|
 | 
						|
    @keyframes scroll {
 | 
						|
      0% {
 | 
						|
        transform: translateX(0);
 | 
						|
      }
 | 
						|
      100% {
 | 
						|
        transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  .tech-stack-slider:hover .slider-track {
 | 
						|
    animation-play-state: paused;
 | 
						|
  }
 | 
						|
 | 
						|
  .skill-card {
 | 
						|
    transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 | 
						|
    position: relative;
 | 
						|
    overflow: hidden;
 | 
						|
  }
 | 
						|
 | 
						|
  .skill-card:hover {
 | 
						|
    z-index: 10;
 | 
						|
  }
 | 
						|
 | 
						|
  /* Reduce animation complexity on mobile */
 | 
						|
  @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%;
 | 
						|
    }
 | 
						|
  }
 | 
						|
</style>
 |