227 lines
6.3 KiB
Plaintext
227 lines
6.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' as any, {
|
|
fields: ['*'],
|
|
sort: ['-date_created'],
|
|
})
|
|
)) as unknown) as Skill[];
|
|
---
|
|
|
|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
|
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
|
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 card-base transform hover:-translate-y-2 hover:scale-105 transition-all duration-300 mx-2 min-w-55 sm:mx-4 sm:min-w-70">
|
|
<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="flex items-center justify-center rounded-lg text-primary">
|
|
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
|
</div>
|
|
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
|
|
{skill.title}
|
|
</h3>
|
|
</div>
|
|
<span class=" bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 font-mono text-xs sm:text-sm rounded-full px-2 sm:px-2.5 py-0.5 sm:py-1">
|
|
{skill.level}%
|
|
</span>
|
|
</div>
|
|
<div class="relative bg-stone-500/20 dark:bg-stone-500/20 rounded-full h-1.5 sm:h-2 w-full overflow-hidden">
|
|
<div
|
|
class="progress-bar-animate bg-linear-to-r from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full transition-all duration-1000"
|
|
style={`width: ${skill.level}%`}
|
|
/>
|
|
</div>
|
|
<div class="flex justify-between text-secondary font-mono text-[10px] mt-1 sm:mt-2 sm:text-xs">
|
|
<span>Beginner</span>
|
|
<span>Advanced</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<!-- Gradient overlays -->
|
|
<div class="bg-linear-to-r from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 left-0 z-10 w-12 sm:w-24"/>
|
|
<div class="bg-linear-to-l from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 right-0 z-10 w-12 sm:w-24"/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
document.addEventListener('astro:page-load', () => {
|
|
function setupInfiniteScroll() {
|
|
const cards = document.querySelectorAll('.skill-card');
|
|
if (!cards.length) return;
|
|
}
|
|
|
|
setupInfiniteScroll();
|
|
|
|
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>
|
|
/* Specific css to enable sliding effect */
|
|
.slider-track {
|
|
width: fit-content;
|
|
animation: scroll 40s linear infinite;
|
|
}
|
|
|
|
@keyframes scroll {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateX(calc(-220px * 6 - 16px * 6));
|
|
}
|
|
}
|
|
|
|
@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));
|
|
}
|
|
}
|
|
}
|
|
|
|
.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>
|