Files
nearle_web_nextjs/src/components/home/Hero.tsx
2026-07-24 17:04:09 +05:30

407 lines
18 KiB
TypeScript

"use client";
import { Fragment, useRef, type MouseEvent } from "react";
import Image from "next/image";
import Link from "next/link";
import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { MagneticButton } from "@/components/ui/MagneticButton";
import { HeroLogo } from "@/components/heroes/HeroLogo";
import { viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
// power4.out approximation, matches HeroLogo's entrance ease
const EASE_LOGO = [0.16, 1, 0.3, 1] as [number, number, number, number];
// 1. Verified Store SVG
function VerifiedStoreSVG({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center">
<motion.div
className="absolute h-10 w-10 rounded-full bg-[#6D28D9]/15 blur-md pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.6, 0.25] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" className="relative z-10 w-8 h-8 md:w-9 md:h-9">
<motion.path
d="M 8 16 L 36 16 L 33 8 L 11 8 Z"
fill="#A855F7"
animate={reduce ? {} : { y: [0, -2, 0] }}
transition={{ duration: 2.8, repeat: Infinity, ease: "easeInOut" }}
/>
<rect x="10" y="16" width="24" height="20" rx="2" fill="#6D28D9" />
<rect x="18" y="24" width="8" height="12" rx="1.5" fill="#E4D1FF" />
<motion.circle cx="32" cy="28" r="5" fill="#10B981"
animate={reduce ? {} : { scale: [0.85, 1.15, 0.85] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
/>
<path d="M 29.5 28 L 31.5 30 L 34.5 26.5" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
}
// 2. Neighborhood Pin SVG
function NeighborhoodPinSVG({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center">
<motion.div
className="absolute h-10 w-10 rounded-full bg-[#6D28D9]/15 blur-md pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.6, 0.25] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" className="relative z-10 w-8 h-8 md:w-9 md:h-9">
<motion.circle cx="22" cy="30" r="11" stroke="#A855F7" strokeWidth="1.5" strokeDasharray="3 3"
animate={reduce ? {} : { scale: [0.8, 1.3, 0.8], opacity: [0.3, 0.8, 0.3] }}
transition={{ duration: 2.8, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.path
d="M 22 6 C 15 6 9 12 9 19 C 9 28 22 38 22 38 C 22 38 35 28 35 19 C 35 12 29 6 22 6 Z"
fill="#6D28D9"
animate={reduce ? {} : { y: [0, -3, 0] }}
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
/>
<circle cx="22" cy="18" r="4" fill="#FFFFFF" />
</svg>
</div>
);
}
// 3. Happy Customers SVG
function HappyCustomerSVG({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center">
<motion.div
className="absolute h-10 w-10 rounded-full bg-[#EC4899]/15 blur-md pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.6, 0.25] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" className="relative z-10 w-8 h-8 md:w-9 md:h-9">
<motion.circle cx="22" cy="22" r="16" fill="#F472B6"
animate={reduce ? {} : { scale: [1, 1.06, 1] }}
transition={{ duration: 2.6, repeat: Infinity, ease: "easeInOut" }}
/>
<circle cx="16" cy="18" r="2.5" fill="#FFFFFF" />
<circle cx="28" cy="18" r="2.5" fill="#FFFFFF" />
<motion.path d="M 15 26 C 18 31 26 31 29 26" stroke="#FFFFFF" strokeWidth="2.5" strokeLinecap="round"
animate={reduce ? {} : { opacity: [0.8, 1, 0.8] }}
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
/>
</svg>
</div>
);
}
// 4. Trusted Star SVG
function TrustedStarSVG({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center">
<motion.div
className="absolute h-10 w-10 rounded-full bg-[#F59E0B]/20 blur-md pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.6, 0.25] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" className="relative z-10 w-8 h-8 md:w-9 md:h-9">
<motion.path
d="M 22 4 L 27.5 15.5 L 40 17 L 31 26 L 33.5 38 L 22 32 L 10.5 38 L 13 26 L 4 17 L 16.5 15.5 Z"
fill="#F59E0B"
stroke="#FDE047"
strokeWidth="1.5"
style={{ transformOrigin: "22px 22px" }}
animate={reduce ? {} : { scale: [1, 1.1, 1], rotate: [0, 4, 0, -4, 0] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
</svg>
</div>
);
}
const PROOF_ITEMS = [
{
label: "Verified",
text: "Local Stores",
dotColor: "bg-[#6D28D9]",
renderSVG: (reduce: boolean) => <VerifiedStoreSVG reduce={reduce} />,
},
{
label: "Neighborhood",
text: "Coverage",
dotColor: "bg-[#A855F7]",
renderSVG: (reduce: boolean) => <NeighborhoodPinSVG reduce={reduce} />,
},
{
label: "Growing",
text: "Happy Customers",
dotColor: "bg-[#EC4899]",
renderSVG: (reduce: boolean) => <HappyCustomerSVG reduce={reduce} />,
},
{
label: "Trusted",
text: "Local Platform",
dotColor: "bg-[#F59E0B]",
renderSVG: (reduce: boolean) => <TrustedStarSVG reduce={reduce} />,
},
];
// Shared cascade timing: logo settles at ~1.2s, then badge → heading → description
const badgeVariant = {
hidden: { opacity: 0, y: 25 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.9, ease: EASE } },
};
const lineStagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.13, delayChildren: 1.0 } },
};
const lineReveal = {
hidden: { y: 60, opacity: 0, filter: "blur(8px)" },
visible: { y: 0, opacity: 1, filter: "blur(0px)", transition: { duration: 0.9, ease: EASE_LOGO } },
};
const descVariant = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.4, ease: EASE } },
};
const ctaVariant = {
hidden: { opacity: 0, y: 22 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.6, ease: EASE } },
};
const statStagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
};
const statItem = {
hidden: { opacity: 0, y: 14 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
export function Hero() {
const shouldReduce = useReducedMotion();
const sectionRef = useRef<HTMLDivElement>(null);
// Subtle mouse parallax: background drifts 5px toward the cursor
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const springX = useSpring(mouseX, { stiffness: 60, damping: 20, mass: 0.5 });
const springY = useSpring(mouseY, { stiffness: 60, damping: 20, mass: 0.5 });
const bgParallaxX = useTransform(springX, [-0.5, 0.5], [-5, 5]);
const bgParallaxY = useTransform(springY, [-0.5, 0.5], [-5, 5]);
function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
if (shouldReduce) return;
const rect = e.currentTarget.getBoundingClientRect();
mouseX.set((e.clientX - rect.left) / rect.width - 0.5);
mouseY.set((e.clientY - rect.top) / rect.height - 0.5);
}
function handleMouseLeave() {
mouseX.set(0);
mouseY.set(0);
}
return (
<section className="bg-white select-none w-full">
<div
ref={sectionRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="relative overflow-hidden bg-[#FAF8FC]"
>
<motion.div
className="absolute inset-0 z-0"
style={shouldReduce ? undefined : { x: bgParallaxX, y: bgParallaxY }}
initial={shouldReduce ? undefined : { scale: 1.08 }}
animate={{ scale: 1 }}
transition={{ duration: 3, ease: "easeOut" }}
>
<Image
src="/images/hero/home1.webp"
alt=""
fill
sizes="100vw"
className="absolute inset-0 h-full w-full object-cover object-[70%_center] md:object-[66%_center] xl:object-[60%_center]"
priority
fetchPriority="high"
aria-hidden="true"
/>
</motion.div>
<motion.div
className="absolute inset-0 z-[1] bg-black/40 md:bg-[radial-gradient(ellipse_100%_100%_at_50%_45%,rgba(0,0,0,0.6)_0%,rgba(0,0,0,0.4)_40%,rgba(0,0,0,0.1)_100%)]"
initial={shouldReduce ? undefined : { opacity: 0.6 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.2, ease: "easeOut" }}
/>
<div className="page-container relative z-10 flex min-h-[560px] items-center justify-center px-6 pt-20 pb-10 sm:px-8 sm:pt-24 md:min-h-[85vh] md:py-6 lg:py-8">
{/* `compact` logo spacing: this hero now carries CTAs and the evolution
strip inside it, the same reason BrandsHero uses it. */}
<div className="relative z-20 flex w-full max-w-[860px] flex-col items-center text-center px-6 py-6 md:px-14 md:py-2 lg:py-3">
<HeroLogo spacing="compact" />
{/* Badge */}
<motion.span
variants={badgeVariant}
initial="hidden"
animate="visible"
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6D28D9] shadow-md uppercase mb-5 w-fit"
>
Welcome to Nearle
</motion.span>
{/* H1: line-by-line reveal. Two lines, with the closing word
highlighted in the Nearle purple. */}
<motion.h1
variants={lineStagger}
initial="hidden"
animate="visible"
className="font-display font-extrabold text-white tracking-[-0.045em] [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
>
<span className="block overflow-hidden pb-1">
<motion.span
variants={lineReveal}
className="block text-[30px] leading-[1.06] text-balance sm:text-[40px] lg:text-[50px] xl:text-[56px]"
>
Everything Local.
</motion.span>
</span>
<span className="block overflow-hidden pb-1">
<motion.span
variants={lineReveal}
className="block text-[30px] leading-[1.06] text-balance sm:text-[40px] lg:text-[50px] xl:text-[56px]"
>
Everything <span className="text-[#A855F7]">Near.</span>
</motion.span>
</span>
</motion.h1>
{/* Description */}
<motion.div
variants={descVariant}
initial="hidden"
animate="visible"
className="mt-5 max-w-[700px] [text-shadow:0_2px_16px_rgba(0,0,0,0.6)]"
>
<p className="text-base md:text-lg text-white/90 leading-[1.6] font-medium">
Discover verified local stores, exclusive offers, fast delivery, and a
better way to shop within your neighbourhood.
</p>
</motion.div>
{/* CTAs */}
<motion.div
variants={ctaVariant}
initial="hidden"
animate="visible"
className="mt-7 flex flex-wrap items-center justify-center gap-3.5"
>
<MagneticButton>
<Link
href="#ai-commerce"
className="flex items-center gap-2 rounded-xl bg-[#6330D6] px-7 py-3.5 text-sm font-black text-white shadow-lg transition-all duration-200 hover:bg-[#5225b8] hover:shadow-xl active:scale-95 sm:px-8"
>
<MaterialIcon icon="auto_awesome" size={18} />
<span>Discover Nearby Stores</span>
</Link>
</MagneticButton>
<MagneticButton>
<Link
href="#nearle-in-action"
className="flex items-center gap-2 rounded-xl border border-white/25 bg-white/10 px-7 py-3.5 text-sm font-black text-white backdrop-blur-md transition-all duration-200 hover:bg-white/20 active:scale-95 sm:px-8"
>
<MaterialIcon icon="play_circle" size={18} />
<span>See How Nearle Works</span>
</Link>
</MagneticButton>
</motion.div>
</div>
</div>
</div>
{/* ─── BOTTOM PROOF STATS ROW ─── */}
<div className="border-t border-[#EEE7F4]/80 bg-white">
<div className="px-6 sm:px-10 max-w-[1600px] mx-auto py-6 md:py-8 lg:py-10">
<motion.div
className="bg-white rounded-[32px] border border-[#E9D8FD] shadow-[0_10px_35px_rgba(124,58,237,0.08),0_2px_8px_rgba(124,58,237,0.05)] px-6 py-6 md:px-8 md:py-7 lg:px-12 lg:py-8 relative overflow-hidden group"
variants={statStagger}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{/* Floating Particle Glow Dots on Hover */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden z-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100">
<motion.span
className="absolute h-1.5 w-1.5 rounded-full bg-[#6D28D9] shadow-[0_0_8px_#6D28D9]"
style={{ left: "12%", top: "35%" }}
animate={shouldReduce ? {} : { y: [0, -10, 0], opacity: [0.3, 0.9, 0.3] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.span
className="absolute h-1.5 w-1.5 rounded-full bg-[#A855F7] shadow-[0_0_8px_#A855F7]"
style={{ left: "38%", top: "60%" }}
animate={shouldReduce ? {} : { y: [0, -12, 0], opacity: [0.3, 0.9, 0.3] }}
transition={{ duration: 4.8, delay: 0.8, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.span
className="absolute h-1.5 w-1.5 rounded-full bg-[#EC4899] shadow-[0_0_8px_#EC4899]"
style={{ left: "62%", top: "25%" }}
animate={shouldReduce ? {} : { y: [0, -10, 0], opacity: [0.3, 0.9, 0.3] }}
transition={{ duration: 4.2, delay: 1.2, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.span
className="absolute h-1.5 w-1.5 rounded-full bg-[#F59E0B] shadow-[0_0_8px_#F59E0B]"
style={{ left: "85%", top: "55%" }}
animate={shouldReduce ? {} : { y: [0, -12, 0], opacity: [0.3, 0.9, 0.3] }}
transition={{ duration: 5, delay: 0.4, repeat: Infinity, ease: "easeInOut" }}
/>
</div>
<div className="relative z-10 flex flex-col gap-6 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-6 lg:flex lg:flex-row lg:items-center lg:justify-between lg:gap-0">
{PROOF_ITEMS.map((item, idx) => (
<Fragment key={item.label}>
<motion.div
variants={statItem}
className="relative flex items-center gap-[18px] group/item cursor-default transition-transform duration-[250ms] hover:-translate-y-1 lg:flex-1 lg:py-2"
>
{/* Corner Hover Glowing Dot on Item */}
<div className="absolute -top-1 right-2 z-20 flex items-center gap-2 opacity-0 transition-opacity duration-300 group-hover/item:opacity-100">
<span className="relative flex h-2 w-2">
<span className={`absolute inline-flex h-full w-full animate-ping rounded-full ${item.dotColor} opacity-75`} />
<span className={`relative inline-flex h-2 w-2 rounded-full ${item.dotColor} shadow-[0_0_6px_currentColor]`} />
</span>
</div>
<div className="w-16 h-16 rounded-full bg-[#F5EDFF] text-[#6D28D9] flex items-center justify-center shrink-0 transition-all duration-[250ms] group-hover/item:bg-[#E4D1FF] group-hover/item:scale-110">
{item.renderSVG(!!shouldReduce)}
</div>
<div>
<div className="text-2xl lg:text-[28px] font-black text-[#17051F] leading-none tracking-tight transition-colors duration-[250ms] group-hover/item:text-[#6D28D9]">
{item.label}
</div>
<div className="text-sm lg:text-[15px] font-medium text-[#6B7280] mt-1.5">
{item.text}
</div>
</div>
</motion.div>
{idx < PROOF_ITEMS.length - 1 && (
<div
className="hidden lg:block w-px h-[72px] bg-[#E9D8FD] shrink-0 self-center lg:mx-4 xl:mx-6"
aria-hidden="true"
/>
)}
</Fragment>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}