feat: replace slider with preline marquee
This commit is contained in:
@@ -5,57 +5,57 @@
|
||||
"": {
|
||||
"name": "site-profile",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "latest",
|
||||
"@astrojs/node": "latest",
|
||||
"@astrojs/rss": "latest",
|
||||
"@astrojs/sitemap": "latest",
|
||||
"@directus/sdk": "latest",
|
||||
"@iconify-json/mdi": "latest",
|
||||
"@iconify-json/pajamas": "latest",
|
||||
"@iconify-json/simple-icons": "latest",
|
||||
"@playform/compress": "latest",
|
||||
"@swup/astro": "latest",
|
||||
"@tailwindcss/postcss": "latest",
|
||||
"@tailwindcss/vite": "latest",
|
||||
"@types/unist": "latest",
|
||||
"astro": "latest",
|
||||
"astro-compress": "latest",
|
||||
"astro-icon": "latest",
|
||||
"dayjs": "latest",
|
||||
"markdown-it": "latest",
|
||||
"marked": "latest",
|
||||
"marked-shiki": "latest",
|
||||
"mdast-util-to-string": "latest",
|
||||
"photoswipe": "latest",
|
||||
"preline": "latest",
|
||||
"reading-time": "latest",
|
||||
"sharp": "latest",
|
||||
"sharp-ico": "latest",
|
||||
"shiki": "latest",
|
||||
"tailwindcss": "latest",
|
||||
"ultrahtml": "latest",
|
||||
"@astrojs/check": "0.9.9",
|
||||
"@astrojs/node": "10.1.1",
|
||||
"@astrojs/rss": "4.0.18",
|
||||
"@astrojs/sitemap": "3.7.2",
|
||||
"@directus/sdk": "21.3.0",
|
||||
"@iconify-json/mdi": "1.2.3",
|
||||
"@iconify-json/pajamas": "1.2.15",
|
||||
"@iconify-json/simple-icons": "1.2.83",
|
||||
"@playform/compress": "0.2.3",
|
||||
"@swup/astro": "1.8.0",
|
||||
"@tailwindcss/postcss": "4.3.0",
|
||||
"@tailwindcss/vite": "4.3.0",
|
||||
"@types/unist": "3.0.3",
|
||||
"astro": "6.3.6",
|
||||
"astro-compress": "2.4.1",
|
||||
"astro-icon": "1.1.5",
|
||||
"dayjs": "1.11.20",
|
||||
"markdown-it": "14.1.1",
|
||||
"marked": "18.0.4",
|
||||
"marked-shiki": "1.2.1",
|
||||
"mdast-util-to-string": "4.0.0",
|
||||
"photoswipe": "5.4.4",
|
||||
"preline": "4.2.0",
|
||||
"reading-time": "1.5.0",
|
||||
"sharp": "0.34.5",
|
||||
"sharp-ico": "0.1.5",
|
||||
"shiki": "4.1.0",
|
||||
"tailwindcss": "4.3.0",
|
||||
"ultrahtml": "1.6.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "latest",
|
||||
"@semantic-release/changelog": "latest",
|
||||
"@semantic-release/commit-analyzer": "latest",
|
||||
"@semantic-release/git": "latest",
|
||||
"@semantic-release/npm": "latest",
|
||||
"@semantic-release/release-notes-generator": "latest",
|
||||
"@tailwindcss/forms": "latest",
|
||||
"@tailwindcss/typography": "latest",
|
||||
"@types/markdown-it": "latest",
|
||||
"eslint": "latest",
|
||||
"eslint-config-prettier": "latest",
|
||||
"eslint-plugin-astro": "latest",
|
||||
"eslint-plugin-format": "latest",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-astro": "latest",
|
||||
"prettier-plugin-tailwindcss": "latest",
|
||||
"semantic-release": "latest",
|
||||
"semantic-release-export-data": "latest",
|
||||
"typescript": "latest",
|
||||
"typescript-eslint": "latest",
|
||||
"@saithodev/semantic-release-gitea": "2.1.0",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/npm": "13.1.5",
|
||||
"@semantic-release/release-notes-generator": "14.1.1",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-astro": "1.7.0",
|
||||
"eslint-plugin-format": "2.0.1",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-astro": "0.14.1",
|
||||
"prettier-plugin-tailwindcss": "0.8.0",
|
||||
"semantic-release": "25.0.3",
|
||||
"semantic-release-export-data": "1.2.0",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ const experiences = ((await directus.request(
|
||||
</time>
|
||||
</header>
|
||||
<div class="relative flex flex-col sm:col-span-12 pb-6">
|
||||
<div class="absolute bg-accent -translate-x-[1.71rem] rounded-full h-2 w-2 mt-3"/>
|
||||
<div class="absolute bg-accent translate-x-[-1.71rem] rounded-full h-2 w-2 mt-3"/>
|
||||
<h3>
|
||||
<div
|
||||
class="inline-flex items-center text-2xl leading-tight font-semibold"
|
||||
|
||||
@@ -18,150 +18,117 @@ const skills = ((await directus.request(
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||
Skills
|
||||
</h3>
|
||||
<div>
|
||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8 mask-[linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
|
||||
<!-- 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 class="smooth-reveal relative flex flex-col gap-2 mask-[linear-gradient(to_right,transparent,black_10%,black_90%,transparent)] before:pointer-events-none before:absolute before:inset-y-0 before:inset-s-0 before:z-2 before:w-20 after:pointer-events-none after:absolute after:inset-y-0 after:inset-e-0 after:z-2 after:w-20">
|
||||
<div class="flex">
|
||||
<div class="marquee-track-x animate-[marquee-x_120s_linear_infinite] hover:[animation-play-state:paused] flex w-max gap-4 py-10">
|
||||
<div class="flex gap-4">
|
||||
{[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||
return (
|
||||
<figure 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>
|
||||
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
|
||||
{skill.title}
|
||||
</h3>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</figure>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
function setupInfiniteScroll() {
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
if (!cards.length) return;
|
||||
}
|
||||
function initSkillCards() {
|
||||
const cards = document.querySelectorAll<HTMLElement>('.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 = '';
|
||||
});
|
||||
cards.forEach((card) => {
|
||||
// Desktop/Mouse events
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.setProperty('transition', 'transform 0.1s ease, box-shadow 0.1s ease', 'important');
|
||||
});
|
||||
} 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);
|
||||
});
|
||||
card.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
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.setProperty('transform', `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`, 'important');
|
||||
|
||||
const shadowX = (x - centerX) / 25;
|
||||
const shadowY = (y - centerY) / 25;
|
||||
card.style.setProperty('box-shadow', `
|
||||
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
||||
0 10px 20px rgba(0, 0, 0, 0.05)
|
||||
`, 'important');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.removeProperty('transform');
|
||||
card.style.removeProperty('box-shadow');
|
||||
card.style.removeProperty('transition');
|
||||
});
|
||||
|
||||
// Mobile/Touch events
|
||||
card.addEventListener('touchstart', () => {
|
||||
card.classList.add('is-touched');
|
||||
}, { passive: true });
|
||||
|
||||
card.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
card.classList.remove('is-touched');
|
||||
}, 300);
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Run exactly once on initial load, and again if navigating via Astro View Transitions
|
||||
initSkillCards();
|
||||
document.addEventListener('astro:after-swap', initSkillCards);
|
||||
</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 effects */
|
||||
.skill-card {
|
||||
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.skill-card:hover {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Reduce animation complexity on mobile */
|
||||
@media (max-width: 640px) {
|
||||
.skill-card {
|
||||
transition:
|
||||
|
||||
+29
-2
@@ -50,7 +50,7 @@
|
||||
--color-prose-blog-body: var(--color-neutral-700);
|
||||
--color-prose-blog-headings: var(--color-neutral-900);
|
||||
--color-prose-blog-links: var(--color-orange-300);
|
||||
|
||||
|
||||
--color-prose-blog-invert-body: var(--color-neutral-400);
|
||||
--color-prose-blog-invert-headings: var(--color-neutral-200);
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
:root {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
|
||||
--theme-transition: 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,33 @@
|
||||
border-color var(--theme-transition);
|
||||
}
|
||||
|
||||
/* Preline marquee */
|
||||
@keyframes marquee-x {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-50% - 0.5rem));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-y {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-50% - 0.5rem));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.marquee-track-x,
|
||||
.marquee-track-y {
|
||||
animation-duration: 1ms;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.prose blockquote {
|
||||
font-style: normal;
|
||||
|
||||
Reference in New Issue
Block a user