update animation pages

This commit is contained in:
2026-06-29 11:34:56 +05:30
parent a89cd2939a
commit 9449accea6
19 changed files with 468 additions and 158 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 657 KiB

43
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "nearle-new",
"version": "0.1.0",
"dependencies": {
"framer-motion": "^12.42.0",
"jsdom": "^29.1.1",
"next": "16.2.9",
"puppeteer-core": "^24.43.1",
@@ -4306,6 +4307,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/framer-motion": {
"version": "12.42.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz",
"integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.42.0",
"motion-utils": "^12.39.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5811,6 +5839,21 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/motion-dom": {
"version": "12.42.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz",
"integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.39.0"
}
},
"node_modules/motion-utils": {
"version": "12.39.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
"integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"framer-motion": "^12.42.0",
"jsdom": "^29.1.1",
"next": "16.2.9",
"puppeteer-core": "^24.43.1",

View File

@@ -262,7 +262,7 @@ export default function BrandsPage() {
Your Category Has a Place on Nearle
</h2>
<p className="text-sm md:text-base text-[#5E5866] max-w-2xl mx-auto leading-relaxed">
Nearle's neighbourhood store network stocks a wide range of everyday product categories. If customers buy it daily, it belongs here.
Nearle&apos;s neighbourhood store network stocks a wide range of everyday product categories. If customers buy it daily, it belongs here.
</p>
</div>

View File

@@ -64,7 +64,7 @@ export default function ContactPage() {
</div>
<div className="text-left">
<h4 className="text-[#16001D] text-sm font-black">Email Us</h4>
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">support@nearle.in partners@nearle.in</p>
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">nearle@care.in</p>
</div>
</div>
@@ -74,13 +74,13 @@ export default function ContactPage() {
<MaterialIcon icon="location_on" size={18} />
</div>
<div className="text-left">
<h4 className="text-[#16001D] text-sm font-black">Visit Us</h4>
<h4 className="text-[#16001D] text-sm font-black">Address </h4>
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">424, DB Rd, R.S. Puram, Coimbatore, TN 641002</p>
</div>
</div>
{/* Helpdesk */}
<div className="flex gap-4 py-4.5 border-b border-[#6330D6]/10 items-center">
{/* <div 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="support_agent" size={18} />
</div>
@@ -88,7 +88,7 @@ export default function ContactPage() {
<h4 className="text-[#16001D] text-sm font-black">Helpdesk</h4>
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">24/7 Phone Helpdesk Active</p>
</div>
</div>
</div> */}
</div>
</div>

View File

@@ -365,7 +365,7 @@ export default function DeliveryPartnersPage() {
</section>
{/* ─── Earnings Illustration ─── */}
<section id="earnings" className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
{/* <section id="earnings" className="py-10 md:py-14 xl:py-12 bg-[#FAF7FD] border-b border-[#6330D6]/10">
<div className="px-6 md:px-12 lg:px-16 xl:px-20 max-w-[1600px] mx-auto">
<div className="text-center mb-10 md:mb-12">
<span className="text-xs text-secondary uppercase tracking-widest font-black bg-secondary/5 px-3.5 py-1 rounded-full">
@@ -410,7 +410,7 @@ export default function DeliveryPartnersPage() {
Estimates include base delivery fees only. Peak-hour surge pricing, performance bonuses, and festive incentives are additional. Earnings vary by zone.
</p>
</div>
</section>
</section> */}
{/* ─── FAQ ─── */}
<section className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">

View File

@@ -79,6 +79,7 @@ export default function RootLayout({
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
{/* eslint-disable-next-line @next/next/no-page-custom-font */}
<link
href="https://fonts.googleapis.com/css2?family=Jost:wght@400;600;700;800&display=swap"
rel="stylesheet"

View File

@@ -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 (7681279px) : 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>

View File

@@ -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" },
@@ -50,22 +59,39 @@ export function Hero() {
<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">
<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
</span>
</motion.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]">
<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,11 +104,16 @@ 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">
@@ -155,15 +186,21 @@ export function Hero() {
</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>

View File

@@ -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>
);

View File

@@ -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 = [
{
@@ -37,7 +41,13 @@ export function HyperlocalFeatures() {
<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,31 +145,54 @@ 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>
</motion.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.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">
@@ -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>

View File

@@ -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>
);
}

View File

@@ -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" },
@@ -22,7 +26,13 @@ export function StoreQRSection() {
<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">

View File

@@ -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>
);
}

View 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>;
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,56 @@
import type { Variants } from "framer-motion";
export const fadeUp: Variants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
},
};
// Use for elements with CSS transform-based hover effects (e.g. .premium-card:hover)
// to avoid inline-style override of CSS hover transforms.
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 0.45, ease: "easeOut" },
},
};
export const scaleIn: Variants = {
hidden: { opacity: 0, scale: 0.95 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
},
};
export const slideInLeft: Variants = {
hidden: { opacity: 0, x: -40 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
},
};
export const slideInRight: Variants = {
hidden: { opacity: 0, x: 40 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
},
};
export const staggerContainer: Variants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.09 },
},
};
export const viewport = { once: true, margin: "-60px" } as const;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const puppeteer = require('puppeteer-core');
const path = require('path');