update animation pages
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, scaleIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
@@ -68,17 +72,17 @@ export function BusinessSection() {
|
||||
<div className="pointer-events-none absolute -left-16 top-24 h-64 w-64 rounded-full bg-primary/5 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -right-20 bottom-10 h-64 w-64 rounded-full bg-secondary/5 blur-3xl" />
|
||||
|
||||
{/*
|
||||
* Grid layout:
|
||||
* mobile (<768px) : 1 col — text → cards (1/row) → success story [via CSS order]
|
||||
* tablet (768–1279px) : 1 col — text → success story → cards 2×2 [via CSS order]
|
||||
* desktop (≥1280px) : 2 col — [left: text + success story] | [right: cards 2×2]
|
||||
*/}
|
||||
<div className="relative grid gap-6 grid-cols-1 md:gap-7
|
||||
xl:grid-cols-[420px_1fr] xl:gap-8 xl:items-start">
|
||||
|
||||
{/* ── 1. Left text content (always col-1 row-1) ── */}
|
||||
<div className="min-w-0 xl:col-start-1 xl:row-start-1">
|
||||
{/* ── 1. Left text content ── */}
|
||||
<motion.div
|
||||
className="min-w-0 xl:col-start-1 xl:row-start-1"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{/* Badge */}
|
||||
<div className="flex w-fit items-center gap-2 rounded-full border border-secondary/20 bg-secondary/5 px-3 py-1.5">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-secondary/15">
|
||||
@@ -133,20 +137,23 @@ export function BusinessSection() {
|
||||
<MaterialIcon icon="arrow_forward" size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── 2. Feature cards — 2×2 grid (right col on desktop) ──
|
||||
mobile : order-2 → appears after text, before success story
|
||||
tablet : order-3 → appears after text + success story
|
||||
desktop : explicit col-2, row-span-2
|
||||
*/}
|
||||
<div className="min-w-0
|
||||
{/* ── 2. Feature cards ── */}
|
||||
<motion.div
|
||||
className="min-w-0
|
||||
order-2 md:order-3
|
||||
xl:order-none xl:col-start-2 xl:row-start-1 xl:row-span-2
|
||||
grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
grid grid-cols-1 gap-4 md:grid-cols-2"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{FEATURES.map((feature) => (
|
||||
<article
|
||||
<motion.article
|
||||
key={feature.title}
|
||||
variants={fadeUp}
|
||||
className="merchant-feature-card flex min-w-0 flex-col overflow-hidden rounded-[1.5rem] border border-primary/10 bg-white shadow-[0_4px_24px_rgba(22,0,29,0.07)]"
|
||||
>
|
||||
<div className="p-5">
|
||||
@@ -174,20 +181,22 @@ export function BusinessSection() {
|
||||
className="block h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── 3. Success story ──
|
||||
mobile : order-3 → appears after feature cards
|
||||
tablet : order-2 → appears after text, before cards
|
||||
desktop : explicit col-1, row-2 (below text in left column)
|
||||
*/}
|
||||
<div className="min-w-0
|
||||
{/* ── 3. Success story ── */}
|
||||
<motion.div
|
||||
className="min-w-0
|
||||
order-3 md:order-2
|
||||
xl:order-none xl:col-start-1 xl:row-start-2
|
||||
overflow-hidden rounded-[1.5rem] border border-primary/10 bg-white/90 shadow-[0_4px_24px_rgba(22,0,29,0.07)]
|
||||
grid grid-cols-[100px_minmax(0,1fr)]">
|
||||
grid grid-cols-[100px_minmax(0,1fr)]"
|
||||
variants={scaleIn}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="relative min-h-[128px] overflow-hidden bg-brand-accent-light">
|
||||
<Image
|
||||
src="/images/nearle-merchant/success-owner.webp"
|
||||
@@ -208,14 +217,20 @@ export function BusinessSection() {
|
||||
Nearle helped us reach more customers in our area and manage orders effortlessly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ── Benefits strip ── */}
|
||||
<div className="relative mt-6 overflow-hidden rounded-[1.5rem] border border-primary/10 bg-white/90 shadow-[0_4px_24px_rgba(22,0,29,0.06)]">
|
||||
<motion.div
|
||||
className="relative mt-6 overflow-hidden rounded-[1.5rem] border border-primary/10 bg-white/90 shadow-[0_4px_24px_rgba(22,0,29,0.06)]"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 divide-y sm:divide-y-0 sm:divide-x divide-primary/8 xl:divide-x xl:divide-y-0">
|
||||
{BENEFITS.map((b) => (
|
||||
<div key={b.title} className="flex items-start gap-3 p-5">
|
||||
<motion.div key={b.title} variants={fadeIn} className="flex items-start gap-3 p-5">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-secondary/10 text-secondary">
|
||||
<MaterialIcon icon={b.icon} size={20} />
|
||||
</div>
|
||||
@@ -223,10 +238,10 @@ export function BusinessSection() {
|
||||
<div className="text-sm font-black text-brand-dark">{b.title}</div>
|
||||
<div className="mt-0.5 text-xs leading-relaxed text-on-surface-variant">{b.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import {
|
||||
fadeUp,
|
||||
slideInRight,
|
||||
staggerContainer,
|
||||
viewport,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const PROOF_ITEMS = [
|
||||
{ label: "Verified", text: "Local Stores", icon: "store" },
|
||||
@@ -13,7 +22,7 @@ export function Hero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gradient-to-b from-[#FAF8FC] to-[#FFFFFF] select-none w-full">
|
||||
{/* ─── DECORATIONS (BEHIND CONTENT) ─── */}
|
||||
|
||||
|
||||
{/* 1. Large pale lavender partial circle (top-left) */}
|
||||
<div className="hidden md:block absolute top-[-100px] left-[-100px] w-[350px] h-[350px] rounded-full border-[1.5px] border-[#6D28D9]/15 bg-[#6D28D9]/5 opacity-[0.55] z-0 pointer-events-none" />
|
||||
|
||||
@@ -21,10 +30,10 @@ export function Hero() {
|
||||
<div className="hidden lg:block absolute right-[22%] top-[38%] w-[84px] h-[84px] rounded-full bg-[#6D28D9] z-0 shadow-[0_8px_32px_rgba(109,40,217,0.3)] pointer-events-none animate-pulse" />
|
||||
|
||||
{/* 3. Striped purple circle (lower-middle collage area) */}
|
||||
<div
|
||||
<div
|
||||
className="hidden lg:block absolute left-[45%] bottom-[15%] w-[100px] h-[100px] rounded-full opacity-80 z-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(45deg, rgba(109, 40, 217, 0.2) 0px, rgba(109, 40, 217, 0.2) 2px, transparent 2px, transparent 8px)'
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(45deg, rgba(109, 40, 217, 0.2) 0px, rgba(109, 40, 217, 0.2) 2px, transparent 2px, transparent 8px)'
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -48,24 +57,41 @@ export function Hero() {
|
||||
|
||||
{/* ─── TWO-COLUMN CONTENT GRID ─── */}
|
||||
<div className="page-container relative grid min-h-[650px] grid-cols-1 lg:grid-cols-[0.92fr_1.08fr] items-center py-16 md:py-20 lg:py-24 z-10 gap-12 lg:gap-6">
|
||||
|
||||
{/* LEFT COLUMN: Editorial Typography & Copy */}
|
||||
<div className="flex flex-col text-left max-w-xl z-20 relative">
|
||||
<span 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
|
||||
</span>
|
||||
|
||||
<h1 className="font-display text-4xl sm:text-5xl lg:text-[68px] xl:text-[76px] font-black text-[#16001D] leading-[0.95] tracking-[-0.05em]">
|
||||
{/* LEFT COLUMN: Editorial Typography & Copy */}
|
||||
<motion.div
|
||||
className="flex flex-col text-left max-w-xl z-20 relative"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
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>
|
||||
|
||||
<motion.h1
|
||||
variants={fadeUp}
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[68px] xl:text-[76px] font-black text-[#16001D] leading-[0.95] tracking-[-0.05em]"
|
||||
>
|
||||
Everything Local.
|
||||
<br />
|
||||
Everything <span className="text-[#6D28D9]">Near.</span>
|
||||
</h1>
|
||||
</motion.h1>
|
||||
|
||||
<p className="max-w-[480px] text-base md:text-lg text-[#6D6172] leading-relaxed font-bold mt-6">
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="max-w-[480px] text-base md:text-lg text-[#6D6172] leading-relaxed font-bold mt-6"
|
||||
>
|
||||
Discover verified local stores, exclusive offers, fast delivery, and a better way to shop within your neighbourhood.
|
||||
</p>
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto">
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
|
||||
>
|
||||
<Link
|
||||
href="/people"
|
||||
className="bg-[#6D28D9] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-all duration-200 active:scale-[0.98]"
|
||||
@@ -78,20 +104,25 @@ export function Hero() {
|
||||
>
|
||||
Get the App
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* RIGHT COLUMN: Creative Overlapping Strips Collage */}
|
||||
<div className="w-full z-20">
|
||||
|
||||
<motion.div
|
||||
className="w-full z-20"
|
||||
variants={slideInRight}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
|
||||
{/* DESKTOP LAYOUT (lg and above) */}
|
||||
<div className="hidden lg:block relative h-[500px] lg:h-[560px] w-full">
|
||||
{/* left image: absolute left-[10%] top-[20%] w-[170px] h-[360px] */}
|
||||
<div className="absolute left-[10%] top-[20%] w-[170px] h-[360px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10 transition-transform duration-300 hover:scale-[1.02]">
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Local grocery products"
|
||||
fill
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Local grocery products"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="170px"
|
||||
/>
|
||||
@@ -113,10 +144,10 @@ export function Hero() {
|
||||
|
||||
{/* right image: absolute right-[2%] top-[18%] w-[180px] h-[390px] */}
|
||||
<div className="absolute right-[2%] top-[18%] w-[180px] h-[390px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10 transition-transform duration-300 hover:scale-[1.02]">
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Local customer shopping"
|
||||
fill
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Local customer shopping"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="180px"
|
||||
/>
|
||||
@@ -126,44 +157,50 @@ export function Hero() {
|
||||
{/* MOBILE/TABLET COLLAGE (below lg) */}
|
||||
<div className="lg:hidden flex items-center justify-center gap-2 mt-2 w-full relative h-[280px]">
|
||||
<div className="relative w-[95px] h-[200px] rounded-2xl overflow-hidden border border-white shadow-md z-10">
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Grocery products"
|
||||
fill
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Grocery products"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="95px"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-[115px] h-[240px] rounded-2xl border-4 border-white shadow-lg bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-3 -mx-5 z-20">
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={100}
|
||||
height={100}
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={100}
|
||||
height={100}
|
||||
className="object-contain max-h-[90px] w-auto animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-[105px] h-[220px] rounded-2xl overflow-hidden border border-white shadow-md z-10">
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Customer"
|
||||
fill
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Customer"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="105px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ─── BOTTOM PROOF STATS ROW ─── */}
|
||||
<div className="border-t border-[#EEE7F4]/80 bg-white">
|
||||
<div className="page-container py-8 grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 relative z-20">
|
||||
<motion.div
|
||||
className="page-container py-8 grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 relative z-20"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{PROOF_ITEMS.map((item, idx) => (
|
||||
<div key={item.label} className="flex items-center gap-4 text-left md:justify-center relative">
|
||||
<motion.div key={item.label} variants={fadeUp} className="flex items-center gap-4 text-left md:justify-center relative">
|
||||
{/* Subtle vertical separator for desktop, except for the first item */}
|
||||
{idx > 0 && (
|
||||
<div className="hidden md:block absolute left-0 top-1/2 -translate-y-1/2 w-[1px] h-10 bg-[#EEE7F4]" />
|
||||
@@ -179,9 +216,9 @@ export function Hero() {
|
||||
{item.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import {
|
||||
fadeUp,
|
||||
scaleIn,
|
||||
staggerContainer,
|
||||
slideInLeft,
|
||||
slideInRight,
|
||||
viewport,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
@@ -174,7 +185,6 @@ function PhoneMapVisual() {
|
||||
|
||||
function StepVisual({ type }: { type: string }) {
|
||||
if (type === "store") {
|
||||
// Inset box gives the storefront image breathing room on all sides
|
||||
return (
|
||||
<div className="absolute inset-x-4 bottom-0 top-2">
|
||||
<div className="relative h-full w-full">
|
||||
@@ -198,7 +208,6 @@ function StepVisual({ type }: { type: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Bag: chips on the left, image on the right – keep bag from overflowing right edge
|
||||
return (
|
||||
<div className="flex h-full items-end justify-end gap-2 px-2">
|
||||
<div className="mb-4 hidden shrink-0 space-y-2 sm:block">
|
||||
@@ -227,7 +236,13 @@ export function HowItWorks() {
|
||||
{/* HERO AREA */}
|
||||
<div className="mx-auto w-[calc(100%-96px)] max-w-[1720px]">
|
||||
<div className="grid min-w-0 grid-cols-1 items-center gap-10 lg:grid-cols-[38fr_62fr]">
|
||||
<div className="min-w-0">
|
||||
<motion.div
|
||||
className="min-w-0"
|
||||
variants={slideInLeft}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-secondary/15 bg-secondary/5 px-4 py-2 text-xs font-black uppercase tracking-widest text-secondary">
|
||||
<MaterialIcon icon="eco" size={16} />
|
||||
Together, We Grow
|
||||
@@ -245,9 +260,15 @@ export function HowItWorks() {
|
||||
<MaterialIcon icon="forum" size={20} />
|
||||
Talk to an Expert
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative flex min-h-[320px] justify-center md:min-h-[440px] xl:min-h-[400px]">
|
||||
<motion.div
|
||||
className="relative flex min-h-[320px] justify-center md:min-h-[440px] xl:min-h-[400px]"
|
||||
variants={scaleIn}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="relative w-full max-w-[680px]">
|
||||
<div className="relative h-[280px] overflow-hidden rounded-[2rem] bg-brand-accent-light shadow-[0_26px_70px_rgba(22,0,29,0.16)] md:h-[440px] xl:h-[400px]">
|
||||
<Image
|
||||
@@ -263,7 +284,7 @@ export function HowItWorks() {
|
||||
<StorePhone />
|
||||
<GrowthPill />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -274,7 +295,13 @@ export function HowItWorks() {
|
||||
|
||||
{/* CARDS SECTION */}
|
||||
<div className="mx-auto w-full max-w-[2200px] px-6 lg:px-10 xl:px-12">
|
||||
<div className="mt-10 text-center md:mt-14 xl:mt-10">
|
||||
<motion.div
|
||||
className="mt-10 text-center md:mt-14 xl:mt-10"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<span className="inline-flex rounded-full bg-primary/8 px-4 py-2 text-xs font-black uppercase tracking-widest text-primary">
|
||||
How It Works
|
||||
</span>
|
||||
@@ -284,12 +311,19 @@ export function HowItWorks() {
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base leading-relaxed text-on-surface-variant md:text-lg">
|
||||
Three simple milestones to scale your retail reach from offline streets straight into neighbourhood homes.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-10 grid min-w-0 grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<motion.div
|
||||
className="mt-10 grid min-w-0 grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{STEPS.map((step) => (
|
||||
<article
|
||||
<motion.article
|
||||
key={step.num}
|
||||
variants={fadeUp}
|
||||
className="relative flex min-h-[380px] min-w-0 flex-col overflow-hidden rounded-3xl border border-primary/10 bg-white p-7 shadow-[0_18px_45px_rgba(22,0,29,0.08)] md:min-h-[420px] lg:min-h-[460px] xl:min-h-[420px] lg:p-8"
|
||||
>
|
||||
<span className="absolute right-5 top-4 font-display text-6xl font-black leading-none text-primary/[0.07]">
|
||||
@@ -309,9 +343,9 @@ export function HowItWorks() {
|
||||
<div className={`relative z-0 mt-auto w-full overflow-hidden rounded-[1.5rem] bg-brand-accent-light/60 ${step.visual === "phone" ? "h-64 lg:h-72" : "h-48 lg:h-52"}`}>
|
||||
<StepVisual type={step.visual} />
|
||||
</div>
|
||||
</article>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, slideInLeft, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
@@ -33,11 +37,17 @@ export function HyperlocalFeatures() {
|
||||
<div className="absolute right-[15%] top-1/2 translate-x-1/2 -translate-y-1/2 w-[55%] h-[55%] rounded-full bg-primary/[0.03] blur-[130px] pointer-events-none z-0" />
|
||||
|
||||
{/* Inner container */}
|
||||
<div className="relative z-10 mx-auto w-full max-w-[2200px] px-2 md:px-4 lg:px-5 xl:px-6">
|
||||
<div className="relative z-10 mx-auto w-full max-w-[2200px] px-2 md:px-4 lg:px-5 xl:px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[0.95fr_1.05fr] gap-8 lg:gap-10 items-center">
|
||||
|
||||
|
||||
{/* Left Column: Visual Storytelling */}
|
||||
<div className="w-full max-w-[880px] justify-self-center relative rounded-[2rem] border border-outline-variant/25 bg-[#F8F3FC] z-10 overflow-hidden aspect-[10/11] lg:h-[580px] shadow-[0_24px_80px_rgba(101,20,154,0.10)]">
|
||||
<motion.div
|
||||
className="w-full max-w-[880px] justify-self-center relative rounded-[2rem] border border-outline-variant/25 bg-[#F8F3FC] z-10 overflow-hidden aspect-[10/11] lg:h-[580px] shadow-[0_24px_80px_rgba(101,20,154,0.10)]"
|
||||
variants={slideInLeft}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_52%_42%,rgba(124,58,237,0.12),transparent_34%),linear-gradient(145deg,rgba(255,255,255,0.94),rgba(243,232,246,0.62))]" />
|
||||
<div className="absolute inset-6 rounded-[1.5rem] overflow-hidden bg-white/55 ring-1 ring-white/80 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)]">
|
||||
<svg
|
||||
@@ -135,32 +145,55 @@ export function HyperlocalFeatures() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: Feature Panel directly on page background */}
|
||||
<div className="w-full py-6 md:py-8 lg:py-10 pl-4 md:pl-8 lg:pl-12 xl:pl-44 relative z-10 text-[#16001D]">
|
||||
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/10 px-3.5 py-1.5 rounded-full border border-primary/20">
|
||||
<motion.span
|
||||
className="text-xs text-primary uppercase tracking-widest font-black bg-primary/10 px-3.5 py-1.5 rounded-full border border-primary/20"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
Community & Trust
|
||||
</span>
|
||||
|
||||
<h3 className="text-4xl md:text-5xl lg:text-[44px] font-display font-black text-brand-dark mt-4 mb-6 leading-tight tracking-tight">
|
||||
</motion.span>
|
||||
|
||||
<motion.h3
|
||||
className="text-4xl md:text-5xl lg:text-[44px] font-display font-black text-brand-dark mt-4 mb-6 leading-tight tracking-tight"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
Built Around Your<br />
|
||||
<span className="text-primary font-black">Neighbourhood</span>
|
||||
</h3>
|
||||
</motion.h3>
|
||||
|
||||
<p className="text-body-lg text-on-surface-variant mb-10 max-w-lg leading-relaxed">
|
||||
<motion.p
|
||||
className="text-body-lg text-on-surface-variant mb-10 max-w-lg leading-relaxed"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
Nearle brings the convenience of an app to the stores you already know and trust, making it easy to support local shops.
|
||||
</p>
|
||||
</motion.p>
|
||||
|
||||
{/* Features List with vertical connector line */}
|
||||
<div className="relative space-y-8">
|
||||
|
||||
<motion.div
|
||||
className="relative space-y-8"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{/* Vertical connector line */}
|
||||
<div className="absolute left-6 top-6 bottom-6 w-0.5 bg-primary/20 -translate-x-1/2 z-0" />
|
||||
|
||||
|
||||
{FEATURES.map((item) => (
|
||||
<div key={item.title} className="flex gap-5 group relative z-10">
|
||||
|
||||
<motion.div key={item.title} variants={fadeUp} className="flex gap-5 group relative z-10">
|
||||
|
||||
{/* Icon circle */}
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-primary text-white flex items-center justify-center shadow-md relative z-10 group-hover:scale-105 transition-transform duration-300">
|
||||
<MaterialIcon
|
||||
@@ -169,7 +202,7 @@ export function HyperlocalFeatures() {
|
||||
size={22}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg md:text-xl font-black text-brand-dark group-hover:text-primary transition-colors">
|
||||
{item.title}
|
||||
@@ -178,15 +211,21 @@ export function HyperlocalFeatures() {
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Divider Line above CTA Area */}
|
||||
<hr className="border-t border-[#EEE7F4] my-6 relative z-10" />
|
||||
|
||||
{/* Onboarding Call to Action */}
|
||||
<div className="mt-2 flex flex-wrap gap-6 items-center relative z-10">
|
||||
<motion.div
|
||||
className="mt-2 flex flex-wrap gap-6 items-center relative z-10"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<Link
|
||||
href="/#download"
|
||||
className="bg-primary hover:bg-primary-container text-white font-black px-8 py-3.5 rounded-full text-sm md:text-base hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0 active:scale-95 transition-all duration-200"
|
||||
@@ -204,7 +243,7 @@ export function HyperlocalFeatures() {
|
||||
className="transition-transform group-hover:translate-x-1"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const MotionLink = motion(Link);
|
||||
|
||||
const CATEGORIES = [
|
||||
{
|
||||
@@ -58,7 +64,13 @@ export function ServiceCategories() {
|
||||
<section className="section bg-white w-full">
|
||||
<div className="page-container">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-10 md:mb-12 xl:mb-10 gap-6">
|
||||
<motion.div
|
||||
className="flex flex-col md:flex-row justify-between items-start md:items-end mb-10 md:mb-12 xl:mb-10 gap-6"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div>
|
||||
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/10 px-3 py-1.5 rounded-full">
|
||||
Neighbourhood Offerings
|
||||
@@ -77,14 +89,22 @@ export function ServiceCategories() {
|
||||
View All Categories
|
||||
<MaterialIcon icon="arrow_forward" size={18} className="transition-transform group-hover:translate-x-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Categories Premium Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{/* fadeIn (opacity only) preserves .premium-card:hover CSS transform */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{CATEGORIES.map((cat) => (
|
||||
<Link
|
||||
<MotionLink
|
||||
key={cat.name}
|
||||
href="/people"
|
||||
variants={fadeIn}
|
||||
className="bg-white rounded-[2rem] border border-outline-variant/10 shadow-lg overflow-hidden premium-card flex flex-col group h-full cursor-pointer"
|
||||
>
|
||||
{/* Image Container */}
|
||||
@@ -132,11 +152,10 @@ export function ServiceCategories() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</MotionLink>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, scaleIn, slideInRight, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const FLOW_STEPS = [
|
||||
{ label: "Scan QR", icon: "qr_code_scanner" },
|
||||
@@ -17,12 +21,18 @@ export function StoreQRSection() {
|
||||
<div className="absolute left-[-10%] bottom-[-10%] w-[50%] h-[50%] rounded-full bg-primary/10 blur-[120px] pointer-events-none" />
|
||||
|
||||
<div className="w-full relative z-10">
|
||||
|
||||
|
||||
{/* ─── DESKTOP LAYOUT (lg and above) ─── */}
|
||||
<div className="hidden lg:grid lg:grid-cols-12 gap-12 xl:gap-16 items-center">
|
||||
|
||||
|
||||
{/* Left Column: Heading, flow steps, download buttons */}
|
||||
<div className="lg:col-span-6">
|
||||
<motion.div
|
||||
className="lg:col-span-6"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<span className="text-xs text-brand-accent uppercase tracking-widest font-black bg-brand-accent/10 px-3.5 py-1.5 rounded-full border border-brand-accent/20">
|
||||
Instant Connections & App
|
||||
</span>
|
||||
@@ -83,12 +93,21 @@ export function StoreQRSection() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: Visual components (QR Sticker & Phone Mockup) */}
|
||||
<div className="lg:col-span-6 flex items-center justify-center gap-8 relative select-none">
|
||||
<motion.div
|
||||
className="lg:col-span-6 flex items-center justify-center gap-8 relative select-none"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{/* Small supporting visual: QR Sticker (Secondary) */}
|
||||
<div className="relative w-[210px] shrink-0 aspect-[9/11.5] bg-white rounded-[2rem] p-6 text-brand-dark flex flex-col justify-between items-center shadow-2xl border-2 border-white/20">
|
||||
<motion.div
|
||||
variants={scaleIn}
|
||||
className="relative w-[210px] shrink-0 aspect-[9/11.5] bg-white rounded-[2rem] p-6 text-brand-dark flex flex-col justify-between items-center shadow-2xl border-2 border-white/20"
|
||||
>
|
||||
<div className="text-center w-full">
|
||||
<p className="text-[10px] font-black text-primary tracking-wider uppercase">Order Online</p>
|
||||
<h4 className="font-display font-black text-brand-dark text-base mt-0.5 leading-none">Store QR</h4>
|
||||
@@ -108,10 +127,13 @@ export function StoreQRSection() {
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
Scan to Shop
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Primary large visual: Phone Mockup (Significantly Larger) */}
|
||||
<div className="relative w-[290px] shrink-0 aspect-[9/18.5] bg-[#120217] rounded-[2.75rem] border-[7px] border-brand-dark-surface shadow-2xl overflow-hidden ring-4 ring-outline-variant/10">
|
||||
<motion.div
|
||||
variants={slideInRight}
|
||||
className="relative w-[290px] shrink-0 aspect-[9/18.5] bg-[#120217] rounded-[2.75rem] border-[7px] border-brand-dark-surface shadow-2xl overflow-hidden ring-4 ring-outline-variant/10"
|
||||
>
|
||||
<Image
|
||||
src="/mobile image.jpeg"
|
||||
alt="Nearle mobile app interface"
|
||||
@@ -119,16 +141,19 @@ export function StoreQRSection() {
|
||||
height={760}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ─── MOBILE & TABLET LAYOUT (below lg) ─── */}
|
||||
<div className="lg:hidden flex flex-col gap-12 text-center">
|
||||
{/* 1. Heading and description */}
|
||||
<div>
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<span className="text-xs text-brand-accent uppercase tracking-widest font-black bg-brand-accent/10 px-3.5 py-1.5 rounded-full border border-brand-accent/20 inline-block">
|
||||
Instant Connections & App
|
||||
</span>
|
||||
@@ -138,10 +163,16 @@ export function StoreQRSection() {
|
||||
<p className="text-body-md text-white/70 max-w-md mx-auto leading-relaxed">
|
||||
Scan Nearle QRs to instantly save local stores, and order from them anytime in one convenient app.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 2. QR Sticker */}
|
||||
<div className="mx-auto w-[240px] aspect-[9/11.5] bg-white rounded-[2rem] p-6 text-brand-dark flex flex-col justify-between items-center shadow-xl border-2 border-white/20 select-none">
|
||||
<motion.div
|
||||
className="mx-auto w-[240px] aspect-[9/11.5] bg-white rounded-[2rem] p-6 text-brand-dark flex flex-col justify-between items-center shadow-xl border-2 border-white/20 select-none"
|
||||
variants={scaleIn}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="text-center w-full">
|
||||
<p className="text-[10px] font-black text-primary tracking-wider uppercase">Order Online</p>
|
||||
<h4 className="font-display font-black text-brand-dark text-base mt-0.5">Store QR</h4>
|
||||
@@ -161,7 +192,7 @@ export function StoreQRSection() {
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
Scan to Shop
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 3. Scan → Save → Order steps */}
|
||||
<div className="flex items-center justify-center gap-4 max-w-sm mx-auto select-none">
|
||||
@@ -183,7 +214,13 @@ export function StoreQRSection() {
|
||||
</div>
|
||||
|
||||
{/* 4. Phone Mockup */}
|
||||
<div className="mx-auto w-[240px] aspect-[9/18.5] bg-[#120217] rounded-[2.5rem] border-[5px] border-brand-dark-surface shadow-xl relative overflow-hidden select-none">
|
||||
<motion.div
|
||||
className="mx-auto w-[240px] aspect-[9/18.5] bg-[#120217] rounded-[2.5rem] border-[5px] border-brand-dark-surface shadow-xl relative overflow-hidden select-none"
|
||||
variants={slideInRight}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<Image
|
||||
src="/mobile image.jpeg"
|
||||
alt="Nearle mobile app interface"
|
||||
@@ -191,7 +228,7 @@ export function StoreQRSection() {
|
||||
height={760}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 5. App store buttons — TODO: replace with real URLs once app is live */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
@@ -41,7 +45,13 @@ export function WhyNearle() {
|
||||
<section className="section section-soft w-full overflow-hidden">
|
||||
<div className="page-container relative">
|
||||
{/* Header Section */}
|
||||
<div className="relative text-center mb-8 md:mb-10">
|
||||
<motion.div
|
||||
className="relative text-center mb-8 md:mb-10"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="pointer-events-none absolute -left-2 top-0 hidden h-36 w-36 overflow-hidden rounded-[2rem] bg-primary/5 shadow-[0_18px_45px_rgba(101,20,154,0.12)] md:block lg:-left-4 lg:h-44 lg:w-44">
|
||||
<Image
|
||||
src="/images/nearle-redesign/heading-shopper.webp"
|
||||
@@ -69,13 +79,20 @@ export function WhyNearle() {
|
||||
<p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed">
|
||||
Nearle brings the convenience of an app to the stores you already know and trust.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-7">
|
||||
{/* Cards Grid — fadeIn preserves .premium-card:hover CSS transform */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-7"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{CARDS.map((card) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={card.title}
|
||||
variants={fadeIn}
|
||||
className="premium-card bg-white rounded-3xl p-5 md:p-6 border border-outline-variant/15 shadow-lg flex flex-col h-full hover:scale-[1.02] transition-transform duration-300"
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
|
||||
@@ -96,11 +113,10 @@ export function WhyNearle() {
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
8
src/components/layout/MotionProvider.tsx
Normal file
8
src/components/layout/MotionProvider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { MotionConfig } from "framer-motion";
|
||||
import React from "react";
|
||||
|
||||
export function MotionProvider({ children }: { children: React.ReactNode }) {
|
||||
return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { PremiumNavbar } from "./PremiumNavbar";
|
||||
import { PremiumFooter } from "./PremiumFooter";
|
||||
import { MotionProvider } from "./MotionProvider";
|
||||
|
||||
type PremiumPageShellProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -9,16 +10,18 @@ type PremiumPageShellProps = {
|
||||
|
||||
export function PremiumPageShell({ children, className = "" }: PremiumPageShellProps) {
|
||||
return (
|
||||
<div className={`min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col ${className}`}>
|
||||
{/* Header/Navbar */}
|
||||
<PremiumNavbar />
|
||||
<MotionProvider>
|
||||
<div className={`min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col ${className}`}>
|
||||
{/* Header/Navbar */}
|
||||
<PremiumNavbar />
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="w-full overflow-x-hidden bg-white">
|
||||
{children}
|
||||
</main>
|
||||
{/* Page Content */}
|
||||
<main className="w-full overflow-x-hidden bg-white">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<PremiumFooter />
|
||||
</div>
|
||||
<PremiumFooter />
|
||||
</div>
|
||||
</MotionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user