410 lines
14 KiB
TypeScript
410 lines
14 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect, useRef } from 'react';
|
||
|
|
import Image from 'next/image';
|
||
|
|
|
||
|
|
// 数字增长动画Hook
|
||
|
|
function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
|
||
|
|
const [mounted, setMounted] = useState(false);
|
||
|
|
const [count, setCount] = useState(end);
|
||
|
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||
|
|
const elementRef = useRef<HTMLDivElement>(null);
|
||
|
|
const startValueRef = useRef<number>(end);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setMounted(true);
|
||
|
|
startValueRef.current = Math.floor(end * (startRangePercent + Math.random() * 0.15));
|
||
|
|
setCount(startValueRef.current);
|
||
|
|
}, [end, startRangePercent]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!mounted) return;
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver(
|
||
|
|
(entries) => {
|
||
|
|
if (entries[0].isIntersecting && !hasAnimated) {
|
||
|
|
setHasAnimated(true);
|
||
|
|
const start = startValueRef.current;
|
||
|
|
const startTime = Date.now();
|
||
|
|
|
||
|
|
const timer = setInterval(() => {
|
||
|
|
const elapsed = Date.now() - startTime;
|
||
|
|
const progress = Math.min(elapsed / duration, 1);
|
||
|
|
|
||
|
|
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
|
||
|
|
const currentCount = Math.floor(start + (end - start) * easeOutCubic);
|
||
|
|
|
||
|
|
setCount(currentCount);
|
||
|
|
|
||
|
|
if (progress === 1) {
|
||
|
|
setCount(end);
|
||
|
|
clearInterval(timer);
|
||
|
|
}
|
||
|
|
}, 16);
|
||
|
|
|
||
|
|
return () => clearInterval(timer);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{ threshold: 0.1 }
|
||
|
|
);
|
||
|
|
|
||
|
|
if (elementRef.current) {
|
||
|
|
observer.observe(elementRef.current);
|
||
|
|
}
|
||
|
|
|
||
|
|
return () => observer.disconnect();
|
||
|
|
}, [end, duration, hasAnimated, mounted]);
|
||
|
|
|
||
|
|
return { count, elementRef };
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function HowItWorksSection() {
|
||
|
|
const [mounted, setMounted] = useState(false);
|
||
|
|
const [visible, setVisible] = useState(false);
|
||
|
|
const [activeStep, setActiveStep] = useState(1); // 默认第1步激活
|
||
|
|
const sectionRef = useRef<HTMLElement>(null);
|
||
|
|
|
||
|
|
// 数字动画
|
||
|
|
const investAmount = useCountUp(100000, 1500, 0.85);
|
||
|
|
const earnAmount = useCountUp(5150, 1500, 0.85);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setMounted(true);
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver(
|
||
|
|
(entries) => {
|
||
|
|
entries.forEach((entry) => {
|
||
|
|
if (entry.isIntersecting && !visible) {
|
||
|
|
setVisible(true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold: 0.1,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if (sectionRef.current) {
|
||
|
|
observer.observe(sectionRef.current);
|
||
|
|
}
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
if (sectionRef.current) {
|
||
|
|
observer.unobserve(sectionRef.current);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, [visible]);
|
||
|
|
|
||
|
|
const steps = [
|
||
|
|
{
|
||
|
|
number: 1,
|
||
|
|
title: 'Deposit & Mint WUSD',
|
||
|
|
description: 'Connect your wallet and deposit USDC to swap WUSD. This serves as the native stablecoin and gateway to the entire AssetX ecosystem.',
|
||
|
|
hasLine: true
|
||
|
|
},
|
||
|
|
{
|
||
|
|
number: 2,
|
||
|
|
title: 'Dual Investment Options',
|
||
|
|
description: 'Choose your strategy: use WUSD to purchase specific Yield-Bearing Assets for real-world returns, or provide liquidity in DeFi Pools to earn trading fees.',
|
||
|
|
hasLine: true
|
||
|
|
},
|
||
|
|
{
|
||
|
|
number: 3,
|
||
|
|
title: 'Earn & Boost',
|
||
|
|
description: 'Receive daily yield distributions. Use Asset Tokens as collateral to borrow WUSD and leverage up to 2.5x for maximized returns.',
|
||
|
|
hasLine: false
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section
|
||
|
|
ref={sectionRef}
|
||
|
|
className="bg-[#f9fafb] flex flex-col items-center justify-center flex-shrink-0 w-full relative"
|
||
|
|
style={{
|
||
|
|
borderStyle: 'solid',
|
||
|
|
borderColor: '#e5e7eb',
|
||
|
|
borderWidth: '1px 0px',
|
||
|
|
padding: '80px 0px'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* .container20 - Main container */}
|
||
|
|
<div className="flex flex-row items-start justify-between flex-shrink-0 relative w-[1200px]">
|
||
|
|
|
||
|
|
{/* Left Side - Steps (.container21) */}
|
||
|
|
<div className="flex flex-col gap-10 items-start justify-start flex-shrink-0 relative w-[520px]">
|
||
|
|
|
||
|
|
{/* Title (.heading-2) */}
|
||
|
|
<div className="flex flex-col items-start justify-start flex-shrink-0 relative">
|
||
|
|
<h2
|
||
|
|
className="text-[#111827] text-left relative font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '48px',
|
||
|
|
lineHeight: '120%',
|
||
|
|
letterSpacing: '-0.01em',
|
||
|
|
fontWeight: 700
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
How it works
|
||
|
|
</h2>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Steps Container (.frame-34) */}
|
||
|
|
<div className="flex flex-col gap-6 items-start justify-start flex-shrink-0 relative">
|
||
|
|
|
||
|
|
{steps.map((step, index) => {
|
||
|
|
const isActive = activeStep === step.number;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={step.number}
|
||
|
|
onClick={() => setActiveStep(step.number)}
|
||
|
|
className={`flex flex-row gap-6 items-start justify-start flex-shrink-0 relative cursor-pointer transition-all duration-300 ease-out hover:opacity-80 ${
|
||
|
|
mounted && visible
|
||
|
|
? 'translate-x-0 opacity-100'
|
||
|
|
: '-translate-x-12 opacity-0'
|
||
|
|
}`}
|
||
|
|
style={{
|
||
|
|
transitionDelay: `${index * 150}ms`
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* Number and Line Container */}
|
||
|
|
<div className="pt-2 flex flex-col items-center justify-start self-stretch flex-shrink-0 relative">
|
||
|
|
|
||
|
|
{/* Number Badge */}
|
||
|
|
{isActive ? (
|
||
|
|
// Active step (black background)
|
||
|
|
<div
|
||
|
|
className="bg-[#111827] rounded-[999px] flex items-center justify-center flex-shrink-0 w-8 h-[21.63px] relative transition-all duration-300"
|
||
|
|
style={{
|
||
|
|
padding: '0.31px 0px 1.32px 0px'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
className="text-[#fcfcfd] text-center relative flex items-center justify-center font-inter transition-all duration-300"
|
||
|
|
style={{
|
||
|
|
fontSize: '14px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 700
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{step.number}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
// Inactive step (border)
|
||
|
|
<div
|
||
|
|
className="bg-[#f9fafb] rounded-[9999px] border-2 border-[#d1d5db] flex items-center justify-center flex-shrink-0 w-8 h-[24.5px] relative transition-all duration-300"
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
className="text-[#9ca1af] text-center relative flex items-center justify-center font-inter transition-all duration-300"
|
||
|
|
style={{
|
||
|
|
fontSize: '14px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 700
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{step.number}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Connecting Line */}
|
||
|
|
{step.hasLine && (
|
||
|
|
<div className="pt-6 flex flex-col items-start justify-center flex-1 w-[2px] relative">
|
||
|
|
<div className="bg-[#e5e7eb] flex-1 w-[2px] relative" />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Text Content */}
|
||
|
|
<div
|
||
|
|
className="flex flex-col gap-2 items-start justify-start flex-1 relative"
|
||
|
|
style={{
|
||
|
|
paddingBottom: step.hasLine ? '32px' : '0px'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<h3
|
||
|
|
className="text-[#111827] text-left relative flex items-center justify-start font-inter transition-all duration-300"
|
||
|
|
style={{
|
||
|
|
fontSize: '24px',
|
||
|
|
lineHeight: '130%',
|
||
|
|
letterSpacing: '-0.005em',
|
||
|
|
fontWeight: isActive ? 700 : 500
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{step.title}
|
||
|
|
</h3>
|
||
|
|
<p
|
||
|
|
className="text-[#9ca1af] text-left relative self-stretch flex items-center justify-start font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '16px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 400
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{step.description}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Right Side - Calculator Card */}
|
||
|
|
<div
|
||
|
|
className={`calculator-card-container flex flex-col items-start justify-start flex-shrink-0 w-[558px] relative transition-all duration-700 ease-out ${
|
||
|
|
mounted && visible
|
||
|
|
? 'translate-y-0 opacity-100'
|
||
|
|
: 'translate-y-12 opacity-0'
|
||
|
|
}`}
|
||
|
|
style={{
|
||
|
|
transitionDelay: '450ms',
|
||
|
|
height: '439px'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* Coin Images Container - Rotated Card */}
|
||
|
|
<div
|
||
|
|
className="bg-[#111827] rounded-[16px] flex flex-row gap-4 items-start justify-start h-[162px] absolute z-0"
|
||
|
|
style={{
|
||
|
|
padding: '25px 25px 1px 25px',
|
||
|
|
left: '205.43px',
|
||
|
|
top: '15.96px',
|
||
|
|
boxShadow: '0px 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||
|
|
transformOrigin: '0 0',
|
||
|
|
transform: 'rotate(6.535deg) scale(1, 1)'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* USDC Logo */}
|
||
|
|
<Image
|
||
|
|
src="/usd-coin-usdc-logo-10.svg"
|
||
|
|
alt="USDC"
|
||
|
|
width={64}
|
||
|
|
height={64}
|
||
|
|
className="flex-shrink-0"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* WUSD Logo */}
|
||
|
|
<div className="flex-shrink-0 w-[66px] h-[66px] relative">
|
||
|
|
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-green-400 to-emerald-500" />
|
||
|
|
<Image
|
||
|
|
src="/image-220.png"
|
||
|
|
alt="WUSD"
|
||
|
|
width={66}
|
||
|
|
height={66}
|
||
|
|
className="absolute inset-0 rounded-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Calculator Card */}
|
||
|
|
<div
|
||
|
|
className="bg-white/85 backdrop-blur-md rounded-[24px] border border-[#e5e7eb] p-8 absolute left-0 top-[99px] w-full shadow-lg z-10"
|
||
|
|
style={{
|
||
|
|
transform: 'rotate(-3deg)',
|
||
|
|
transformOrigin: 'center center'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex flex-row items-center justify-between mb-6">
|
||
|
|
<div className="flex flex-row gap-3 items-center">
|
||
|
|
<div className="bg-[#111827] rounded-[12px] w-10 h-10 flex items-center justify-center">
|
||
|
|
<Image
|
||
|
|
src="/icon1.svg"
|
||
|
|
alt="Portfolio"
|
||
|
|
width={24}
|
||
|
|
height={24}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<span
|
||
|
|
className="text-[#111827] font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '16px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 500
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Portfolio Overview Simulator
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="bg-green-50 rounded-lg px-3 py-1">
|
||
|
|
<span className="text-green-600 font-inter text-sm font-bold">+5.2% APY</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Investment Amount */}
|
||
|
|
<div className="mb-4">
|
||
|
|
<div className="flex flex-col gap-2">
|
||
|
|
<span
|
||
|
|
className="text-[#9ca1af] font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '14px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 400
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
If you invest
|
||
|
|
</span>
|
||
|
|
<span
|
||
|
|
ref={investAmount.elementRef}
|
||
|
|
className="text-[#111827] font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '32px',
|
||
|
|
lineHeight: '120%',
|
||
|
|
letterSpacing: '-0.01em',
|
||
|
|
fontWeight: 700
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
${investAmount.count.toLocaleString()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Range Slider */}
|
||
|
|
<div className="mt-4 h-2 bg-[#e5e7eb] rounded-full relative">
|
||
|
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 bg-[#111827] rounded-full" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Earnings */}
|
||
|
|
<div className="mt-6 pt-6 border-t border-[#e5e7eb]">
|
||
|
|
<span
|
||
|
|
className="text-[#9ca1af] font-inter block mb-2"
|
||
|
|
style={{
|
||
|
|
fontSize: '14px',
|
||
|
|
lineHeight: '150%',
|
||
|
|
fontWeight: 400
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
You earn per year
|
||
|
|
</span>
|
||
|
|
<div className="flex flex-row gap-2 items-center">
|
||
|
|
<Image
|
||
|
|
src="/icon2.svg"
|
||
|
|
alt="Earn"
|
||
|
|
width={24}
|
||
|
|
height={24}
|
||
|
|
/>
|
||
|
|
<span
|
||
|
|
ref={earnAmount.elementRef}
|
||
|
|
className="text-green-600 font-inter"
|
||
|
|
style={{
|
||
|
|
fontSize: '32px',
|
||
|
|
lineHeight: '120%',
|
||
|
|
letterSpacing: '-0.01em',
|
||
|
|
fontWeight: 700
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
+${earnAmount.count.toLocaleString()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|