animation fixed in section

This commit is contained in:
2026-06-29 16:52:46 +05:30
parent 9449accea6
commit 114b49570b
36 changed files with 2308 additions and 1301 deletions

View File

@@ -1,7 +1,9 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
// ─── Feature SVG Illustrations ───────────────────────────────────────────────
// Flat vector style · Nearle brand palette · no text inside SVGs
@@ -428,46 +430,50 @@ export function BusinessFeaturesGrid() {
>
{/* ── LEFT — clean text feature grid, no illustrations ── */}
<div>
{/* Badge */}
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
Our features
</span>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{/* Badge + Heading */}
<motion.div variants={fadeUp}>
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
Our features
</span>
<h2
className="font-display font-black text-[#16001D] mt-4"
style={{
fontSize: "clamp(36px, 3.4vw, 60px)",
lineHeight: 1.08,
letterSpacing: "-0.04em",
maxWidth: 780,
marginBottom: 0,
}}
>
Move Your Business Forward
</h2>
<p
className="text-[#5E5866]"
style={{ fontSize: 18, lineHeight: 1.65, maxWidth: 760, marginTop: 16, marginBottom: 36 }}
>
Be a part of a community of like-minded people. Brand your business on Nearle and reach out to your neighborhood with ease.
</p>
</motion.div>
{/* Heading */}
<h2
className="font-display font-black text-[#16001D] mt-4"
style={{
fontSize: "clamp(36px, 3.4vw, 60px)",
lineHeight: 1.08,
letterSpacing: "-0.04em",
maxWidth: 780,
marginBottom: 0,
}}
>
Move Your Business Forward
</h2>
{/* Description */}
<p
className="text-[#5E5866]"
style={{ fontSize: 18, lineHeight: 1.65, maxWidth: 760, marginTop: 16, marginBottom: 36 }}
>
Be a part of a community of like-minded people. Brand your business on Nearle and reach out to your neighborhood with ease.
</p>
{/* Feature list check + text only, no illustration columns */}
<div
{/* Feature list */}
<motion.div
className="grid grid-cols-1 md:grid-cols-2"
style={{ columnGap: 48, rowGap: 32 }}
variants={staggerSlow}
>
{FEATURES.map((feat) => (
<div
<motion.div
key={feat.title}
variants={fadeUp}
className="grid items-start"
style={{ gridTemplateColumns: "30px 1fr", columnGap: 16 }}
>
{/* Green check icon */}
<div
className="rounded-full bg-[#DCFCE7] flex items-center justify-center shrink-0"
style={{ width: 30, height: 30, marginTop: 3 }}
@@ -476,8 +482,6 @@ export function BusinessFeaturesGrid() {
<path d="M3.5 8.5L6.5 11.5L12.5 5" stroke="#16A34A" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Title + description */}
<div className="min-w-0">
<h4
className="font-display font-black text-[#16001D]"
@@ -489,30 +493,33 @@ export function BusinessFeaturesGrid() {
{feat.descr}
</p>
</div>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
</motion.div>
{/* ── RIGHT — 4 cards + illustration strip below ─────── */}
<div className="self-start flex flex-col" style={{ gap: 28 }}>
{/* 2×2 feature benefit cards */}
{/* ── RIGHT — 2×2 feature benefit cards ─────── */}
<motion.div
className="self-start flex flex-col"
style={{ gap: 28 }}
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<div className="grid grid-cols-2" style={{ gap: 28 }}>
{CARDS.map((card) => {
const { CardIllus } = card;
return (
<div
<motion.div
key={card.title}
variants={scaleIn}
className={`relative flex flex-col overflow-hidden border border-[#6330D6]/10 shadow-[0_4px_28px_rgba(99,48,214,0.09)] ${card.bg}`}
style={{ borderRadius: 34, padding: 28, minHeight: 330 }}
>
{/* Top icon */}
<div className={`w-9 h-9 rounded-full flex items-center justify-center shrink-0 ${card.iconBg}`}>
<MaterialIcon icon={card.iconName} size={18} />
</div>
{/* Illustration */}
<div
className="flex-1 flex items-center justify-center overflow-hidden"
style={{ margin: "8px 0" }}
@@ -521,8 +528,6 @@ export function BusinessFeaturesGrid() {
<CardIllus />
</div>
</div>
{/* Bottom label */}
<div className="mt-auto shrink-0">
<p
className={`font-display font-black leading-none tracking-tight ${card.titleColor}`}
@@ -537,13 +542,11 @@ export function BusinessFeaturesGrid() {
{card.subtitle}
</p>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</motion.div>
</div>
</div>

View File

@@ -1,8 +1,27 @@
"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
// Stagger form contents after the card slides in from right (card enters at delay 0.25, duration 0.65 → visible ~t=0.7s)
const formContentStagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.75 } },
};
const fieldVariant = {
hidden: { opacity: 0, y: 14 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
const submitVariant = {
hidden: { opacity: 0, y: 10, scale: 0.98 },
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.4, ease: EASE } },
};
type FormData = {
name: string;
email: string;
@@ -100,16 +119,21 @@ export function ContactForm() {
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-secondary/5 rounded-full blur-2xl" />
<div className="relative z-10">
<span className="text-label-sm font-black text-primary uppercase tracking-widest">
<motion.div
className="relative z-10"
variants={formContentStagger}
initial="hidden"
animate="visible"
>
<motion.span variants={fieldVariant} className="text-label-sm font-black text-primary uppercase tracking-widest block">
Write to us
</span>
<h2 className="font-display text-2xl md:text-3xl font-black text-brand-dark mt-2 mb-8">
</motion.span>
<motion.h2 variants={fieldVariant} className="font-display text-2xl md:text-3xl font-black text-brand-dark mt-2 mb-8">
Send Message
</h2>
</motion.h2>
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<motion.div variants={fieldVariant} className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Name field */}
<div className="flex flex-col">
<label htmlFor="field-name" className="text-label-sm text-on-surface-variant font-bold mb-2">
@@ -171,10 +195,10 @@ export function ContactForm() {
</span>
)}
</div>
</div>
</motion.div>
{/* Message field */}
<div className="flex flex-col">
<motion.div variants={fieldVariant} className="flex flex-col">
<label htmlFor="field-message" className="text-label-sm text-on-surface-variant font-bold mb-2">
Message
</label>
@@ -202,28 +226,30 @@ export function ContactForm() {
{errors.message}
</span>
)}
</div>
</motion.div>
{/* Submit button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-[#6330D6] hover:bg-[#5225b8] disabled:bg-[#6330D6]/60 text-on-primary font-black px-8 py-4 rounded-xl text-label-md hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] flex items-center justify-center gap-2 cursor-pointer"
>
{isSubmitting ? (
<>
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Sending Message...</span>
</>
) : (
<>
<MaterialIcon icon="send" size={18} />
<span>Submit Message</span>
</>
)}
</button>
{/* Submit button — appears last */}
<motion.div variants={submitVariant}>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-[#6330D6] hover:bg-[#5225b8] disabled:bg-[#6330D6]/60 text-on-primary font-black px-8 py-4 rounded-xl text-label-md hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] flex items-center justify-center gap-2 cursor-pointer"
>
{isSubmitting ? (
<>
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Sending Message...</span>
</>
) : (
<>
<MaterialIcon icon="send" size={18} />
<span>Submit Message</span>
</>
)}
</button>
</motion.div>
</form>
</div>
</motion.div>
</div>
{/* ─── Success Toast Notification ─── */}

View File

@@ -0,0 +1,71 @@
"use client";
import { motion } from "framer-motion";
type TimelineItem = {
step: string;
title: string;
desc: string;
time: string;
};
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const containerVariant = {
hidden: {},
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.15 } },
};
const itemVariant = {
hidden: { opacity: 0, x: -24 },
visible: { opacity: 1, x: 0, transition: { duration: 0.55, ease: EASE } },
};
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 0.9, ease: EASE } },
};
export function AnimatedTimeline({ items }: { items: TimelineItem[] }) {
return (
<div className="relative max-w-3xl mx-auto">
{/* Vertical connector line — animated scaleY from origin-top */}
<motion.div
className="absolute left-[22px] md:left-[26px] top-8 bottom-8 w-0.5 bg-gradient-to-b from-[#6330D6]/30 via-[#6330D6]/20 to-transparent hidden sm:block origin-top"
variants={connectorVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
/>
<motion.div
className="space-y-6"
variants={containerVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
>
{items.map((item, index) => (
<motion.div
key={item.step}
className="relative flex gap-5 sm:gap-7"
variants={itemVariant}
>
<div className="flex-shrink-0 w-11 h-11 md:w-12 md:h-12 rounded-full bg-[#6330D6] text-white flex items-center justify-center text-xs font-black shadow-[0_8px_20px_rgba(99,48,214,0.25)] relative z-10">
{String(index + 1).padStart(2, "0")}
</div>
<div className="flex-1 pb-2 pt-1">
<div className="flex items-center gap-3 mb-1.5">
<h3 className="text-base font-black text-[#16001D] leading-tight">{item.title}</h3>
<span className="px-2.5 py-0.5 rounded-full bg-[#6330D6]/8 text-[#6330D6] text-[10px] font-black uppercase tracking-wide shrink-0">
{item.time}
</span>
</div>
<p className="text-sm text-[#5E5866] leading-relaxed">{item.desc}</p>
</div>
</motion.div>
))}
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
import type { Variants } from "framer-motion";
import {
fadeInSlow,
fadeUp,
staggerSlow,
viewport,
} from "@/lib/animations";
const HERO_STATS = [
{ title: "2km Coverage", icon: "explore", desc: "Hyperlocal radius" },
{ title: "Verified Local Stores", icon: "store", desc: "Growing local commerce" },
{ title: "Happy Customers", icon: "mood", desc: "Neighbourhood community" },
{ title: "Coimbatore & Beyond", icon: "location_city", desc: "Growing neighbourhood reach" },
];
const COLLAGE_CARDS = [
{ text: "Local First", icon: "store" },
{ text: "Trusted Network", icon: "verified" },
{ text: "Connected Communities", icon: "groups" },
];
// Choreography: deliberate, slow-stagger sequence — thoughtful and purposeful
// badge fades in (opacity only) → h1 sweeps from left → p fades up → CTAs fade up
// The collage scales up softly, reinforcing the "built carefully" feeling
const badgeVariant = fadeInSlow;
const titleVariant = {
hidden: { opacity: 0, x: -30 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const descVariant = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const ctaVariant = fadeUp;
const collageVariant = {
hidden: { opacity: 0, scale: 0.92 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.95, ease: [0.22, 1, 0.36, 1] as [number, number, number, number], delay: 0.2 },
},
};
// Trust-feeling card variant: no spring bounce, deliberate easing
const trustCardVariant: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
export function AboutHero() {
return (
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
<div className="border-b border-[#6330D6]/10">
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
{/* Left Column: slow, deliberate stagger — thoughtful pacing */}
<motion.div
className="flex flex-col text-left max-w-xl"
variants={staggerSlow}
initial="hidden"
animate="visible"
>
<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-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
>
About Nearle
</motion.span>
<motion.h1
variants={titleVariant}
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
>
Built for Every
<br />
<span className="text-[#6330D6]">Neighbourhood.</span>
</motion.h1>
<motion.p
variants={descVariant}
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
>
Nearle is India&apos;s first hyper-local managed &ldquo;Neighbourhood living platform&rdquo; which brings all the amenities and requirements of residents to their fingertips.
</motion.p>
<motion.div
variants={ctaVariant}
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
>
<a
href="#story-section"
className="bg-[#6330D6] 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]"
>
Our Story
</a>
<Link
href="/contact"
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
>
Join Our Journey
</Link>
</motion.div>
</motion.div>
{/* Right Column: soft scale-up — gentle and community-first */}
<motion.div
className="w-full"
variants={collageVariant}
initial="hidden"
animate="visible"
>
<HeroVisualCollage
centerImage="/nearlenewlogo.png"
leftImage="/Nearle Business.png"
rightImage="/nearle people.png"
glassCards={COLLAGE_CARDS}
isPhoneCenter={false}
cardVariant={trustCardVariant}
/>
</motion.div>
</div>
</div>
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
<FeatureStatsRow items={HERO_STATS} />
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,123 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
import {
slideInLeftSubtle,
fadeUp,
fadeIn,
scaleIn,
staggerContainer,
viewport,
} from "@/lib/animations";
const HERO_BENEFITS = [
{ title: "More Visibility Across Nearle", icon: "visibility", desc: "Get discovered by locals" },
{ title: "Easy Order Management", icon: "assignment", desc: "Track and fulfill cleanly" },
{ title: "Grow Repeat Customers", icon: "trending_up", desc: "Build neighborhood loyalty" },
{ title: "Local Delivery Support", icon: "local_shipping", desc: "Deliver fast & verified" },
];
const COLLAGE_CARDS = [
{ text: "Nearby Customers", icon: "people" },
{ text: "Orders & Offers", icon: "local_offer" },
{ text: "Local Delivery Support", icon: "local_shipping" },
];
// Choreography: badge slides left → title fades up → description fades in (opacity only) → CTAs scale in
const badgeVariant = slideInLeftSubtle;
const titleVariant = {
hidden: { opacity: 0, y: 28 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.55, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const descVariant = fadeIn;
const ctaVariant = scaleIn;
export function BusinessHero() {
return (
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
<div className="border-b border-[#6330D6]/10">
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
{/* Left Column: animated with unique stagger */}
<motion.div
className="flex flex-col text-left max-w-xl"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
<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-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
>
Nearle for Business
</motion.span>
<motion.h1
variants={titleVariant}
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
>
Grow Your Local Business,
<br />
<span className="text-[#6330D6]">Neighbourhood by Neighbourhood.</span>
</motion.h1>
<motion.p
variants={descVariant}
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
>
Get discovered by nearby customers, manage orders, and build repeat business from one simple platform.
</motion.p>
<motion.div
variants={ctaVariant}
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
>
<Link
href="/contact"
className="bg-[#6330D6] 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]"
>
List Your Store
</Link>
<Link
href="/contact"
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
>
Explore Solutions
</Link>
</motion.div>
</motion.div>
{/* Right Column: subtle fade-in — HeroVisualCollage internal animations choreograph each element */}
<motion.div
className="w-full"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.2 }}
>
<HeroVisualCollage
centerImage="/nearlenewlogo.png"
leftImage="/images/cat-daily-essentials.png"
rightImage="/Nearle Business.png"
glassCards={COLLAGE_CARDS}
isPhoneCenter={false}
/>
</motion.div>
</div>
</div>
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
<FeatureStatsRow items={HERO_BENEFITS} />
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,139 @@
"use client";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { ContactForm } from "@/components/contact/ContactForm";
import { fadeUp, fadeUpStrong, slideInRight, staggerContainer, viewport } from "@/lib/animations";
const CONTACT_INFO = [
{
icon: "phone",
label: "Call Us",
value: "+91 909 206 8666",
},
{
icon: "mail",
label: "Email Us",
value: "nearle@care.in",
},
{
icon: "location_on",
label: "Address",
value: "424, DB Rd, R.S. Puram, Coimbatore, TN 641002",
},
];
// Choreography: professional, clear sequence
// Left column — each info row staggers in from left; h1 has strong upward entry
// Form card slides in from right with slight delay
// Info rows use a tighter stagger to feel organised and systematic
const infoRowVariant = {
hidden: { opacity: 0, x: -18 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.45, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const formVariant = {
hidden: { opacity: 0, x: 40 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as [number, number, number, number], delay: 0.25 },
},
};
const infoStagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
};
export function ContactHero() {
return (
<section className="relative border-b border-[#6330D6]/10 px-4 py-10 md:px-6 md:py-12 lg:px-8 lg:py-14 xl:px-10 xl:py-16 2xl:px-12 2xl:py-20">
{/* Decorative map grid — not animated so it stays subtle */}
<div className="absolute left-[5%] top-[10%] w-[400px] h-[300px] opacity-[0.04] select-none pointer-events-none -z-10">
<svg className="w-full h-full text-[#6330D6]" stroke="currentColor" fill="none" strokeWidth="1.5" viewBox="0 0 400 300">
<path d="M0,50 L400,50 M0,130 L400,130 M0,210 L400,210 M80,0 L80,300 M200,0 L200,300 M320,0 L320,300" />
<path d="M 80,130 C 120,90 160,170 200,130 C 240,90 280,170 320,130" strokeDasharray="3,3" />
<circle cx="200" cy="130" r="4" fill="currentColor" />
<circle cx="200" cy="130" r="14" strokeWidth="1" />
<circle cx="80" cy="50" r="3" fill="currentColor" />
<circle cx="320" cy="210" r="3" fill="currentColor" />
</svg>
</div>
<div className="relative z-10 mx-auto w-full max-w-[1520px]">
<div className="grid grid-cols-1 items-start gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:gap-12 xl:gap-16">
{/* Left Column — Contact Info: sequential stagger from top */}
<motion.div
className="w-full max-w-[760px] py-6 md:py-8 lg:py-10 relative z-10 text-[#16001D]"
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-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
>
Contact Us
</motion.span>
<motion.h1
variants={fadeUpStrong}
className="font-display text-4xl font-black leading-[1.05] text-[#16001D] md:text-5xl lg:text-[58px] xl:text-[64px] 2xl:text-[70px]"
>
We&apos;re Here to <br />
<span className="text-[#6330D6]">Help You.</span>
</motion.h1>
<motion.p
variants={fadeUp}
className="mt-6 max-w-[620px] text-sm font-bold leading-8 text-[#5E5866] md:text-base xl:text-lg"
>
Whether you are a customer, local store owner, or delivery partner, our team is here to help you get started with Nearle.
</motion.p>
{/* Info rows: each row staggers in from left */}
<motion.div
className="mt-8 border-t border-[#6330D6]/10"
variants={infoStagger}
initial="hidden"
animate="visible"
>
{CONTACT_INFO.map((item) => (
<motion.div
key={item.label}
variants={infoRowVariant}
className="flex gap-4 py-4.5 border-b border-[#6330D6]/10 items-center"
>
<div className="w-10 h-10 rounded-full bg-[#6330D6]/10 text-[#6330D6] flex items-center justify-center shrink-0">
<MaterialIcon icon={item.icon} size={18} />
</div>
<div className="text-left">
<h4 className="text-[#16001D] text-sm font-black">{item.label}</h4>
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">{item.value}</p>
</div>
</motion.div>
))}
</motion.div>
</motion.div>
{/* Right Column: form slides in from right */}
<motion.div
className="w-full max-w-[660px] justify-self-end pt-10 lg:pt-14 xl:pt-26"
variants={formVariant}
initial="hidden"
animate="visible"
>
<ContactForm />
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,117 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
import {
scaleInSpring,
fadeUpStrong,
fadeUp,
slideInLeftSubtle,
fadeIn,
staggerFast,
} from "@/lib/animations";
const HERO_STATS = [
{ title: "Verified Local Stores", icon: "store", desc: "Growing local commerce" },
{ title: "2km Neighbourhood", icon: "location_on", desc: "Hyperlocal service radius" },
{ title: "Happy Customers", icon: "mood", desc: "Growing neighbourhood community" },
{ title: "Trusted Platform", icon: "star", desc: "Loved by local shoppers" },
];
const COLLAGE_CARDS = [
{ text: "Discover Nearby", icon: "explore" },
{ text: "Save Favourites", icon: "favorite" },
{ text: "Order Anytime", icon: "shopping_bag" },
];
// Choreography: badge bounces in (spring) → h1 strong fade-up → p subtle up → CTAs slide from left
// Fast stagger (0.07) gives a snappy, energetic feel — phone/fast/mobile-first vibe
const badgeVariant = scaleInSpring;
const titleVariant = fadeUpStrong;
const descVariant = fadeUp;
const ctaVariant = slideInLeftSubtle;
export function PeopleHero() {
return (
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
<div className="border-b border-[#6330D6]/10">
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
{/* Left Column: snappy fast-stagger sequence */}
<motion.div
className="flex flex-col text-left max-w-xl"
variants={staggerFast}
initial="hidden"
animate="visible"
>
<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-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
>
Nearle for People
</motion.span>
<motion.h1
variants={titleVariant}
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
>
Your Neighbourhood,
<br />
Now in Your <span className="text-[#6330D6]">Pocket.</span>
</motion.h1>
<motion.p
variants={descVariant}
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
>
Discover verified local stores, save your favorite merchants, and order everyday items with superfast local fulfillment.
</motion.p>
<motion.div
variants={ctaVariant}
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
>
<a
href="#download-section"
className="bg-[#6330D6] 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]"
>
Get the App
</a>
<Link
href="/"
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
>
Explore Stores
</Link>
</motion.div>
</motion.div>
{/* Right Column: subtle fade — HeroVisualCollage's internal animations handle phone, images, badges individually */}
<motion.div
className="w-full"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.15 }}
>
<HeroVisualCollage
centerImage="/images/app-mockup.png"
leftImage="/images/cat-hot-food.png"
rightImage="/images/cat-health-wellness.png"
glassCards={COLLAGE_CARDS}
isPhoneCenter={true}
/>
</motion.div>
</div>
</div>
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
<FeatureStatsRow items={HERO_STATS} />
</div>
</div>
</section>
);
}

View File

@@ -69,8 +69,16 @@ export function BusinessSection() {
<section className="section bg-white w-full overflow-hidden">
<div className="mx-auto w-full max-w-[2200px] px-2 md:px-4 lg:px-5 xl:px-6">
<div className="relative overflow-hidden rounded-[2rem] bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_50%,#ede4f5_100%)] border border-primary/10 shadow-[0_24px_70px_rgba(22,0,29,0.08)] p-5 md:p-8 lg:p-10">
<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" />
<motion.div
className="pointer-events-none absolute -left-16 top-24 h-64 w-64 rounded-full bg-primary/5 blur-3xl"
animate={{ y: [0, -16, 0] }}
transition={{ duration: 13, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.div
className="pointer-events-none absolute -right-20 bottom-10 h-64 w-64 rounded-full bg-secondary/5 blur-3xl"
animate={{ y: [0, 14, 0] }}
transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
/>
<div className="relative grid gap-6 grid-cols-1 md:gap-7
xl:grid-cols-[420px_1fr] xl:gap-8 xl:items-start">

View File

@@ -2,14 +2,11 @@
import Link from "next/link";
import Image from "next/image";
import { motion } from "framer-motion";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import {
fadeUp,
slideInRight,
staggerContainer,
viewport,
} from "@/lib/animations";
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" },
@@ -18,16 +15,86 @@ const PROOF_ITEMS = [
{ label: "Trusted", text: "Local Platform", icon: "star" },
];
// Left column: badge → line1 mask-reveal → line2 mask-reveal → desc → primary CTA → secondary CTA
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 primaryCtaVariant = {
hidden: { opacity: 0, y: 14, scale: 0.97 },
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.45, ease: EASE } },
};
const secondaryCtaVariant = {
hidden: { opacity: 0, x: -14 },
visible: { opacity: 1, x: 0, transition: { duration: 0.4, ease: EASE } },
};
// Right column: each element enters independently
const imgFromLeft = {
hidden: { opacity: 0, x: -36 },
visible: { opacity: 1, x: 0, transition: { duration: 0.65, ease: EASE } },
};
const centerCardReveal = {
hidden: { opacity: 0, scale: 0.86, y: 24 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.75, ease: EASE } },
};
const imgFromRight = {
hidden: { opacity: 0, x: 36 },
visible: { opacity: 1, x: 0, transition: { duration: 0.65, 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 reducedMotion = useReducedMotion();
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" />
<motion.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"
animate={{ y: [0, -14, 0] }}
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut" }}
/>
{/* 2. Purple solid circle (center-right around collage) */}
<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" />
<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 motion-safe:animate-pulse" />
{/* 3. Striped purple circle (lower-middle collage area) */}
<div
@@ -53,7 +120,11 @@ export function Hero() {
<div className="absolute bottom-[14%] right-[5%] text-[#6D28D9]/25 font-bold text-xl pointer-events-none select-none z-0"></div>
{/* 6. Large partial purple circle (bottom-right corner) */}
<div className="absolute bottom-[-100px] right-[-100px] w-[200px] h-[200px] rounded-full bg-[#7C3AED]/80 z-0 pointer-events-none" />
<motion.div
className="absolute bottom-[-100px] right-[-100px] w-[200px] h-[200px] rounded-full bg-[#7C3AED]/80 z-0 pointer-events-none"
animate={{ y: [0, 12, 0] }}
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut" }}
/>
{/* ─── 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">
@@ -61,64 +132,84 @@ export function Hero() {
{/* LEFT COLUMN: Editorial Typography & Copy */}
<motion.div
className="flex flex-col text-left max-w-xl z-20 relative"
variants={staggerContainer}
variants={heroStagger}
initial="hidden"
animate="visible"
>
{/* Badge */}
<motion.span
variants={fadeUp}
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>
<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>
</motion.h1>
{/* H1: line-by-line mask reveal */}
<motion.div variants={lineStagger} className="font-display text-4xl 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={fadeUp}
variants={descVariant}
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.
</motion.p>
<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]"
{/* CTAs: primary scales in, secondary slides from left */}
<div className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto">
<motion.div
variants={primaryCtaVariant}
whileHover={{ scale: 1.03, transition: { duration: 0.2, ease: EASE } }}
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
>
Explore Stores
</Link>
<Link
href="/people#download-section"
className="border-2 border-[#6D28D9]/20 hover:border-[#6D28D9]/40 text-[#6D28D9] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6D28D9]/5 active:scale-[0.98]"
<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-colors duration-200 block"
>
Explore Stores
</Link>
</motion.div>
<motion.div
variants={secondaryCtaVariant}
whileHover={{ scale: 1.03, transition: { duration: 0.2, ease: EASE } }}
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
>
Get the App
</Link>
</motion.div>
<Link
href="/people#download-section"
className="border-2 border-[#6D28D9]/20 hover:border-[#6D28D9]/40 text-[#6D28D9] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-colors duration-200 hover:bg-[#6D28D9]/5 block"
>
Get the App
</Link>
</motion.div>
</div>
</motion.div>
{/* RIGHT COLUMN: Creative Overlapping Strips Collage */}
<motion.div
className="w-full z-20"
variants={slideInRight}
initial="hidden"
animate="visible"
>
{/* RIGHT COLUMN: Individual image + center card animations */}
<div className="w-full z-20">
{/* 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]">
{/* Left image: slides in from left */}
<motion.div
className="absolute left-[10%] top-[20%] w-[170px] h-[360px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10"
variants={imgFromLeft}
initial="hidden"
animate="visible"
transition={{ delay: 0.3 }}
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
>
<Image
src="/images/cat-daily-essentials.png"
alt="Local grocery products"
@@ -126,37 +217,63 @@ export function Hero() {
className="object-cover"
sizes="170px"
/>
</div>
</motion.div>
{/* middle image: central logo panel absolute left-[38%] top-[8%] */}
<div className="absolute left-[38%] top-[8%] z-20 transition-transform duration-300 hover:scale-[1.02]">
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
<Image
src="/nearlenewlogo.png"
alt="Central logo panel"
width={160}
height={160}
className="object-contain max-h-[140px] w-auto animate-[pulse_6s_infinite]"
priority
/>
</div>
</div>
{/* Center card: scales up from below, then gently bobs */}
<motion.div
className="absolute left-[38%] top-[8%] z-20"
variants={centerCardReveal}
initial="hidden"
animate="visible"
transition={{ delay: 0.15 }}
>
<motion.div
animate={!reducedMotion ? { y: [0, -10, 0] } : {}}
transition={{ delay: 0.9, duration: 4, repeat: Infinity, ease: "easeInOut" }}
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
>
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
<Image
src="/nearlenewlogo.png"
alt="Central logo panel"
width={160}
height={160}
className="object-contain max-h-[140px] w-auto motion-safe:animate-[pulse_6s_infinite]"
priority
/>
</div>
</motion.div>
</motion.div>
{/* 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]">
{/* Right image: slides in from right */}
<motion.div
className="absolute right-[2%] top-[18%] w-[180px] h-[390px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10"
variants={imgFromRight}
initial="hidden"
animate="visible"
transition={{ delay: 0.42 }}
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
>
<Image
src="/nearle people.png"
alt="Local customer shopping"
fill
priority
className="object-cover"
sizes="180px"
/>
</div>
</motion.div>
</div>
{/* 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">
<motion.div
className="relative w-[95px] h-[200px] rounded-2xl overflow-hidden border border-white shadow-md z-10"
variants={imgFromLeft}
initial="hidden"
animate="visible"
transition={{ delay: 0.28 }}
>
<Image
src="/images/cat-daily-essentials.png"
alt="Grocery products"
@@ -164,18 +281,37 @@ export function Hero() {
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}
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">
</motion.div>
<motion.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"
variants={centerCardReveal}
initial="hidden"
animate="visible"
transition={{ delay: 0.15 }}
>
<motion.div
animate={!reducedMotion ? { y: [0, -6, 0] } : {}}
transition={{ delay: 0.85, duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
>
<Image
src="/nearlenewlogo.png"
alt="Central logo panel"
width={100}
height={100}
className="object-contain max-h-[90px] w-auto motion-safe:animate-[pulse_6s_infinite]"
priority
/>
</motion.div>
</motion.div>
<motion.div
className="relative w-[105px] h-[220px] rounded-2xl overflow-hidden border border-white shadow-md z-10"
variants={imgFromRight}
initial="hidden"
animate="visible"
transition={{ delay: 0.36 }}
>
<Image
src="/nearle people.png"
alt="Customer"
@@ -183,10 +319,10 @@ export function Hero() {
className="object-cover"
sizes="105px"
/>
</div>
</motion.div>
</div>
</motion.div>
</div>
</div>
@@ -194,14 +330,13 @@ export function Hero() {
<div className="border-t border-[#EEE7F4]/80 bg-white">
<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}
variants={statStagger}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{PROOF_ITEMS.map((item, idx) => (
<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 */}
<motion.div key={item.label} variants={statItem} className="flex items-center gap-4 text-left md:justify-center relative">
{idx > 0 && (
<div className="hidden md:block absolute left-0 top-1/2 -translate-y-1/2 w-[1px] h-10 bg-[#EEE7F4]" />
)}

View File

@@ -256,10 +256,14 @@ export function HowItWorks() {
<span className="font-black text-primary">Yours</span>{" "}
establish a local online presence and empower neighbourhood customers to discover and interact with you directly.
</p>
<button className="mt-8 inline-flex w-full items-center justify-center gap-2 rounded-full bg-primary px-8 py-4 text-base font-black text-white shadow-[0_16px_30px_rgba(101,20,154,0.22)] transition-transform duration-200 hover:-translate-y-0.5 sm:w-auto">
<motion.button
className="mt-8 inline-flex w-full items-center justify-center gap-2 rounded-full bg-primary px-8 py-4 text-base font-black text-white shadow-[0_16px_30px_rgba(101,20,154,0.22)] sm:w-auto"
whileHover={{ y: -2, scale: 1.02, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
>
<MaterialIcon icon="forum" size={20} />
Talk to an Expert
</button>
</motion.button>
</motion.div>
<motion.div

View File

@@ -6,7 +6,7 @@ import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
const MotionLink = motion(Link);
const MotionLink = motion.create(Link);
const CATEGORIES = [
{
@@ -106,6 +106,8 @@ export function ServiceCategories() {
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"
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
>
{/* Image Container */}
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container/30">

View File

@@ -17,8 +17,16 @@ export function StoreQRSection() {
<div className="w-full">
<div id="download" className="w-full bg-gradient-to-br from-[#1A0322] to-[#0A000D] rounded-3xl md:rounded-[2rem] overflow-hidden text-white relative flex items-center py-12 md:py-16 px-6 md:px-12 lg:px-16 xl:px-20 border border-white/10 shadow-[0_20px_50px_rgba(20,0,30,0.22)]">
{/* Background ambient glowing details */}
<div className="absolute right-[-10%] top-[-10%] w-[50%] h-[50%] rounded-full bg-brand-accent/5 blur-[120px] pointer-events-none" />
<div className="absolute left-[-10%] bottom-[-10%] w-[50%] h-[50%] rounded-full bg-primary/10 blur-[120px] pointer-events-none" />
<motion.div
className="absolute right-[-10%] top-[-10%] w-[50%] h-[50%] rounded-full bg-brand-accent/5 blur-[120px] pointer-events-none"
animate={{ y: [0, -18, 0] }}
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.div
className="absolute left-[-10%] bottom-[-10%] w-[50%] h-[50%] rounded-full bg-primary/10 blur-[120px] pointer-events-none"
animate={{ y: [0, 16, 0] }}
transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
/>
<div className="w-full relative z-10">
@@ -124,7 +132,7 @@ export function StoreQRSection() {
</div>
<span className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[10px] font-black tracking-wider uppercase bg-primary/5 text-primary leading-none">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
<span className="w-1.5 h-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
Scan to Shop
</span>
</motion.div>
@@ -189,7 +197,7 @@ export function StoreQRSection() {
</div>
<span className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[10px] font-black tracking-wider uppercase bg-primary/5 text-primary">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
<span className="w-1.5 h-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
Scan to Shop
</span>
</motion.div>

View File

@@ -81,7 +81,7 @@ export function WhyNearle() {
</p>
</motion.div>
{/* Cards Grid — fadeIn preserves .premium-card:hover CSS transform */}
{/* Cards Grid */}
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-7"
variants={staggerContainer}
@@ -93,7 +93,9 @@ export function WhyNearle() {
<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"
className="premium-card bg-white rounded-3xl p-5 md:p-6 border border-outline-variant/15 shadow-lg flex flex-col h-full"
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
>
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
<MaterialIcon icon={card.icon} size={27} />

View File

@@ -1,5 +1,10 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { GlassInfoCard } from "../ui/GlassInfoCard";
import { scaleIn, staggerContainer, viewport } from "@/lib/animations";
import type { Variants } from "framer-motion";
type GlassCardItem = {
text: string;
@@ -12,6 +17,40 @@ type HeroVisualCollageProps = {
rightImage?: string;
glassCards: GlassCardItem[];
isPhoneCenter?: boolean;
cardVariant?: Variants;
};
const leftPanelVariant: Variants = {
hidden: { opacity: 0, x: -28 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const rightPanelVariant: Variants = {
hidden: { opacity: 0, x: 28 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const glasscardVariant: Variants = {
hidden: { opacity: 0, scale: 0.88, y: 8 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: "spring", stiffness: 260, damping: 22 },
},
};
const glassCardStagger: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } },
};
export function HeroVisualCollage({
@@ -20,14 +59,14 @@ export function HeroVisualCollage({
rightImage,
glassCards,
isPhoneCenter = true,
cardVariant,
}: HeroVisualCollageProps) {
const activeCardVariant = cardVariant ?? glasscardVariant;
return (
<div className="relative w-full h-[380px] sm:h-[450px] lg:h-[500px] max-w-[540px] mx-auto flex items-center justify-center select-none">
{/* Decorative backgrounds */}
{/* 1. Soft purple circle */}
<div className="absolute w-[240px] h-[240px] rounded-full bg-[#7C3AED]/10 blur-[60px] -z-10" />
{/* 2. SVG Dot grid */}
<div className="absolute -top-4 -right-4 w-32 h-32 text-[#6330D6]/10 -z-10 opacity-20">
<svg className="w-full h-full" fill="currentColor">
<pattern id="collage-dots" x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse">
@@ -37,13 +76,19 @@ export function HeroVisualCollage({
</svg>
</div>
{/* 3. Small x marks */}
<div className="absolute top-12 left-6 text-[#6330D6]/15 font-display text-xl font-bold select-none pointer-events-none"></div>
<div className="absolute bottom-16 right-8 text-[#6330D6]/15 font-display text-xl font-bold select-none pointer-events-none"></div>
{/* LEFT IMAGE PANEL (Hidden or compact on small mobile) */}
{/* LEFT IMAGE PANEL */}
{leftImage && (
<div className="absolute left-2 sm:left-4 top-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10 transition-transform hover:scale-105 duration-300">
<motion.div
className="absolute left-2 sm:left-4 top-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10"
variants={leftPanelVariant}
initial="hidden"
whileInView="visible"
viewport={viewport}
whileHover={{ scale: 1.04, transition: { duration: 0.25, ease: [0.22, 1, 0.36, 1] } }}
>
<Image
src={leftImage}
alt="Collage left asset"
@@ -51,12 +96,19 @@ export function HeroVisualCollage({
className="object-cover"
sizes="(max-width: 640px) 100px, 130px"
/>
</div>
</motion.div>
)}
{/* RIGHT IMAGE PANEL */}
{rightImage && (
<div className="absolute right-2 sm:right-4 bottom-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10 transition-transform hover:scale-105 duration-300">
<motion.div
className="absolute right-2 sm:right-4 bottom-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10"
variants={rightPanelVariant}
initial="hidden"
whileInView="visible"
viewport={viewport}
whileHover={{ scale: 1.04, transition: { duration: 0.25, ease: [0.22, 1, 0.36, 1] } }}
>
<Image
src={rightImage}
alt="Collage right asset"
@@ -64,13 +116,18 @@ export function HeroVisualCollage({
className="object-cover"
sizes="(max-width: 640px) 100px, 130px"
/>
</div>
</motion.div>
)}
{/* CENTER COMPONENT */}
<div className="relative z-20">
<motion.div
className="relative z-20"
variants={scaleIn}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{isPhoneCenter ? (
/* Phone Frame Container */
<div className="relative w-[180px] sm:w-[220px] h-[360px] sm:h-[440px] rounded-[36px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_50px_rgba(99,48,214,0.12)] bg-[#FAF8FC] overflow-hidden flex flex-col justify-center items-center">
<Image
src={centerImage}
@@ -82,56 +139,70 @@ export function HeroVisualCollage({
/>
</div>
) : (
/* Central Logo / Visual Panel */
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
<Image
src={centerImage}
alt="Central logo panel"
width={160}
height={160}
className="object-contain max-h-[140px] w-auto animate-[pulse_6s_infinite]"
className="object-contain max-h-[140px] w-auto motion-safe:animate-[pulse_6s_infinite]"
priority
/>
</div>
)}
</div>
</motion.div>
{/* FLOATING GLASS CARDS (Positioned Absolute, scaled down on mobile) */}
{/* FLOATING GLASS CARDS — stagger in after center */}
{glassCards.length > 0 && (
<div className="absolute inset-0 z-30 pointer-events-none">
{/* Card 1: Top Right */}
<motion.div
className="absolute inset-0 z-30 pointer-events-none"
variants={glassCardStagger}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{glassCards[0] && (
<div className="absolute right-0 sm:right-[5%] top-[10%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
<motion.div
variants={activeCardVariant}
className="absolute right-0 sm:right-[5%] top-[10%] pointer-events-auto"
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
>
<GlassInfoCard
text={glassCards[0].text}
icon={glassCards[0].icon}
className="scale-90 sm:scale-100"
/>
</div>
</motion.div>
)}
{/* Card 2: Bottom Left */}
{glassCards[1] && (
<div className="absolute left-0 sm:left-[5%] bottom-[10%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
<motion.div
variants={activeCardVariant}
className="absolute left-0 sm:left-[5%] bottom-[10%] pointer-events-auto"
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
>
<GlassInfoCard
text={glassCards[1].text}
icon={glassCards[1].icon}
className="scale-90 sm:scale-100"
/>
</div>
</motion.div>
)}
{/* Card 3: Mid Left/Right */}
{glassCards[2] && (
<div className="absolute left-[5%] top-[45%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
<motion.div
variants={activeCardVariant}
className="absolute left-[5%] top-[45%] pointer-events-auto"
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
>
<GlassInfoCard
text={glassCards[2].text}
icon={glassCards[2].icon}
className="scale-90 sm:scale-100"
/>
</div>
</motion.div>
)}
</div>
</motion.div>
)}
</div>
);

View File

@@ -1,120 +0,0 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
import { usePathname } from "next/navigation";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const NAV_LINKS = [
{ label: "Home", href: "/" },
{ label: "Nearle for Business", href: "/businesses" },
{ label: "Nearle for People", href: "/people" },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
];
export function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const pathname = usePathname();
return (
<div className="fixed top-0 left-0 right-0 z-50 w-full flex justify-center pointer-events-none">
<div className="w-full max-w-[1920px] px-4 md:px-6 lg:px-8 xl:px-10 2xl:px-12 pt-[18px] md:pt-[24px] pointer-events-auto relative">
<nav className="w-full h-[68px] lg:h-[76px] rounded-full bg-[rgba(61,45,143,0.88)] border border-white/20 shadow-sm backdrop-blur-md px-5 md:px-8 lg:px-10 flex items-center transition-all duration-300">
<div className="grid grid-cols-2 md:grid-cols-[1fr_auto_1fr] items-center w-full h-full">
{/* Left: Brand */}
<div className="flex items-center justify-start">
<Link
href="/"
className="flex items-center hover:scale-[1.02] transition-transform active:scale-95 duration-200"
>
<Image
src="/logo-light.png"
alt="Nearle"
width={400}
height={50}
className="h-8 lg:h-9 w-auto object-contain"
priority
/>
</Link>
</div>
{/* Center: Navigation Links */}
<div className="hidden md:flex items-center justify-center gap-6 lg:gap-8 xl:gap-10">
{NAV_LINKS.map((link) => {
const isActive = pathname === link.href;
return (
<Link
key={link.label}
href={link.href}
className={`relative py-2 text-[15px] font-semibold leading-5 transition-all duration-200 whitespace-nowrap ${
isActive ? "text-white" : "text-white/75 hover:text-white"
} group`}
>
{link.label}
<span
className={`absolute bottom-[-6px] left-1/2 -translate-x-1/2 h-0.5 bg-white rounded-full transition-all duration-300 ${
isActive
? "w-5 opacity-100"
: "w-0 opacity-0 group-hover:w-4 group-hover:opacity-60"
}`}
/>
</Link>
);
})}
</div>
{/* Right: Actions */}
<div className="flex items-center justify-end gap-3">
<Link
href="/#download"
className="hidden md:inline-flex items-center justify-center bg-white text-[rgba(61,45,143,0.88)] font-bold px-7 h-[46px] rounded-full text-[15px] hover:bg-white/90 hover:shadow-md hover:scale-[1.02] active:scale-95 transition-all duration-300 whitespace-nowrap"
>
Get App
</Link>
{/* Mobile menu toggle */}
<button
className="md:hidden flex items-center justify-center w-10 h-10 rounded-full hover:bg-white/10 text-white transition-all active:scale-90 cursor-pointer"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
<MaterialIcon icon={mobileOpen ? "close" : "menu"} size={22} />
</button>
</div>
</div>
</nav>
{/* Mobile Drawer */}
{mobileOpen && (
<div className="absolute top-[calc(100%+12px)] left-4 right-4 z-40 bg-[rgba(61,45,143,0.95)] border border-white/20 rounded-2xl shadow-[0_20px_40px_rgba(100,60,120,0.15)] backdrop-blur-md p-4 md:hidden">
<div className="flex flex-col gap-2">
{NAV_LINKS.map((link) => {
const isActive = pathname === link.href;
return (
<Link
key={link.label}
href={link.href}
className={`px-4 py-3 text-body-md font-bold rounded-xl transition-all duration-200 flex items-center justify-between ${
isActive
? "bg-white/10 text-white"
: "text-white/70 hover:text-white hover:bg-white/5"
}`}
onClick={() => setMobileOpen(false)}
>
{link.label}
{isActive && (
<span className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
)}
</Link>
);
})}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -3,13 +3,16 @@
import Link from "next/link";
import Image from "next/image";
import React from "react";
import { motion } from "framer-motion";
import { FaFacebookF, FaInstagram, FaLinkedinIn } from "react-icons/fa";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, fadeIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const COMPANY = [
{ label: "About Us", href: "/about" },
{ label: "Careers", href: "#" },
// { label: "Press & News", href: "#" },
{ label: "Help Center", href: "#" },
];
@@ -17,26 +20,87 @@ const PARTNERSHIPS = [
{ label: "Merchant Partner", href: "/merchants" },
{ label: "Delivery Partner", href: "/delivery-partners" },
{ label: "Direct to Customer Brands", href: "/brands" },
// { label: "Local Real Estate", href: "#" },
];
const LEGAL = [
{ label: "Privacy Policy", href: "/privacy" },
{ label: "Terms of Service", href: "/terms" },
{ label: "Cookie Policy", href: "/cookies" },
// { label: "Sitemap", href: "#" },
];
const socialButtonClass =
"flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition-all duration-300 xl:h-[54px] xl:w-[54px]";
"flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition-[background-color] duration-300 xl:h-[54px] xl:w-[54px]";
const linkColumnVariant = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: EASE },
},
};
const footerReveal = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: EASE },
},
};
// Renders an animated link list used in all three footer link columns
function FooterLinkList({ links }: { links: { label: string; href: string }[] }) {
return (
<ul className="flex flex-col gap-4 xl:gap-5">
{links.map((link) => (
<li key={link.label}>
<motion.span
className="relative inline-block"
initial="rest"
whileHover="hover"
animate="rest"
>
<Link
href={link.href}
className="text-sm leading-normal text-white/70 transition-colors duration-200 hover:text-white md:text-base xl:text-[17px]"
>
{link.label}
</Link>
{/* Underline grows from left on hover */}
<motion.span
className="absolute -bottom-0.5 left-0 right-0 h-px rounded-full bg-white/50"
style={{ originX: 0 }}
variants={{ rest: { scaleX: 0 }, hover: { scaleX: 1 } }}
transition={{ duration: 0.2, ease: EASE }}
/>
</motion.span>
</li>
))}
</ul>
);
}
export function PremiumFooter() {
return (
<section className="bg-white px-2 pb-2 md:px-3 md:pb-3 lg:px-4 lg:pb-4 xl:px-5 xl:pb-4 2xl:px-6 2xl:pb-5">
<footer className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#16001D] to-[#25002F] text-white shadow-[0_-12px_40px_rgba(22,0,29,0.12)]">
{/* Footer reveal: the whole footer fades up as it enters the viewport */}
<motion.footer
className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#16001D] to-[#25002F] text-white shadow-[0_-12px_40px_rgba(22,0,29,0.12)]"
variants={footerReveal}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<div className="flex flex-col px-6 pb-8 pt-10 md:px-8 md:pb-8 md:pt-12 xl:px-16 xl:pb-8 xl:pt-12 2xl:px-20 2xl:pt-10 2xl:pb-8">
<div className="grid w-full grid-cols-1 items-start gap-10 text-left md:grid-cols-2 md:gap-x-12 md:gap-y-12 lg:grid-cols-[1.6fr_1fr_1fr_1fr] lg:gap-10 xl:gap-14">
<div className="flex flex-col gap-5">
{/* Main grid: columns stagger in via parent footer variant propagation */}
<motion.div
className="grid w-full grid-cols-1 items-start gap-10 text-left md:grid-cols-2 md:gap-x-12 md:gap-y-12 lg:grid-cols-[1.6fr_1fr_1fr_1fr] lg:gap-10 xl:gap-14"
variants={staggerSlow}
>
{/* Brand column */}
<motion.div variants={fadeUp} className="flex flex-col gap-5">
<Link href="/" className="inline-block w-fit">
<Image
src="/logo-light.png"
@@ -52,100 +116,81 @@ export function PremiumFooter() {
communities and verified local businesses within a 2km radius.
</p>
{/* Social icons with scale spring on hover */}
<div className="mt-1 flex gap-3 xl:mt-2 xl:gap-4">
<a
<motion.a
href="https://www.facebook.com/nearledigital"
target="_blank"
rel="noopener noreferrer"
aria-label="Facebook"
className={`${socialButtonClass} hover:bg-[#1877F2]`}
whileHover={{ scale: 1.12 }}
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<FaFacebookF size={20} />
</a>
</motion.a>
<a
<motion.a
href="https://www.instagram.com/nearledigital/"
target="_blank"
rel="noopener noreferrer"
aria-label="Instagram"
className={`${socialButtonClass} hover:bg-[#E4405F]`}
whileHover={{ scale: 1.12 }}
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<FaInstagram size={21} />
</a>
</motion.a>
<a
<motion.a
href="https://www.linkedin.com/company/nearledigital/"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
className={`${socialButtonClass} hover:bg-[#0A66C2]`}
whileHover={{ scale: 1.12 }}
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<FaLinkedinIn size={20} />
</a>
</motion.a>
</div>
</div>
</motion.div>
<div className="flex flex-col">
{/* Company links */}
<motion.div variants={linkColumnVariant} className="flex flex-col">
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
Company
</h5>
<FooterLinkList links={COMPANY} />
</motion.div>
<ul className="flex flex-col gap-4 xl:gap-5">
{COMPANY.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
<div className="flex flex-col">
{/* Nearle links */}
<motion.div variants={linkColumnVariant} className="flex flex-col">
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
Nearle
</h5>
<FooterLinkList links={PARTNERSHIPS} />
</motion.div>
<ul className="flex flex-col gap-4 xl:gap-5">
{PARTNERSHIPS.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
<div className="flex flex-col">
{/* Support links */}
<motion.div variants={linkColumnVariant} className="flex flex-col">
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
Support
</h5>
<ul className="flex flex-col gap-4 xl:gap-5">
{LEGAL.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
</div>
<FooterLinkList links={LEGAL} />
</motion.div>
</motion.div>
<div className="my-8 w-full border-t border-white/10 xl:my-10" />
<div className="flex w-full flex-col items-center justify-between gap-8 md:flex-row xl:gap-12">
{/* Bottom row */}
<motion.div
className="flex w-full flex-col items-center justify-between gap-8 md:flex-row xl:gap-12"
variants={fadeUp}
>
<div className="flex min-h-[120px] w-full max-w-[560px] items-start gap-3 rounded-2xl border border-white/10 bg-white/5 p-4 md:w-auto md:p-5 xl:min-h-[130px] xl:gap-4 xl:p-6">
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/10 text-white xl:h-[58px] xl:w-[58px] xl:rounded-2xl">
<MaterialIcon icon="location_on" size={20} />
@@ -155,7 +200,6 @@ export function PremiumFooter() {
<h6 className="text-sm font-black text-white md:text-base xl:text-lg">
Nearle
</h6>
<p className="mt-0.5 text-xs leading-normal text-white/60 md:text-sm xl:mt-1 xl:text-base">
424, Diwan Bahadur Rd, Opp. Sri Krishna Sweets, R.S. Puram,
Coimbatore, TN 641002
@@ -181,11 +225,15 @@ export function PremiumFooter() {
</span>
</a>
</div>
</div>
</motion.div>
<div className="mb-4 mt-8 w-full border-t border-white/5 xl:mb-6 xl:mt-10" />
<div className="flex w-full flex-col items-center justify-between gap-4 text-[13px] text-white/40 md:flex-row md:text-[14px] xl:text-[15px]">
{/* Copyright bar */}
<motion.div
className="flex w-full flex-col items-center justify-between gap-4 text-[13px] text-white/40 md:flex-row md:text-[14px] xl:text-[15px]"
variants={fadeIn}
>
<p className="text-center md:text-left">
© {new Date().getFullYear()} Nearle. All rights reserved.
</p>
@@ -203,9 +251,9 @@ export function PremiumFooter() {
Help Center
</Link>
</div>
</div>
</motion.div>
</div>
</footer>
</motion.footer>
</section>
);
}

View File

@@ -2,10 +2,18 @@
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import {
motion,
AnimatePresence,
useScroll,
useMotionValueEvent,
} from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const NAV_LINKS = [
{ label: "Home", href: "/" },
{ label: "Nearle for Business", href: "/businesses" },
@@ -14,9 +22,44 @@ const NAV_LINKS = [
{ label: "Contact", href: "/contact" },
];
const mobileMenuVariants = {
hidden: { opacity: 0, height: 0 },
visible: {
opacity: 1,
height: "auto",
transition: { duration: 0.3, ease: EASE },
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.2, ease: "easeIn" as const },
},
};
const mobileStagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.04, delayChildren: 0.06 } },
};
const mobileItemVariants = {
hidden: { opacity: 0, x: -10 },
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
};
export function PremiumNavbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const pathname = usePathname();
const { scrollY } = useScroll();
useMotionValueEvent(scrollY, "change", (y) => {
setScrolled(y > 20);
});
// Close mobile menu whenever the route changes (navbar no longer remounts on navigation)
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
const isActiveLink = (href: string) => {
if (href === "/") return pathname === "/";
@@ -24,10 +67,18 @@ export function PremiumNavbar() {
};
return (
// <header className="sticky top-0 z-50 w-full border-b border-white/10 bg-[#3D2D8F]/90 text-white backdrop-blur-xl">
<header className="sticky top-0 z-50 w-full bg-[#83429f] border-b border-white/10 text-white">
<motion.header
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
scrolled
? "bg-[#83429f]/82 backdrop-blur-lg shadow-[0_6px_32px_rgba(0,0,0,0.35)] border-b border-white/15"
: "bg-[#83429f] border-b border-white/10"
}`}
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE }}
>
<div className="relative mx-auto flex h-16 w-full max-w-[1600px] items-center px-6 md:h-20 md:px-10 xl:px-16">
{/* Logo - Left */}
{/* Logo */}
<Link
href="/"
className="flex shrink-0 items-center transition-transform duration-200 hover:scale-[1.02] active:scale-95"
@@ -43,38 +94,55 @@ export function PremiumNavbar() {
/>
</Link>
{/* Desktop Navigation - Center */}
{/* Desktop Navigation */}
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-6 lg:flex lg:gap-8 xl:gap-10">
{NAV_LINKS.map((link) => {
const isActive = isActiveLink(link.href);
return (
<Link
<motion.div
key={link.label}
href={link.href}
className={`relative whitespace-nowrap py-2 text-sm font-bold transition-colors duration-200 ${
isActive
? "text-white"
: "text-white/70 hover:text-white"
}`}
className="relative py-2"
initial="rest"
whileHover="hover"
animate="rest"
>
{link.label}
<span
className={`absolute bottom-0 left-1/2 h-0.5 -translate-x-1/2 rounded-full bg-white transition-all duration-300 ${
isActive ? "w-full" : "w-0"
<Link
href={link.href}
className={`whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
isActive ? "text-white" : "text-white/70 hover:text-white"
}`}
/>
</Link>
>
{link.label}
</Link>
{/* Active underline: layoutId lets it slide between links on navigation */}
{isActive && (
<motion.span
layoutId="activeNavUnderline"
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
transition={{ duration: 0.3, ease: EASE }}
/>
)}
{/* Hover underline: grows from left on inactive links */}
{!isActive && (
<motion.span
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white/40"
style={{ originX: 0 }}
variants={{ rest: { scaleX: 0 }, hover: { scaleX: 1 } }}
transition={{ duration: 0.2, ease: EASE }}
/>
)}
</motion.div>
);
})}
</nav>
{/* Mobile Toggle - Right */}
{/* Mobile Toggle */}
<button
type="button"
className="ml-auto flex h-10 w-10 items-center justify-center rounded-full text-white transition-all duration-200 hover:bg-white/10 active:scale-90 lg:hidden"
onClick={() => setMobileOpen((value) => !value)}
onClick={() => setMobileOpen((v) => !v)}
aria-label={mobileOpen ? "Close menu" : "Open menu"}
aria-expanded={mobileOpen}
>
@@ -82,39 +150,48 @@ export function PremiumNavbar() {
</button>
</div>
{/* Mobile Menu */}
<div
className={`overflow-hidden border-t border-white/10 bg-[#30226F]/98 backdrop-blur-xl transition-all duration-300 lg:hidden ${
mobileOpen
? "max-h-[500px] opacity-100"
: "pointer-events-none max-h-0 opacity-0"
}`}
>
<nav className="mx-auto flex w-full max-w-[1600px] flex-col gap-1 px-6 py-5 md:px-10">
{NAV_LINKS.map((link) => {
const isActive = isActiveLink(link.href);
return (
<Link
key={link.label}
href={link.href}
onClick={() => setMobileOpen(false)}
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
isActive
? "bg-white/15 text-white"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
{link.label}
{isActive && (
<span className="h-2 w-2 rounded-full bg-white" />
)}
</Link>
);
})}
</nav>
</div>
</header>
{/* Mobile Menu with enter/exit animation */}
<AnimatePresence>
{mobileOpen && (
<motion.div
key="mobile-menu"
variants={mobileMenuVariants}
initial="hidden"
animate="visible"
exit="exit"
className="overflow-hidden border-t border-white/10 bg-[#30226F]/98 backdrop-blur-xl lg:hidden"
>
<motion.nav
className="mx-auto flex w-full max-w-[1600px] flex-col gap-1 px-6 py-5 md:px-10"
variants={mobileStagger}
initial="hidden"
animate="visible"
>
{NAV_LINKS.map((link) => {
const isActive = isActiveLink(link.href);
return (
<motion.div key={link.label} variants={mobileItemVariants}>
<Link
href={link.href}
onClick={() => setMobileOpen(false)}
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
isActive
? "bg-white/15 text-white"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
{link.label}
{isActive && (
<span className="h-2 w-2 rounded-full bg-white" />
)}
</Link>
</motion.div>
);
})}
</motion.nav>
</motion.div>
)}
</AnimatePresence>
</motion.header>
);
}
}

View File

@@ -1,27 +1,12 @@
import React from "react";
import { PremiumNavbar } from "./PremiumNavbar";
import { PremiumFooter } from "./PremiumFooter";
import { MotionProvider } from "./MotionProvider";
type PremiumPageShellProps = {
children: React.ReactNode;
className?: string;
};
export function PremiumPageShell({ children, className = "" }: PremiumPageShellProps) {
return (
<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>
<PremiumFooter />
</div>
</MotionProvider>
);
// Navbar, footer, and MotionProvider are rendered in the root layout (app/layout.tsx)
// and persist across all navigations. This component is kept for call-site compatibility.
export function PremiumPageShell({ children }: PremiumPageShellProps) {
return <>{children}</>;
}

View File

@@ -0,0 +1,42 @@
"use client";
import { motion } from "framer-motion";
import type { ReactNode, CSSProperties } from "react";
import { cardReveal, staggerCards, viewport20 } from "@/lib/animations";
type Props = {
children: ReactNode;
className?: string;
style?: CSSProperties;
};
export function AnimatedGrid({ children, className, style }: Props) {
return (
<motion.div
className={className}
style={style}
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
>
{children}
</motion.div>
);
}
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
export function AnimatedCard({ children, className, style }: Props) {
return (
<motion.div
className={className}
style={style}
variants={cardReveal}
whileHover={{ y: -4, transition: { duration: 0.22, ease: EASE } }}
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
>
{children}
</motion.div>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
import type { Variants } from "framer-motion";
const VARIANTS: Record<string, Variants> = {
fadeUp,
stagger: staggerContainer,
};
type AnimatedSectionProps = {
children: ReactNode;
className?: string;
variant?: "fadeUp" | "stagger";
delay?: number;
};
export function AnimatedSection({
children,
className = "",
variant = "fadeUp",
delay = 0,
}: AnimatedSectionProps) {
const chosen = VARIANTS[variant] ?? fadeUp;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const withDelay: Variants = {
hidden: chosen.hidden as any,
visible: {
...(chosen.visible as any),
transition: {
...((chosen.visible as any)?.transition ?? {}),
delay,
},
},
};
return (
<motion.div
className={className}
variants={withDelay}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{children}
</motion.div>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const VP = { once: true, amount: 0.25 } as const;
const stagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.13 } },
};
const textReveal = {
hidden: { opacity: 0, y: 18 },
visible: { opacity: 1, y: 0, transition: { duration: 0.52, ease: EASE } },
};
const buttonReveal = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { duration: 0.42, ease: EASE } },
};
const decorReveal = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 1.1, delay: 0.38 } },
};
type CTABannerProps = {
badge?: { icon: string; label: string };
heading: React.ReactNode;
body: string;
primary: { href: string; icon: string; label: string };
secondary: { href: string; icon: string; label: string };
decorations?: string[];
};
export function CTABanner({ badge, heading, body, primary, secondary, decorations }: CTABannerProps) {
return (
<section className="bg-white px-2 pt-2 pb-2 md:px-3 md:pt-3 md:pb-3 lg:px-4 lg:pt-4 lg:pb-4 xl:px-5 xl:pt-4 xl:pb-4 2xl:px-6 2xl:pb-5">
<motion.div
className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] text-white"
variants={stagger}
initial="hidden"
whileInView="visible"
viewport={VP}
>
<div className="px-6 py-12 md:px-10 md:py-14 lg:px-14 xl:py-12 text-center relative">
{decorations?.map((grad, i) => (
<motion.div
key={i}
className="absolute inset-0 pointer-events-none"
style={{ background: `radial-gradient(${grad})` }}
variants={decorReveal}
/>
))}
<div className="relative z-10 max-w-2xl mx-auto">
{badge && (
<motion.span
className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-white/15 text-white/70 text-xs font-black uppercase tracking-widest mb-6"
variants={textReveal}
>
<MaterialIcon icon={badge.icon} size={14} />
{badge.label}
</motion.span>
)}
<motion.h2
className="font-display text-3xl md:text-4xl xl:text-5xl font-black mb-4 tracking-tight leading-tight"
variants={textReveal}
>
{heading}
</motion.h2>
<motion.p
className="text-sm md:text-base text-white/70 mb-8 leading-relaxed max-w-lg mx-auto"
variants={textReveal}
>
{body}
</motion.p>
<motion.div className="flex flex-wrap justify-center gap-3.5" variants={buttonReveal}>
<Link
href={primary.href}
className="bg-[#6330D6] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm hover:shadow-xl transition-all duration-200 active:scale-95 flex items-center gap-2"
>
<MaterialIcon icon={primary.icon} size={18} />
<span>{primary.label}</span>
</Link>
<Link
href={secondary.href}
className="bg-white/10 hover:bg-white/15 border border-white/20 text-white font-black px-8 py-3.5 rounded-xl text-sm transition-all duration-200 active:scale-95 flex items-center gap-2"
>
<MaterialIcon icon={secondary.icon} size={18} />
<span>{secondary.label}</span>
</Link>
</motion.div>
</div>
</div>
</motion.div>
</section>
);
}

View File

@@ -1,8 +1,11 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { MaterialIcon } from "./MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
export type FAQItem = {
question: string;
answer: string;
@@ -31,25 +34,30 @@ export function FAQAccordion({ items }: FAQAccordionProps) {
<span className="text-sm md:text-base font-black text-[#16001D] leading-snug">
{item.question}
</span>
<div
className={`flex-shrink-0 w-8 h-8 rounded-full bg-[#6330D6]/8 flex items-center justify-center text-[#6330D6] transition-transform duration-300 ${
openIndex === index ? "rotate-180" : ""
}`}
<motion.div
className="flex-shrink-0 w-8 h-8 rounded-full bg-[#6330D6]/8 flex items-center justify-center text-[#6330D6]"
animate={{ rotate: openIndex === index ? 180 : 0 }}
transition={{ duration: 0.3, ease: EASE }}
>
<MaterialIcon icon="expand_more" size={20} />
</div>
</motion.div>
</button>
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
openIndex === index
? "max-h-[500px] opacity-100"
: "max-h-0 opacity-0"
}`}
>
<p className="px-6 pb-5 text-sm md:text-base text-[#5E5866] leading-relaxed border-t border-[#6330D6]/5 pt-4">
{item.answer}
</p>
</div>
<AnimatePresence initial={false}>
{openIndex === index && (
<motion.div
key="content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: EASE }}
style={{ overflow: "hidden" }}
>
<p className="px-6 pb-5 text-sm md:text-base text-[#5E5866] leading-relaxed border-t border-[#6330D6]/5 pt-4">
{item.answer}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>

View File

@@ -1,4 +1,8 @@
"use client";
import { motion } from "framer-motion";
import { MaterialIcon } from "./MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
type FeatureStatsItem = {
icon: string;
@@ -13,11 +17,18 @@ type FeatureStatsRowProps = {
export function FeatureStatsRow({ items, className = "" }: FeatureStatsRowProps) {
return (
<div className={`w-full py-6 md:py-8 ${className}`}>
<motion.div
className={`w-full py-6 md:py-8 ${className}`}
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<div className="grid grid-cols-2 lg:flex lg:flex-row items-center justify-between gap-y-6 gap-x-4 lg:gap-0">
{items.map((item, index) => (
<div
<motion.div
key={item.title}
variants={fadeUp}
className={`flex items-center gap-3.5 lg:flex-1 px-3 lg:px-8 text-left ${
index < items.length - 1
? "lg:border-r lg:border-[#6330D6]/10"
@@ -37,9 +48,9 @@ export function FeatureStatsRow({ items, className = "" }: FeatureStatsRowProps)
</p>
)}
</div>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
);
}

View File

@@ -16,7 +16,7 @@ export function GlassInfoCard({ text, icon, className = "" }: GlassInfoCardProps
<MaterialIcon icon={icon} size={14} />
</div>
) : (
<span className="w-2.5 h-2.5 rounded-full bg-[#6330D6] shrink-0 animate-pulse" />
<span className="w-2.5 h-2.5 rounded-full bg-[#6330D6] shrink-0 motion-safe:animate-pulse" />
)}
<span className="text-sm font-extrabold text-[#16001D] whitespace-nowrap">
{text}