Skip to content

Commit 18e3bff

Browse files
ANIMATIONS_UTILS
1 parent 39b275d commit 18e3bff

4 files changed

Lines changed: 803 additions & 0 deletions

File tree

src/hooks/useListAnimations.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Custom hook for managing sophisticated list and grid animations
3+
* Provides optimized animation controls for data-driven components
4+
* Performance optimized with reduced re-renders and debounced updates
5+
*/
6+
7+
import { useAnimation } from "framer-motion";
8+
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
9+
10+
export const useListAnimations = ({
11+
items = [],
12+
loading = false,
13+
error = null,
14+
animateOnDataChange = true,
15+
}) => {
16+
const controls = useAnimation();
17+
const containerRef = useRef(null);
18+
const [isAnimating, setIsAnimating] = useState(false);
19+
const previousItemsLength = useRef(items.length);
20+
const animationTimeoutRef = useRef(null);
21+
22+
// Debounced animation trigger to prevent excessive animations
23+
const triggerAnimation = useCallback(
24+
(variant) => {
25+
if (animationTimeoutRef.current) {
26+
clearTimeout(animationTimeoutRef.current);
27+
}
28+
29+
animationTimeoutRef.current = setTimeout(() => {
30+
setIsAnimating(true);
31+
controls.start(variant).then(() => {
32+
setIsAnimating(false);
33+
});
34+
}, 50); // Small debounce
35+
},
36+
[controls]
37+
);
38+
39+
// Optimized data change detection
40+
const hasSignificantChange = useMemo(() => {
41+
const lengthDiff = Math.abs(items.length - previousItemsLength.current);
42+
return lengthDiff > 0 && lengthDiff < 10; // Only animate for reasonable changes
43+
}, [items.length]);
44+
45+
// Animate when data changes - optimized
46+
useEffect(() => {
47+
if (!animateOnDataChange || !hasSignificantChange) return;
48+
49+
if (!loading && !error) {
50+
triggerAnimation("visible");
51+
}
52+
53+
previousItemsLength.current = items.length;
54+
}, [
55+
hasSignificantChange,
56+
loading,
57+
error,
58+
triggerAnimation,
59+
animateOnDataChange,
60+
]);
61+
62+
// Handle loading state - simplified
63+
useEffect(() => {
64+
if (loading) {
65+
setIsAnimating(true);
66+
} else if (!error && items.length > 0) {
67+
triggerAnimation("visible");
68+
}
69+
}, [loading, error, items.length, triggerAnimation]);
70+
71+
// Cleanup timeouts
72+
useEffect(() => {
73+
return () => {
74+
if (animationTimeoutRef.current) {
75+
clearTimeout(animationTimeoutRef.current);
76+
}
77+
};
78+
}, []);
79+
80+
// Optimized scroll to top
81+
const scrollToTop = useCallback((smooth = true) => {
82+
if (containerRef.current) {
83+
const behavior = smooth ? "smooth" : "auto";
84+
containerRef.current.scrollIntoView({
85+
behavior,
86+
block: "start",
87+
});
88+
}
89+
}, []);
90+
91+
// Simplified refresh without complex animations
92+
const triggerRefresh = useCallback(() => {
93+
triggerAnimation("visible");
94+
}, [triggerAnimation]);
95+
96+
return {
97+
controls,
98+
containerRef,
99+
isAnimating,
100+
scrollToTop,
101+
triggerRefresh,
102+
103+
// Animation states - memoized for performance
104+
shouldShowItems: !loading && !error && items.length > 0,
105+
shouldShowLoading: loading,
106+
shouldShowEmpty: !loading && !error && items.length === 0,
107+
shouldShowError: error && !loading,
108+
};
109+
};
110+
111+
// Simplified pagination animations
112+
export const usePaginationAnimations = (currentPage, totalPages) => {
113+
const controls = useAnimation();
114+
const [isPageChanging, setIsPageChanging] = useState(false);
115+
const previousPage = useRef(currentPage);
116+
117+
useEffect(() => {
118+
if (currentPage !== previousPage.current && currentPage > 0) {
119+
setIsPageChanging(true);
120+
121+
// Simple fade animation
122+
controls.start("visible").then(() => {
123+
setIsPageChanging(false);
124+
});
125+
}
126+
previousPage.current = currentPage;
127+
}, [currentPage, controls]);
128+
129+
return {
130+
controls,
131+
isPageChanging,
132+
shouldAnimate: totalPages > 1,
133+
};
134+
};
135+
136+
// Simplified search animations
137+
export const useSearchAnimations = (searchQuery, hasResults) => {
138+
const controls = useAnimation();
139+
const [isSearching, setIsSearching] = useState(false);
140+
const searchTimeoutRef = useRef(null);
141+
142+
const triggerSearchAnimation = useCallback(() => {
143+
if (searchTimeoutRef.current) {
144+
clearTimeout(searchTimeoutRef.current);
145+
}
146+
147+
setIsSearching(true);
148+
searchTimeoutRef.current = setTimeout(() => {
149+
controls.start("visible").then(() => {
150+
setIsSearching(false);
151+
});
152+
}, 100);
153+
}, [controls]);
154+
155+
useEffect(() => {
156+
if (searchQuery) {
157+
triggerSearchAnimation();
158+
}
159+
}, [searchQuery, hasResults, triggerSearchAnimation]);
160+
161+
useEffect(() => {
162+
return () => {
163+
if (searchTimeoutRef.current) {
164+
clearTimeout(searchTimeoutRef.current);
165+
}
166+
};
167+
}, []);
168+
169+
return {
170+
controls,
171+
isSearching,
172+
triggerSearchAnimation,
173+
};
174+
};

