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.", "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", url: "https://nearle.in/brands",
}, },
alternates: { canonical: "/brands" },
}; };
const WHAT_WE_OFFER = [ const WHAT_WE_OFFER = [
@@ -35,7 +36,7 @@ const WHAT_WE_OFFER = [
{ {
icon: "storefront", icon: "storefront",
title: "Retail Shelf Visibility", 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", tone: "bg-secondary/8 text-secondary",
badge: "Visibility", badge: "Visibility",
}, },

View File

@@ -14,8 +14,23 @@ export const metadata: Metadata = {
title: "Nearle for Business", title: "Nearle for Business",
description: 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.", "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() { function PhoneMapVisual() {
@@ -235,6 +250,56 @@ export default function BusinessesPage() {
{/* ─── Our Features / Business Grid ─── */} {/* ─── Our Features / Business Grid ─── */}
<BusinessFeaturesGrid /> <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 ─── */} {/* ─── Join/Download Call to Action ─── */}
<CTABanner <CTABanner
heading="Nearle is Essential for Business Growth" heading="Nearle is Essential for Business Growth"

View File

@@ -70,6 +70,35 @@ export const viewport: Viewport = {
initialScale: 1, 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({ export default function RootLayout({
children, children,
}: { }: {
@@ -83,6 +112,10 @@ export default function RootLayout({
suppressHydrationWarning suppressHydrationWarning
> >
<body className="bg-[#E8DCF2] text-on-surface font-body antialiased" suppressHydrationWarning> <body className="bg-[#E8DCF2] text-on-surface font-body antialiased" suppressHydrationWarning>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<MotionProvider> <MotionProvider>
<div className="min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col"> <div className="min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col">
<PremiumNavbar /> <PremiumNavbar />

View File

@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/layout/PremiumPageShell"; import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { Hero } from "@/components/home/Hero"; import { Hero } from "@/components/home/Hero";
import { ServiceCategories } from "@/components/home/ServiceCategories"; import { ServiceCategories } from "@/components/home/ServiceCategories";
@@ -5,8 +6,22 @@ import { WhyNearle } from "@/components/home/WhyNearle";
import { HowItWorks } from "@/components/home/HowItWorks"; import { HowItWorks } from "@/components/home/HowItWorks";
import { HyperlocalFeatures } from "@/components/home/HyperlocalFeatures"; import { HyperlocalFeatures } from "@/components/home/HyperlocalFeatures";
import { BusinessSection } from "@/components/home/BusinessSection"; import { BusinessSection } from "@/components/home/BusinessSection";
import { WhyAICommerceWins } from "@/components/home/WhyAICommerceWins";
import { StoreQRSection } from "@/components/home/StoreQRSection"; 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() { export default function HomePage() {
return ( return (
<PremiumPageShell> <PremiumPageShell>
@@ -16,6 +31,7 @@ export default function HomePage() {
<HowItWorks /> <HowItWorks />
<HyperlocalFeatures /> <HyperlocalFeatures />
<BusinessSection /> <BusinessSection />
<WhyAICommerceWins />
<StoreQRSection /> <StoreQRSection />
</PremiumPageShell> </PremiumPageShell>
); );

View File

@@ -5,6 +5,7 @@ import { PremiumPageShell } from "@/components/layout/PremiumPageShell";
import { PeopleHero } from "@/components/heroes/PeopleHero"; import { PeopleHero } from "@/components/heroes/PeopleHero";
import { AnimatedSection } from "@/components/motion/AnimatedSection"; import { AnimatedSection } from "@/components/motion/AnimatedSection";
import { AnimatedGrid, AnimatedCard } from "@/components/motion/AnimatedGrid"; import { AnimatedGrid, AnimatedCard } from "@/components/motion/AnimatedGrid";
import { AnimatedTimeline } from "@/components/delivery/AnimatedTimeline";
import { siteContent } from "@/config/site-content"; import { siteContent } from "@/config/site-content";
import type { Metadata } from "next"; import type { Metadata } from "next";
@@ -12,8 +13,65 @@ export const metadata: Metadata = {
title: "Nearle for People", title: "Nearle for People",
description: description:
"Discover verified local stores, save your favourite merchants, and order everyday essentials with superfast local delivery from shops within your neighbourhood.", "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 = [ const OFFERINGS = [
{ {
title: "Elegant Living", title: "Elegant Living",
@@ -224,6 +282,59 @@ export default function PeoplePage() {
</AnimatedGrid> </AnimatedGrid>
</section> </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 ─── */} {/* ─── 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 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"> <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"; "use client";
import React from "react"; import React from "react";
import Image from "next/image";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations"; import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
@@ -415,12 +416,29 @@ const CARDS = [
export function BusinessFeaturesGrid() { export function BusinessFeaturesGrid() {
return ( return (
<section <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)" }} 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 */} {/* Centered page container */}
<div <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 }} style={{ maxWidth: 1800 }}
> >
{/* Main two-column grid */} {/* Main two-column grid */}
@@ -439,7 +457,7 @@ export function BusinessFeaturesGrid() {
{/* Badge + Heading */} {/* Badge + Heading */}
<motion.div variants={fadeUp}> <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]"> <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> </span>
<h2 <h2
className="font-display font-black text-[#16001D] mt-4" 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 = [ const FEATURES = [
{ {
title: "Digital Catalogue", title: "AI Catalogue",
desc: "Showcase your products online and keep your catalogue updated.", desc: "Turn your shelf into a live online catalogue with ProductLLM — no manual data entry.",
icon: "menu_book", icon: "menu_book",
visual: "/images/nearle-merchant/catalogue-ui.webp", visual: "/images/nearle-merchant/catalogue-ui.webp",
alt: "Clean grocery catalogue interface preview", alt: "Clean grocery catalogue interface preview",

View File

@@ -82,19 +82,16 @@ export function StoreQRSection() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Download Nearle on the App Store" 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"> <Image
<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" /> src="/apple-download.svg"
</svg> alt="Download Nearle on the App Store"
<div className="text-left leading-tight"> width={144}
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60"> height={60}
App Store className="h-[60px] w-auto object-contain"
</div> priority
<div className="text-sm font-black"> />
Available on
</div>
</div>
</a> </a>
<a <a
@@ -102,19 +99,15 @@ export function StoreQRSection() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Download Nearle on Google Play" 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"> <Image
<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" /> src="/play-download.svg"
</svg> alt="Download Nearle on Google Play"
<div className="text-left leading-tight"> width={155}
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60"> height={60}
Google Play className="h-[60px] w-auto object-contain"
</div> />
<div className="text-sm font-black">
Available on
</div>
</div>
</a> </a>
</div> </div>
</motion.div> </motion.div>
@@ -264,19 +257,15 @@ export function StoreQRSection() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Download Nearle on the App Store" 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"> <Image
<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" /> src="/apple-download.svg"
</svg> alt="Download Nearle on the App Store"
<div className="text-left leading-tight"> width={134}
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60"> height={56}
App Store className="h-14 w-auto object-contain"
</div> />
<div className="text-sm font-black">
Available on
</div>
</div>
</a> </a>
<a <a
@@ -284,19 +273,15 @@ export function StoreQRSection() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Download Nearle on Google Play" 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"> <Image
<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" /> src="/play-download.svg"
</svg> alt="Download Nearle on Google Play"
<div className="text-left leading-tight"> width={145}
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60"> height={56}
Google Play className="h-14 w-auto object-contain"
</div> />
<div className="text-sm font-black">
Available on
</div>
</div>
</a> </a>
</div> </div>
</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 Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState, useRef } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { import {
motion, 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 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: "Home", href: "/" },
{ label: "Nearle for Business", href: "/businesses" }, { label: "Why Nearle", href: "/why-nearle" },
{ label: "Nearle for People", href: "/people" }, { label: "Platform", href: "/platform" },
{ label: "Nearle for D2C Brands", href: "/brands" }, { label: "Solutions", href: "/businesses", children: SOLUTIONS_CHILDREN },
{ label: "About", href: "/about" }, { label: "About", href: "/about" },
{ label: "Contact", href: "/contact" }, { label: "Contact", href: "/contact" },
]; ];
@@ -47,11 +56,44 @@ const mobileItemVariants = {
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } }, 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() { export function PremiumNavbar() {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [mobileSolutionsOpen, setMobileSolutionsOpen] = useState(false);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const { scrollY } = useScroll(); const { scrollY } = useScroll();
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useMotionValueEvent(scrollY, "change", (y) => { useMotionValueEvent(scrollY, "change", (y) => {
setScrolled(y > 20); setScrolled(y > 20);
@@ -62,6 +104,28 @@ export function PremiumNavbar() {
return pathname.startsWith(href); 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 ( return (
<motion.header <motion.header
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${ 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> </Link>
{/* Desktop Navigation */} {/* 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) => { {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 ( return (
<motion.div <motion.div
key={link.label} key={link.label}
@@ -164,12 +301,72 @@ export function PremiumNavbar() {
animate="visible" animate="visible"
> >
{NAV_LINKS.map((link) => { {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); const isActive = isActiveLink(link.href);
return ( return (
<motion.div key={link.label} variants={mobileItemVariants}> <motion.div key={link.label} variants={mobileItemVariants}>
<Link <Link
href={link.href} 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 ${ className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
isActive isActive
? "bg-white/15 text-white" ? "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, 600800ms
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; // 0100 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