407 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}
|