src/styles/animations.css

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/* Performance-optimized animations CSS */
2+
3+
/* Enable hardware acceleration for smooth animations */
4+
.motion-safe {
5+
will-change: transform;
6+
transform: translateZ(0);
7+
backface-visibility: hidden;
8+
}
9+
10+
/* Optimize repaints for grid layouts */
11+
.grid-container {
12+
contain: layout style paint;
13+
}
14+
15+
/* Smooth transitions for interactive elements */
16+
.smooth-hover {
17+
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
18+
}
19+
20+
.smooth-hover:hover {
21+
transform: translateY(-1px);
22+
}
23+
24+
/* Reduce motion for users who prefer it */
25+
@media (prefers-reduced-motion: reduce) {
26+
*,
27+
*::before,
28+
*::after {
29+
animation-duration: 0.01ms !important;
30+
animation-iteration-count: 1 !important;
31+
transition-duration: 0.01ms !important;
32+
scroll-behavior: auto !important;
33+
}
34+
35+
.motion-safe {
36+
will-change: auto;
37+
transform: none;
38+
}
39+
}
40+
41+
/* Optimize for GPU acceleration */
42+
.gpu-accelerated {
43+
transform: translate3d(0, 0, 0);
44+
will-change: transform;
45+
}
46+
47+
/* Loading optimizations */
48+
.loading-skeleton {
49+
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
50+
background-size: 200% 100%;
51+
animation: loading 1.5s infinite;
52+
}
53+
54+
@keyframes loading {
55+
0% {
56+
background-position: 200% 0;
57+
}
58+
100% {
59+
background-position: -200% 0;
60+
}
61+
}
62+
63+
/* Card optimizations */
64+
.card-container {
65+
contain: layout style;
66+
will-change: transform;
67+
}
68+
69+
.card-container:hover {
70+
transform: translateY(-1px) translateZ(0);
71+
}
72+
73+
/* Stagger animation optimization */
74+
.stagger-item {
75+
opacity: 0;
76+
transform: translateY(10px);
77+
animation: staggerIn 0.3s ease forwards;
78+
}
79+
80+
@keyframes staggerIn {
81+
to {
82+
opacity: 1;
83+
transform: translateY(0);
84+
}
85+
}
86+
87+
/* Apply stagger delays via CSS custom properties */
88+
.stagger-item:nth-child(1) {
89+
animation-delay: 0ms;
90+
}
91+
.stagger-item:nth-child(2) {
92+
animation-delay: 30ms;
93+
}
94+
.stagger-item:nth-child(3) {
95+
animation-delay: 60ms;
96+
}
97+
.stagger-item:nth-child(4) {
98+
animation-delay: 90ms;
99+
}
100+
.stagger-item:nth-child(5) {
101+
animation-delay: 120ms;
102+
}
103+
.stagger-item:nth-child(6) {
104+
animation-delay: 150ms;
105+
}
106+
.stagger-item:nth-child(7) {
107+
animation-delay: 180ms;
108+
}
109+
.stagger-item:nth-child(8) {
110+
animation-delay: 210ms;
111+
}
112+
.stagger-item:nth-child(9) {
113+
animation-delay: 240ms;
114+
}
115+
.stagger-item:nth-child(10) {
116+
animation-delay: 270ms;
117+
}
118+
.stagger-item:nth-child(11) {
119+
animation-delay: 300ms;
120+
}
121+
.stagger-item:nth-child(12) {
122+
animation-delay: 330ms;
123+
}

0 commit comments

Comments
 (0)