Skip to content

Commit c556f6f

Browse files
committed
[e.cash] Power Payments section improvements
Summary: The "Powering Internet-scale Payments" section on the homepage had some performance issues. The animations were not very smooth, and sometimes items would flash or jitter. Added some changes to improve this. Seems much smoother now Test Plan: preview and check To compare with current, run locally and test, then switch to master and hard refresh and test Reviewers: #bitcoin_abc, bytesofman Reviewed By: #bitcoin_abc, bytesofman Subscribers: bytesofman Differential Revision: https://reviews.bitcoinabc.org/D18511
1 parent 25ea5d6 commit c556f6f

2 files changed

Lines changed: 142 additions & 88 deletions

File tree

web/e.cash_v2/app/components/Home/PoweringPayments.tsx

Lines changed: 141 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
"use client";
6-
import { useRef, useState, useEffect } from "react";
6+
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
77
import ContentContainer from "../Atoms/ContentContainer";
88
import Image from "next/image";
99
import { cn } from "../../utils/cn";
10-
import { motion } from "framer-motion";
10+
import { motion, AnimatePresence } from "framer-motion";
1111

1212
const eCashFeatures = [
1313
{
@@ -37,11 +37,8 @@ const eCashFeatures = [
3737
];
3838

3939
export default function PoweringPayments() {
40-
const [rawHoveredIndex, setRawHoveredIndex] = useState<number | null>(null);
4140
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
4241
const [activeIndex, setActiveIndex] = useState<number>(0);
43-
const [prevHoveredIndex, setPrevHoveredIndex] = useState<number | null>(null);
44-
const [fadeKey, setFadeKey] = useState<number>(0);
4542
const containerRef = useRef<HTMLDivElement>(null);
4643

4744
type FeatureCardProps = {
@@ -51,22 +48,49 @@ export default function PoweringPayments() {
5148
};
5249

5350
const FeatureCard = ({ name, index, onHover }: FeatureCardProps) => {
51+
const isHovered = hoveredIndex === index;
52+
5453
return (
55-
<div
54+
<motion.div
5655
onMouseEnter={() => onHover(index)}
5756
onMouseLeave={() => onHover(null)}
58-
className="custom-box relative flex h-[160px] w-full max-w-[270px] cursor-pointer items-center rounded-xl bg-gradient-to-br from-[#0E0E21] to-[#19193B] p-8 hover:from-[#130D3F] hover:to-[#7316d1]"
57+
className="custom-box relative flex h-[160px] w-full max-w-[270px] cursor-pointer items-center rounded-xl bg-gradient-to-br from-[#0E0E21] to-[#19193B] p-8"
58+
whileHover={{
59+
background: "linear-gradient(135deg, #130D3F 0%, #7316d1 100%)",
60+
transition: { duration: 0.2, ease: "easeOut" },
61+
}}
5962
>
60-
{index === 2 && hoveredIndex === 2 && (
61-
<div className="absolute left-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]" />
63+
{/* Connection lines with smooth animations */}
64+
{index === 2 && (
65+
<motion.div
66+
className="absolute left-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]"
67+
initial={{ scaleX: 0, opacity: 0 }}
68+
animate={{
69+
scaleX: isHovered ? 1 : 0,
70+
opacity: isHovered ? 1 : 0,
71+
}}
72+
transition={{ duration: 0.3, ease: "easeOut" }}
73+
style={{ transformOrigin: "left center" }}
74+
/>
6275
)}
63-
{index === 5 && hoveredIndex === 5 && (
64-
<div className="absolute right-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]" />
76+
{index === 5 && (
77+
<motion.div
78+
className="absolute right-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]"
79+
initial={{ scaleX: 0, opacity: 0 }}
80+
animate={{
81+
scaleX: isHovered ? 1 : 0,
82+
opacity: isHovered ? 1 : 0,
83+
}}
84+
transition={{ duration: 0.3, ease: "easeOut" }}
85+
style={{ transformOrigin: "right center" }}
86+
/>
6587
)}
88+
6689
<div className="flex flex-col items-start">
6790
<span className="text-secondaryText text-sm">0{index}</span>
6891
<h4 className="text-lg font-bold leading-snug">{name}</h4>
6992
</div>
93+
7094
<div className="absolute bottom-0 right-0 h-full w-[60%]">
7195
<Image
7296
src={`/${name.toLowerCase()}.png`}
@@ -75,51 +99,30 @@ export default function PoweringPayments() {
7599
className="object-contain object-bottom"
76100
/>
77101
</div>
78-
</div>
102+
</motion.div>
79103
);
80104
};
81105

82-
useEffect(() => {
106+
// Memoized scroll handler to prevent unnecessary re-renders
107+
const handleScroll = useCallback(() => {
83108
const el = containerRef.current;
84109
if (!el) return;
85110

86-
const handleScroll = () => {
87-
const child = el.children[0] as HTMLElement;
88-
const cardWidth = child.offsetWidth + 16; // gap-4 = 1rem = 16px
89-
const index = Math.round(el.scrollLeft / cardWidth);
90-
setActiveIndex(index);
91-
};
92-
93-
el.addEventListener("scroll", handleScroll);
94-
return () => el.removeEventListener("scroll", handleScroll);
111+
const child = el.children[0] as HTMLElement;
112+
const cardWidth = child.offsetWidth + 16; // gap-4 = 1rem = 16px
113+
const index = Math.round(el.scrollLeft / cardWidth);
114+
setActiveIndex(index);
95115
}, []);
96116

97117
useEffect(() => {
98-
if (hoveredIndex !== prevHoveredIndex) {
99-
setFadeKey((prev) => prev + 1);
100-
setPrevHoveredIndex(hoveredIndex);
101-
}
102-
}, [hoveredIndex, prevHoveredIndex]);
103-
104-
useEffect(() => {
105-
let timeout: NodeJS.Timeout;
106-
107-
if (rawHoveredIndex === null) {
108-
// Long delay when hovering off all tiles
109-
timeout = setTimeout(() => {
110-
setHoveredIndex(null);
111-
}, 500);
112-
} else {
113-
// Short delay between switching tiles
114-
timeout = setTimeout(() => {
115-
setHoveredIndex(rawHoveredIndex);
116-
}, 300);
117-
}
118+
const el = containerRef.current;
119+
if (!el) return;
118120

119-
return () => clearTimeout(timeout);
120-
}, [rawHoveredIndex]);
121+
el.addEventListener("scroll", handleScroll, { passive: true });
122+
return () => el.removeEventListener("scroll", handleScroll);
123+
}, [handleScroll]);
121124

122-
const scrollToIndex = (index: number) => {
125+
const scrollToIndex = useCallback((index: number) => {
123126
const el = containerRef.current;
124127
if (!el) return;
125128

@@ -130,7 +133,51 @@ export default function PoweringPayments() {
130133
inline: "start",
131134
});
132135
}
133-
};
136+
}, []);
137+
138+
// Memoized content to prevent unnecessary re-renders
139+
const centerContent = useMemo(
140+
() => (
141+
<div className="absolute flex h-full w-full items-center justify-center overflow-hidden">
142+
<AnimatePresence mode="wait">
143+
{hoveredIndex !== null ? (
144+
<motion.div
145+
key={`text-${hoveredIndex}`}
146+
initial={{ opacity: 0, y: 20 }}
147+
animate={{ opacity: 1, y: 0 }}
148+
exit={{ opacity: 0, y: -20 }}
149+
transition={{ duration: 0.3, ease: "easeOut" }}
150+
className="absolute inset-0 flex items-center justify-center"
151+
>
152+
<div>
153+
<p className="bg-background/70 max-w-[330px] rounded border border-white/10 p-6 text-center backdrop-blur-sm">
154+
{eCashFeatures[hoveredIndex - 1]?.text}
155+
</p>
156+
</div>
157+
</motion.div>
158+
) : (
159+
<motion.div
160+
key="title"
161+
initial={{ opacity: 0, y: 20 }}
162+
animate={{ opacity: 1, y: 0 }}
163+
exit={{ opacity: 0, y: -20 }}
164+
transition={{ duration: 0.3, ease: "easeOut" }}
165+
className="absolute inset-0 flex items-center justify-center"
166+
>
167+
<div>
168+
<h2 className="max-w-[330px] text-center">
169+
Powering{" "}
170+
<span className="pink-gradient-text">Internet-Scale</span>{" "}
171+
Payments
172+
</h2>
173+
</div>
174+
</motion.div>
175+
)}
176+
</AnimatePresence>
177+
</div>
178+
),
179+
[hoveredIndex]
180+
);
134181

135182
return (
136183
<ContentContainer className="mb-30 max-w-[1400px]">
@@ -147,60 +194,57 @@ export default function PoweringPayments() {
147194
key={index}
148195
index={index + 1}
149196
name={feature.name}
150-
onHover={setRawHoveredIndex}
197+
onHover={setHoveredIndex}
151198
/>
152199
))}
153200
</div>
201+
154202
<div className="relative z-10 aspect-[550/721] w-full lg:h-[721px] lg:w-[550px] lg:shrink-0">
155203
<Image
156204
src="/powering-payments-bg.jpg"
157205
alt="Powering Internet-Scale Payments"
158206
fill
159207
className="object-contain"
160208
/>
161-
{hoveredIndex !== null && (
162-
<Image
163-
key={hoveredIndex}
164-
src={`/overlay-${hoveredIndex}.png`}
165-
alt="Powering Internet-Scale Payments"
166-
fill
167-
className="hidden object-contain lg:inline-block"
168-
/>
169-
)}
170209

171-
<div className="absolute flex h-full w-full items-center justify-center overflow-hidden">
172-
<div
173-
key={fadeKey}
174-
className="animate-fade-in-up absolute inset-0 flex items-center justify-center opacity-0"
175-
>
176-
<div>
177-
{hoveredIndex !== null ? (
178-
<p className="bg-background/70 max-w-[330px] rounded border border-white/10 p-6 text-center">
179-
{eCashFeatures[hoveredIndex - 1]?.text}
180-
</p>
181-
) : (
182-
<h2 className="max-w-[330px] text-center">
183-
Powering{" "}
184-
<span className="pink-gradient-text">Internet-Scale</span>{" "}
185-
Payments
186-
</h2>
187-
)}
188-
</div>
189-
</div>
190-
</div>
210+
{/* Overlay image with smooth transitions */}
211+
<AnimatePresence mode="wait">
212+
{hoveredIndex !== null && (
213+
<motion.div
214+
key={`overlay-${hoveredIndex}`}
215+
initial={{ opacity: 0 }}
216+
animate={{ opacity: 1 }}
217+
exit={{ opacity: 0 }}
218+
transition={{ duration: 0.3, ease: "easeOut" }}
219+
className="absolute inset-0"
220+
>
221+
<Image
222+
src={`/overlay-${hoveredIndex}.png`}
223+
alt="Powering Internet-Scale Payments"
224+
fill
225+
className="hidden object-contain lg:inline-block"
226+
/>
227+
</motion.div>
228+
)}
229+
</AnimatePresence>
230+
231+
{centerContent}
191232
</div>
233+
192234
<div className="hidden grow basis-0 flex-col items-start justify-center gap-2 self-stretch lg:flex [&>*:nth-child(2)]:self-end">
193235
{eCashFeatures.slice(3, 6).map((feature, index) => (
194236
<FeatureCard
195237
key={index}
196238
index={index + 4}
197239
name={feature.name}
198-
onHover={setRawHoveredIndex}
240+
onHover={setHoveredIndex}
199241
/>
200242
))}
201243
</div>
202244
</motion.div>
245+
203246
<div className="bg-background relative z-20 m-auto mt-[-50px] h-[20px] w-full max-w-[750px] rounded border border-b-0 border-x-[#211a36] border-t-[#211a36] lg:mt-0" />
247+
204248
<div className="bg-background relative z-20 flex w-full items-center justify-between gap-4 px-2 lg:hidden">
205249
{eCashFeatures.map((feature, index) => (
206250
<div
@@ -221,26 +265,36 @@ export default function PoweringPayments() {
221265
</div>
222266
<span
223267
className={cn(
224-
"text-sm",
268+
"text-sm transition-opacity duration-200",
225269
activeIndex === index ? "opacity-100" : "opacity-40"
226270
)}
227271
>
228272
0{index + 1}
229273
</span>
230274

231275
<div className="relative mt-2 aspect-square w-[15px]">
232-
{activeIndex === index && (
233-
<Image
234-
src="/arrow-up.png"
235-
alt="arrow"
236-
fill
237-
className="object-contain"
238-
/>
239-
)}
276+
<AnimatePresence>
277+
{activeIndex === index && (
278+
<motion.div
279+
initial={{ opacity: 0, scale: 0.8 }}
280+
animate={{ opacity: 1, scale: 1 }}
281+
exit={{ opacity: 0, scale: 0.8 }}
282+
transition={{ duration: 0.2, ease: "easeOut" }}
283+
>
284+
<Image
285+
src="/arrow-up.png"
286+
alt="arrow"
287+
fill
288+
className="object-contain"
289+
/>
290+
</motion.div>
291+
)}
292+
</AnimatePresence>
240293
</div>
241294
</div>
242295
))}
243296
</div>
297+
244298
<div
245299
className="scrollx-container m-auto mt-3 flex w-[95%] snap-x snap-mandatory gap-4 overflow-x-auto scroll-smooth pb-3 lg:hidden"
246300
ref={containerRef}
@@ -249,7 +303,7 @@ export default function PoweringPayments() {
249303
<div
250304
key={index}
251305
className={cn(
252-
"w-[85%] shrink-0 snap-start transition-opacity",
306+
"w-[85%] shrink-0 snap-start transition-all duration-300 ease-out",
253307
activeIndex === index ? "opacity-100" : "opacity-40"
254308
)}
255309
>

web/e.cash_v2/app/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ h2 {
205205

206206
.custom-box {
207207
box-shadow: 0px 2px 0px 0px rgba(255, 255, 255, 0.1) inset;
208-
backdrop-filter: blur(14px);
208+
transition: all 0.2s ease-out;
209209
}
210210

211211
.custom-filter {

0 commit comments

Comments
 (0)