166 lines
6.6 KiB
TypeScript
166 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { Fragment } from "react";
|
|
import Image from "next/image";
|
|
import { motion, useReducedMotion } from "framer-motion";
|
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
|
import { viewport } from "@/lib/animations";
|
|
|
|
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
|
|
|
const PROOF_ITEMS = [
|
|
{ label: "Verified", text: "Local Stores", icon: "store" },
|
|
{ label: "2km", text: "Neighbourhood Coverage", icon: "location_on" },
|
|
{ label: "Growing", text: "Customer Community", icon: "mood" },
|
|
{ label: "Trusted", text: "Local Platform", icon: "star" },
|
|
];
|
|
|
|
const heroStagger = {
|
|
hidden: {},
|
|
visible: { transition: { staggerChildren: 0.12, delayChildren: 0.05 } },
|
|
};
|
|
|
|
const badgeVariant = {
|
|
hidden: { opacity: 0, y: 10, scale: 0.95 },
|
|
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.38, ease: EASE } },
|
|
};
|
|
|
|
// Lines stagger internally
|
|
const lineStagger = {
|
|
hidden: {},
|
|
visible: { transition: { staggerChildren: 0.09 } },
|
|
};
|
|
|
|
// True mask reveal: text slides up from below a clipping parent
|
|
const lineReveal = {
|
|
hidden: { y: "110%" },
|
|
visible: { y: 0, transition: { duration: 0.72, ease: EASE } },
|
|
};
|
|
|
|
const descVariant = {
|
|
hidden: { opacity: 0, y: 18 },
|
|
visible: { opacity: 1, y: 0, transition: { duration: 0.55, 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();
|
|
|
|
return (
|
|
<section className="bg-white select-none w-full">
|
|
<div className="relative overflow-hidden bg-[#FAF8FC]">
|
|
<motion.div
|
|
className="absolute inset-0 z-0"
|
|
initial={{ scale: 1 }}
|
|
animate={shouldReduce ? undefined : { scale: 1.06 }}
|
|
transition={{ duration: 28, ease: "linear", repeat: Infinity, repeatType: "reverse" }}
|
|
>
|
|
<Image
|
|
src="/images/home1.png"
|
|
alt=""
|
|
fill
|
|
sizes="100vw"
|
|
className="absolute inset-0 h-full w-full object-cover object-[62%_center] md:object-[58%_center] xl:object-center"
|
|
priority
|
|
aria-hidden="true"
|
|
/>
|
|
</motion.div>
|
|
<div className="absolute inset-0 z-[1] bg-[linear-gradient(180deg,rgba(0,0,0,0.14)_0%,rgba(0,0,0,0.05)_40%,rgba(0,0,0,0)_70%)] md:bg-[radial-gradient(ellipse_55%_55%_at_50%_38%,rgba(0,0,0,0.13)_0%,rgba(0,0,0,0.04)_55%,rgba(0,0,0,0)_80%)]" />
|
|
|
|
<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-20 lg:py-24">
|
|
|
|
<motion.div
|
|
className="relative z-20 flex w-full max-w-[780px] flex-col items-center text-center"
|
|
variants={heroStagger}
|
|
initial="hidden"
|
|
animate="visible"
|
|
>
|
|
{/* Badge */}
|
|
<motion.span
|
|
variants={badgeVariant}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6D28D9]/10 text-[#6D28D9] uppercase mb-6 w-fit"
|
|
>
|
|
Welcome to Nearle
|
|
</motion.span>
|
|
|
|
{/* H1: line-by-line mask reveal */}
|
|
<motion.div variants={lineStagger} className="font-display text-[34px] sm:text-5xl lg:text-[68px] xl:text-[76px] font-black text-[#16001D] tracking-[-0.05em]">
|
|
<div className="overflow-hidden pb-1">
|
|
<motion.div variants={lineReveal} className="leading-[0.95]">
|
|
Everything Local.
|
|
</motion.div>
|
|
</div>
|
|
<div className="overflow-hidden pb-1">
|
|
<motion.div variants={lineReveal} className="leading-[0.95]">
|
|
Everything <span className="text-[#6D28D9]">Near.</span>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Description */}
|
|
<motion.p
|
|
variants={descVariant}
|
|
className="max-w-[520px] text-base md:text-lg text-[#16001D] leading-relaxed font-bold mt-6 [text-shadow:0_2px_6px_rgba(0,0,0,0.12)]"
|
|
>
|
|
Discover verified local stores, exclusive offers, fast delivery, and a better way to shop within your neighbourhood.
|
|
</motion.p>
|
|
</motion.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"
|
|
variants={statStagger}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={viewport}
|
|
>
|
|
<div className="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="flex items-center gap-[18px] group cursor-default transition-transform duration-[250ms] hover:-translate-y-1 lg:flex-1 lg:py-2"
|
|
>
|
|
<div className="w-16 h-16 rounded-full bg-[#F5EDFF] text-[#6D28D9] flex items-center justify-center shrink-0 transition-colors duration-[250ms] group-hover:bg-[#E4D1FF]">
|
|
<MaterialIcon icon={item.icon} size={28} />
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl lg:text-[28px] font-black text-[#17051F] leading-none tracking-tight transition-colors duration-[250ms] group-hover: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"
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
);
|
|
}
|