diff --git a/src/app/page.tsx b/src/app/page.tsx index cbecf3a1c..e8598c49c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,12 +5,13 @@ import { BusinessIntegrate, FAQs, Hero, - Marquee, NoFees, SecurityBuiltIn, SendInSeconds, YourMoney, } from '@/components/LandingPage' +import { MarqueeComp } from '@/components/Global/MarqueeWrapper' +import { HandThumbsUp } from '@/assets' import { useFooterVisibility } from '@/context/footerVisibility' import { useEffect, useState, useRef } from 'react' @@ -30,7 +31,6 @@ export default function LandingPage() { const hero = { heading: 'Peanut', marquee: { - visible: true, message: ['No fees', 'Instant', '24/7', 'USD', 'EUR', 'CRYPTO', 'GLOBAL', 'SELF-CUSTODIAL'], }, primaryCta: { @@ -158,7 +158,11 @@ export default function LandingPage() { } }, [isScrollFrozen, animationComplete, shrinkingPhase, hasGrown]) - const marqueeProps = { visible: hero.marquee.visible, message: hero.marquee.message } + const marqueeProps = { + message: hero.marquee.message, + imageSrc: HandThumbsUp.src, + backgroundColor: 'bg-secondary-1', + } return ( @@ -168,21 +172,35 @@ export default function LandingPage() { buttonVisible={buttonVisible} buttonScale={buttonScale} /> - +
+ +
- +
+ +
- +
+ +
- +
+ +
- +
+ +
- +
+ +
- +
+ +
) } diff --git a/src/components/LandingPage/businessIntegrate.tsx b/src/components/LandingPage/businessIntegrate.tsx index 45e4d2fb9..ab0b983d3 100644 --- a/src/components/LandingPage/businessIntegrate.tsx +++ b/src/components/LandingPage/businessIntegrate.tsx @@ -4,15 +4,9 @@ import { Star } from '@/assets' import scribble from '@/assets/scribble.svg' import peanutMeans from '@/assets/illustrations/peanut-means.svg' -// Define the background color as a constant -const businessBgColor = '#90A8ED' - export function BusinessIntegrate() { return ( -
+
{/* Main heading */}
diff --git a/src/components/LandingPage/hero.tsx b/src/components/LandingPage/hero.tsx index 5b11a7c27..ad8111848 100644 --- a/src/components/LandingPage/hero.tsx +++ b/src/components/LandingPage/hero.tsx @@ -1,11 +1,11 @@ import { ButterySmoothGlobalMoney, PeanutGuyGIF, Sparkle } from '@/assets' import { Stack } from '@chakra-ui/react' import { motion } from 'framer-motion' -import { useEffect, useState } from 'react' import { twMerge } from 'tailwind-merge' import { CloudImages, HeroImages } from './imageAssets' import Image from 'next/image' import instantlySendReceive from '@/assets/illustrations/instantly-send-receive.svg' +import { useResizeHandler, useScrollHandler, createButtonAnimation } from '@/hooks/useAnimations' type CTAButton = { label: string @@ -22,29 +22,6 @@ type HeroProps = { } // Helper functions moved outside component for better performance -const getInitialAnimation = (variant: 'primary' | 'secondary') => ({ - opacity: 0, - translateY: 4, - translateX: variant === 'primary' ? 0 : 4, - rotate: 0.75, -}) - -const getAnimateAnimation = (variant: 'primary' | 'secondary', buttonVisible?: boolean, buttonScale?: number) => ({ - opacity: buttonVisible ? 1 : 0, - translateY: buttonVisible ? 0 : 20, - translateX: buttonVisible ? (variant === 'primary' ? 0 : 0) : 20, - rotate: buttonVisible ? 0 : 1, - scale: buttonScale || 1, - pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const), -}) - -const getHoverAnimation = (variant: 'primary' | 'secondary') => ({ - translateY: 6, - translateX: variant === 'primary' ? 0 : 3, - rotate: 0.75, -}) - -const transitionConfig = { type: 'spring', damping: 15 } as const const getButtonContainerClasses = (variant: 'primary' | 'secondary') => `relative z-20 mt-8 md:mt-12 ${variant === 'primary' ? 'mx-auto w-fit' : 'right-[calc(50%-120px)]'}` @@ -100,38 +77,34 @@ const renderArrows = (variant: 'primary' | 'secondary', arrowOpacity: number, bu ) export function Hero({ heading, primaryCta, secondaryCta, buttonVisible, buttonScale = 1 }: HeroProps) { - const [screenWidth, setScreenWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200) - const [scrollY, setScrollY] = useState(0) - - useEffect(() => { - const handleResize = () => { - setScreenWidth(window.innerWidth) - } - - const handleScroll = () => { - setScrollY(window.scrollY) - } - - handleResize() - window.addEventListener('resize', handleResize) - window.addEventListener('scroll', handleScroll) + const screenWidth = useResizeHandler() + const scrollY = useScrollHandler() + + const primaryButtonAnimation = createButtonAnimation( + buttonVisible || false, + buttonScale, + { translateX: 0 }, + { translateX: 0 } + ) - return () => { - window.removeEventListener('resize', handleResize) - window.removeEventListener('scroll', handleScroll) - } - }, []) + const secondaryButtonAnimation = createButtonAnimation( + buttonVisible || false, + buttonScale, + { translateX: 4 }, + { translateX: 3 } + ) const renderCTAButton = (cta: CTAButton, variant: 'primary' | 'secondary') => { const arrowOpacity = 1 // Always visible + const animation = variant === 'primary' ? primaryButtonAnimation : secondaryButtonAnimation return ( {/* {renderSparkle(variant)} */} @@ -161,7 +134,10 @@ export function Hero({ heading, primaryCta, secondaryCta, buttonVisible, buttonS alt="Peanut Guy" /> - + { - const handleResize = () => { - setScreenWidth(window.innerWidth) - } + const starAnimation1 = createStarAnimation(0.2, 5, { rotate: 22 }, { rotate: 22 }) + const starAnimation2 = createStarAnimation(0.4, 5, { translateY: 28, translateX: -5, rotate: -17 }, { rotate: -17 }) + const starAnimation3 = createStarAnimation(0.6, 5, { rotate: 22 }, { rotate: 22 }) + const starAnimation4 = createStarAnimation(0.8, 5, { translateY: 15, translateX: -5, rotate: -7 }, { rotate: -7 }) + const starAnimation5 = createStarAnimation(1.0, 5, { translateY: 25, translateX: -5, rotate: -5 }, { rotate: -5 }) - handleResize() - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) - - const createCloudAnimation = (side: 'left' | 'right', top: string, width: number, speed: number) => { - const vpWidth = screenWidth || 1080 - const totalDistance = vpWidth + width - - return { - initial: { x: side === 'left' ? -width : vpWidth }, - animate: { x: side === 'left' ? vpWidth : -width }, - transition: { - ease: 'linear', - duration: totalDistance / speed, - repeat: Infinity, - }, - } - } + const cloud1Animation = createCloudAnimation('left', 200, 35, screenWidth) + const cloud2Animation = createCloudAnimation('right', 220, 40, screenWidth) return (
@@ -47,14 +32,14 @@ export function NoFees() { alt="Floating Border Cloud" className="absolute left-0" style={{ top: '20%', width: 200 }} - {...createCloudAnimation('left', '20%', 200, 35)} + {...cloud1Animation} />
@@ -63,52 +48,32 @@ export function NoFees() { {/* Main stylized headline */}
diff --git a/src/components/LandingPage/sendInSeconds.tsx b/src/components/LandingPage/sendInSeconds.tsx index 1e7a1c688..4c17f8939 100644 --- a/src/components/LandingPage/sendInSeconds.tsx +++ b/src/components/LandingPage/sendInSeconds.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' import { motion } from 'framer-motion' import Image from 'next/image' import borderCloud from '@/assets/illustrations/border-cloud.svg' @@ -6,59 +6,27 @@ import exclamations from '@/assets/illustrations/exclamations.svg' import payZeroFees from '@/assets/illustrations/pay-zero-fees.svg' import mobileSendInSeconds from '@/assets/illustrations/mobile-send-in-seconds.svg' import { Star, Sparkle } from '@/assets' +import { + useResizeHandler, + createButtonAnimation, + createStarAnimation, + createCloudAnimation, +} from '@/hooks/useAnimations' export function SendInSeconds() { - const [screenWidth, setScreenWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200) + const screenWidth = useResizeHandler() - useEffect(() => { - const handleResize = () => { - setScreenWidth(window.innerWidth) - } + const buttonAnimation = createButtonAnimation(true, 1) - handleResize() - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) + const starAnimation1 = createStarAnimation(0.2) + const starAnimation2 = createStarAnimation(0.4, 5, { translateY: 25, translateX: -5, rotate: -10 }, { rotate: -10 }) + const starAnimation3 = createStarAnimation(0.6, 5, { translateY: 18, translateX: 5, rotate: -22 }, { rotate: -22 }) + const starAnimation4 = createStarAnimation(0.8, 5, { translateY: 22, translateX: -5, rotate: 12 }, { rotate: 12 }) - const createCloudAnimation = (side: 'left' | 'right', width: number, speed: number) => { - const vpWidth = screenWidth || 1080 - const totalDistance = vpWidth + width - - return { - initial: { x: side === 'left' ? -width : vpWidth }, - animate: { x: side === 'left' ? vpWidth : -width }, - transition: { - ease: 'linear', - duration: totalDistance / speed, - repeat: Infinity, - }, - } - } - - // Button helper functions adapted from hero.tsx - const getInitialAnimation = () => ({ - opacity: 0, - translateY: 4, - translateX: 0, - rotate: 0.75, - }) - - const getAnimateAnimation = (buttonVisible: boolean, buttonScale: number = 1) => ({ - opacity: buttonVisible ? 1 : 0, - translateY: buttonVisible ? 0 : 20, - translateX: buttonVisible ? 0 : 20, - rotate: buttonVisible ? 0 : 1, - scale: buttonScale, - pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const), - }) - - const getHoverAnimation = () => ({ - translateY: 6, - translateX: 0, - rotate: 0.75, - }) - - const transitionConfig = { type: 'spring', damping: 15 } as const + const cloud1Animation = createCloudAnimation('left', 320, 35, screenWidth) + const cloud2Animation = createCloudAnimation('right', 200, 40, screenWidth) + const cloud3Animation = createCloudAnimation('left', 180, 45, screenWidth) + const cloud4Animation = createCloudAnimation('right', 320, 30, screenWidth) const getButtonClasses = () => `btn bg-white fill-n-1 text-n-1 hover:bg-white/90 px-9 md:px-11 py-4 md:py-10 text-lg md:text-2xl btn-shadow-primary-4` @@ -114,28 +82,28 @@ export function SendInSeconds() { alt="Floating Border Cloud" className="absolute left-0" style={{ top: '15%', width: 320 }} - {...createCloudAnimation('left', 320, 35)} + {...cloud1Animation} />
@@ -143,52 +111,38 @@ export function SendInSeconds() { - {/* Exclamations */} - Exclamations + {/* Exclamations - only show on screens larger than 1780px */} + {screenWidth > 1740 && ( + Exclamations + )} {/* Main content */}
@@ -224,10 +178,10 @@ export function SendInSeconds() {
TRY NOW diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 000000000..ae2595f26 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,8 @@ +export { + useScrollHandler, + useResizeHandler, + createButtonAnimation, + createStarAnimation, + createCloudAnimation, + createAccordionAnimation, +} from './useAnimations' diff --git a/src/hooks/useAnimations.ts b/src/hooks/useAnimations.ts new file mode 100644 index 000000000..1682fb1fa --- /dev/null +++ b/src/hooks/useAnimations.ts @@ -0,0 +1,146 @@ +import { useEffect, useState, useMemo } from 'react' + +// Event handler hooks +export function useScrollHandler(throttleMs = 16) { + const [scrollY, setScrollY] = useState(0) + + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null + + const handleScroll = () => { + if (timeoutId) return + + timeoutId = setTimeout(() => { + setScrollY(window.scrollY) + timeoutId = null + }, throttleMs) + } + + handleScroll() + window.addEventListener('scroll', handleScroll) + + return () => { + window.removeEventListener('scroll', handleScroll) + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }, [throttleMs]) + + return scrollY +} + +export function useResizeHandler(throttleMs = 16) { + const [screenWidth, setScreenWidth] = useState(1080) + + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null + + const handleResize = () => { + if (timeoutId) return + + timeoutId = setTimeout(() => { + setScreenWidth(window.innerWidth) + timeoutId = null + }, throttleMs) + } + + handleResize() + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }, [throttleMs]) + + return screenWidth +} + +// Animation factory functions +export function createButtonAnimation(buttonVisible: boolean, buttonScale = 1, customInitial = {}, customHover = {}) { + return { + initial: { + opacity: 0, + translateY: 4, + translateX: 0, + rotate: 0.75, + ...customInitial, + }, + animate: { + opacity: buttonVisible ? 1 : 0, + translateY: buttonVisible ? 0 : 20, + translateX: buttonVisible ? 0 : 20, + rotate: buttonVisible ? 0 : 1, + scale: buttonScale, + pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const), + }, + hover: { + translateY: 6, + translateX: 0, + rotate: 0.75, + ...customHover, + }, + transition: { type: 'spring', damping: 15 }, + } +} + +export function createStarAnimation(delay = 0.2, damping = 5, customInitial = {}, customAnimate = {}) { + return { + initial: { + opacity: 0, + translateY: 20, + translateX: 5, + rotate: 45, + ...customInitial, + }, + whileInView: { + opacity: 1, + translateY: 0, + translateX: 0, + rotate: 45, + ...customAnimate, + }, + transition: { + type: 'spring', + damping, + delay, + }, + } +} + +export function createCloudAnimation(side: 'left' | 'right', width: number, speed: number, screenWidth: number) { + const vpWidth = screenWidth || 1080 + const totalDistance = vpWidth + width + + return { + initial: { x: side === 'left' ? -width : vpWidth }, + animate: { x: side === 'left' ? vpWidth : -width }, + transition: { + ease: 'linear', + duration: totalDistance / speed, + repeat: Infinity, + }, + } +} + +export function createAccordionAnimation(isOpen: boolean, duration = 0.4, iconRotation = 180) { + return { + container: { + animate: { height: 'auto' }, + transition: { duration }, + }, + icon: { + animate: { rotate: isOpen ? iconRotation : 0 }, + transition: { duration: duration * 0.75, transformOrigin: 'center' }, + }, + content: { + initial: { height: 0, opacity: 0 }, + animate: { height: 'auto', opacity: 1 }, + exit: { height: 0, opacity: 0 }, + transition: { duration: duration * 0.5 }, + }, + } +}