refactor(style): utilize tailwind instead of CSS

This commit is contained in:
Alfarish Fizikri
2025-07-24 15:59:18 +07:00
parent 12f72fe164
commit 67bae8e23a

View File

@@ -1,76 +1,29 @@
<template> <template>
<div <div
:class="['bounceCardsContainer', className]" :class="['relative flex items-center justify-center', className]"
:style="{ :style="{
position: 'relative',
width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth, width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,
height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight, height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}" }"
> >
<div <div
v-for="(src, idx) in images" v-for="(src, idx) in images"
:key="idx" :key="idx"
:class="`card card-${idx}`" ref="cardRefs"
:style="{ class="absolute w-[200px] aspect-square border-[5px] border-white rounded-[25px] overflow-hidden shadow-[0_4px_10px_rgba(0,0,0,0.2)] bg-[#f8f9fa] opacity-0"
position: 'absolute', :style="{ transform: transformStyles[idx] ?? 'none' }"
width: '200px',
aspectRatio: '1',
border: '5px solid #fff',
borderRadius: '25px',
overflow: 'hidden',
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)',
transform: transformStyles[idx] ?? 'none',
backgroundColor: '#f8f9fa'
}"
@mouseenter="() => pushSiblings(idx)" @mouseenter="() => pushSiblings(idx)"
@mouseleave="resetSiblings" @mouseleave="resetSiblings"
> >
<div <div v-if="!imageLoaded[idx]" class="absolute inset-0 z-[1] flex items-center justify-center bg-black/80">
v-if="!imageLoaded[idx]" <div class="w-[75px] h-[75px] border-[3px] border-gray-400 border-t-[#27FF64] rounded-full animate-spin"></div>
class="placeholder"
:style="{
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1
}"
>
<div
class="loading-spinner"
:style="{
width: '75px',
height: '75px',
border: '3px solid #a3a3a3',
borderTop: '3px solid #27FF64',
borderRadius: '50%',
}"
></div>
</div> </div>
<img <img
class="image" class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ease-in-out z-[2]"
:src="src" :src="src"
:alt="`card-${idx}`" :alt="`card-${idx}`"
:style="{ :style="{ opacity: imageLoaded[idx] ? 1 : 0 }"
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: imageLoaded[idx] ? 1 : 0,
transition: 'opacity 0.3s ease',
zIndex: 2
}"
@load="() => onImageLoad(idx)" @load="() => onImageLoad(idx)"
@error="() => onImageError(idx)" @error="() => onImageError(idx)"
/> />
@@ -79,7 +32,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
export interface BounceCardsProps { export interface BounceCardsProps {
@@ -113,6 +66,7 @@ const props = withDefaults(defineProps<BounceCardsProps>(), {
}); });
const imageLoaded = ref(new Array(props.images.length).fill(false)); const imageLoaded = ref(new Array(props.images.length).fill(false));
const cardRefs = ref<HTMLElement[]>([]);
const getNoRotationTransform = (transformStr: string): string => { const getNoRotationTransform = (transformStr: string): string => {
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr); const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
@@ -133,9 +87,7 @@ const getPushedTransform = (baseTransform: string, offsetX: number): string => {
const newX = currentX + offsetX; const newX = currentX + offsetX;
return baseTransform.replace(translateRegex, `translate(${newX}px)`); return baseTransform.replace(translateRegex, `translate(${newX}px)`);
} else { } else {
return baseTransform === 'none' return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;
? `translate(${offsetX}px)`
: `${baseTransform} translate(${offsetX}px)`;
} }
}; };
@@ -143,13 +95,13 @@ const pushSiblings = (hoveredIdx: number) => {
if (!props.enableHover) return; if (!props.enableHover) return;
props.images.forEach((_, i) => { props.images.forEach((_, i) => {
gsap.killTweensOf(`.card-${i}`); gsap.killTweensOf(cardRefs.value[i]);
const baseTransform = props.transformStyles[i] || 'none'; const baseTransform = props.transformStyles[i] || 'none';
if (i === hoveredIdx) { if (i === hoveredIdx) {
const noRotationTransform = getNoRotationTransform(baseTransform); const noRotationTransform = getNoRotationTransform(baseTransform);
gsap.to(`.card-${i}`, { gsap.to(cardRefs.value[i], {
transform: noRotationTransform, transform: noRotationTransform,
duration: 0.4, duration: 0.4,
ease: 'back.out(1.4)', ease: 'back.out(1.4)',
@@ -158,11 +110,10 @@ const pushSiblings = (hoveredIdx: number) => {
} else { } else {
const offsetX = i < hoveredIdx ? -160 : 160; const offsetX = i < hoveredIdx ? -160 : 160;
const pushedTransform = getPushedTransform(baseTransform, offsetX); const pushedTransform = getPushedTransform(baseTransform, offsetX);
const distance = Math.abs(hoveredIdx - i); const distance = Math.abs(hoveredIdx - i);
const delay = distance * 0.05; const delay = distance * 0.05;
gsap.to(`.card-${i}`, { gsap.to(cardRefs.value[i], {
transform: pushedTransform, transform: pushedTransform,
duration: 0.4, duration: 0.4,
ease: 'back.out(1.4)', ease: 'back.out(1.4)',
@@ -177,9 +128,9 @@ const resetSiblings = () => {
if (!props.enableHover) return; if (!props.enableHover) return;
props.images.forEach((_, i) => { props.images.forEach((_, i) => {
gsap.killTweensOf(`.card-${i}`); gsap.killTweensOf(cardRefs.value[i]);
const baseTransform = props.transformStyles[i] || 'none'; const baseTransform = props.transformStyles[i] || 'none';
gsap.to(`.card-${i}`, { gsap.to(cardRefs.value[i], {
transform: baseTransform, transform: baseTransform,
duration: 0.4, duration: 0.4,
ease: 'back.out(1.4)', ease: 'back.out(1.4)',
@@ -196,59 +147,31 @@ const onImageError = (idx: number) => {
imageLoaded.value[idx] = true; imageLoaded.value[idx] = true;
}; };
onMounted(() => { const playEntranceAnimation = () => {
gsap.killTweensOf(cardRefs.value);
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
gsap.fromTo( gsap.fromTo(
'.card', cardRefs.value,
{ scale: 0 }, { scale: 0, opacity: 0 },
{ {
scale: 1, scale: 1,
opacity: 1,
stagger: props.animationStagger, stagger: props.animationStagger,
ease: props.easeType, ease: props.easeType,
delay: props.animationDelay delay: props.animationDelay
} }
); );
};
onMounted(playEntranceAnimation);
watch(() => props.images, async () => {
await nextTick();
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
playEntranceAnimation();
}); });
onUnmounted(() => { onUnmounted(() => {
gsap.killTweensOf('.card'); gsap.killTweensOf(cardRefs.value);
props.images.forEach((_, i) => {
gsap.killTweensOf(`.card-${i}`);
});
}); });
</script> </script>
<style scoped>
.bounceCardsContainer {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 400px;
height: 400px;
}
.card {
position: absolute;
width: 200px;
aspect-ratio: 1;
border: 5px solid #fff;
border-radius: 25px;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
background-color: transparent !important;
}
.card .image {
width: 100%;
height: 100%;
object-fit: cover;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg);}
100% { transform: rotate(360deg);}
}
</style>