update why nearle page

This commit is contained in:
2026-07-19 23:07:18 +05:30
parent 4073dc8386
commit 9abd59ec85
51 changed files with 3748 additions and 62 deletions

1
Apple download.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

1
Dlivery Map.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 186 KiB

1
Food.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 431 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 188 KiB

1
customer.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

BIN
platfrom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

1
play 1.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 105 KiB

1
public/Food.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 431 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

1
public/customers.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

1
public/location.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 186 KiB

BIN
public/platform.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

1
public/play-download.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 105 KiB

1
public/playstore.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

BIN
public/superhero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

1
public/trust.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/why-nearle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -22,6 +22,7 @@ export const metadata: Metadata = {
"Partner with Nearle to get your brand into neighbourhood stores. Distribution, retail visibility, marketing campaigns, and real-time analytics, all in one platform.",
url: "https://nearle.in/brands",
},
alternates: { canonical: "/brands" },
};
const WHAT_WE_OFFER = [
@@ -35,7 +36,7 @@ const WHAT_WE_OFFER = [
{
icon: "storefront",
title: "Retail Shelf Visibility",
desc: "Premium placement in Nearle's digital catalogue and in-store QR listings. Customers browsing nearby stores see your brand first, creating daily brand touchpoints at zero extra cost.",
desc: "Premium placement in Nearle's AI catalogue by ProductLLM and in-store QR listings. Customers browsing nearby stores see your brand first, creating daily brand touchpoints at zero extra cost.",
tone: "bg-secondary/8 text-secondary",
badge: "Visibility",
},

View File

@@ -14,8 +14,23 @@ export const metadata: Metadata = {
title: "Nearle for Business",
description:
"Grow your local business with Nearle. Get discovered by nearby customers, manage online orders, run local offers, and build repeat sales, all from one simple hyperlocal platform.",
openGraph: {
title: "Nearle for Business — MerchantOS for Neighbourhood Stores",
description:
"Bring your store online with MerchantOS: a free POS, an AI catalog by ProductLLM, QR ordering, and local discovery. Keep your customers — add the technology.",
url: "https://nearle.in/businesses",
},
alternates: { canonical: "/businesses" },
};
const GROWTH_STEPS = [
{ icon: "storefront", title: "Merchant", desc: "Onboard your store in minutes with a free POS." },
{ icon: "auto_awesome", title: "AI Catalog", desc: "ProductLLM builds your catalog from a photo." },
{ icon: "receipt_long", title: "Orders", desc: "Take QR and online orders from nearby customers." },
{ icon: "analytics", title: "Analytics", desc: "See what sells and who's buying, in real time." },
{ icon: "trending_up", title: "Growth", desc: "Turn local demand into repeat business." },
];
function PhoneMapVisual() {
@@ -235,6 +250,56 @@ export default function BusinessesPage() {
{/* ─── Our Features / Business Grid ─── */}
<BusinessFeaturesGrid />
{/* ─── Business Growth Workflow ─── */}
<section className="py-10 md:py-14 xl:py-12 border-b border-[#6330D6]/10 bg-white overflow-hidden">
<div className="w-[calc(100%-32px)] lg:w-[calc(100%-64px)] max-w-[1280px] mx-auto">
<AnimatedSection>
<div className="text-center mb-10 lg:mb-12 xl:mb-10">
<span className="inline-flex items-center rounded-full bg-[#6330D6]/5 px-4 py-1.5 text-xs font-black uppercase tracking-widest text-[#6330D6]">
Growth Workflow
</span>
<h2 className="font-display text-[30px] md:text-4xl lg:text-[44px] font-black text-[#16001D] mt-4 leading-tight tracking-tight">
From shelf to steady growth
</h2>
<p className="mx-auto mt-3 max-w-[700px] text-base md:text-lg text-[#5E5866] leading-relaxed">
Every step on Nearle builds on the last — powered by MerchantOS and
an AI catalog from ProductLLM.
</p>
</div>
</AnimatedSection>
<AnimatedGrid className="flex flex-col items-stretch gap-4 lg:flex-row lg:items-center lg:gap-2">
{GROWTH_STEPS.map((step, i) => (
<div key={step.title} className="contents">
<AnimatedCard className="flex-1 rounded-[24px] border border-[#6330D6]/10 bg-white p-6 text-center shadow-[0_10px_30px_rgba(22,0,29,0.05)]">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-[#6330D6]/8 text-[#6330D6]">
<MaterialIcon icon={step.icon} size={24} />
</div>
<p className="mt-4 font-display font-black text-[#16001D] text-lg leading-tight">
{step.title}
</p>
<p className="mt-2 text-sm leading-relaxed text-[#5E5866]">
{step.desc}
</p>
</AnimatedCard>
{i < GROWTH_STEPS.length - 1 && (
<div aria-hidden="true" className="flex shrink-0 items-center justify-center py-1 lg:px-0.5">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#6330D6]/8 text-[#6330D6]">
<span className="flex lg:hidden">
<MaterialIcon icon="arrow_downward" size={18} />
</span>
<span className="hidden lg:flex">
<MaterialIcon icon="arrow_forward" size={18} />
</span>
</div>
</div>
)}
</div>
))}
</AnimatedGrid>
</div>
</section>
{/* ─── Join/Download Call to Action ─── */}
<CTABanner
heading="Nearle is Essential for Business Growth"

View File

@@ -70,6 +70,35 @@ export const viewport: Viewport = {
initialScale: 1,
};
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://nearle.in/#organization",
name: "Nearle",
url: "https://nearle.in",
logo: "https://nearle.in/logo.png",
description:
"Nearle is an AI Commerce platform that digitizes neighbourhood stores — with AI catalogs by ProductLLM, QR ordering, and local discovery — instead of replacing them.",
sameAs: [
"https://www.instagram.com/nearle",
"https://www.linkedin.com/company/nearle",
"https://www.facebook.com/nearle",
],
},
{
"@type": "WebSite",
"@id": "https://nearle.in/#website",
url: "https://nearle.in",
name: "Nearle",
description:
"AI Commerce platform for neighbourhood stores. MerchantOS, ProductLLM, and ConsumerOS.",
publisher: { "@id": "https://nearle.in/#organization" },
},
],
};
export default function RootLayout({
children,
}: {
@@ -83,6 +112,10 @@ export default function RootLayout({
suppressHydrationWarning
>
<body className="bg-[#E8DCF2] text-on-surface font-body antialiased" suppressHydrationWarning>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<MotionProvider>
<div className="min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col">
<PremiumNavbar />

View File

@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { Hero } from "@/components/home/Hero";
import { ServiceCategories } from "@/components/home/ServiceCategories";
@@ -5,8 +6,22 @@ import { WhyNearle } from "@/components/home/WhyNearle";
import { HowItWorks } from "@/components/home/HowItWorks";
import { HyperlocalFeatures } from "@/components/home/HyperlocalFeatures";
import { BusinessSection } from "@/components/home/BusinessSection";
import { WhyAICommerceWins } from "@/components/home/WhyAICommerceWins";
import { StoreQRSection } from "@/components/home/StoreQRSection";
export const metadata: Metadata = {
title: "AI Commerce Platform for Neighbourhood Stores",
description:
"Nearle is the AI Commerce platform that digitizes neighbourhood stores instead of replacing them. AI catalogs by ProductLLM, QR ordering, and local discovery — commerce that runs on your neighbourhood.",
openGraph: {
title: "Nearle — AI Commerce Platform for Neighbourhood Stores",
description:
"Not another delivery app. Nearle adds AI, a digital storefront, and ordering to the local stores you already trust.",
url: "https://nearle.in",
},
alternates: { canonical: "/" },
};
export default function HomePage() {
return (
<PremiumPageShell>
@@ -16,6 +31,7 @@ export default function HomePage() {
<HowItWorks />
<HyperlocalFeatures />
<BusinessSection />
<WhyAICommerceWins />
<StoreQRSection />
</PremiumPageShell>
);

View File

@@ -5,6 +5,7 @@ import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { PeopleHero } from "@/components/heroes/PeopleHero";
import { AnimatedSection } from "@/components/motion/AnimatedSection";
import { AnimatedGrid, AnimatedCard } from "@/components/motion/AnimatedGrid";
import { AnimatedTimeline } from "@/components/delivery/AnimatedTimeline";
import { siteContent } from "@/config/site-content";
import type { Metadata } from "next";
@@ -12,8 +13,65 @@ export const metadata: Metadata = {
title: "Nearle for People",
description:
"Discover verified local stores, save your favourite merchants, and order everyday essentials with superfast local delivery from shops within your neighbourhood.",
openGraph: {
title: "Nearle for People — Shop Your Neighbourhood",
description:
"Find verified local stores near you, with accurate AI-generated listings by ProductLLM. Discover, order, and reorder from the shops you already trust.",
url: "https://nearle.in/people",
},
alternates: { canonical: "/people" },
};
const HOW_IT_WORKS = [
{
step: "01",
title: "Discover stores near you",
desc: "Open the app and see verified neighbourhood shops within walking distance — not warehouses across town.",
time: "Discover",
},
{
step: "02",
title: "Order in a few taps",
desc: "Browse live catalogs built by ProductLLM, add what you need, and check out. Prices and stock stay accurate.",
time: "Order",
},
{
step: "03",
title: "Pickup or local delivery",
desc: "Grab it on your way past, or get a short local delivery straight from the store to your door.",
time: "Fulfil",
},
{
step: "04",
title: "Reorder in one tap",
desc: "Your favourite stores and usuals stay saved, so buying again is instant.",
time: "Reorder",
},
];
const TRUST_POINTS = [
{
icon: "verified_user",
title: "Verified Merchants",
desc: "Every store on Nearle is a real, verified neighbourhood business.",
},
{
icon: "auto_awesome",
title: "Accurate AI Listings",
desc: "Product listings are generated by ProductLLM, so details and prices stay right.",
},
{
icon: "storefront",
title: "Real Neighbourhood Stores",
desc: "You shop the shops on your street — the ones your community already trusts.",
},
{
icon: "lock",
title: "Safe Ordering",
desc: "Order with confidence, from discovery to doorstep.",
},
];
const OFFERINGS = [
{
title: "Elegant Living",
@@ -224,6 +282,59 @@ export default function PeoplePage() {
</AnimatedGrid>
</section>
{/* ─── How It Works ─── */}
<section className="py-10 md:py-14 xl:py-10 px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24 bg-[#FAF7FD] border-b border-[#6330D6]/10">
<AnimatedSection>
<div className="text-center mb-10 xl:mb-8">
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
How It Works
</span>
<h2 className="font-display text-3xl font-black text-[#16001D] mt-4">
From your street to your doorstep
</h2>
<p className="mx-auto mt-3 max-w-xl text-sm md:text-base text-[#5E5866]">
Shopping your neighbourhood on Nearle takes just a few taps.
</p>
</div>
</AnimatedSection>
<AnimatedTimeline items={HOW_IT_WORKS} />
</section>
{/* ─── Trust Section ─── */}
<section className="py-10 md:py-14 xl:py-10 px-6 md:px-12 lg:px-16 xl:px-20 2xl:px-24 text-center bg-white border-b border-[#6330D6]/10">
<AnimatedSection>
<div className="mb-10 xl:mb-8">
<span className="text-xs text-[#6330D6] uppercase tracking-widest font-black bg-[#6330D6]/5 px-3.5 py-1 rounded-full">
Trust &amp; Safety
</span>
<h2 className="font-display text-3xl font-black text-[#16001D] mt-4">
Every Store is Verified
</h2>
<p className="mx-auto mt-3 max-w-xl text-sm md:text-base text-[#5E5866]">
Real shops, accurate listings, and safe ordering — so you always know
exactly who you&rsquo;re buying from.
</p>
</div>
</AnimatedSection>
<AnimatedGrid className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 text-left">
{TRUST_POINTS.map((t) => (
<AnimatedCard
key={t.title}
className="premium-card bg-white rounded-[24px] p-6 border border-[#6330D6]/10 shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#6330D6]/8 text-[#6330D6]">
<MaterialIcon icon={t.icon} size={24} />
</div>
<p className="mt-4 font-display font-black text-[#16001D] text-lg leading-tight">
{t.title}
</p>
<p className="mt-2 text-sm leading-relaxed text-[#5E5866]">{t.desc}</p>
</AnimatedCard>
))}
</AnimatedGrid>
</section>
{/* ─── Ready to Download Section ─── */}
<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">
<section id="download-section" className="relative w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#16001D] to-[#25002F] p-8 text-center text-white md:p-10 lg:p-12 xl:p-10">

78
src/app/platform/page.tsx Normal file
View File

@@ -0,0 +1,78 @@
import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { PlatformHero } from "@/components/platform/PlatformHero";
import { PlatformArchitecture } from "@/components/platform/PlatformArchitecture";
import { ProductFeatureGrid } from "@/components/platform/ProductFeatureGrid";
import { ProductLLMWorkflow } from "@/components/platform/ProductLLMWorkflow";
import { CTABanner } from "@/components/ui/CTABanner";
export const metadata: Metadata = {
title: "Platform — MerchantOS, ConsumerOS & ProductLLM",
description:
"The Nearle platform: MerchantOS runs the store, ProductLLM builds the catalog from a shelf photo, and ConsumerOS brings the neighbourhood to shop. AI infrastructure for local retail.",
openGraph: {
title: "The Nearle Platform | MerchantOS · ProductLLM · ConsumerOS",
description:
"AI infrastructure behind every neighbourhood store — a free POS and AI catalog for merchants, and local discovery, ordering, and rewards for shoppers.",
url: "https://nearle.in/platform",
},
alternates: { canonical: "/platform" },
};
const MERCHANT_OS = [
{ icon: "point_of_sale", title: "Free POS", desc: "A complete point-of-sale, free for every merchant." },
{ icon: "wifi_off", title: "Offline Billing", desc: "Keep billing customers even when the network drops." },
{ icon: "inventory_2", title: "Inventory", desc: "Live stock counts that stay accurate across channels." },
{ icon: "analytics", title: "Analytics", desc: "See what sells, when, and to whom — in real time." },
{ icon: "loyalty", title: "Loyalty", desc: "Reward regulars and bring them back more often." },
{ icon: "auto_awesome", title: "AI Catalog", desc: "A shoppable catalog built by ProductLLM from a photo." },
{ icon: "qr_code", title: "QR Ordering", desc: "Customers scan, browse, and order in seconds." },
{ icon: "dashboard", title: "Store Dashboard", desc: "Run the whole store from one simple screen." },
];
const CONSUMER_OS = [
{ icon: "explore", title: "Local Discovery", desc: "Find the stores and products right around you." },
{ icon: "photo_camera", title: "Camera Search", desc: "Point your camera to find a product nearby." },
{ icon: "favorite", title: "Favorites", desc: "Save the stores and items you buy again and again." },
{ icon: "storefront", title: "Nearby Stores", desc: "Shop shops within walking distance, not warehouses." },
{ icon: "shopping_bag", title: "Pickup", desc: "Order ahead and grab it on your way past." },
{ icon: "local_shipping", title: "Delivery", desc: "Short, local trips from the store to your door." },
{ icon: "redeem", title: "Rewards", desc: "Earn as you shop your neighbourhood." },
{ icon: "history", title: "Fast Reordering", desc: "Re-buy your usuals in a single tap." },
];
export default function PlatformPage() {
return (
<PremiumPageShell>
<PlatformHero />
<PlatformArchitecture />
<ProductFeatureGrid
id="merchantos"
eyebrow="MerchantOS"
name="Everything a store needs to run"
tagline="A free operating system for the neighbourhood shop — billing, inventory, catalog, and orders, working online and off."
features={MERCHANT_OS}
variant="light"
showcase="dashboard"
footnote="AI Catalog powered by ProductLLM."
/>
<ProductLLMWorkflow />
<ProductFeatureGrid
id="consumeros"
eyebrow="ConsumerOS"
name="How the neighbourhood shops"
tagline="Discovery, search, ordering, and rewards — the app that brings local customers to nearby stores."
features={CONSUMER_OS}
variant="lavender"
showcase="phone"
/>
<CTABanner
badge={{ icon: "hub", label: "Built on Nearle" }}
heading="Put your store on the platform"
body="Join the neighbourhood stores already running on MerchantOS — with an AI catalog by ProductLLM and a growing base of local shoppers on ConsumerOS."
primary={{ href: "/businesses", icon: "storefront", label: "For Business" }}
secondary={{ href: "/why-nearle", icon: "auto_awesome", label: "Why AI Commerce" }}
/>
</PremiumPageShell>
);
}

14
src/app/robots.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { MetadataRoute } from "next";
const BASE_URL = "https://nearle.in";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${BASE_URL}/sitemap.xml`,
host: BASE_URL,
};
}

30
src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,30 @@
import type { MetadataRoute } from "next";
const BASE_URL = "https://nearle.in";
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date();
const routes: { path: string; priority: number; changeFrequency: MetadataRoute.Sitemap[number]["changeFrequency"] }[] = [
{ path: "/", priority: 1, changeFrequency: "weekly" },
{ path: "/why-nearle", priority: 0.9, changeFrequency: "monthly" },
{ path: "/platform", priority: 0.9, changeFrequency: "monthly" },
{ path: "/businesses", priority: 0.8, changeFrequency: "monthly" },
{ path: "/people", priority: 0.8, changeFrequency: "monthly" },
{ path: "/brands", priority: 0.8, changeFrequency: "monthly" },
{ path: "/merchants", priority: 0.6, changeFrequency: "monthly" },
{ path: "/delivery-partners", priority: 0.6, changeFrequency: "monthly" },
{ path: "/about", priority: 0.5, changeFrequency: "monthly" },
{ path: "/contact", priority: 0.5, changeFrequency: "monthly" },
{ path: "/privacy", priority: 0.2, changeFrequency: "yearly" },
{ path: "/terms", priority: 0.2, changeFrequency: "yearly" },
{ path: "/cookies", priority: 0.2, changeFrequency: "yearly" },
];
return routes.map((route) => ({
url: `${BASE_URL}${route.path}`,
lastModified: now,
changeFrequency: route.changeFrequency,
priority: route.priority,
}));
}

View File

@@ -0,0 +1,61 @@
import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { WhyNearleHero } from "@/components/why-nearle/WhyNearleHero";
import { CommerceEvolution } from "@/components/why-nearle/CommerceEvolution";
import { AICommerceExplainer } from "@/components/why-nearle/AICommerceExplainer";
import { BeforeAfterFlow } from "@/components/why-nearle/BeforeAfterFlow";
import { NeighborhoodStores } from "@/components/why-nearle/NeighborhoodStores";
import { EcosystemDiagram } from "@/components/why-nearle/EcosystemDiagram";
import { ProductLLMPipeline } from "@/components/why-nearle/ProductLLMPipeline";
import { ProductLLMCompare } from "@/components/why-nearle/ProductLLMCompare";
import { AICommerceComparison } from "@/components/ui/AICommerceComparison";
import { FutureVision } from "@/components/why-nearle/FutureVision";
import { PullQuote } from "@/components/ui/PullQuote";
import { AICommerceCTA } from "@/components/why-nearle/AICommerceCTA";
export const metadata: Metadata = {
title: "What is AI Commerce?",
description:
"AI Commerce starts where quick commerce stops. Nearle digitizes neighbourhood stores with AI, digital storefronts, and ordering — instead of replacing them. Learn how AI Commerce works.",
openGraph: {
title: "What is AI Commerce? | Nearle",
description:
"Neighbourhood stores already have trust, customers, and location. Nearle adds AI, a digital storefront, inventory, and ordering. This is AI Commerce.",
url: "https://nearle.in/why-nearle",
},
alternates: { canonical: "/why-nearle" },
};
export default function WhyNearlePage() {
return (
<PremiumPageShell>
<WhyNearleHero />
{/* 1 · Why AI Commerce — evolution timeline */}
<CommerceEvolution />
{/* 2 · The Problem — stores already have vs Nearle adds */}
<AICommerceExplainer />
{/* 3 · Before vs After — traditional vs AI commerce flow */}
<BeforeAfterFlow />
{/* 4 · Why neighborhood stores matter */}
<NeighborhoodStores />
{/* 5 · The AI Commerce ecosystem */}
<EcosystemDiagram />
{/* 6 · ProductLLM workflow pipeline */}
<ProductLLMPipeline />
{/* 7 · Why ProductLLM — manual vs AI catalogs */}
<ProductLLMCompare />
{/* Supporting comparison — AI Commerce vs Quick Commerce */}
<AICommerceComparison />
<PullQuote
lines={[
"Quick commerce tried to out-run the neighborhood.",
"AI Commerce runs on it.",
]}
/>
{/* 8 · Future vision roadmap */}
<FutureVision />
{/* 9 · Premium closing CTA */}
<AICommerceCTA />
</PremiumPageShell>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import React from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
@@ -415,12 +416,29 @@ const CARDS = [
export function BusinessFeaturesGrid() {
return (
<section
className="bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
className="relative bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
style={{ paddingTop: "clamp(44px, 4.5vw, 64px)", paddingBottom: "clamp(44px, 4.5vw, 64px)" }}
>
{/* Decorative mascot watermark — left side only, never crosses into the feature cards */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 left-0 z-0 flex items-center select-none"
style={{ width: "clamp(360px, 34vw, 600px)" }}
>
<div className="relative h-[88%] w-full -translate-x-[2%]">
<Image
src="/superhero.png"
alt=""
fill
sizes="600px"
className="object-contain object-left opacity-[0.12] blur-[1px]"
/>
</div>
</div>
{/* Centered page container */}
<div
className="mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
className="relative z-10 mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
style={{ maxWidth: 1800 }}
>
{/* Main two-column grid */}
@@ -439,7 +457,7 @@ export function BusinessFeaturesGrid() {
{/* 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
MerchantOS
</span>
<h2
className="font-display font-black text-[#16001D] mt-4"

View File

@@ -8,8 +8,8 @@ import { fadeUp, fadeIn, scaleIn, staggerContainer, viewport } from "@/lib/anima
const FEATURES = [
{
title: "Digital Catalogue",
desc: "Showcase your products online and keep your catalogue updated.",
title: "AI Catalogue",
desc: "Turn your shelf into a live online catalogue with ProductLLM — no manual data entry.",
icon: "menu_book",
visual: "/images/nearle-merchant/catalogue-ui.webp",
alt: "Clean grocery catalogue interface preview",

View File

@@ -82,19 +82,16 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on the App Store"
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
App Store
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/apple-download.svg"
alt="Download Nearle on the App Store"
width={144}
height={60}
className="h-[60px] w-auto object-contain"
priority
/>
</a>
<a
@@ -102,19 +99,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on Google Play"
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
Google Play
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/play-download.svg"
alt="Download Nearle on Google Play"
width={155}
height={60}
className="h-[60px] w-auto object-contain"
/>
</a>
</div>
</motion.div>
@@ -264,19 +257,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on the App Store"
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
App Store
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/apple-download.svg"
alt="Download Nearle on the App Store"
width={134}
height={56}
className="h-14 w-auto object-contain"
/>
</a>
<a
@@ -284,19 +273,15 @@ export function StoreQRSection() {
target="_blank"
rel="noopener noreferrer"
aria-label="Download Nearle on Google Play"
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
>
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
</svg>
<div className="text-left leading-tight">
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
Google Play
</div>
<div className="text-sm font-black">
Available on
</div>
</div>
<Image
src="/play-download.svg"
alt="Download Nearle on Google Play"
width={145}
height={56}
className="h-14 w-auto object-contain"
/>
</a>
</div>
</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";
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
const CARDS = [
{
title: "Neighbourhood Trust",
desc: "Every order is fulfilled by a real local shop your street already knows — not an anonymous warehouse.",
icon: "handshake",
color: "bg-primary/5 text-primary border-primary/10",
},
{
title: "AI-Powered Catalog",
desc: "ProductLLM turns a shelf into a live, shoppable catalog in minutes — so any store can go online without the busywork.",
icon: "auto_awesome",
color: "bg-secondary/5 text-secondary border-secondary/10",
},
{
title: "Offline-First Commerce",
desc: "Billing and inventory keep working even when the network drops. Built for how neighbourhood stores really run.",
icon: "wifi_off",
color: "bg-[#de96f9]/10 text-primary border-[#de96f9]/20",
},
];
export function WhyAICommerceWins() {
return (
<section className="section bg-white w-full overflow-hidden">
<div className="page-container relative">
<motion.div
className="relative text-center mb-8 md:mb-10"
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/5 px-3.5 py-1.5 rounded-full">
AI Commerce
</span>
<h2 className="text-4xl md:text-5xl lg:text-[54px] font-display font-black text-brand-dark mt-3 leading-tight tracking-tight">
Why AI Commerce <span className="text-primary font-normal">Wins</span>
</h2>
<p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed">
Nearle isn&rsquo;t another delivery app. It&rsquo;s the AI layer that brings
neighbourhood stores online — without replacing them.
</p>
</motion.div>
<motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-5 lg:gap-7"
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{CARDS.map((card) => (
<motion.div
key={card.title}
variants={fadeIn}
className="premium-card bg-white rounded-3xl p-6 md:p-7 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} />
</div>
<p className="font-display text-xl font-black text-brand-dark mb-3">
{card.title}
</p>
<p className="text-sm md:text-base text-on-surface-variant leading-relaxed">
{card.desc}
</p>
</motion.div>
))}
</motion.div>
<motion.div
className="mt-9 flex justify-center"
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
<Link
href="/why-nearle"
className="group inline-flex items-center gap-2 text-base font-black text-primary transition-colors hover:text-[#4C1D95]"
>
Learn Why Nearle
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</div>
</section>
);
}

View File

@@ -2,7 +2,7 @@
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
import { useState, useRef } from "react";
import { usePathname } from "next/navigation";
import {
motion,
@@ -14,11 +14,20 @@ import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const NAV_LINKS = [
type NavChild = { label: string; href: string };
type NavLink = { label: string; href: string; children?: NavChild[] };
const SOLUTIONS_CHILDREN: NavChild[] = [
{ label: "For Business", href: "/businesses" },
{ label: "For People", href: "/people" },
{ label: "For D2C Brands", href: "/brands" },
];
const NAV_LINKS: NavLink[] = [
{ label: "Home", href: "/" },
{ label: "Nearle for Business", href: "/businesses" },
{ label: "Nearle for People", href: "/people" },
{ label: "Nearle for D2C Brands", href: "/brands" },
{ label: "Why Nearle", href: "/why-nearle" },
{ label: "Platform", href: "/platform" },
{ label: "Solutions", href: "/businesses", children: SOLUTIONS_CHILDREN },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
];
@@ -47,11 +56,44 @@ const mobileItemVariants = {
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
};
const dropdownVariants = {
hidden: { opacity: 0, y: 8, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.22, ease: EASE },
},
exit: {
opacity: 0,
y: 6,
scale: 0.98,
transition: { duration: 0.15, ease: "easeIn" as const },
},
};
const mobileSubmenuVariants = {
hidden: { opacity: 0, height: 0 },
visible: {
opacity: 1,
height: "auto",
transition: { duration: 0.26, ease: EASE },
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.18, ease: "easeIn" as const },
},
};
export function PremiumNavbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [mobileSolutionsOpen, setMobileSolutionsOpen] = useState(false);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [scrolled, setScrolled] = useState(false);
const pathname = usePathname();
const { scrollY } = useScroll();
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useMotionValueEvent(scrollY, "change", (y) => {
setScrolled(y > 20);
@@ -62,6 +104,28 @@ export function PremiumNavbar() {
return pathname.startsWith(href);
};
const isGroupActive = (link: NavLink) => {
if (link.children) {
return link.children.some((c) => isActiveLink(c.href));
}
return isActiveLink(link.href);
};
const openMenu = (label: string) => {
if (closeTimer.current) clearTimeout(closeTimer.current);
setOpenDropdown(label);
};
const scheduleClose = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => setOpenDropdown(null), 120);
};
const closeMobile = () => {
setMobileOpen(false);
setMobileSolutionsOpen(false);
};
return (
<motion.header
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
@@ -91,9 +155,82 @@ export function PremiumNavbar() {
</Link>
{/* Desktop Navigation */}
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-10">
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-9">
{NAV_LINKS.map((link) => {
const isActive = isActiveLink(link.href);
const isActive = isGroupActive(link);
if (link.children) {
const isOpen = openDropdown === link.label;
return (
<div
key={link.label}
className="relative py-2"
onMouseEnter={() => openMenu(link.label)}
onMouseLeave={scheduleClose}
>
<button
type="button"
className={`flex items-center gap-1 whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
isActive ? "text-white" : "text-white/70 hover:text-white"
}`}
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setOpenDropdown(isOpen ? null : link.label)}
onFocus={() => openMenu(link.label)}
>
{link.label}
<motion.span
className="flex"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2, ease: EASE }}
>
<MaterialIcon icon="expand_more" size={16} />
</motion.span>
</button>
{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 }}
/>
)}
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute left-1/2 top-full z-50 mt-2 w-56 -translate-x-1/2 overflow-hidden rounded-2xl border border-white/12 bg-[#30226F]/98 p-1.5 shadow-[0_18px_45px_rgba(20,0,30,0.45)] backdrop-blur-xl"
role="menu"
>
{link.children.map((child) => {
const childActive = isActiveLink(child.href);
return (
<Link
key={child.href}
href={child.href}
role="menuitem"
onClick={() => setOpenDropdown(null)}
className={`block rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
childActive
? "bg-white/15 text-white"
: "text-white/75 hover:bg-white/10 hover:text-white"
}`}
>
{child.label}
</Link>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
return (
<motion.div
key={link.label}
@@ -164,12 +301,72 @@ export function PremiumNavbar() {
animate="visible"
>
{NAV_LINKS.map((link) => {
if (link.children) {
const groupActive = isGroupActive(link);
return (
<motion.div key={link.label} variants={mobileItemVariants}>
<button
type="button"
onClick={() => setMobileSolutionsOpen((v) => !v)}
aria-expanded={mobileSolutionsOpen}
className={`flex w-full items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
groupActive
? "bg-white/15 text-white"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
{link.label}
<motion.span
className="flex"
animate={{ rotate: mobileSolutionsOpen ? 180 : 0 }}
transition={{ duration: 0.2, ease: EASE }}
>
<MaterialIcon icon="expand_more" size={20} />
</motion.span>
</button>
<AnimatePresence initial={false}>
{mobileSolutionsOpen && (
<motion.div
variants={mobileSubmenuVariants}
initial="hidden"
animate="visible"
exit="exit"
className="overflow-hidden"
>
<div className="mt-1 flex flex-col gap-1 pl-3">
{link.children.map((child) => {
const childActive = isActiveLink(child.href);
return (
<Link
key={child.href}
href={child.href}
onClick={closeMobile}
className={`flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
childActive
? "bg-white/12 text-white"
: "text-white/60 hover:bg-white/10 hover:text-white"
}`}
>
<span className="h-1 w-1 rounded-full bg-white/40" />
{child.label}
</Link>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
const isActive = isActiveLink(link.href);
return (
<motion.div key={link.label} variants={mobileItemVariants}>
<Link
href={link.href}
onClick={() => setMobileOpen(false)}
onClick={closeMobile}
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"

View File

@@ -0,0 +1,78 @@
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { PhoneMockup } from "@/components/platform/DeviceShowcase";
/**
* ConsumerOS showcase — a premium, sequential product demo.
*
* Only ONE illustration is ever visible at a time:
* 1. The ConsumerOS phone mockup, held completely static for PHONE_DURATION.
* 2. A premium fade + horizontal-slide transition (TRANSITION_DURATION).
* 3. The Food.svg artwork, held static so its OWN internal SVG animation can
* play through uninterrupted. The wrapper never floats or scales.
* 4. The same transition back to the phone. Repeats infinitely.
*
* The PhoneMockup artwork and the Food.svg artwork are used untouched.
*/
// Per-illustration display durations (configurable).
const PHONE_DURATION = 6000; // ConsumerOS held static
// Food.svg's internal SMIL animation is a continuous loop of dur="13.467s"
// (repeatCount="indefinite"). We hold it for exactly one full loop so the art
// returns to its opening frame before we transition — never cut mid-animation.
const FOOD_DURATION = 13467;
const TRANSITION_DURATION = 700; // premium fade + slide, 600–800ms
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
function FoodArt() {
return (
// Re-mounts on each cycle (keyed AnimatePresence) so the SVG's internal
// animation restarts and plays from the beginning every time it appears.
// eslint-disable-next-line @next/next/no-img-element
<img
src="/Food.svg"
alt=""
aria-hidden="true"
className="pointer-events-none h-auto w-full max-w-[240px] select-none"
/>
);
}
function PhoneArt() {
return (
<div className="w-full max-w-[220px]">
<PhoneMockup />
</div>
);
}
export function ConsumerShowcase() {
const reduce = useReducedMotion();
const [index, setIndex] = useState(0); // 0 = ConsumerOS phone, 1 = Food.svg
useEffect(() => {
if (reduce) return; // Reduced motion: hold the phone, never cycle.
const delay = index === 0 ? PHONE_DURATION : FOOD_DURATION;
const id = setTimeout(() => setIndex((i) => (i === 0 ? 1 : 0)), delay);
return () => clearTimeout(id);
}, [index, reduce]);
return (
<div className="relative flex h-[300px] w-full items-center justify-center overflow-hidden sm:h-[320px] md:h-[360px] lg:h-[400px]">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={index}
initial={{ opacity: 0, x: 28 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -28 }}
transition={{ duration: TRANSITION_DURATION / 1000, ease: EASE }}
className="flex w-full items-center justify-center"
>
{index === 0 ? <PhoneArt /> : <FoodArt />}
</motion.div>
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,297 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
/**
* Large animated device illustrations used to replace the flat feature grids.
* - DashboardMockup → MerchantOS (a live store dashboard)
* - PhoneMockup → ConsumerOS (the neighbourhood shopping app)
*
* Purely decorative. GPU-friendly (opacity / transform / pathLength only)
* and fully reduced-motion aware.
*/
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rise = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
};
const pop = {
hidden: { opacity: 0, scale: 0.9 },
visible: { opacity: 1, scale: 1, transition: { duration: 0.5, ease: EASE } },
};
const draw = {
hidden: { pathLength: 0, opacity: 0 },
visible: { pathLength: 1, opacity: 1, transition: { duration: 1.5, ease: EASE } },
};
const growBar = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 0.7, ease: EASE } },
};
const svgContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } },
};
// ─── MerchantOS — store dashboard ────────────────────────────────────────────
export function DashboardMockup() {
const reduce = useReducedMotion();
const floatA = reduce ? {} : { y: [0, -7, 0] };
const floatB = reduce ? {} : { y: [0, 6, 0] };
return (
<motion.svg
viewBox="0 0 440 336"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="MerchantOS store dashboard preview"
variants={svgContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
{/* Soft ground shadow */}
<ellipse cx="220" cy="312" rx="150" ry="14" fill="#6330D6" opacity="0.08" />
{/* Main dashboard window */}
<motion.g variants={rise}>
<rect x="40" y="30" width="360" height="256" rx="22" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
{/* Top chrome */}
<circle cx="66" cy="54" r="4" fill="#E6D9F7" />
<circle cx="80" cy="54" r="4" fill="#E6D9F7" />
<circle cx="94" cy="54" r="4" fill="#E6D9F7" />
<rect x="150" y="48" width="140" height="13" rx="6.5" fill="#F3E8F6" />
<line x1="40" y1="72" x2="400" y2="72" stroke="#F0E9F8" strokeWidth="1.5" />
</motion.g>
{/* Sidebar */}
<motion.g variants={rise}>
<rect x="52" y="82" width="58" height="192" rx="14" fill="#FAF7FD" />
<rect x="64" y="98" width="34" height="8" rx="4" fill="#6330D6" />
<rect x="64" y="120" width="30" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="138" width="34" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="156" width="26" height="7" rx="3.5" fill="#D9CBEF" />
<rect x="64" y="174" width="32" height="7" rx="3.5" fill="#D9CBEF" />
</motion.g>
{/* KPI chips */}
<motion.g variants={pop}>
<rect x="124" y="84" width="80" height="46" rx="12" fill="#F5F1FB" />
<rect x="136" y="96" width="30" height="7" rx="3.5" fill="#B9A6E0" />
<rect x="136" y="110" width="46" height="10" rx="5" fill="#6330D6" />
</motion.g>
<motion.g variants={pop}>
<rect x="212" y="84" width="80" height="46" rx="12" fill="#ECFBF1" />
<rect x="224" y="96" width="30" height="7" rx="3.5" fill="#86E0A6" />
<rect x="224" y="110" width="40" height="10" rx="5" fill="#16A34A" />
</motion.g>
<motion.g variants={pop}>
<rect x="300" y="84" width="80" height="46" rx="12" fill="#FFF7E8" />
<rect x="312" y="96" width="30" height="7" rx="3.5" fill="#F1C876" />
<rect x="312" y="110" width="34" height="10" rx="5" fill="#D97706" />
</motion.g>
{/* Chart panel */}
<motion.g variants={rise}>
<rect x="124" y="142" width="168" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
<rect x="138" y="156" width="44" height="8" rx="4" fill="#C9B8EC" />
{/* gridlines */}
<line x1="138" y1="196" x2="278" y2="196" stroke="#EFE7FB" strokeWidth="1" />
<line x1="138" y1="222" x2="278" y2="222" stroke="#EFE7FB" strokeWidth="1" />
{/* Area + animated line */}
<motion.path
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176 L278 248 L140 248 Z"
fill="url(#dashArea)"
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 1, delay: 0.5 } } }}
/>
<motion.path
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176"
fill="none"
stroke="#6330D6"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
variants={draw}
/>
<motion.circle cx="278" cy="176" r="4" fill="#6330D6" stroke="#fff" strokeWidth="1.5" variants={pop} />
</motion.g>
{/* Mini bar chart */}
<motion.g variants={rise}>
<rect x="300" y="142" width="80" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
{[
{ x: 314, h: 34, c: "#DDD0F4" },
{ x: 332, h: 52, c: "#B99BEC" },
{ x: 350, h: 44, c: "#9B78E4" },
{ x: 368, h: 66, c: "#6330D6" },
].map((b) => (
<motion.rect
key={b.x}
x={b.x}
y={250 - b.h}
width="11"
height={b.h}
rx="4"
fill={b.c}
variants={growBar}
style={{ transformBox: "fill-box", transformOrigin: "bottom" }}
/>
))}
</motion.g>
{/* Floating "new order" card */}
<motion.g animate={floatA} transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}>
<motion.g variants={pop}>
<rect x="316" y="24" width="104" height="42" rx="13" fill="#16001D" />
<circle cx="336" cy="45" r="10" fill="#7C3AED" />
<path d="M332 45 l3 3 l6 -6.5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<rect x="352" y="38" width="52" height="6" rx="3" fill="#ffffff" opacity="0.9" />
<rect x="352" y="49" width="34" height="5" rx="2.5" fill="#C9A2FF" />
</motion.g>
</motion.g>
{/* Floating live-stock pill */}
<motion.g animate={floatB} transition={{ repeat: Infinity, duration: 4.2, ease: "easeInOut" }}>
<motion.g variants={pop}>
<rect x="22" y="196" width="96" height="38" rx="12" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
<motion.circle
cx="42" cy="215" r="6" fill="#22C55E"
animate={reduce ? {} : { opacity: [1, 0.4, 1], scale: [1, 1.15, 1] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
/>
<rect x="56" y="208" width="48" height="6" rx="3" fill="#16001D" opacity="0.75" />
<rect x="56" y="219" width="30" height="5" rx="2.5" fill="#B9A6E0" />
</motion.g>
</motion.g>
<defs>
<linearGradient id="dashArea" x1="0" y1="176" x2="0" y2="248" gradientUnits="userSpaceOnUse">
<stop stopColor="#6330D6" stopOpacity="0.22" />
<stop offset="1" stopColor="#6330D6" stopOpacity="0" />
</linearGradient>
</defs>
</motion.svg>
);
}
// ─── ConsumerOS — neighbourhood shopping app ─────────────────────────────────
export function PhoneMockup() {
const reduce = useReducedMotion();
return (
<motion.svg
viewBox="0 0 360 336"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="ConsumerOS neighbourhood shopping app preview"
variants={svgContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
<ellipse cx="180" cy="316" rx="120" ry="13" fill="#6330D6" opacity="0.08" />
{/* Phone body */}
<motion.g variants={rise}>
<rect x="118" y="14" width="150" height="306" rx="34" fill="#16001D" />
<rect x="126" y="22" width="134" height="290" rx="28" fill="#FBF9FE" />
{/* notch */}
<rect x="168" y="30" width="50" height="10" rx="5" fill="#16001D" />
</motion.g>
{/* Search bar */}
<motion.g variants={pop}>
<rect x="140" y="52" width="106" height="26" rx="13" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.4" />
<circle cx="156" cy="65" r="5" fill="none" stroke="#6330D6" strokeWidth="1.8" />
<line x1="160" y1="69" x2="164" y2="73" stroke="#6330D6" strokeWidth="1.8" strokeLinecap="round" />
<rect x="170" y="62" width="60" height="6" rx="3" fill="#D9CBEF" />
</motion.g>
{/* Map region with roads */}
<motion.g variants={rise}>
<rect x="140" y="86" width="106" height="88" rx="14" fill="#EEF4FF" />
<path d="M140 128 H246" stroke="#FFFFFF" strokeWidth="5" />
<path d="M193 86 V174" stroke="#FFFFFF" strokeWidth="5" />
<path d="M140 104 Q168 104 168 128" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
<path d="M218 128 Q218 152 246 156" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
{/* animated route */}
<motion.path
d="M162 158 L162 128 L193 128 L193 108 L224 108"
fill="none"
stroke="#6330D6"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="4 5"
variants={draw}
/>
</motion.g>
{/* Floating map pins */}
<motion.g
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
>
<motion.g variants={pop}>
<path d="M162 150 C157 150 153 146 153 141 C153 135 162 126 162 126 C162 126 171 135 171 141 C171 146 167 150 162 150 Z" fill="#6330D6" />
<circle cx="162" cy="140" r="3.4" fill="#fff" />
</motion.g>
</motion.g>
<motion.g
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.9, ease: "easeInOut", delay: 0.4 }}
>
<motion.g variants={pop}>
<path d="M224 122 C219 122 215 118 215 113 C215 107 224 98 224 98 C224 98 233 107 233 113 C233 118 229 122 224 122 Z" fill="#22C55E" />
<circle cx="224" cy="112" r="3.4" fill="#fff" />
</motion.g>
</motion.g>
{/* Product cards */}
{[0, 1].map((i) => (
<motion.g key={i} variants={pop}>
<rect x="140" y={184 + i * 40} width="106" height="34" rx="11" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.3" />
<rect x="148" y={190 + i * 40} width="22" height="22" rx="7" fill={i === 0 ? "#EDE9FE" : "#DCFCE7"} />
<rect x="178" y={192 + i * 40} width="42" height="6" rx="3" fill="#16001D" opacity="0.72" />
<rect x="178" y={202 + i * 40} width="28" height="5" rx="2.5" fill="#C1B2E4" />
<circle cx="234" cy={201 + i * 40} r="8" fill={i === 0 ? "#6330D6" : "#22C55E"} />
<path d={`M230 ${201 + i * 40} h8 M234 ${197 + i * 40} v8`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" />
</motion.g>
))}
{/* Bottom nav */}
<motion.g variants={pop}>
<rect x="140" y="272" width="106" height="30" rx="12" fill="#FFFFFF" stroke="#F0E9F8" strokeWidth="1.2" />
<circle cx="160" cy="287" r="4.5" fill="#6330D6" />
<circle cx="184" cy="287" r="4" fill="#D9CBEF" />
<circle cx="208" cy="287" r="4" fill="#D9CBEF" />
<circle cx="230" cy="287" r="4" fill="#D9CBEF" />
</motion.g>
{/* Floating rewards chip */}
<motion.g
animate={reduce ? {} : { y: [0, 7, 0] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
>
<motion.g variants={pop}>
<rect x="250" y="196" width="96" height="40" rx="13" fill="#16001D" />
<circle cx="270" cy="216" r="10" fill="#F59E0B" />
<path d="M270 211 l1.6 3.3 l3.6 0.5 l-2.6 2.5 l0.6 3.6 l-3.2 -1.7 l-3.2 1.7 l0.6 -3.6 l-2.6 -2.5 l3.6 -0.5 Z" fill="#fff" />
<rect x="286" y="209" width="50" height="6" rx="3" fill="#ffffff" opacity="0.9" />
<rect x="286" y="220" width="32" height="5" rx="2.5" fill="#F5C97B" />
</motion.g>
</motion.g>
</motion.svg>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleInSpring, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
const LAYERS = [
{
name: "MerchantOS",
tag: "For the store",
icon: "storefront",
desc: "Billing, inventory, catalog, and orders — the operating system a neighborhood shop runs on.",
accent: "from-[#6330D6] to-[#7C3AED]",
},
{
name: "ProductLLM",
tag: "The intelligence",
icon: "auto_awesome",
desc: "Nearle's product AI. It reads a shelf and turns it into an accurate, live digital catalog.",
accent: "from-[#8B5CF6] to-[#A855F7]",
highlight: true,
},
{
name: "ConsumerOS",
tag: "For the shopper",
icon: "smartphone",
desc: "Discovery, search, ordering, and rewards — how the neighborhood finds and shops nearby stores.",
accent: "from-[#6330D6] to-[#4C1D95]",
},
];
/** A gradient link with a pulse of light traveling between two layers. */
function FlowConnector() {
const reduce = useReducedMotion();
return (
<motion.div
variants={fadeUp}
aria-hidden="true"
className="flex shrink-0 items-center justify-center py-1 lg:px-2"
>
{/* mobile — vertical */}
<div className="relative h-9 w-0.5 lg:hidden">
<div className="absolute inset-0 rounded-full bg-gradient-to-b from-[#6330D6]/45 to-[#A855F7]/45" />
{!reduce && (
<motion.span
className="absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
animate={{ y: [0, 30], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
/>
)}
</div>
{/* desktop — horizontal */}
<div className="relative hidden h-0.5 w-12 lg:block">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#6330D6]/45 to-[#A855F7]/45" />
{!reduce && (
<motion.span
className="absolute left-0 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
animate={{ x: [0, 42], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
/>
)}
</div>
</motion.div>
);
}
export function PlatformArchitecture() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
Platform Architecture
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
One connected stack
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Every order flows through the same three layers — from the shopkeeper&rsquo;s
counter to the shopper&rsquo;s phone.
</motion.p>
</motion.div>
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-12 flex flex-col items-stretch gap-2 lg:flex-row lg:items-center lg:gap-2"
>
{LAYERS.map((layer, i) => (
<div key={layer.name} className="contents">
<motion.div
variants={scaleInSpring}
className={`premium-card group relative flex-1 overflow-hidden rounded-[28px] border p-7 md:p-8 ${
layer.highlight
? "border-[#6330D6]/25 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_40px_rgba(99,48,214,0.14)]"
: "border-[#6330D6]/10 bg-white shadow-[0_18px_45px_rgba(22,0,29,0.07)]"
}`}
>
{/* animated aura on the intelligence layer */}
{layer.highlight && !reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.20), transparent 70%)" }}
animate={{ scale: [1, 1.18, 1], opacity: [0.6, 0.9, 0.6] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
/>
)}
<motion.div
className={`relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br ${layer.accent} text-white shadow-[0_10px_24px_rgba(99,48,214,0.28)]`}
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.4 + i * 0.4, ease: "easeInOut" }}
>
<MaterialIcon icon={layer.icon} size={28} />
</motion.div>
<p className="relative mt-5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
{layer.tag}
</p>
<p className="relative mt-1 font-display font-black text-[#16001D] text-2xl leading-tight">
{layer.name}
</p>
<p className="relative mt-3 text-[15px] leading-relaxed text-[#5E5866]">
{layer.desc}
</p>
</motion.div>
{i < LAYERS.length - 1 && <FlowConnector />}
</div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { motion, useReducedMotion } from "framer-motion";
import { HeroLogo } from "@/components/heroes/HeroLogo";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const badgeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
};
const headingVariant = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
};
const descVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
};
const pillsVariant = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
};
const PILLARS = ["MerchantOS", "ProductLLM", "ConsumerOS"];
export function PlatformHero() {
const shouldReduce = useReducedMotion();
return (
<section className="w-full flex flex-col select-none bg-white">
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
{/* Ambient background image */}
<motion.div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0"
initial={shouldReduce ? undefined : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.4, ease: "easeOut" }}
>
<Image
src="/platform.png"
alt=""
fill
sizes="100vw"
className="absolute inset-0 h-full w-full object-cover object-center"
priority
fetchPriority="high"
/>
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
</motion.div>
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
<div className="relative z-20 flex w-full max-w-[900px] flex-col items-center text-center px-2 md:px-8">
<HeroLogo />
<motion.span
variants={badgeVariant}
initial="hidden"
animate="visible"
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
>
The Nearle Platform
</motion.span>
<motion.h1
variants={headingVariant}
initial="hidden"
animate="visible"
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
>
The AI Infrastructure
<br />
Behind Every
<br />
<span className="text-[#C9A2FF]">Neighborhood Store.</span>
</motion.h1>
<motion.p
variants={descVariant}
initial="hidden"
animate="visible"
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
>
Three systems, one platform. MerchantOS runs the store, ProductLLM
understands the products, and ConsumerOS brings the neighborhood to
shop.
</motion.p>
<motion.div
variants={pillsVariant}
initial="hidden"
animate="visible"
className="mt-9 flex flex-wrap items-center justify-center gap-3"
>
{PILLARS.map((p) => (
<span
key={p}
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-4 py-2 text-sm font-black text-white backdrop-blur-sm"
>
<span className="h-1.5 w-1.5 rounded-full bg-[#C9A2FF]" />
{p}
</span>
))}
</motion.div>
<motion.div
variants={pillsVariant}
initial="hidden"
animate="visible"
className="mt-8"
>
<Link
href="/businesses"
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
Bring Your Store Online
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,153 @@
"use client";
import { useRef } from "react";
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { DashboardMockup } from "@/components/platform/DeviceShowcase";
import { ConsumerShowcase } from "@/components/platform/ConsumerShowcase";
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
export type ProductFeature = {
icon: string;
title: string;
desc: string;
};
type ProductFeatureGridProps = {
id?: string;
eyebrow: string;
name: string;
tagline: string;
features: ProductFeature[];
variant?: "light" | "lavender";
footnote?: string;
/** Which animated device illustration to pair with the feature list. */
showcase?: "dashboard" | "phone";
};
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rowVariant = {
hidden: { opacity: 0, y: 18 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
};
export function ProductFeatureGrid({
id,
eyebrow,
name,
tagline,
features,
variant = "light",
footnote,
showcase = "dashboard",
}: ProductFeatureGridProps) {
const bg = variant === "lavender" ? "section-soft" : "bg-white";
// Alternate the media side to give the two sections opposite rhythm.
const mediaRight = variant === "light";
const isPhone = showcase === "phone";
const reduce = useReducedMotion();
const sectionRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const mediaY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [42, -42]);
return (
<section
id={id}
ref={sectionRef}
className={`section w-full border-b border-[#6330D6]/10 overflow-hidden ${bg}`}
>
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="max-w-[760px]"
>
<motion.span
variants={fadeUp}
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]"
>
{eyebrow}
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
{name}
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
{tagline}
</motion.p>
</motion.div>
<div className="mt-12 grid items-center gap-10 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)] lg:gap-16">
{/* ── Animated device illustration (parallax) ── */}
<motion.div
style={{ y: mediaY }}
className={`relative mx-auto w-full ${isPhone ? "max-w-[600px]" : "max-w-[440px]"} ${mediaRight ? "lg:order-2" : "lg:order-1"}`}
>
{/* ambient glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 blur-2xl"
style={{
background:
"radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)",
}}
/>
{isPhone ? <ConsumerShowcase /> : <DashboardMockup />}
</motion.div>
{/* ── Editorial feature list (was a 4-col card grid) ── */}
<motion.ul
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
className={`relative ${mediaRight ? "lg:order-1" : "lg:order-2"}`}
>
{/* connection line threading the icons */}
<span
aria-hidden="true"
className="pointer-events-none absolute left-[23px] top-6 bottom-6 w-px bg-gradient-to-b from-[#6330D6]/25 via-[#6330D6]/12 to-transparent"
/>
{features.map((f) => (
<motion.li
key={f.title}
variants={rowVariant}
className="group relative flex items-start gap-4 rounded-2xl px-3 py-3.5 transition-colors duration-200 hover:bg-[#6330D6]/[0.04]"
>
<span className="relative z-10 mt-0.5 flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-xl bg-[#6330D6]/[0.08] text-[#6330D6] transition-transform duration-200 group-hover:scale-110 group-hover:bg-[#6330D6] group-hover:text-white">
<MaterialIcon icon={f.icon} size={20} />
</span>
<span className="min-w-0">
<span className="block font-display font-black text-[#16001D] text-[17px] leading-tight">
{f.title}
</span>
<span className="mt-1 block text-[14px] leading-relaxed text-[#5E5866]">
{f.desc}
</span>
</span>
</motion.li>
))}
</motion.ul>
</div>
{footnote && (
<p className="mt-8 text-sm font-bold text-[#6330D6]">{footnote}</p>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,322 @@
"use client";
import { useRef } from "react";
import Image from "next/image";
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STEPS = [
{ icon: "photo_camera", title: "Shelf Scan", desc: "A single photo of the shelf starts everything." },
{ icon: "barcode_reader", title: "Barcode Detection", desc: "Every barcode on the shelf is found and read." },
{ icon: "visibility", title: "Product Recognition", desc: "Each item is matched to a known product." },
{ icon: "category", title: "Category", desc: "Products are sorted into the right aisles." },
{ icon: "nutrition", title: "Nutrition", desc: "Nutritional facts are pulled in automatically." },
{ icon: "science", title: "Ingredients", desc: "Ingredient lists are attached to each product." },
{ icon: "sell", title: "Pricing", desc: "Prices are set and kept in sync with the store." },
{ icon: "inventory_2", title: "Inventory", desc: "Live stock counts flow into the catalog." },
{ icon: "check_circle", title: "Ready", desc: "A complete, shoppable catalog — in minutes." },
];
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1.1, ease: EASE } },
};
const stepVariant = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: EASE } },
};
/** A shelf photo being scanned and turned into live catalog cards. */
function ShelfScanSVG() {
const reduce = useReducedMotion();
const detect = {
hidden: { opacity: 0, scale: 0.7 },
visible: (i: number) => ({
opacity: 1,
scale: 1,
transition: { delay: 0.5 + i * 0.18, duration: 0.4, ease: EASE },
}),
};
const cardIn = {
hidden: { opacity: 0, x: 14 },
visible: (i: number) => ({
opacity: 1,
x: 0,
transition: { delay: 1.1 + i * 0.16, duration: 0.5, ease: EASE },
}),
};
return (
<motion.svg
viewBox="0 0 460 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
role="img"
aria-label="ProductLLM scanning a shelf photo into a digital catalog"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
{/* ── Photo of the shelf ── */}
<motion.g variants={fadeUp}>
<rect x="20" y="40" width="210" height="220" rx="18" fill="#160022" stroke="#3B2357" strokeWidth="1.5" />
<rect x="34" y="54" width="182" height="14" rx="7" fill="#2A1740" />
<circle cx="45" cy="61" r="3" fill="#C9A2FF" />
{/* shelves */}
{[96, 150, 204].map((y) => (
<rect key={y} x="34" y={y} width="182" height="4" rx="2" fill="#3B2357" />
))}
{/* products on shelves */}
{[
{ x: 44, y: 74, w: 20, h: 22, c: "#7C3AED" },
{ x: 70, y: 70, w: 16, h: 26, c: "#A855F7" },
{ x: 92, y: 78, w: 22, h: 18, c: "#6330D6" },
{ x: 120, y: 72, w: 18, h: 24, c: "#8B5CF6" },
{ x: 146, y: 76, w: 24, h: 20, c: "#7C3AED" },
{ x: 178, y: 70, w: 18, h: 26, c: "#A855F7" },
{ x: 46, y: 128, w: 18, h: 22, c: "#6330D6" },
{ x: 72, y: 124, w: 22, h: 26, c: "#8B5CF6" },
{ x: 102, y: 130, w: 18, h: 20, c: "#A855F7" },
{ x: 128, y: 122, w: 20, h: 28, c: "#7C3AED" },
{ x: 156, y: 126, w: 22, h: 24, c: "#6330D6" },
{ x: 184, y: 128, w: 16, h: 22, c: "#8B5CF6" },
{ x: 50, y: 182, w: 22, h: 22, c: "#8B5CF6" },
{ x: 80, y: 178, w: 18, h: 26, c: "#7C3AED" },
{ x: 106, y: 184, w: 22, h: 20, c: "#6330D6" },
{ x: 138, y: 180, w: 18, h: 24, c: "#A855F7" },
{ x: 166, y: 184, w: 24, h: 20, c: "#7C3AED" },
].map((p, i) => (
<rect key={i} x={p.x} y={p.y} width={p.w} height={p.h} rx="3" fill={p.c} opacity="0.9" />
))}
{/* detection boxes */}
{[
{ x: 42, y: 68, w: 46, h: 30 },
{ x: 118, y: 70, w: 54, h: 28 },
{ x: 70, y: 120, w: 50, h: 32 },
{ x: 154, y: 122, w: 46, h: 30 },
].map((b, i) => (
<motion.rect
key={i}
x={b.x}
y={b.y}
width={b.w}
height={b.h}
rx="4"
fill="none"
stroke="#4ADE80"
strokeWidth="1.6"
custom={i}
variants={detect}
/>
))}
{/* scanning line */}
{!reduce && (
<motion.rect
x="30"
y="52"
width="190"
height="3"
rx="1.5"
fill="#C9A2FF"
style={{ filter: "drop-shadow(0 0 6px rgba(201,162,255,0.9))" }}
animate={{ y: [56, 244, 56] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
</motion.g>
{/* ── Flow arrow ── */}
<motion.g
variants={fadeUp}
animate={reduce ? {} : { x: [0, 5, 0] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
>
<path d="M244 150 h28" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="3 4" />
<path d="M270 143 l9 7 l-9 7" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</motion.g>
{/* ── Resulting catalog cards ── */}
{[0, 1, 2].map((i) => (
<motion.g key={i} custom={i} variants={cardIn}>
<rect x="292" y={58 + i * 62} width="150" height="52" rx="12" fill="#FFFFFF" />
<rect x="304" y={70 + i * 62} width="30" height="28" rx="7" fill={["#EDE9FE", "#DCFCE7", "#FFF3D6"][i]} />
<rect x="344" y={72 + i * 62} width="76" height="7" rx="3.5" fill="#16001D" opacity="0.8" />
<rect x="344" y={84 + i * 62} width="48" height="6" rx="3" fill="#B9A6E0" />
<circle cx="428" cy={76 + i * 62} r="7" fill="#22C55E" />
<path d={`M425 ${76 + i * 62} l2 2 l4 -4.5`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</motion.g>
))}
</motion.svg>
);
}
export function ProductLLMWorkflow() {
const reduce = useReducedMotion();
const sectionRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const heroY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [56, -56]);
const illustrationY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [40, -40]);
return (
<section
id="productllm"
ref={sectionRef}
className="section w-full border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] overflow-hidden"
>
<div className="page-container">
<div className="grid items-center gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:gap-14">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="max-w-[620px]"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
<MaterialIcon icon="auto_awesome" size={14} />
The Differentiator
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
ProductLLM turns a shelf into a store.
</motion.h2>
<motion.p
variants={fadeUp}
className="text-white/70 mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
The hardest part of going online is the catalog. ProductLLM does it
from a single photo — recognizing every product, then enriching it
with category, nutrition, ingredients, price, and stock.
</motion.p>
</motion.div>
{/* animated scan illustration */}
<div className="w-full">
<ShelfScanSVG />
</div>
</div>
{/* Workflow timeline (left) supported by branded illustration (right) */}
<div className="mt-14 grid items-center gap-12 lg:grid-cols-[minmax(0,58fr)_minmax(0,42fr)] lg:gap-16">
<div className="relative w-full max-w-3xl">
{/* Animated vertical connector */}
<motion.div
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#C9A2FF]/60 via-[#8B5CF6]/40 to-[#22C55E]/50 hidden sm:block"
variants={connectorVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.05 }}
/>
{/* Pulse of light travelling down the pipeline */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute left-[23px] md:left-[27px] top-8 hidden h-3 w-3 rounded-full bg-[#C9A2FF] shadow-[0_0_14px_4px_rgba(201,162,255,0.6)] sm:block"
animate={{ y: [0, 380], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}
/>
)}
<motion.div
className="space-y-4"
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.09, delayChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.05 }}
>
{STEPS.map((step, i) => {
const isLast = i === STEPS.length - 1;
return (
<motion.div
key={step.title}
className="relative flex gap-5 sm:gap-7"
variants={stepVariant}
>
<motion.div
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full border shadow-[0_8px_20px_rgba(20,0,30,0.4)] ${
isLast
? "border-[#22C55E]/40 bg-[#22C55E]/15 text-[#4ADE80]"
: "border-white/12 bg-white/[0.07] text-[#C9A2FF]"
}`}
animate={isLast && !reduce ? { scale: [1, 1.06, 1] } : {}}
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
>
<MaterialIcon icon={step.icon} size={26} />
</motion.div>
<div
className={`flex-1 rounded-[22px] border p-5 md:p-6 ${
isLast
? "border-[#22C55E]/25 bg-[#22C55E]/[0.08]"
: "border-white/10 bg-white/[0.04]"
}`}
>
<div className="flex items-center gap-3">
<span className="text-xs font-black tracking-widest text-white/40">
{String(i + 1).padStart(2, "0")}
</span>
<p className="font-display font-black text-white text-lg leading-tight">
{step.title}
</p>
</div>
<p className="mt-1.5 text-sm leading-relaxed text-white/65">
{step.desc}
</p>
</div>
</motion.div>
);
})}
</motion.div>
</div>
{/* Branded illustration filling the right side */}
<motion.div
style={{ y: illustrationY }}
className="relative flex items-center justify-center justify-self-center lg:justify-self-end"
>
{/* soft purple radial glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 h-[75%] w-[75%] -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl"
style={{
background:
"radial-gradient(circle, rgba(139,92,246,0.38) 0%, rgba(124,58,237,0.18) 45%, transparent 72%)",
}}
/>
{/* subtle floating illustration */}
<motion.div
animate={reduce ? {} : { y: [0, -14, 0] }}
transition={{ repeat: Infinity, duration: 7, ease: "easeInOut" }}
className="relative"
>
<Image
src="/superhero.png"
alt="ProductLLM building a live catalog"
width={1341}
height={1533}
sizes="(max-width: 640px) 300px, (max-width: 1024px) 380px, 520px"
className="h-auto w-[300px] sm:w-[320px] md:w-[380px] lg:w-full lg:max-w-[520px]"
/>
</motion.div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,192 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import {
fadeUp,
slideInLeftSubtle,
slideInRight,
staggerContainer,
staggerCards,
viewport,
viewport20,
} from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const rowContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.06 } },
};
type Row = {
topic: string;
icon: string;
quick: string;
ai: string;
};
const ROWS: Row[] = [
{
topic: "Fulfillment",
icon: "local_shipping",
quick: "Central dark stores hold the stock and ship it out.",
ai: "The neighborhood store you already trust fulfills the order.",
},
{
topic: "Store Ownership",
icon: "storefront",
quick: "Inventory and brand belong to the platform.",
ai: "The merchant keeps their shop, stock, and customers.",
},
{
topic: "Trust",
icon: "verified_user",
quick: "A logo and a delivery promise.",
ai: "A shopkeeper the neighborhood has known for years.",
},
{
topic: "AI Catalog",
icon: "auto_awesome",
quick: "Catalogs are built and maintained by hand.",
ai: "ProductLLM turns a shelf into a live digital catalog.",
},
{
topic: "Sustainability",
icon: "eco",
quick: "New warehouses, new fleets, new footprint.",
ai: "Runs on stores and trips that already exist.",
},
{
topic: "Offline Support",
icon: "wifi_off",
quick: "Everything depends on being online.",
ai: "Billing and inventory keep working offline.",
},
{
topic: "Customer Relationship",
icon: "favorite",
quick: "Owned by the platform, not the store.",
ai: "Stays between the shopper and their local store.",
},
];
export function AICommerceComparison() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-y border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[760px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
The Difference
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
AI Commerce vs Quick Commerce
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Two ways to bring local shopping online. One builds around the
neighborhood store — the other around a warehouse.
</motion.p>
</motion.div>
{/* Column headers (desktop) */}
<div className="mt-12 hidden md:grid grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] gap-5 px-1">
<div className="pl-2">
<span className="text-xs font-black uppercase tracking-widest text-[#5E5866]">
Quick Commerce
</span>
</div>
<div className="pl-2">
<span className="text-xs font-black uppercase tracking-widest text-[#6330D6]">
AI Commerce · Nearle
</span>
</div>
</div>
<motion.div
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
className="mt-3 flex flex-col gap-4"
>
{ROWS.map((row) => (
<motion.div
key={row.topic}
variants={rowContainer}
className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] md:items-stretch"
>
{/* Quick commerce side — slides in from the left */}
<motion.div
variants={slideInLeftSubtle}
whileHover={reduce ? undefined : { y: -3 }}
transition={{ duration: 0.25, ease: EASE }}
className="relative rounded-[24px] border border-[#6330D6]/10 bg-white p-6 shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#F3F3F6] text-[#6D6172]">
<MaterialIcon icon={row.icon} size={20} />
</div>
<p className="font-display font-black text-[#16001D] text-base md:hidden">
{row.topic}
</p>
<span className="ml-auto text-[10px] font-black uppercase tracking-widest text-[#6D6172] md:hidden">
Quick
</span>
</div>
<p className="mt-3 text-[15px] leading-relaxed text-[#5E5866]">
{row.quick}
</p>
</motion.div>
{/* AI commerce side — slides in from the right */}
<motion.div
variants={slideInRight}
whileHover={reduce ? undefined : { y: -5 }}
transition={{ duration: 0.25, ease: EASE }}
className="group relative overflow-hidden rounded-[24px] border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-6 shadow-[0_4px_28px_rgba(99,48,214,0.10)] transition-shadow duration-300 hover:shadow-[0_14px_40px_rgba(99,48,214,0.18)]"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#6330D6]/10 text-[#6330D6]">
<MaterialIcon icon={row.icon} size={20} />
</div>
<p className="font-display font-black text-[#16001D] text-base">
{row.topic}
</p>
<motion.span
className="ml-auto flex h-6 w-6 items-center justify-center rounded-full bg-[#DCFCE7] text-[#16A34A]"
animate={reduce ? {} : { scale: [1, 1.12, 1] }}
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
>
<MaterialIcon icon="check" size={14} />
</motion.span>
</div>
<p className="mt-3 text-[15px] leading-relaxed text-[#3F3A47]">
{row.ai}
</p>
</motion.div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import { motion } from "framer-motion";
import { fadeUp, staggerSlow, viewport } from "@/lib/animations";
type PullQuoteProps = {
lines: string[];
attribution?: string;
};
export function PullQuote({ lines, attribution }: PullQuoteProps) {
return (
<section className="bg-white px-2 py-2 md:px-3 md:py-3 lg:px-4 lg:py-4">
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="relative overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] px-6 py-16 text-center md:px-12 md:py-20"
>
{/* Ambient glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0"
style={{
background:
"radial-gradient(circle at 30% 20%, rgba(124,58,237,0.28), transparent 45%), radial-gradient(circle at 75% 80%, rgba(168,85,247,0.18), transparent 45%)",
}}
/>
<motion.span
variants={fadeUp}
aria-hidden="true"
className="relative block font-display text-6xl font-black leading-none text-[#C9A2FF]/40"
>
&ldquo;
</motion.span>
<motion.blockquote
variants={fadeUp}
className="relative mx-auto max-w-[860px] font-display font-black text-white tracking-tight"
style={{ fontSize: "clamp(26px, 3.4vw, 46px)", lineHeight: 1.18 }}
>
{lines.map((line, i) => (
<span key={i} className="block">
{i === lines.length - 1 ? (
<span className="text-[#C9A2FF]">{line}</span>
) : (
line
)}
</span>
))}
</motion.blockquote>
{attribution && (
<motion.p
variants={fadeUp}
className="relative mt-6 text-sm font-black uppercase tracking-widest text-white/50"
>
{attribution}
</motion.p>
)}
</motion.div>
</section>
);
}

View File

@@ -0,0 +1,121 @@
"use client";
import Link from "next/link";
import { motion, useReducedMotion } 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.3 } as const;
const stagger = { hidden: {}, visible: { transition: { staggerChildren: 0.12 } } };
const reveal = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
const VALUES = [
{ icon: "auto_awesome", label: "AI-built catalog" },
{ icon: "qr_code_2", label: "QR ordering" },
{ icon: "point_of_sale", label: "Free POS" },
];
export function AICommerceCTA() {
const reduce = useReducedMotion();
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 2xl:px-6 2xl:pb-5">
<motion.div
variants={stagger}
initial="hidden"
whileInView="visible"
viewport={VP}
className="relative w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] text-white"
>
{/* Ambient animated glows */}
{!reduce && (
<>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -left-24 -top-24 h-72 w-72 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.45), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.4, 0.75, 0.4], scale: [1, 1.15, 1] }}
transition={{ repeat: Infinity, duration: 5, ease: "easeInOut" }}
/>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-24 -bottom-24 h-72 w-72 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(201,162,255,0.35), rgba(201,162,255,0) 70%)" }}
animate={{ opacity: [0.35, 0.65, 0.35], scale: [1.1, 1, 1.1] }}
transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }}
/>
</>
)}
<div className="relative z-10 px-6 py-16 text-center md:px-10 md:py-20 lg:px-14">
<div className="mx-auto max-w-2xl">
<motion.span
variants={reveal}
className="mb-6 inline-flex items-center gap-2 rounded-full border border-white/15 px-3 py-1 text-xs font-black uppercase tracking-widest text-white/70"
>
<MaterialIcon icon="hub" size={14} />
Join the ecosystem
</motion.span>
<motion.h2
variants={reveal}
className="font-display font-black tracking-tight leading-[1.08]"
style={{ fontSize: "clamp(32px, 4vw, 56px)" }}
>
Bring your store into
<br />
the age of <span className="text-[#C9A2FF]">AI Commerce</span>
</motion.h2>
<motion.p
variants={reveal}
className="mx-auto mt-5 max-w-lg text-base leading-relaxed text-white/70 md:text-lg"
>
Keep the trust, the customers and the corner you&rsquo;ve built.
Nearle adds the technology — for free.
</motion.p>
{/* Value chips */}
<motion.div variants={reveal} className="mt-8 flex flex-wrap justify-center gap-2.5">
{VALUES.map((v) => (
<span
key={v.label}
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-bold text-white/85"
>
<MaterialIcon icon={v.icon} size={16} className="text-[#C9A2FF]" />
{v.label}
</span>
))}
</motion.div>
<motion.div variants={reveal} className="mt-10 flex flex-wrap justify-center gap-3.5">
<Link
href="/businesses"
className="group flex items-center gap-2 rounded-xl bg-white px-8 py-3.5 text-sm font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
<MaterialIcon icon="storefront" size={18} />
<span>Start Your Store</span>
<MaterialIcon
icon="arrow_forward"
size={18}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
<Link
href="/platform"
className="flex items-center gap-2 rounded-xl border border-white/20 bg-white/10 px-8 py-3.5 text-sm font-black text-white transition-colors duration-200 hover:bg-white/15 active:scale-95"
>
<MaterialIcon icon="hub" size={18} />
<span>Explore the Platform</span>
</Link>
</motion.div>
</div>
</div>
</motion.div>
</section>
);
}

View File

@@ -0,0 +1,195 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { cardReveal, fadeUp, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const HAVE = [
{ img: "/trust.svg", label: "Trust", desc: "Built over years, face to face." },
{ img: "/customers.svg", label: "Customers", desc: "Regulars who already walk in." },
{ img: "/location.svg", label: "Location", desc: "Minutes from every doorstep." },
];
const ADD = [
{ icon: "auto_awesome", label: "AI", desc: "ProductLLM builds the catalog." },
{ icon: "storefront", label: "Digital Storefront", desc: "Their shop, now online." },
{ icon: "inventory_2", label: "Inventory", desc: "Live stock, always accurate." },
{ icon: "shopping_bag", label: "Ordering", desc: "Pickup and delivery, built in." },
];
export function AICommerceExplainer() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[760px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
The Idea
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
AI Commerce doesn&rsquo;t replace the neighborhood store. It upgrades it.
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Local shops already own the hardest part of retail. Nearle simply
adds the technology layer on top.
</motion.p>
</motion.div>
<div className="relative mt-12 grid gap-6 lg:grid-cols-2 lg:items-stretch">
{/* Center merge badge — trust + technology */}
<motion.div
aria-hidden="true"
initial={{ opacity: 0, scale: 0.6 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] lg:flex"
>
{!reduce && (
<motion.span
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.5], opacity: [0.5, 0] }}
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
/>
)}
<MaterialIcon icon="add" size={26} />
</motion.div>
{/* Already have */}
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="rounded-[32px] border border-[#6330D6]/10 bg-[#FAF7FD] p-7 md:p-9 shadow-[0_18px_45px_rgba(22,0,29,0.06)]"
>
<motion.p
variants={fadeUp}
className="text-xs font-black uppercase tracking-widest text-[#5E5866]"
>
Stores already have
</motion.p>
<div className="mt-5 flex flex-col gap-4">
{HAVE.map((item, i) => (
<motion.div
key={item.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.3, ease: EASE }}
className="group flex min-h-[220px] flex-col gap-3 rounded-3xl bg-white px-6 py-5 md:min-h-0 md:px-7 md:py-6 border border-[#6330D6]/[0.08] shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_22px_50px_rgba(99,48,214,0.14)] md:flex-row md:items-center md:gap-6"
>
{/* Left — heading + supporting copy (48%) */}
<div className="min-w-0 md:basis-[48%]">
<p className="font-display font-black text-[#16001D] leading-[1.05] tracking-tight" style={{ fontSize: "clamp(24px, 2.1vw, 30px)" }}>
{item.label}
</p>
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
{item.desc}
</p>
</div>
{/* Right — animated illustration (52%, floating + gentle rotation) */}
<div className="relative flex w-full items-center justify-center md:basis-[52%]">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 m-auto rounded-full opacity-70 blur-2xl transition-opacity duration-300 group-hover:opacity-100"
style={{
width: "58%",
height: "58%",
background:
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
}}
/>
<motion.img
src={item.img}
alt=""
aria-hidden="true"
className="relative h-auto"
style={{ width: "clamp(148px, 34vw, 168px)" }}
animate={reduce ? {} : { y: [0, -9, 0], rotate: [-1.5, 1.5, -1.5] }}
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Nearle adds */}
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="flex flex-col rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-7 md:p-9 shadow-[0_20px_50px_rgba(20,0,30,0.28)]"
>
<motion.p
variants={fadeUp}
className="text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
Nearle adds
</motion.p>
<div className="mt-6 grid flex-1 gap-4 sm:grid-cols-2 sm:auto-rows-fr">
{ADD.map((item, i) => (
<motion.div
key={item.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.28, ease: EASE }}
className="group relative flex min-h-[196px] flex-col items-start overflow-hidden rounded-3xl bg-white/[0.05] p-6 border border-white/10 transition-colors duration-300 hover:border-[#C9A2FF]/45 hover:bg-white/[0.09]"
>
{/* Illustration — glowing icon medallion (top of the stack) */}
<div className="relative mb-5">
{/* Stronger purple radial glow behind the illustration */}
{!reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -inset-5 rounded-full blur-2xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.55), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.5, 0.9, 0.5], scale: [1, 1.14, 1] }}
transition={{ repeat: Infinity, duration: 3.6 + i * 0.4, ease: "easeInOut" }}
/>
)}
<motion.div
className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-[#C9A2FF]/35 bg-gradient-to-br from-[#7C3AED]/45 to-[#4C1D95]/25 text-[#EBDBFF] shadow-[0_12px_32px_rgba(124,58,237,0.5)] transition-colors duration-300 group-hover:border-[#C9A2FF]/60"
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 5 + i * 0.5, ease: "easeInOut" }}
>
<MaterialIcon icon={item.icon} size={30} />
</motion.div>
</div>
{/* Heading */}
<p className="relative z-10 font-display font-black text-white text-xl leading-tight">
{item.label}
</p>
{/* Description */}
<p className="relative z-10 mt-1.5 text-sm leading-relaxed text-white/65">{item.desc}</p>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,196 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const TRADITIONAL = [
{ icon: "edit_note", label: "Manual Catalog" },
{ icon: "store", label: "Offline Store" },
{ icon: "directions_walk", label: "Walk-in Customers" },
{ icon: "receipt_long", label: "Manual Billing" },
];
const NEARLE = [
{ icon: "auto_awesome", label: "ProductLLM" },
{ icon: "storefront", label: "Digital Storefront" },
{ icon: "shopping_bag", label: "Omnichannel Ordering" },
{ icon: "travel_explore", label: "AI Powered Discovery" },
];
const nodeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
};
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 0.5, ease: EASE } },
};
function FlowColumn({
eyebrow,
items,
tone,
reduce,
}: {
eyebrow: string;
items: { icon: string; label: string }[];
tone: "muted" | "brand";
reduce: boolean | null;
}) {
const isBrand = tone === "brand";
return (
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
className={`relative flex flex-col items-center rounded-[32px] border p-8 md:p-10 ${
isBrand
? "border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
: "border-[#6330D6]/10 bg-[#FAF7FD] shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
}`}
>
<motion.p
variants={nodeVariant}
className={`text-xs font-black uppercase tracking-widest ${
isBrand ? "text-[#C9A2FF]" : "text-[#5E5866]"
}`}
>
{eyebrow}
</motion.p>
<div className="mt-8 flex w-full max-w-[300px] flex-col items-center">
{items.map((item, i) => (
<div key={item.label} className="flex w-full flex-col items-center">
<motion.div
variants={nodeVariant}
whileHover={reduce ? undefined : { y: -4, scale: 1.02 }}
transition={{ duration: 0.25, ease: EASE }}
className={`flex w-full items-center gap-4 rounded-2xl border px-5 py-4 ${
isBrand
? "border-white/10 bg-white/[0.06] hover:border-[#C9A2FF]/40 hover:bg-white/[0.1]"
: "border-[#6330D6]/10 bg-white shadow-[0_8px_22px_rgba(22,0,29,0.04)]"
} transition-colors duration-300`}
>
<div
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${
isBrand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.08] text-[#6330D6]"
}`}
>
<MaterialIcon icon={item.icon} size={22} />
</div>
<p
className={`font-display font-black leading-tight ${
isBrand ? "text-white" : "text-[#16001D]"
} text-[15px] md:text-base`}
>
{item.label}
</p>
</motion.div>
{/* Animated connector + travelling pulse */}
{i < items.length - 1 && (
<div className="relative my-1.5 h-8 w-0.5">
<motion.span
variants={connectorVariant}
className={`absolute inset-0 origin-top rounded-full ${
isBrand ? "bg-[#C9A2FF]/40" : "bg-[#6330D6]/25"
}`}
/>
{!reduce && (
<motion.span
aria-hidden="true"
className={`absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full ${
isBrand
? "bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.6)]"
: "bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.5)]"
}`}
animate={{ y: [0, 26], opacity: [0, 1, 0] }}
transition={{
repeat: Infinity,
duration: 1.8,
ease: "easeInOut",
delay: i * 0.3,
}}
/>
)}
</div>
)}
</div>
))}
</div>
</motion.div>
);
}
export function BeforeAfterFlow() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
Before vs After
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
The same store, reimagined
</motion.h2>
</motion.div>
<div className="relative mt-14 grid gap-6 md:grid-cols-2 md:items-start md:gap-8">
<FlowColumn
eyebrow="Traditional Commerce"
items={TRADITIONAL}
tone="muted"
reduce={reduce}
/>
{/* Center VS badge (desktop) */}
<motion.div
aria-hidden="true"
initial={{ opacity: 0, scale: 0.6 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5, delay: 0.25, ease: EASE }}
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-xs font-black uppercase tracking-widest text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] md:flex"
>
{!reduce && (
<motion.span
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.45], opacity: [0.5, 0] }}
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
/>
)}
VS
</motion.div>
<FlowColumn
eyebrow="Nearle AI Commerce"
items={NEARLE}
tone="brand"
reduce={reduce}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STAGES = [
{
era: "The corner shop",
title: "Traditional Retail",
desc: "Trust and relationships, built one street at a time.",
icon: "storefront",
},
{
era: "The catalog goes online",
title: "E-Commerce",
desc: "Endless shelves, but the neighborhood disappears.",
icon: "shopping_cart",
},
{
era: "Speed at all costs",
title: "Quick Commerce",
desc: "Minutes-fast delivery, powered by dark stores.",
icon: "bolt",
},
{
era: "The store, upgraded",
title: "AI Commerce",
desc: "Local stores digitized by AI — trust and technology together.",
icon: "auto_awesome",
highlight: true,
},
];
const connectorVariant = {
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1, ease: EASE } },
};
export function CommerceEvolution() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
The Evolution
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
How commerce got here
</motion.h2>
</motion.div>
<div className="relative mt-12 max-w-2xl mx-auto">
{/* Vertical connector */}
<motion.div
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#6330D6]/30 via-[#6330D6]/25 to-[#6330D6]/40 hidden sm:block"
variants={connectorVariant}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
/>
{/* Pulse travelling down the timeline */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute left-[23px] md:left-[27px] top-8 hidden h-2.5 w-2.5 rounded-full bg-[#7C3AED] shadow-[0_0_12px_3px_rgba(124,58,237,0.5)] sm:block"
animate={{ y: [0, 300], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
<motion.div
className="space-y-5"
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12, delayChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
>
{STAGES.map((stage, i) => (
<motion.div
key={stage.title}
className="relative flex gap-5 sm:gap-7"
variants={{
hidden: { opacity: 0, x: -24 },
visible: { opacity: 1, x: 0, transition: { duration: 0.55, ease: EASE } },
}}
>
<motion.div
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full shadow-[0_8px_20px_rgba(99,48,214,0.25)] ${
stage.highlight
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white"
: "bg-[#F3E8F6] text-[#6330D6]"
}`}
animate={reduce ? {} : { y: [0, -5, 0] }}
transition={{ repeat: Infinity, duration: 3.2 + i * 0.35, ease: "easeInOut" }}
>
{/* pulsing ring on the current era */}
{stage.highlight && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
animate={{ scale: [1, 1.4], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<MaterialIcon icon={stage.icon} size={26} />
</motion.div>
<div
className={`flex-1 rounded-[24px] border p-5 md:p-6 transition-shadow ${
stage.highlight
? "border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_35px_rgba(99,48,214,0.12)]"
: "border-[#6330D6]/10 bg-white shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
}`}
>
<div className="flex items-center gap-3">
<p className="font-display font-black text-[#16001D] text-lg md:text-xl leading-tight">
{stage.title}
</p>
{stage.highlight && (
<span className="rounded-full bg-[#6330D6] px-2.5 py-0.5 text-[10px] font-black uppercase tracking-wide text-white">
Now
</span>
)}
</div>
<p className="mt-0.5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
{stage.era}
</p>
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
{stage.desc}
</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,149 @@
"use client";
import { Fragment } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const NODES = [
{ icon: "store", label: "Merchant", sub: "The local shop" },
{ icon: "point_of_sale", label: "MerchantOS", sub: "Billing & stock" },
{ icon: "auto_awesome", label: "ProductLLM", sub: "AI catalog engine", hub: true },
{ icon: "smartphone", label: "ConsumerOS", sub: "Shopping app" },
{ icon: "mood", label: "Customer", sub: "The neighbour" },
];
const nodeVariant = {
hidden: { opacity: 0, scale: 0.85, y: 16 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
function Connector({ reduce, delay }: { reduce: boolean | null; delay: number }) {
return (
<motion.div
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.4, ease: EASE } },
}}
className="relative my-1 h-9 w-0.5 overflow-hidden rounded-full bg-white/10 md:mx-2 md:my-0 md:mt-9 md:h-0.5 md:w-auto md:flex-1"
>
{!reduce && (
<>
{/* Vertical pulse (mobile) */}
<motion.span
aria-hidden="true"
className="absolute left-1/2 top-0 h-3 w-1 -translate-x-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:hidden"
animate={{ y: [-4, 36], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut", delay }}
/>
{/* Horizontal pulse (desktop) */}
<motion.span
aria-hidden="true"
className="absolute left-0 top-1/2 hidden h-1 w-4 -translate-y-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:block"
animate={{ x: ["-10%", "460%"], opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut", delay }}
/>
</>
)}
</motion.div>
);
}
export function EcosystemDiagram() {
const reduce = useReducedMotion();
return (
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
The Ecosystem
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
One connected loop, powered by AI
</motion.h2>
<motion.p
variants={fadeUp}
className="text-white/70 mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
Every part of Nearle talks to the next — from the merchant&rsquo;s
shelf to the customer&rsquo;s doorstep.
</motion.p>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="mt-16 flex flex-col items-center md:flex-row md:items-start md:justify-between"
>
{NODES.map((node, i) => (
<Fragment key={node.label}>
<motion.div
variants={nodeVariant}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ duration: 0.25, ease: EASE }}
className="flex shrink-0 flex-col items-center text-center"
>
<div className="relative">
{/* Glow */}
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-full blur-2xl ${
node.hub ? "opacity-90" : "opacity-50"
}`}
style={{
background:
"radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)",
}}
/>
{/* Pulsing ring on the hub */}
{node.hub && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
animate={{ scale: [1, 1.35], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<div
className={`relative flex items-center justify-center rounded-full ${
node.hub
? "h-[84px] w-[84px] bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_16px_40px_rgba(124,58,237,0.5)]"
: "h-[72px] w-[72px] bg-white/[0.08] text-[#C9A2FF] border border-white/15 shadow-[0_12px_30px_rgba(0,0,0,0.35)]"
}`}
>
<MaterialIcon icon={node.icon} size={node.hub ? 36 : 30} />
</div>
</div>
<p className="mt-4 font-display font-black text-white text-base md:text-lg leading-tight">
{node.label}
</p>
<p className="mt-0.5 text-xs text-white/50">{node.sub}</p>
</motion.div>
{i < NODES.length - 1 && <Connector reduce={reduce} delay={i * 0.25} />}
</Fragment>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,153 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const MILESTONES = [
{
phase: "Today",
title: "AI Commerce, live",
desc: "Digital catalogs, QR ordering and live inventory.",
icon: "bolt",
now: true,
},
{
phase: "Next",
title: "Digitizing Stores",
desc: "Every neighbourhood shop, online in minutes.",
icon: "storefront",
},
{
phase: "Soon",
title: "Connected Neighborhoods",
desc: "Stores, shoppers and riders in one network.",
icon: "hub",
},
{
phase: "The Horizon",
title: "Future of AI Commerce",
desc: "Local commerce that runs itself.",
icon: "rocket_launch",
},
];
const nodeVariant = {
hidden: { opacity: 0, y: 22 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
};
export function FutureVision() {
const reduce = useReducedMotion();
return (
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
>
The Road Ahead
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-white mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Where AI Commerce is headed
</motion.h2>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.16 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.25 }}
className="relative mt-16"
>
{/* Rails */}
<div className="absolute left-[8%] right-[8%] top-[26px] hidden h-0.5 bg-white/10 md:block" />
<motion.div
className="absolute left-[8%] top-[26px] hidden h-0.5 origin-left bg-gradient-to-r from-[#C9A2FF] via-[#7C3AED] to-[#6330D6] md:block"
style={{ width: "84%" }}
variants={{ hidden: { scaleX: 0 }, visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } } }}
/>
{/* travelling glow (desktop) */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute top-[22px] left-[8%] hidden h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_14px_4px_rgba(201,162,255,0.7)] md:block"
animate={{ left: ["8%", "92%"], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
/>
)}
{/* vertical rail (mobile) */}
<div className="absolute left-[26px] top-2 bottom-2 w-0.5 -translate-x-1/2 bg-white/10 md:hidden" />
<div className="grid gap-8 md:grid-cols-4 md:gap-6">
{MILESTONES.map((m, i) => (
<motion.div
key={m.title}
variants={nodeVariant}
className="relative flex items-start gap-5 md:flex-col md:items-center md:text-center"
>
{/* node dot */}
<div className="relative z-10 shrink-0">
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-full blur-lg ${
m.now ? "opacity-90" : "opacity-40"
}`}
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)" }}
/>
{m.now && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
animate={{ scale: [1, 1.5], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
/>
)}
<div
className={`relative flex h-[52px] w-[52px] items-center justify-center rounded-full ${
m.now
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_14px_34px_rgba(124,58,237,0.5)]"
: "border border-white/15 bg-white/[0.08] text-[#C9A2FF]"
}`}
>
<MaterialIcon icon={m.icon} size={24} />
</div>
</div>
<div className="md:mt-5">
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-black uppercase tracking-widest ${
m.now ? "bg-[#6330D6] text-white" : "bg-white/10 text-[#C9A2FF]"
}`}
>
{m.phase}
</span>
<p className="mt-2 font-display font-black text-white text-lg leading-tight">
{m.title}
</p>
<p className="mt-1 text-sm leading-relaxed text-white/55 md:max-w-[220px] md:mx-auto">
{m.desc}
</p>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { cardReveal, fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const CARDS = [
{
img: "/trust.svg",
label: "Trust",
desc: "A face the street has known for years.",
},
{
img: "/customers.svg",
label: "Community",
desc: "Regulars who shop where they belong.",
},
{
img: "/location.svg",
label: "Convenience",
desc: "Always minutes from the doorstep.",
},
];
export function NeighborhoodStores() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
Why It Works
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Why neighborhood stores matter
</motion.h2>
</motion.div>
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.14 } } }}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-14 grid gap-6 md:grid-cols-3 md:items-stretch"
>
{CARDS.map((card, i) => (
<motion.div
key={card.label}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -8 }}
transition={{ duration: 0.3, ease: EASE }}
className="group relative flex flex-col items-center overflow-hidden rounded-[32px] border border-[#6330D6]/[0.08] bg-[linear-gradient(180deg,#ffffff_0%,#faf7fd_100%)] px-8 pt-12 pb-9 text-center shadow-[0_14px_40px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_26px_60px_rgba(99,48,214,0.14)]"
>
{/* Soft glow behind the illustration */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-14 h-48 w-48 -translate-x-1/2 rounded-full opacity-60 blur-3xl transition-opacity duration-300 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
}}
/>
{/* Large floating illustration — the visual focus */}
<motion.img
src={card.img}
alt=""
aria-hidden="true"
className="relative h-auto"
style={{ width: "clamp(190px, 22vw, 240px)" }}
animate={reduce ? {} : { y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
/>
<p className="relative mt-8 font-display font-black text-[#16001D] leading-tight tracking-tight" style={{ fontSize: "clamp(24px, 2.2vw, 30px)" }}>
{card.label}
</p>
<p className="relative mt-2 max-w-[240px] text-[15px] leading-relaxed text-[#5E5866]">
{card.desc}
</p>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,186 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const MANUAL_POINTS = [
{ icon: "keyboard", label: "Typed in by hand" },
{ icon: "error_outline", label: "Missing details & typos" },
{ icon: "hourglass_empty", label: "Goes stale quickly" },
];
const AI_POINTS = [
{ icon: "photo_camera", label: "Recognised from a photo" },
{ icon: "verified", label: "Full, structured attributes" },
{ icon: "sync", label: "Always kept in sync" },
];
const panelReveal = {
hidden: { opacity: 0, y: 28 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
};
function Bar({
value,
brand,
}: {
value: number; // 0–100 target width
brand: boolean;
}) {
return (
<div
className={`mt-6 h-2.5 w-full overflow-hidden rounded-full ${
brand ? "bg-white/10" : "bg-[#6330D6]/10"
}`}
>
<motion.div
className={`h-full rounded-full ${
brand
? "bg-gradient-to-r from-[#C9A2FF] to-[#7C3AED]"
: "bg-gradient-to-r from-[#B7ADCB] to-[#8A7FA6]"
}`}
initial={{ width: 0 }}
whileInView={{ width: `${value}%` }}
viewport={{ once: true, amount: 0.6 }}
transition={{ duration: 1.2, ease: EASE, delay: 0.2 }}
/>
</div>
);
}
function Point({
icon,
label,
brand,
}: {
icon: string;
label: string;
brand: boolean;
}) {
return (
<motion.li
variants={fadeUp}
className="flex items-center gap-3"
>
<span
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
brand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.07] text-[#8A7FA6]"
}`}
>
<MaterialIcon icon={icon} size={18} />
</span>
<span className={`text-[15px] font-medium ${brand ? "text-white/85" : "text-[#5E5866]"}`}>
{label}
</span>
</motion.li>
);
}
export function ProductLLMCompare() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
Why ProductLLM
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
Catalogs built in minutes, not days
</motion.h2>
</motion.div>
<div className="mt-14 grid gap-6 md:grid-cols-2 md:items-stretch">
{/* Manual */}
<motion.div
variants={panelReveal}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="flex flex-col rounded-[32px] border border-[#6330D6]/10 bg-white p-8 md:p-10 shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
>
<p className="text-xs font-black uppercase tracking-widest text-[#8A7FA6]">
Traditional catalog creation
</p>
<div className="mt-5 flex items-end gap-2">
<span className="font-display font-black leading-none text-[#16001D]" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
~6
</span>
<span className="mb-2 text-lg font-bold text-[#5E5866]">hrs / store</span>
</div>
<Bar value={100} brand={false} />
<motion.ul
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="mt-8 flex flex-col gap-4"
>
{MANUAL_POINTS.map((p) => (
<Point key={p.label} icon={p.icon} label={p.label} brand={false} />
))}
</motion.ul>
</motion.div>
{/* ProductLLM */}
<motion.div
variants={panelReveal}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative flex flex-col overflow-hidden rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-8 md:p-10 shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
>
{/* soft glow */}
{!reduce && (
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -right-10 -top-10 h-48 w-48 rounded-full blur-3xl"
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.5), rgba(124,58,237,0) 70%)" }}
animate={{ opacity: [0.5, 0.85, 0.5], scale: [1, 1.12, 1] }}
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
/>
)}
<p className="relative text-xs font-black uppercase tracking-widest text-[#C9A2FF]">
AI-powered with ProductLLM
</p>
<div className="relative mt-5 flex items-end gap-2">
<span className="font-display font-black leading-none text-white" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
~2
</span>
<span className="mb-2 text-lg font-bold text-white/70">min / store</span>
</div>
<Bar value={14} brand />
<motion.ul
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative mt-8 flex flex-col gap-4"
>
{AI_POINTS.map((p) => (
<Point key={p.label} icon={p.icon} label={p.label} brand />
))}
</motion.ul>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,189 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STAGES = [
{ icon: "photo_camera", label: "Shelf Scan" },
{ icon: "barcode_scanner", label: "Barcode Detection" },
{ icon: "center_focus_strong", label: "Recognition" },
{ icon: "category", label: "Category" },
{ icon: "nutrition", label: "Nutrition" },
{ icon: "science", label: "Ingredients" },
{ icon: "sell", label: "Pricing" },
{ icon: "inventory_2", label: "Inventory" },
{ icon: "check_circle", label: "Ready", done: true },
];
const nodeVariant = {
hidden: { opacity: 0, scale: 0.7, y: 12 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
function StageNode({
stage,
index,
reduce,
size = "md",
}: {
stage: (typeof STAGES)[number];
index: number;
reduce: boolean | null;
size?: "md" | "sm";
}) {
const dim = size === "md" ? "h-[68px] w-[68px]" : "h-14 w-14";
return (
<motion.div variants={nodeVariant} className="relative">
{/* light-up glow */}
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-2xl blur-xl ${
stage.done ? "opacity-90" : "opacity-50"
}`}
style={{
background: stage.done
? "radial-gradient(circle, rgba(34,197,94,0.5), rgba(34,197,94,0) 70%)"
: "radial-gradient(circle, rgba(124,58,237,0.35), rgba(124,58,237,0) 70%)",
}}
/>
{stage.done && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-2xl border-2 border-[#22C55E]"
animate={{ scale: [1, 1.25], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 1.9, ease: "easeOut" }}
/>
)}
<div
className={`relative flex ${dim} items-center justify-center rounded-2xl border ${
stage.done
? "border-transparent bg-gradient-to-br from-[#22C55E] to-[#16A34A] text-white shadow-[0_14px_34px_rgba(34,197,94,0.4)]"
: "border-[#6330D6]/12 bg-white text-[#6330D6] shadow-[0_10px_26px_rgba(99,48,214,0.12)]"
}`}
>
<MaterialIcon icon={stage.icon} size={size === "md" ? 30 : 26} />
<span
className={`absolute -top-2 -right-2 flex h-6 w-6 items-center justify-center rounded-full text-[11px] font-black ${
stage.done ? "bg-white text-[#16A34A]" : "bg-[#6330D6] text-white"
} shadow-md`}
>
{index + 1}
</span>
</div>
</motion.div>
);
}
export function ProductLLMPipeline() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
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]"
>
ProductLLM
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
From a shelf photo to a live catalog
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
One scan runs the full pipeline — no manual data entry, no
spreadsheets.
</motion.p>
</motion.div>
{/* ── Desktop: horizontal processing rail ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative mt-16 hidden lg:block"
>
{/* base rail */}
<div className="absolute left-[5%] right-[5%] top-[34px] h-1 rounded-full bg-[#6330D6]/10" />
{/* filling gradient */}
<motion.div
className="absolute left-[5%] top-[34px] h-1 origin-left rounded-full bg-gradient-to-r from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ width: "90%" }}
variants={{
hidden: { scaleX: 0 },
visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } },
}}
/>
{/* travelling data pulse */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute top-[30px] left-[5%] h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_12px_3px_rgba(124,58,237,0.7)]"
animate={{ left: ["5%", "95%"], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
/>
)}
<div className="relative grid grid-cols-9">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex flex-col items-center px-1 text-center">
<StageNode stage={stage} index={i} reduce={reduce} />
<p className="mt-4 text-[13px] font-bold leading-tight text-[#16001D]">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
{/* ── Mobile / tablet: vertical pipeline ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
className="relative mx-auto mt-12 max-w-md lg:hidden"
>
{/* vertical rail */}
<div className="absolute left-7 top-8 bottom-8 w-1 -translate-x-1/2 rounded-full bg-[#6330D6]/10" />
<motion.div
className="absolute left-7 top-8 w-1 -translate-x-1/2 origin-top rounded-full bg-gradient-to-b from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ bottom: "2rem" }}
variants={{
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1.6, ease: EASE } },
}}
/>
<div className="flex flex-col gap-4">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-5">
<StageNode stage={stage} index={i} reduce={reduce} size="sm" />
<p className="font-display font-black text-[#16001D] text-base leading-tight">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,119 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { motion, useReducedMotion } from "framer-motion";
import { HeroLogo } from "@/components/heroes/HeroLogo";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const badgeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
};
const headingVariant = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
};
const descVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
};
const ctaVariant = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
};
export function WhyNearleHero() {
const shouldReduce = useReducedMotion();
return (
<section className="w-full flex flex-col select-none bg-white">
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
{/* Ambient background image */}
<motion.div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0"
initial={shouldReduce ? undefined : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.4, ease: "easeOut" }}
>
<Image
src="/why-nearle.png"
alt=""
fill
sizes="100vw"
className="absolute inset-0 h-full w-full object-cover object-center"
priority
fetchPriority="high"
/>
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
</motion.div>
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
<div className="relative z-20 flex w-full max-w-[880px] flex-col items-center text-center px-2 md:px-8">
<HeroLogo />
<motion.span
variants={badgeVariant}
initial="hidden"
animate="visible"
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
>
What is AI Commerce?
</motion.span>
<motion.h1
variants={headingVariant}
initial="hidden"
animate="visible"
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
>
AI Commerce Starts
<br />
Where <span className="text-[#C9A2FF]">Quick Commerce</span>
<br />
Stops.
</motion.h1>
<motion.p
variants={descVariant}
initial="hidden"
animate="visible"
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
>
Neighborhood stores already have the trust, the customers, and the
location. Nearle adds the AI, the digital storefront, and the
ordering — so local commerce finally scales without losing what
makes it local.
</motion.p>
<motion.div
variants={ctaVariant}
initial="hidden"
animate="visible"
className="mt-9"
>
<Link
href="/platform"
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
>
Learn About Our Platform
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-0.5"
/>
</Link>
</motion.div>
</div>
</div>
</div>
</section>
);
}

1
trust.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

BIN
why nearle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB