arrenge the home page order

This commit is contained in:
2026-07-24 15:28:46 +05:30
parent a4328a791a
commit 4a0adea793
37 changed files with 2532 additions and 1564 deletions

View File

@@ -8,9 +8,7 @@
<rect x="388" y="27" width="18" height="6.5" rx="1" fill="white"/> <rect x="388" y="27" width="18" height="6.5" rx="1" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.33 26.6079C375.546 26.608 377.678 27.4594 379.283 28.9862C379.404 29.1041 379.598 29.1026 379.717 28.9829L380.873 27.8162C380.933 27.7555 380.967 27.6733 380.966 27.5877C380.966 27.5021 380.931 27.4202 380.87 27.3602C376.655 23.3207 370.005 23.3207 365.79 27.3602C365.729 27.4202 365.694 27.502 365.694 27.5876C365.693 27.6732 365.727 27.7555 365.787 27.8162L366.943 28.9829C367.062 29.1028 367.256 29.1043 367.377 28.9862C368.983 27.4593 371.114 26.6079 373.33 26.6079ZM373.33 30.4036C374.548 30.4035 375.722 30.856 376.624 31.6732C376.747 31.7892 376.939 31.7867 377.058 31.6676L378.212 30.5009C378.273 30.4397 378.307 30.3567 378.306 30.2704C378.305 30.1842 378.27 30.1018 378.208 30.0419C375.46 27.4855 371.204 27.4855 368.455 30.0419C368.393 30.1018 368.358 30.1842 368.357 30.2705C368.356 30.3568 368.39 30.4398 368.451 30.5009L369.605 31.6676C369.724 31.7867 369.917 31.7892 370.039 31.6732C370.941 30.8566 372.114 30.4041 373.33 30.4036ZM375.549 33.1879C375.611 33.1273 375.645 33.0439 375.643 32.9574C375.642 32.8709 375.604 32.7889 375.54 32.7309C374.265 31.652 372.396 31.652 371.121 32.7309C371.057 32.7889 371.019 32.8708 371.017 32.9573C371.015 33.0438 371.049 33.1272 371.111 33.1879L373.109 35.2036C373.167 35.2628 373.247 35.2961 373.33 35.2961C373.414 35.2961 373.494 35.2628 373.552 35.2036L375.549 33.1879Z" fill="white"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M373.33 26.6079C375.546 26.608 377.678 27.4594 379.283 28.9862C379.404 29.1041 379.598 29.1026 379.717 28.9829L380.873 27.8162C380.933 27.7555 380.967 27.6733 380.966 27.5877C380.966 27.5021 380.931 27.4202 380.87 27.3602C376.655 23.3207 370.005 23.3207 365.79 27.3602C365.729 27.4202 365.694 27.502 365.694 27.5876C365.693 27.6732 365.727 27.7555 365.787 27.8162L366.943 28.9829C367.062 29.1028 367.256 29.1043 367.377 28.9862C368.983 27.4593 371.114 26.6079 373.33 26.6079ZM373.33 30.4036C374.548 30.4035 375.722 30.856 376.624 31.6732C376.747 31.7892 376.939 31.7867 377.058 31.6676L378.212 30.5009C378.273 30.4397 378.307 30.3567 378.306 30.2704C378.305 30.1842 378.27 30.1018 378.208 30.0419C375.46 27.4855 371.204 27.4855 368.455 30.0419C368.393 30.1018 368.358 30.1842 368.357 30.2705C368.356 30.3568 368.39 30.4398 368.451 30.5009L369.605 31.6676C369.724 31.7867 369.917 31.7892 370.039 31.6732C370.941 30.8566 372.114 30.4041 373.33 30.4036ZM375.549 33.1879C375.611 33.1273 375.645 33.0439 375.643 32.9574C375.642 32.8709 375.604 32.7889 375.54 32.7309C374.265 31.652 372.396 31.652 371.121 32.7309C371.057 32.7889 371.019 32.8708 371.017 32.9573C371.015 33.0438 371.049 33.1272 371.111 33.1879L373.109 35.2036C373.167 35.2628 373.247 35.2961 373.33 35.2961C373.414 35.2961 373.494 35.2628 373.552 35.2036L375.549 33.1879Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M344.667 31.3334H345.667C346.219 31.3334 346.667 31.7811 346.667 32.3334V34.3334C346.667 34.8857 346.219 35.3334 345.667 35.3334H344.667C344.114 35.3334 343.667 34.8857 343.667 34.3334V32.3334C343.667 31.7811 344.114 31.3334 344.667 31.3334ZM349.333 29.3334H350.333C350.885 29.3334 351.333 29.7811 351.333 30.3334V34.3334C351.333 34.8857 350.885 35.3334 350.333 35.3334H349.333C348.781 35.3334 348.333 34.8857 348.333 34.3334V30.3334C348.333 29.7811 348.781 29.3334 349.333 29.3334ZM354 27.0001H355C355.552 27.0001 356 27.4478 356 28.0001V34.3334C356 34.8857 355.552 35.3334 355 35.3334H354C353.448 35.3334 353 34.8857 353 34.3334V28.0001C353 27.4478 353.448 27.0001 354 27.0001ZM358.667 24.6667H359.667C360.219 24.6667 360.667 25.1145 360.667 25.6667V34.3334C360.667 34.8857 360.219 35.3334 359.667 35.3334H358.667C358.114 35.3334 357.667 34.8857 357.667 34.3334V25.6667C357.667 25.1145 358.114 24.6667 358.667 24.6667Z" fill="white"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M344.667 31.3334H345.667C346.219 31.3334 346.667 31.7811 346.667 32.3334V34.3334C346.667 34.8857 346.219 35.3334 345.667 35.3334H344.667C344.114 35.3334 343.667 34.8857 343.667 34.3334V32.3334C343.667 31.7811 344.114 31.3334 344.667 31.3334ZM349.333 29.3334H350.333C350.885 29.3334 351.333 29.7811 351.333 30.3334V34.3334C351.333 34.8857 350.885 35.3334 350.333 35.3334H349.333C348.781 35.3334 348.333 34.8857 348.333 34.3334V30.3334C348.333 29.7811 348.781 29.3334 349.333 29.3334ZM354 27.0001H355C355.552 27.0001 356 27.4478 356 28.0001V34.3334C356 34.8857 355.552 35.3334 355 35.3334H354C353.448 35.3334 353 34.8857 353 34.3334V28.0001C353 27.4478 353.448 27.0001 354 27.0001ZM358.667 24.6667H359.667C360.219 24.6667 360.667 25.1145 360.667 25.6667V34.3334C360.667 34.8857 360.219 35.3334 359.667 35.3334H358.667C358.114 35.3334 357.667 34.8857 357.667 34.3334V25.6667C357.667 25.1145 358.114 24.6667 358.667 24.6667Z" fill="white"/>
<g clip-path="url(#clip1_312_252)"> <!-- 9:41 static vector text removed to render real-time live clock overlay -->
<path d="M85.1341 35.2563C87.8221 35.2563 89.4187 33.1543 89.4187 29.5874C89.4187 26.0864 87.7195 24.1675 85.1927 24.1675C82.8929 24.1675 81.2669 25.7129 81.2669 27.8735C81.2669 29.9023 82.7317 31.3818 84.7459 31.3818C85.9837 31.3818 86.9871 30.8105 87.4852 29.8071H87.6097C87.5877 32.29 86.7015 33.7109 85.1561 33.7109C84.2479 33.7109 83.5228 33.2129 83.2957 32.4072H81.4134C81.721 34.1284 83.2005 35.2563 85.1341 35.2563ZM85.2 29.9243C83.9695 29.9243 83.1199 29.0601 83.1199 27.8149C83.1199 26.6211 84.0135 25.7056 85.2073 25.7056C86.4012 25.7056 87.2947 26.6357 87.2947 27.8516C87.2947 29.0527 86.4231 29.9243 85.2 29.9243ZM92.4366 28.1592C93.1324 28.1592 93.6085 27.6758 93.6085 27.0239C93.6085 26.3721 93.1324 25.8887 92.4366 25.8887C91.7481 25.8887 91.2647 26.3721 91.2647 27.0239C91.2647 27.6758 91.7481 28.1592 92.4366 28.1592ZM92.4366 33.5352C93.1324 33.5352 93.6085 33.0444 93.6085 32.3999C93.6085 31.748 93.1324 31.2646 92.4366 31.2646C91.7481 31.2646 91.2647 31.748 91.2647 32.3999C91.2647 33.0444 91.7481 33.5352 92.4366 33.5352ZM100.611 35H102.42V32.9712H103.848V31.4111H102.42V24.4312H99.7538C98.3183 26.6138 96.8168 29.0454 95.4472 31.4258V32.9712H100.611V35ZM97.1977 31.3452C98.2304 29.5508 99.4535 27.5952 100.537 25.9399H100.64V31.4551H97.1977V31.3452ZM107.877 35H109.766V24.4312H107.884L105.123 26.3721V28.1519L107.752 26.2915H107.877V35Z" fill="white"/>
</g>
</g> </g>
<path d="M138 5H338V23C338 31.2843 331.284 38 323 38H153C144.716 38 138 31.2843 138 23V5Z" fill="black"/> <path d="M138 5H338V23C338 31.2843 331.284 38 323 38H153C144.716 38 138 31.2843 138 23V5Z" fill="black"/>
<defs> <defs>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 463 KiB

View File

@@ -573,6 +573,48 @@ body {
} }
} }
/* ─── AI Commerce ambient decoration ─── */
/* Very slow, very low-contrast drift. Meant to be felt, never watched.
Transform-only so it stays on the compositor. */
@keyframes ai-orb-drift-a {
0%, 100% {
transform: translate3d(0, 0, 0) scale(1);
}
50% {
transform: translate3d(28px, -22px, 0) scale(1.06);
}
}
@keyframes ai-orb-drift-b {
0%, 100% {
transform: translate3d(0, 0, 0) scale(1);
}
50% {
transform: translate3d(-34px, 18px, 0) scale(1.05);
}
}
@keyframes ai-mote-float {
0%, 100% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d(6px, -18px, 0);
}
}
.ai-orb-a {
animation: ai-orb-drift-a 22s ease-in-out infinite;
}
.ai-orb-b {
animation: ai-orb-drift-b 27s ease-in-out infinite;
}
.ai-mote {
animation: ai-mote-float 14s ease-in-out infinite;
}
/* ─── Footer Layout & Sizing System ─── */ /* ─── Footer Layout & Sizing System ─── */
.footer-container { .footer-container {
box-sizing: border-box; box-sizing: border-box;

View File

@@ -3,6 +3,7 @@ import Link from "next/link";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { PremiumPageShell } from "@/components/common/PremiumPageShell"; import { PremiumPageShell } from "@/components/common/PremiumPageShell";
import { MerchantsHero } from "@/components/heroes/MerchantsHero"; import { MerchantsHero } from "@/components/heroes/MerchantsHero";
import { HowItWorks } from "@/components/home/HowItWorks";
import { FAQAccordion } from "@/components/ui/FAQAccordion"; import { FAQAccordion } from "@/components/ui/FAQAccordion";
import { AnimatedSection } from "@/components/animations/AnimatedSection"; import { AnimatedSection } from "@/components/animations/AnimatedSection";
import { AnimatedGrid, AnimatedCard } from "@/components/animations/AnimatedGrid"; import { AnimatedGrid, AnimatedCard } from "@/components/animations/AnimatedGrid";
@@ -159,6 +160,7 @@ export default function MerchantsPage() {
return ( return (
<PremiumPageShell> <PremiumPageShell>
<MerchantsHero /> <MerchantsHero />
<HowItWorks />
{/* ─── Merchant Benefits Grid ─── */} {/* ─── Merchant Benefits Grid ─── */}
<section className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10"> <section className="py-10 md:py-14 xl:py-12 bg-white border-b border-[#6330D6]/10">

View File

@@ -1,15 +1,15 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/common/PremiumPageShell"; import { PremiumPageShell } from "@/components/common/PremiumPageShell";
import { Hero } from "@/components/home/Hero"; import { Hero } from "@/components/home/Hero";
import { WhyNearleDifferent } from "@/components/home/WhyNearleDifferent";
import { AICommerceIntro } from "@/components/home/AICommerceIntro";
import { VoiceCommerceExperience } from "@/components/home/VoiceCommerceExperience"; import { VoiceCommerceExperience } from "@/components/home/VoiceCommerceExperience";
import { StoreJourney } from "@/components/home/experience/StoreJourney";
import { ServiceCategories } from "@/components/home/ServiceCategories"; import { ServiceCategories } from "@/components/home/ServiceCategories";
import { WhyNearle } from "@/components/home/WhyNearle"; 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 { WhyAICommerceWins } from "@/components/home/WhyAICommerceWins";
import { AICommerceTeaser } from "@/components/home/AICommerceTeaser";
import { StoreQRSection } from "@/components/home/StoreQRSection"; import { StoreQRSection } from "@/components/home/StoreQRSection";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -29,15 +29,15 @@ export default function HomePage() {
return ( return (
<PremiumPageShell> <PremiumPageShell>
<Hero /> <Hero />
<WhyAICommerceWins />
<AICommerceIntro />
<VoiceCommerceExperience /> <VoiceCommerceExperience />
<StoreJourney />
<ServiceCategories />
<WhyNearle /> <WhyNearle />
<WhyNearleDifferent />
<HowItWorks /> <HowItWorks />
<HyperlocalFeatures /> <HyperlocalFeatures />
<ServiceCategories />
<BusinessSection /> <BusinessSection />
<WhyAICommerceWins />
<AICommerceTeaser />
<StoreQRSection /> <StoreQRSection />
</PremiumPageShell> </PremiumPageShell>
); );

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { PremiumPageShell } from "@/components/common/PremiumPageShell"; import { PremiumPageShell } from "@/components/common/PremiumPageShell";
import { PlatformHero } from "@/components/platform/PlatformHero"; import { PlatformHero } from "@/components/platform/PlatformHero";
import { CorePrinciples } from "@/components/home/CorePrinciples";
import { PlatformArchitecture } from "@/components/platform/PlatformArchitecture"; import { PlatformArchitecture } from "@/components/platform/PlatformArchitecture";
import { ProductFeatureGrid } from "@/components/platform/ProductFeatureGrid"; import { ProductFeatureGrid } from "@/components/platform/ProductFeatureGrid";
import { ProductLLMWorkflow } from "@/components/platform/ProductLLMWorkflow"; import { ProductLLMWorkflow } from "@/components/platform/ProductLLMWorkflow";
@@ -55,6 +56,7 @@ export default function PlatformPage() {
primary={{ href: "/businesses", icon: "storefront", label: "For Business" }} primary={{ href: "/businesses", icon: "storefront", label: "For Business" }}
secondary={{ href: "/why-nearle", icon: "auto_awesome", label: "Why AI Commerce" }} secondary={{ href: "/why-nearle", icon: "auto_awesome", label: "Why AI Commerce" }}
/> />
<CorePrinciples />
</PremiumPageShell> </PremiumPageShell>
); );
} }

View File

@@ -14,11 +14,11 @@ import { PullQuote } from "@/components/ui/PullQuote";
import { AICommerceCTA } from "@/components/why-nearle/AICommerceCTA"; import { AICommerceCTA } from "@/components/why-nearle/AICommerceCTA";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "What is AI Commerce?", title: "What is AI Commerce",
description: 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.", "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: { openGraph: {
title: "What is AI Commerce? | Nearle", title: "What is AI Commerce | Nearle",
description: description:
"Neighbourhood stores already have trust, customers, and location. Nearle adds AI, a digital storefront, inventory, and ordering. This is AI Commerce.", "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", url: "https://nearle.in/why-nearle",

View File

@@ -0,0 +1,243 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { Spotlight } from "@/components/ui/Spotlight";
import { fadeUp, staggerContainer, staggerCards, cardReveal, viewport } from "@/lib/animations";
const STEPS = [
{
step: "01",
title: "Natural Intent",
desc: "Tell Nearle what you need in plain Tamil or English. AI understands local store products, brands, and quantities without manual searching.",
icon: "psychology",
badge: "Voice & Text AI",
iconBg: "bg-primary/10 text-primary border-primary/20",
badgeStyle: "bg-primary/10 text-primary border-primary/20",
},
{
step: "02",
title: "Live Local Discovery",
desc: "AI instantly checks live stock and pricing at verified neighbourhood stores right around your street to find exact matches.",
icon: "storefront",
badge: "Real-Time Stock",
iconBg: "bg-secondary/10 text-secondary border-secondary/20",
badgeStyle: "bg-secondary/10 text-secondary border-secondary/20",
},
{
step: "03",
title: "Instant Fulfillment",
desc: "Confirms your order, alerts the local shopkeeper to pack, and dispatches a Nearle rider for fast doorstep delivery — all in 1 chat.",
icon: "bolt",
badge: "1-Tap Fulfillment",
iconBg: "bg-[#7C3AED]/10 text-[#7C3AED] border-[#7C3AED]/20",
badgeStyle: "bg-[#7C3AED]/10 text-[#7C3AED] border-[#7C3AED]/20",
},
];
const HIGHLIGHTS = [
{
title: "Zero App Switching",
sub: "Everything in 1 conversation",
icon: "forum",
iconColor: "bg-primary/10 text-primary",
},
{
title: "Verified Merchants",
sub: "Trusted neighbourhood shops",
icon: "verified_user",
iconColor: "bg-secondary/10 text-secondary",
},
{
title: "Live Inventory",
sub: "Real-time store stock",
icon: "inventory_2",
iconColor: "bg-blue-600/10 text-blue-600",
},
{
title: "Fast Rider Delivery",
sub: "Direct doorstep fulfillment",
icon: "local_shipping",
iconColor: "bg-[#7C3AED]/10 text-[#7C3AED]",
},
];
export function AICommerceIntro() {
const reduce = useReducedMotion();
return (
<section
id="ai-commerce"
aria-labelledby="ai-commerce-title"
className="section section-soft w-full overflow-hidden border-y border-[#6330D6]/10"
>
<div className="page-container relative z-10">
{/* Section Header */}
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[780px] text-center mb-12 md:mb-16"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1.5 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
Nearle AI Engine
</motion.span>
<motion.h2
id="ai-commerce-title"
variants={fadeUp}
className="mt-4 font-display font-black tracking-tight text-[#16001D] text-4xl md:text-5xl lg:text-[54px] leading-tight"
>
Neighbourhood Shopping, <span className="text-[#7C3AED]">Reimagined by AI</span>
</motion.h2>
<motion.p
variants={fadeUp}
className="mx-auto mt-4 max-w-[640px] text-base md:text-lg leading-relaxed text-[#5E5866]"
>
Instead of searching across multiple apps and comparing items manually, simply tell Nearle what you need. AI handles discovery, stock checking, and ordering all in one conversation.
</motion.p>
</motion.div>
{/* Feature Showcase Banner */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[1100px] mb-14"
>
<Spotlight
className="overflow-hidden rounded-[2.5rem] border border-[#6330D6]/15 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-8 md:p-12 shadow-[0_16px_44px_-16px_rgba(99,48,214,0.2)]"
color="rgba(168,85,247,0.14)"
size={500}
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
<div className="lg:col-span-7">
<div className="inline-flex items-center gap-2 rounded-xl bg-[#7C3AED]/10 px-3 py-1 text-xs font-black text-[#7C3AED] mb-4">
<MaterialIcon icon="auto_awesome" size={16} />
<span>Conversational Commerce</span>
</div>
<h3 className="font-display text-2xl md:text-3xl font-black text-[#16001D] leading-tight mb-4">
Talk to Nearle AI like you speak to your local shopkeeper
</h3>
<p className="text-base md:text-lg text-[#5E5866] leading-relaxed">
Nearle connects natural consumer intent directly to verified local merchant inventory. You ask, AI discovers nearby availability, confirms with the merchant, and dispatches delivery.
</p>
</div>
<div className="lg:col-span-5 flex flex-col gap-3">
<div className="flex items-center gap-3.5 rounded-2xl border border-[#6330D6]/10 bg-white/90 p-4 shadow-sm">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<MaterialIcon icon="mic" size={20} />
</div>
<div>
<p className="text-xs font-black text-primary uppercase tracking-wider">Step 1 Intent</p>
<p className="text-sm font-bold text-[#16001D]">Natural Voice & Text Query</p>
</div>
</div>
<div className="flex items-center gap-3.5 rounded-2xl border border-[#6330D6]/10 bg-white/90 p-4 shadow-sm">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#EC4899]/10 text-[#EC4899]">
<MaterialIcon icon="travel_explore" size={20} />
</div>
<div>
<p className="text-xs font-black text-[#EC4899] uppercase tracking-wider">Step 2 Match</p>
<p className="text-sm font-bold text-[#16001D]">Live Local Shop Scan</p>
</div>
</div>
<div className="flex items-center gap-3.5 rounded-2xl border border-[#6330D6]/10 bg-white/90 p-4 shadow-sm">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#10B981]/10 text-[#10B981]">
<MaterialIcon icon="local_shipping" size={20} />
</div>
<div>
<p className="text-xs font-black text-[#10B981] uppercase tracking-wider">Step 3 Deliver</p>
<p className="text-sm font-bold text-[#16001D]">Shopkeeper Pack & Rider Delivery</p>
</div>
</div>
</div>
</div>
</Spotlight>
</motion.div>
{/* 3 Step Cards Grid */}
<motion.div
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 mx-auto max-w-[1100px]"
>
{STEPS.map((step) => (
<motion.div
key={step.title}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
className="flex flex-col justify-between rounded-[2rem] border border-[#16001D]/8 bg-white p-7 md:p-8 shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-all duration-300 hover:border-[#7C3AED]/30 hover:shadow-[0_20px_40px_-15px_rgba(99,48,214,0.15)] h-full"
>
<div>
<div className="flex items-center justify-between mb-6">
<span className={`flex h-12 w-12 items-center justify-center rounded-2xl border ${step.iconBg}`}>
<MaterialIcon icon={step.icon} size={24} />
</span>
<span className="font-display text-xs font-black tracking-widest text-[#7C3AED] uppercase bg-[#7C3AED]/5 border border-[#7C3AED]/15 px-3 py-1 rounded-full">
{step.step}
</span>
</div>
<div className={`inline-block rounded-md border px-2.5 py-1 text-xs font-black mb-3 ${step.badgeStyle}`}>
{step.badge}
</div>
<h3 className="font-display text-2xl font-black tracking-tight text-[#16001D] mb-3">
{step.title}
</h3>
<p className="text-sm md:text-base text-[#5E5866] leading-relaxed">
{step.desc}
</p>
</div>
</motion.div>
))}
</motion.div>
{/* 4 Feature Highlights Grid */}
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-5 mt-12 mx-auto max-w-[1100px]"
>
{HIGHLIGHTS.map((item) => (
<motion.div
key={item.title}
variants={fadeUp}
className="flex items-center gap-4 rounded-2xl border border-[#16001D]/6 bg-white p-4 md:p-5 shadow-sm transition-all hover:border-[#7C3AED]/25 hover:shadow-md"
>
<div className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${item.iconColor}`}>
<MaterialIcon icon={item.icon} size={22} />
</div>
<div className="min-w-0">
<p className="truncate text-sm font-black text-[#16001D]">
{item.title}
</p>
<p className="truncate text-xs text-[#5E5866] font-medium">
{item.sub}
</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -1,126 +0,0 @@
"use client";
import Link from "next/link";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const ROWS = [
{ traditional: "Manual discovery", nearle: "AI discovery" },
{ traditional: "Walk into stores", nearle: "Search nearby instantly" },
{ traditional: "Limited visibility", nearle: "Every local merchant discoverable" },
{ traditional: "Static catalogue", nearle: "AI-organized catalogue" },
];
const rowVariant = {
hidden: { opacity: 0, y: 14 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
// Priority 4: a compact teaser contrasting traditional commerce with Nearle's
// AI commerce. The full seven-row comparison lives on /why-nearle.
export function AICommerceTeaser() {
const reduce = useReducedMotion();
return (
<section className="section section-soft w-full overflow-hidden border-y border-[#6330D6]/10">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[680px] text-center"
>
<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 Shift
</motion.span>
<motion.h2
variants={fadeUp}
className="mt-4 font-display font-black tracking-tight text-[#16001D]"
style={{ fontSize: "clamp(28px, 3vw, 44px)", lineHeight: 1.1 }}
>
Traditional commerce, reimagined by AI
</motion.h2>
</motion.div>
<div className="mx-auto mt-10 max-w-[860px]">
{/* Column headers */}
<div className="grid grid-cols-2 gap-3 px-1 sm:gap-4">
<span className="text-[11px] font-black uppercase tracking-widest text-[#6D6172] sm:text-xs">
Traditional Commerce
</span>
<span className="pl-2 text-[11px] font-black uppercase tracking-widest text-[#6330D6] sm:text-xs">
Nearle AI Commerce
</span>
</div>
<motion.div
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport20}
className="mt-3 flex flex-col gap-3"
>
{ROWS.map((row) => (
<motion.div
key={row.nearle}
variants={rowVariant}
className="grid grid-cols-2 gap-3 sm:gap-4"
>
{/* Traditional side */}
<div className="flex items-center gap-2.5 rounded-2xl border border-[#6330D6]/8 bg-white px-4 py-3.5 shadow-[0_4px_16px_rgba(22,0,29,0.03)]">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#F3F3F6] text-[#6D6172]">
<MaterialIcon icon="remove" size={14} />
</span>
<span className="text-[13px] font-semibold text-[#5E5866] sm:text-[15px]">
{row.traditional}
</span>
</div>
{/* Nearle side */}
<div className="flex items-center gap-2.5 rounded-2xl border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] px-4 py-3.5 shadow-[0_4px_20px_rgba(99,48,214,0.08)]">
<motion.span
className="flex h-6 w-6 shrink-0 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={13} />
</motion.span>
<span className="text-[13px] font-bold text-[#16001D] sm:text-[15px]">
{row.nearle}
</span>
</div>
</motion.div>
))}
</motion.div>
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-8 flex justify-center"
>
<Link
href="/why-nearle"
className="group inline-flex items-center gap-2 text-base font-black text-[#6330D6] transition-colors hover:text-[#4C1D95]"
>
Learn More
<MaterialIcon
icon="arrow_forward"
size={20}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,304 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { Spotlight } from "@/components/ui/Spotlight";
import BorderGlow from "@/components/ui/BorderGlow";
import { fadeUp, staggerContainer, staggerCards, cardReveal, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
// BorderGlow config for the highlighted AI Commerce card. `glowColor` is an HSL
// triplet (the component parses "h s l"), so #A855F7 — the middle of the Nearle
// purple family — becomes "271 91 65". The hex values drive the mesh border.
const GLOW = {
backgroundColor: "#FFFFFF",
glowColor: "271 91 65",
colors: ["#7C3AED", "#A855F7", "#C084FC"],
borderRadius: 28,
edgeSensitivity: 28,
glowRadius: 34,
glowIntensity: 0.85,
coneSpread: 22,
animated: true,
} as const;
type Era = {
step: string;
title: string;
icon: string;
strengths: string[];
limits: string[];
};
const ERAS: Era[] = [
{
step: "01",
title: "Traditional Retail",
icon: "storefront",
strengths: ["Local trust", "Personal relationships", "Community presence"],
limits: ["No digital discovery", "Manual operations"],
},
{
step: "02",
title: "E-Commerce",
icon: "public",
strengths: ["Massive product catalog", "Nationwide delivery", "Digital convenience"],
limits: ["Endless searching", "Generic recommendations", "No local connection"],
},
{
step: "03",
title: "Quick Commerce",
icon: "bolt",
strengths: ["Fast delivery", "Instant convenience"],
limits: ["Dark store dependency", "High operational cost", "Local merchants excluded"],
},
];
const AI_POINTS = [
{ icon: "psychology", label: "Understands customer intent" },
{ icon: "travel_explore", label: "AI-powered discovery" },
{ icon: "inventory_2", label: "Real-time inventory" },
{ icon: "auto_awesome", label: "Intelligent recommendations" },
{ icon: "storefront", label: "Merchant-first commerce" },
{ icon: "eco", label: "Sustainable local ecosystem" },
];
export function CommerceEvolution() {
const reduce = useReducedMotion();
return (
<section
id="commerce-evolution"
aria-labelledby="commerce-evolution-title"
className="section relative w-full scroll-mt-24 overflow-hidden bg-white"
>
{/* Ambient mesh — same violet family as the rest of the AI story */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-0">
<div className="absolute left-1/2 top-0 h-[520px] w-[90%] max-w-[1100px] -translate-x-1/2 rounded-full bg-[#A855F7]/[0.07] blur-[140px]" />
<div className="ai-orb-a absolute left-[4%] top-[30%] h-[300px] w-[300px] rounded-full bg-[#C084FC]/[0.06] blur-[110px]" />
<div className="ai-orb-b absolute right-[4%] bottom-[10%] h-[340px] w-[340px] rounded-full bg-[#F0ABFC]/[0.06] blur-[120px]" />
</div>
<div className="page-container relative z-10">
{/* Header */}
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[820px] text-center"
>
<motion.div variants={fadeUp} className="inline-flex justify-center">
<span className="inline-flex items-center gap-2 rounded-full border border-[#7C3AED]/20 bg-[linear-gradient(135deg,rgba(124,58,237,0.08)_0%,rgba(168,85,247,0.04)_100%)] px-4 py-1.5 text-xs font-black uppercase tracking-widest text-[#7C3AED] shadow-sm backdrop-blur-md">
The Evolution of Commerce
</span>
</motion.div>
<motion.h2
id="commerce-evolution-title"
variants={fadeUp}
className="mt-5 text-balance font-display font-black tracking-tight text-[#16001D]"
style={{ fontSize: "clamp(34px, 4.6vw, 62px)", lineHeight: 1.06 }}
>
Retail Has Entered Its{" "}
<span className="bg-gradient-to-r from-[#7C3AED] via-[#A855F7] to-[#6330D6] bg-clip-text text-transparent">
Next Era.
</span>
</motion.h2>
<motion.p
variants={fadeUp}
className="mx-auto mt-5 max-w-[680px] text-[17px] leading-[1.75] text-[#5E5866] md:text-[18px]"
>
Every generation of commerce solved one problem while creating another.
</motion.p>
<motion.p
variants={fadeUp}
className="mx-auto mt-2.5 max-w-[680px] text-[17px] leading-[1.75] text-[#3B2F44] md:text-[18px]"
>
AI Commerce combines trust, local merchants, and intelligent discovery into one connected
shopping experience.
</motion.p>
</motion.div>
{/* The three eras that came before */}
<motion.div
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-14 grid grid-cols-1 gap-5 md:grid-cols-3"
>
{ERAS.map((era) => (
<motion.div
key={era.title}
variants={cardReveal}
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
className="h-full"
>
<Spotlight
className="flex h-full flex-col rounded-[28px] border border-[#6330D6]/10 bg-white p-6 shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_20px_40px_-14px_rgba(99,48,214,0.18)] md:p-7"
color="rgba(168,85,247,0.10)"
>
<div className="mb-5 flex items-center justify-between">
<span className="flex h-12 w-12 items-center justify-center rounded-2xl border border-[#6330D6]/10 bg-[#F5F1FA] text-[#6D6172]">
<MaterialIcon icon={era.icon} size={24} />
</span>
<span className="font-display text-xs font-black uppercase tracking-widest text-[#C9BFD4]">
{era.step}
</span>
</div>
<h3 className="font-display text-xl font-black tracking-tight text-[#16001D]">
{era.title}
</h3>
{/* flex-1 pushes the limitations block to the card floor, so the
three cards share a bottom edge despite uneven list lengths. */}
<div className="mt-5 flex-1">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#16A34A]">
Strengths
</p>
<ul className="mt-2.5 space-y-2">
{era.strengths.map((s) => (
<li key={s} className="flex items-start gap-2.5 text-[14px] leading-snug text-[#3F3A47]">
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#DCFCE7] text-[#16A34A]">
<MaterialIcon icon="check" size={11} />
</span>
{s}
</li>
))}
</ul>
</div>
<div className="mt-5 border-t border-[#6330D6]/8 pt-5">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#9A8FA6]">
Limitations
</p>
<ul className="mt-2.5 space-y-2">
{era.limits.map((l) => (
<li key={l} className="flex items-start gap-2.5 text-[14px] leading-snug text-[#6D6172]">
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#F1EEF5] text-[#9A8FA6]">
<MaterialIcon icon="remove" size={11} />
</span>
{l}
</li>
))}
</ul>
</div>
</Spotlight>
</motion.div>
))}
</motion.div>
{/* Connector into the current era */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
aria-hidden="true"
className="flex flex-col items-center py-8"
>
<span className="h-10 w-px bg-[linear-gradient(180deg,transparent,rgba(124,58,237,0.35))]" />
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[#7C3AED]/20 bg-white text-[#7C3AED] shadow-[0_4px_14px_rgba(124,58,237,0.18)]">
<MaterialIcon icon="arrow_downward" size={16} />
</span>
<span className="h-10 w-px bg-[linear-gradient(180deg,rgba(124,58,237,0.35),transparent)]" />
</motion.div>
{/* The highlighted era — AI Commerce */}
<motion.div
variants={cardReveal}
initial="hidden"
whileInView="visible"
viewport={viewport}
whileHover={reduce ? undefined : { y: -6 }}
transition={{ type: "spring", stiffness: 240, damping: 24 }}
>
{/* BorderGlow owns the card surface and carries a fixed six-layer
inline box-shadow, so the Nearle elevation is restored through
className with Tailwind v4's postfix `!`. */}
<BorderGlow
{...GLOW}
colors={[...GLOW.colors]}
className="group [box-shadow:0_16px_44px_-16px_rgba(99,48,214,0.28)]!"
>
<div className="relative overflow-hidden rounded-[28px] bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f3ebfd_100%)] p-7 md:p-10 lg:p-12">
<div
aria-hidden="true"
className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full bg-[#A855F7]/12 blur-3xl"
/>
<div className="relative z-10 grid gap-10 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:gap-14">
<div>
<span className="inline-flex items-center gap-2 rounded-full bg-[#7C3AED]/10 px-3.5 py-1.5 text-[11px] font-black uppercase tracking-[0.14em] text-[#7C3AED] ring-1 ring-[#7C3AED]/20">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full rounded-full bg-[#7C3AED] opacity-75 motion-safe:animate-ping" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[#7C3AED]" />
</span>
Now
</span>
<h3
className="mt-4 font-display font-black tracking-tight text-[#16001D]"
style={{ fontSize: "clamp(30px, 3.4vw, 46px)", lineHeight: 1.08 }}
>
<span className="bg-gradient-to-r from-[#7C3AED] via-[#A855F7] to-[#6330D6] bg-clip-text text-transparent">
AI Commerce
</span>
</h3>
<p className="mt-4 max-w-[420px] text-[15px] leading-[1.75] text-[#5E5866]">
The first era that keeps the neighbourhood merchant at the centre while giving shoppers
discovery that actually understands them.
</p>
</div>
<motion.ul
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="grid grid-cols-1 gap-3 sm:grid-cols-2"
>
{AI_POINTS.map((point) => (
<motion.li
key={point.label}
variants={fadeUp}
className="flex items-center gap-3 rounded-2xl border border-[#7C3AED]/12 bg-white/70 p-3.5 shadow-sm backdrop-blur-sm transition-colors duration-200 hover:border-[#7C3AED]/28"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#7C3AED]/10 text-[#7C3AED]">
<MaterialIcon icon={point.icon} size={18} />
</span>
<span className="text-[14px] font-bold leading-snug text-[#16001D]">
{point.label}
</span>
</motion.li>
))}
</motion.ul>
</div>
<motion.p
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
transition={{ duration: 0.5, ease: EASE }}
className="relative z-10 mt-9 border-t border-[#7C3AED]/12 pt-7 text-center text-[16px] font-bold leading-[1.7] text-[#3B2F44] md:text-[18px]"
>
Nearle combines{" "}
<span className="text-[#7C3AED]">MerchantOS</span>,{" "}
<span className="text-[#7C3AED]">ProductLLM</span>,{" "}
<span className="text-[#7C3AED]">ConsumerOS</span>, and AI into one intelligent
commerce platform.
</motion.p>
</div>
</BorderGlow>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,352 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { Spotlight } from "@/components/ui/Spotlight";
import { fadeUp, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
// 1. Trust Graphic (Emerald Shield & Checkmark)
function TrustGraphic({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center p-2">
<motion.div
className="absolute h-20 w-20 rounded-full bg-emerald-500/20 blur-xl pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.5, 0.25] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
/>
<svg
width="110"
height="110"
viewBox="0 0 110 110"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative z-10 w-24 h-24 md:w-28 md:h-28 filter drop-shadow-[0_10px_20px_rgba(0,0,0,0.35)] transition-transform duration-300 group-hover:scale-105"
>
<motion.path
d="M 55 12 L 88 24 C 88 56 72 82 55 96 C 38 82 22 56 22 24 L 55 12 Z"
fill="url(#trust-grad)"
stroke="#6EE7B7"
strokeWidth="2"
animate={reduce ? {} : { y: [0, -4, 0] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.path
d="M 40 52 L 50 62 L 72 40"
stroke="#FFFFFF"
strokeWidth="6"
strokeLinecap="round"
strokeLinejoin="round"
animate={reduce ? {} : { opacity: [0.8, 1, 0.8] }}
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
/>
<defs>
<linearGradient id="trust-grad" x1="22" y1="12" x2="88" y2="96" gradientUnits="userSpaceOnUse">
<stop stopColor="#059669" stopOpacity="0.8" />
<stop offset="1" stopColor="#34D399" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
);
}
// 2. AI Graphic (Purple Sparkles Cluster)
function AIGraphic({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center p-2">
<motion.div
className="absolute h-20 w-20 rounded-full bg-[#7C3AED]/25 blur-xl pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.5, 0.25] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
/>
<svg
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative z-10 w-24 h-24 md:w-28 md:h-28 filter drop-shadow-[0_10px_20px_rgba(0,0,0,0.35)] transition-transform duration-300 group-hover:scale-105"
>
<motion.path
d="M 45 10 C 45 29.33 29.33 45 10 45 C 29.33 45 45 60.67 45 80 C 45 60.67 60.67 45 80 45 C 60.67 45 45 29.33 45 10 Z"
fill="#818CF8"
style={{ transformOrigin: "45px 45px" }}
animate={reduce ? {} : { scale: [1, 1.08, 1], rotate: [0, 4, 0, -4, 0] }}
transition={{ duration: 3.6, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.path
d="M 88 15 C 88 23 81 30 73 30 C 81 30 88 37 88 45 C 88 37 95 30 103 30 C 95 30 88 23 88 15 Z"
fill="#C084FC"
style={{ transformOrigin: "88px 30px" }}
animate={reduce ? {} : { scale: [0.85, 1.2, 0.85], y: [0, -4, 0] }}
transition={{ duration: 2.8, delay: 0.4, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.path
d="M 82 72 C 82 78.6 76.6 84 70 84 C 76.6 84 82 89.4 82 96 C 82 89.4 87.4 84 94 84 C 87.4 84 82 78.6 82 72 Z"
fill="#F5D0FE"
style={{ transformOrigin: "82px 84px" }}
animate={reduce ? {} : { scale: [1.15, 0.8, 1.15], rotate: [0, -8, 0] }}
transition={{ duration: 3.0, delay: 1.1, repeat: Infinity, ease: "easeInOut" }}
/>
</svg>
</div>
);
}
// 3. Merchant First Graphic (Golden Storefront & Canopy)
function MerchantGraphic({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center p-2">
<motion.div
className="absolute h-20 w-20 rounded-full bg-amber-500/20 blur-xl pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.5, 0.25] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
/>
<svg
width="110"
height="110"
viewBox="0 0 110 110"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative z-10 w-24 h-24 md:w-28 md:h-28 filter drop-shadow-[0_10px_20px_rgba(0,0,0,0.35)] transition-transform duration-300 group-hover:scale-105"
>
<motion.path
d="M 20 40 L 90 40 L 82 22 L 28 22 Z"
fill="#FBBF24"
animate={reduce ? {} : { y: [0, -3, 0] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
/>
<path d="M 34 22 L 30 40 M 48 22 L 46 40 M 62 22 L 62 40 M 76 22 L 78 40" stroke="#D97706" strokeWidth="2.5" />
<rect x="25" y="40" width="60" height="46" rx="4" fill="url(#merchant-grad)" stroke="#FDE047" strokeWidth="2" />
<rect x="43" y="56" width="24" height="30" rx="3" fill="#78350F" />
<circle cx="55" cy="48" r="4" fill="#FDE047" />
<defs>
<linearGradient id="merchant-grad" x1="25" y1="40" x2="85" y2="86" gradientUnits="userSpaceOnUse">
<stop stopColor="#D97706" stopOpacity="0.8" />
<stop offset="1" stopColor="#F59E0B" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
);
}
// 4. Local Graphic (Sky Pin & Radar Pulse)
function LocalGraphic({ reduce = false }: { reduce?: boolean }) {
return (
<div className="relative flex items-center justify-center p-2">
<motion.div
className="absolute h-20 w-20 rounded-full bg-sky-500/20 blur-xl pointer-events-none"
animate={reduce ? {} : { scale: [0.9, 1.25, 0.9], opacity: [0.25, 0.5, 0.25] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
/>
<svg
width="110"
height="110"
viewBox="0 0 110 110"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative z-10 w-24 h-24 md:w-28 md:h-28 filter drop-shadow-[0_10px_20px_rgba(0,0,0,0.35)] transition-transform duration-300 group-hover:scale-105"
>
<motion.path
d="M 55 18 C 40 18 28 30 28 45 C 28 65 55 88 55 88 C 55 88 82 65 82 45 C 82 30 70 18 55 18 Z"
fill="url(#local-grad)"
stroke="#7DD3FC"
strokeWidth="2"
animate={reduce ? {} : { y: [0, -6, 0] }}
transition={{ duration: 2.8, repeat: Infinity, ease: "easeInOut" }}
/>
<circle cx="55" cy="43" r="9" fill="#0284C7" />
<circle cx="55" cy="43" r="4" fill="#FFFFFF" />
<defs>
<linearGradient id="local-grad" x1="28" y1="18" x2="82" y2="88" gradientUnits="userSpaceOnUse">
<stop stopColor="#0284C7" stopOpacity="0.8" />
<stop offset="1" stopColor="#38BDF8" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
);
}
const CARD_MOTES = [
{ left: "20%", top: "35%", size: 4, duration: 4.5, delay: 0 },
{ left: "75%", top: "60%", size: 3, duration: 5.5, delay: 0.8 },
];
const PRINCIPLES = [
{
word: "Trust",
icon: "verified_user",
line: "Local relationships cannot be replaced.",
badgeStyle: "bg-emerald-500/15 border-emerald-500/30 text-emerald-400",
dotColor: "bg-emerald-400",
spotlightColor: "rgba(52,211,153,0.18)",
renderGraphic: (reduce: boolean) => <TrustGraphic reduce={reduce} />,
},
{
word: "AI",
icon: "psychology",
line: "Removes complexity instead of adding more choices.",
badgeStyle: "bg-[#7C3AED]/20 border-[#7C3AED]/40 text-[#C084FC]",
dotColor: "bg-[#C084FC]",
spotlightColor: "rgba(168,85,247,0.20)",
renderGraphic: (reduce: boolean) => <AIGraphic reduce={reduce} />,
},
{
word: "Merchant First",
icon: "storefront",
line: "Technology should empower neighbourhood businesses.",
badgeStyle: "bg-amber-500/15 border-amber-500/30 text-amber-400",
dotColor: "bg-amber-400",
spotlightColor: "rgba(251,191,36,0.18)",
renderGraphic: (reduce: boolean) => <MerchantGraphic reduce={reduce} />,
},
{
word: "Local",
icon: "near_me",
line: "Every recommendation starts with nearby trusted merchants.",
badgeStyle: "bg-sky-500/15 border-sky-500/30 text-sky-400",
dotColor: "bg-sky-400",
spotlightColor: "rgba(56,189,248,0.18)",
renderGraphic: (reduce: boolean) => <LocalGraphic reduce={reduce} />,
},
];
export function CorePrinciples() {
const reduce = useReducedMotion();
return (
<section
aria-labelledby="core-principles-title"
className="w-full bg-white px-2 py-6 sm:px-4 md:py-8"
>
<div className="mx-auto max-w-[1840px]">
<div className="section-card-dark relative px-6 py-10 text-white md:px-12 md:py-12 lg:px-14 lg:py-14">
{/* Ambient purple wash */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="absolute left-[-8%] top-[-10%] h-[420px] w-[420px] rounded-full bg-[#7C3AED]/18 blur-[130px]" />
<div className="absolute right-[-6%] bottom-[-12%] h-[380px] w-[380px] rounded-full bg-[#A855F7]/12 blur-[120px]" />
</div>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="relative z-10 max-w-[1100px]"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center gap-2 rounded-full border border-white/15 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-white/70"
>
What We Build On
</motion.span>
<motion.h2
id="core-principles-title"
variants={fadeUp}
className="mt-3 text-balance font-display font-black tracking-tight"
style={{ fontSize: "clamp(32px, 3.8vw, 54px)", lineHeight: 1.06 }}
>
Four principles behind{" "}
<span className="text-[#C084FC]">every decision.</span>
</motion.h2>
</motion.div>
{/* Editorial principle blocks with left text and right distinct SVG graphic */}
<motion.div
variants={staggerSlow}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="relative z-10 mt-8 grid gap-5 overflow-hidden rounded-3xl md:mt-10 md:grid-cols-2 lg:gap-7"
>
{PRINCIPLES.map((principle) => (
<motion.div
key={principle.word}
variants={fadeUp}
whileHover={reduce ? undefined : { y: -5, scale: 1.008 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
className="group relative h-full"
>
<Spotlight
className="relative overflow-hidden h-full rounded-3xl border border-white/10 bg-[#150320] p-6 md:p-8 lg:p-9 transition-all duration-300 group-hover:border-white/25 group-hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.5)]"
color={principle.spotlightColor}
size={460}
>
{/* Floating Flying Particle Glow Dots (Motes) - Shown ONLY on Hover */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden z-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100">
{CARD_MOTES.map((mote, idx) => (
<motion.span
key={idx}
className={`absolute rounded-full ${principle.dotColor} shadow-[0_0_8px_currentColor]`}
style={{
left: mote.left,
top: mote.top,
width: mote.size,
height: mote.size,
}}
animate={
reduce
? {}
: {
y: [0, -12, 0],
x: [0, 6, 0],
opacity: [0.3, 0.9, 0.3],
scale: [0.8, 1.2, 0.8],
}
}
transition={{
duration: mote.duration,
delay: mote.delay,
repeat: Infinity,
ease: "easeInOut",
}}
/>
))}
</div>
{/* Corner Hover Glowing Dot */}
<div className="absolute top-5 right-5 z-20 flex items-center gap-2 opacity-50 transition-opacity duration-300 group-hover:opacity-100">
<span className="relative flex h-2.5 w-2.5">
<span className={`absolute inline-flex h-full w-full animate-ping rounded-full ${principle.dotColor} opacity-75`} />
<span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${principle.dotColor} shadow-[0_0_8px_currentColor]`} />
</span>
</div>
{/* Ambient Corner Glow Orb */}
<div
aria-hidden="true"
className={`pointer-events-none absolute -bottom-10 -right-10 h-44 w-44 rounded-full blur-3xl opacity-20 transition-all duration-500 group-hover:scale-150 group-hover:opacity-50 ${principle.dotColor}`}
/>
<div className="relative z-10 flex items-center justify-between gap-6">
{/* Left Column: Badge + Title + Description */}
<div className="flex-1 min-w-0 pr-2">
<span className={`inline-flex h-10 w-10 items-center justify-center rounded-2xl border ${principle.badgeStyle} mb-4 shadow-sm transition-transform duration-300 group-hover:scale-110`}>
<MaterialIcon icon={principle.icon} size={20} />
</span>
<h3 className="font-display font-black text-white text-xl md:text-2xl lg:text-3xl tracking-tight mb-2">
{principle.word}
</h3>
<p className="text-sm md:text-base leading-relaxed text-white/75 max-w-[500px]">
{principle.line}
</p>
</div>
{/* Right Column: Tailored Animated SVG Graphic */}
<div className="shrink-0 flex items-center justify-center self-center">
{principle.renderGraphic(reduce)}
</div>
</div>
</Spotlight>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -2,8 +2,10 @@
import { Fragment, useRef, type MouseEvent } from "react"; import { Fragment, useRef, type MouseEvent } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from "framer-motion"; import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { MagneticButton } from "@/components/ui/MagneticButton";
import { HeroLogo } from "@/components/heroes/HeroLogo"; import { HeroLogo } from "@/components/heroes/HeroLogo";
import { viewport } from "@/lib/animations"; import { viewport } from "@/lib/animations";
@@ -26,7 +28,7 @@ const badgeVariant = {
const lineStagger = { const lineStagger = {
hidden: {}, hidden: {},
visible: { transition: { staggerChildren: 0.15, delayChildren: 1.05 } }, visible: { transition: { staggerChildren: 0.13, delayChildren: 1.0 } },
}; };
const lineReveal = { const lineReveal = {
@@ -36,7 +38,12 @@ const lineReveal = {
const descVariant = { const descVariant = {
hidden: { opacity: 0, y: 30 }, hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.25, ease: EASE } }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.4, ease: EASE } },
};
const ctaVariant = {
hidden: { opacity: 0, y: 22 },
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.6, ease: EASE } },
}; };
const statStagger = { const statStagger = {
@@ -106,49 +113,88 @@ export function Hero() {
transition={{ duration: 1.2, ease: "easeOut" }} transition={{ duration: 1.2, ease: "easeOut" }}
/> />
<div className="page-container relative z-10 flex min-h-[560px] items-center justify-center px-6 pt-20 pb-10 sm:px-8 sm:pt-24 md:min-h-[85vh] md:py-16 lg:py-16"> <div className="page-container relative z-10 flex min-h-[560px] items-center justify-center px-6 pt-20 pb-10 sm:px-8 sm:pt-24 md:min-h-[85vh] md:py-6 lg:py-8">
<div className="relative z-20 flex w-full max-w-[860px] flex-col items-center text-center px-6 py-8 md:px-14 md:py-6 lg:py-8"> {/* `compact` logo spacing: this hero now carries CTAs and the evolution
<HeroLogo /> strip inside it, the same reason BrandsHero uses it. */}
<div className="relative z-20 flex w-full max-w-[860px] flex-col items-center text-center px-6 py-6 md:px-14 md:py-2 lg:py-3">
<HeroLogo spacing="compact" />
{/* Badge */} {/* Badge */}
<motion.span <motion.span
variants={badgeVariant} variants={badgeVariant}
initial="hidden" initial="hidden"
animate="visible" 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-[#6D28D9] shadow-md uppercase mb-6 w-fit" className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6D28D9] shadow-md uppercase mb-5 w-fit"
> >
Welcome to Nearle Welcome to Nearle
</motion.span> </motion.span>
{/* H1: line-by-line reveal */} {/* H1: line-by-line reveal. Two lines, with the closing word
highlighted in the Nearle purple. */}
<motion.h1 <motion.h1
variants={lineStagger} variants={lineStagger}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
className="font-display text-[34px] sm:text-5xl lg:text-[68px] xl:text-[76px] font-extrabold text-white tracking-[-0.05em] [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]" className="font-display font-extrabold text-white tracking-[-0.045em] [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
> >
<div className="overflow-hidden pb-1"> <span className="block overflow-hidden pb-1">
<motion.div variants={lineReveal} className="leading-[0.95]"> <motion.span
variants={lineReveal}
className="block text-[30px] leading-[1.06] text-balance sm:text-[40px] lg:text-[50px] xl:text-[56px]"
>
Everything Local. Everything Local.
</motion.div> </motion.span>
</div> </span>
<div className="overflow-hidden pb-1"> <span className="block overflow-hidden pb-1">
<motion.div variants={lineReveal} className="leading-[0.95]"> <motion.span
variants={lineReveal}
className="block text-[30px] leading-[1.06] text-balance sm:text-[40px] lg:text-[50px] xl:text-[56px]"
>
Everything <span className="text-[#A855F7]">Near.</span> Everything <span className="text-[#A855F7]">Near.</span>
</motion.div> </motion.span>
</div> </span>
</motion.h1> </motion.h1>
{/* Description */} {/* Description */}
<motion.p <motion.div
variants={descVariant} variants={descVariant}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
className="max-w-[520px] text-lg md:text-xl text-white/95 leading-[1.6] font-medium mt-6 [text-shadow:0_2px_16px_rgba(0,0,0,0.6)]" className="mt-5 max-w-[700px] [text-shadow:0_2px_16px_rgba(0,0,0,0.6)]"
> >
Discover verified local stores, exclusive offers, fast delivery, and a better way to shop within your neighbourhood. <p className="text-base md:text-lg text-white/90 leading-[1.6] font-medium">
</motion.p> Discover verified local stores, exclusive offers, fast delivery, and a
better way to shop within your neighbourhood.
</p>
</motion.div>
{/* CTAs */}
<motion.div
variants={ctaVariant}
initial="hidden"
animate="visible"
className="mt-7 flex flex-wrap items-center justify-center gap-3.5"
>
<MagneticButton>
<Link
href="#ai-commerce"
className="flex items-center gap-2 rounded-xl bg-[#6330D6] px-7 py-3.5 text-sm font-black text-white shadow-lg transition-all duration-200 hover:bg-[#5225b8] hover:shadow-xl active:scale-95 sm:px-8"
>
<MaterialIcon icon="auto_awesome" size={18} />
<span>Explore AI Commerce</span>
</Link>
</MagneticButton>
<MagneticButton>
<Link
href="#nearle-in-action"
className="flex items-center gap-2 rounded-xl border border-white/25 bg-white/10 px-7 py-3.5 text-sm font-black text-white backdrop-blur-md transition-all duration-200 hover:bg-white/20 active:scale-95 sm:px-8"
>
<MaterialIcon icon="play_circle" size={18} />
<span>See How Nearle Works</span>
</Link>
</MagneticButton>
</motion.div>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,37 @@
import Image from "next/image"; 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, slideInLeft, viewport } from "@/lib/animations"; import {
import { MerchantJourney } from "@/components/home/merchant/MerchantJourney"; fadeUp,
scaleIn,
staggerContainer,
slideInLeft,
viewport,
} from "@/lib/animations";
const STEPS = [
{
num: "01",
icon: "storefront",
title: "Get Online",
desc: "Introduce your business to your Neighbourhood. Compete with online businesses through your offline store.",
visual: "store",
},
{
num: "02",
icon: "campaign",
title: "Get Noticed",
desc: "Your products get published to Nearle customers in your neighbourhood through the app. Customers search based on location, products, discounts, and promos.",
visual: "phone",
},
{
num: "03",
icon: "auto_awesome",
title: "Get Simplified",
desc: "Delight the community with a personalized online experience. Unlock revenue opportunities and marketing strategies that help your business grow further.",
visual: "bag",
},
];
function OrdersCard() { function OrdersCard() {
return ( return (
@@ -80,9 +109,169 @@ function GrowthPill() {
); );
} }
function PhoneMapVisual() {
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="absolute bottom-1 h-6 w-32 rounded-full bg-brand-dark/20 blur-xl" />
{/* REALISTIC MOBILE DEVICE FRAME */}
<div className="relative aspect-[140/280] h-[95%] min-h-[240px] max-h-[280px] rounded-[24px] bg-[linear-gradient(145deg,#d1d5db_0%,#4b5563_15%,#111827_85%,#4b5563_100%)] p-[2px] shadow-[0_20px_50px_rgba(22,0,29,0.25),0_0_0_1px_rgba(0,0,0,0.1),inset_0_0_2px_rgba(255,255,255,0.4)]">
{/* Hardware Buttons */}
<div className="absolute -left-[2px] top-[18%] h-4 w-[2px] rounded-l bg-gradient-to-r from-[#9ca3af] to-[#374151] shadow-sm" />
<div className="absolute -left-[2px] top-[26%] h-7 w-[2px] rounded-l bg-gradient-to-r from-[#9ca3af] to-[#374151] shadow-sm" />
<div className="absolute -left-[2px] top-[34%] h-7 w-[2px] rounded-l bg-gradient-to-r from-[#9ca3af] to-[#374151] shadow-sm" />
<div className="absolute -right-[2px] top-[28%] h-9 w-[2px] rounded-r bg-gradient-to-l from-[#9ca3af] to-[#374151] shadow-sm" />
{/* Inner Black Bezel */}
<div className="relative h-full w-full overflow-hidden rounded-[22px] bg-black p-[4px] shadow-[inset_0_0_4px_rgba(255,255,255,0.1)]">
{/* Inner Screen */}
<div className="relative h-full w-full overflow-hidden rounded-[18px] bg-[#FAF8FC] shadow-[inset_0_0_8px_rgba(0,0,0,0.05)]">
{/* Dynamic Island */}
<div className="absolute left-1/2 top-1.5 z-50 flex h-[12px] w-[38px] -translate-x-1/2 items-center justify-end rounded-full bg-black px-1 shadow-[inset_0_-1px_1px_rgba(255,255,255,0.1)] ring-[0.5px] ring-white/10">
<div className="relative h-[6px] w-[6px] overflow-hidden rounded-full bg-[#050515] shadow-[inset_0_0_1px_rgba(255,255,255,0.2)]">
<div className="absolute right-0 top-0 h-[3px] w-[3px] rounded-full bg-blue-400/30 blur-[0.5px]" />
</div>
</div>
{/* Status Bar */}
<div className="absolute inset-x-0 top-0 z-40 flex items-center justify-between px-3 pt-2 text-[5px] font-black text-brand-dark">
<span className="tracking-tighter">9:41</span>
<div className="flex items-center gap-[2px] opacity-80">
{/* Signal */}
<div className="flex items-end gap-[0.5px]">
<div className="h-[3px] w-[1px] rounded-full bg-brand-dark" />
<div className="h-[4px] w-[1px] rounded-full bg-brand-dark" />
<div className="h-[5px] w-[1px] rounded-full bg-brand-dark" />
<div className="h-[6px] w-[1px] rounded-full bg-brand-dark/30" />
</div>
{/* Wifi */}
<svg width="6" height="6" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21C11.5 21 11.1 20.9 10.7 20.6C8 18.2 4.4 17.5 1.4 18.3C0.8 18.5 0.2 18.1 0 17.5C-0.2 16.9 0.2 16.3 0.8 16.1C4.8 15 9.4 15.9 12.8 18.9C13.2 19.3 13.3 19.9 12.9 20.3C12.7 20.8 12.3 21 12 21Z" />
<path d="M12 16C11.6 16 11.2 15.9 10.9 15.7C5.3 12.2 0.4 14 0.3 14C-0.3 14.2 -0.8 13.9 -0.9 13.3C-1.1 12.7 -0.8 12.2 -0.2 12.1C0.2 11.9 5.9 9.8 12 13.7C12.5 14 12.6 14.6 12.3 15.1C12.2 15.7 12 16 12 16Z" opacity="0.7"/>
</svg>
{/* Battery */}
<div className="relative flex h-[5px] w-[10px] items-center rounded-[1.5px] border-[1px] border-brand-dark/80 p-[0.5px]">
<div className="h-full w-[80%] rounded-[0.5px] bg-brand-dark" />
<div className="absolute -right-[1.5px] top-1/2 h-[2px] w-[1px] -translate-y-1/2 rounded-r-[0.5px] bg-brand-dark/80" />
</div>
</div>
</div>
{/* Map Visual Content */}
<div className="absolute inset-0 top-3 bg-[linear-gradient(145deg,#FAF8FC,#F1E7F8)]">
<svg className="absolute inset-0 h-full w-full" viewBox="0 0 140 255" fill="none" aria-hidden="true" preserveAspectRatio="xMidYMid slice">
<rect width="140" height="255" fill="#FAF8FC" />
<path d="M-20 50C12 40 41 42 64 54C91 68 112 64 158 48" stroke="#E3D9EA" strokeWidth="8" strokeLinecap="round" />
<path d="M-18 178C24 158 51 164 78 179C102 192 124 192 158 174" stroke="#DDF0E3" strokeWidth="14" strokeLinecap="round" opacity="0.75" />
<path d="M24 -20L35 282" stroke="#E9E2F0" strokeWidth="5" strokeLinecap="round" />
<path d="M83 -24L73 280" stroke="#DED4E8" strokeWidth="5" strokeLinecap="round" />
<path d="M126 -16L112 268" stroke="#E9E2F0" strokeWidth="5" strokeLinecap="round" />
<path d="M-18 72H158" stroke="#E7DFEE" strokeWidth="5" strokeLinecap="round" />
<path d="M-18 122H158" stroke="#DED5E8" strokeWidth="5" strokeLinecap="round" />
<path d="M-18 212H158" stroke="#E9E2F0" strokeWidth="5" strokeLinecap="round" />
<path d="M-12 244L150 28" stroke="#E6DEED" strokeWidth="5" strokeLinecap="round" />
<path d="M6 180C35 155 48 130 70 117C92 104 108 91 125 70" stroke="#7C3AED" strokeWidth="1.8" strokeDasharray="3 5" strokeLinecap="round" opacity="0.55" />
<path d="M70 117C47 103 30 92 14 73" stroke="#006D37" strokeWidth="1.6" strokeDasharray="3 5" strokeLinecap="round" opacity="0.38" />
</svg>
{[
["left-[17%] top-[30%]", "bg-primary"],
["right-[16%] top-[28%]", "bg-[#006D37]"],
["left-[20%] bottom-[33%]", "bg-[#006D37]"],
["right-[18%] bottom-[32%]", "bg-primary"],
["right-[35%] top-[20%]", "bg-primary"],
].map(([position, color]) => (
<div key={position} className={`absolute ${position} z-10`}>
<div className={`relative h-4 w-4 -rotate-45 rounded-[50%_50%_50%_10%] ${color} shadow-[0_4px_8px_rgba(22,0,29,0.12)]`}>
<div className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white" />
</div>
</div>
))}
<div className="absolute left-1/2 top-[38%] z-20 -translate-x-1/2 -translate-y-1/2">
<div className="absolute left-1/2 top-[82%] h-8 w-14 -translate-x-1/2 rounded-full bg-primary/18 blur-sm" />
<div className="relative h-12 w-12 -rotate-45 rounded-[50%_50%_50%_12%] bg-[linear-gradient(135deg,#8B5CF6,#65149A)] shadow-[0_10px_20px_rgba(101,20,154,0.30)]">
<div className="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white" />
</div>
</div>
<div className="absolute bottom-2 left-2 right-2 z-30 rounded-xl bg-white/94 p-2 shadow-[0_8px_20px_rgba(22,0,29,0.12)] backdrop-blur">
<p className="truncate text-[8.5px] font-black leading-none text-brand-dark">Your Store</p>
<div className="mt-1 flex flex-wrap items-center gap-x-1 gap-y-0.5 text-[6.5px] font-bold text-on-surface-variant">
<span className="inline-flex items-center gap-[1.5px]">
<MaterialIcon icon="star" size={8} fill className="text-amber-500" />
4.8
</span>
<span className="font-black text-[#006D37]">Open</span>
<span>0.8 km</span>
</div>
</div>
</div>
</div>
</div>
<div className="absolute -left-7 top-[29%] z-40 rounded-full bg-white px-2.5 py-1 text-[8px] font-black text-[#006D37] shadow-[0_8px_16px_rgba(22,0,29,0.12)] ring-1 ring-[#006D37]/10">
Offers
</div>
<div className="absolute -right-6 top-[43%] z-40 rounded-full bg-primary px-2.5 py-1 text-[8px] font-black text-white shadow-[0_8px_16px_rgba(101,20,154,0.20)]">
Shop
</div>
</div>
</div>
);
}
function StepVisual({ type }: { type: string }) {
if (type === "store") {
return (
<div className="absolute inset-x-4 bottom-0 top-2">
<div className="relative h-full w-full">
<Image
src="/images/nearle-how/how-storefront.webp"
alt="3D local shop storefront with purple awning and now online board"
fill
sizes="(max-width: 768px) 80vw, 28vw"
className="object-contain object-bottom"
/>
</div>
</div>
);
}
if (type === "phone") {
return (
<div className="flex h-full items-center justify-center">
<PhoneMapVisual />
</div>
);
}
return (
<div className="flex h-full items-end justify-end gap-2 px-2">
<div className="mb-4 hidden shrink-0 space-y-2 sm:block">
{["Offers & Discounts", "Loyal Customers", "Repeat Orders"].map((chip) => (
<div key={chip} className="rounded-full border border-primary/10 bg-white px-3 py-2 text-[11px] font-black text-brand-dark shadow-sm">
{chip}
</div>
))}
</div>
<div className="relative h-40 w-36 shrink-0">
<Image
src="/images/nearle-how/how-grocery-bag.webp"
alt="Nearle grocery bag filled with vegetables and packaged products"
fill
sizes="(max-width: 768px) 60vw, 18vw"
className="object-contain object-bottom"
/>
</div>
</div>
);
}
export function HowItWorks() { export function HowItWorks() {
return ( return (
<>
<section className="section bg-white w-full overflow-hidden"> <section className="section bg-white w-full overflow-hidden">
{/* HERO AREA */} {/* HERO AREA */}
<div className="mx-auto w-[calc(100%-96px)] max-w-[1720px]"> <div className="mx-auto w-[calc(100%-96px)] max-w-[1720px]">
@@ -147,32 +336,62 @@ export function HowItWorks() {
<div className="mx-auto mt-10 w-[calc(100%-96px)] max-w-[1720px] md:mt-14 xl:mt-10"> <div className="mx-auto mt-10 w-[calc(100%-96px)] max-w-[1720px] md:mt-14 xl:mt-10">
<div className="h-px w-full bg-gradient-to-r from-transparent via-primary/10 to-transparent" /> <div className="h-px w-full bg-gradient-to-r from-transparent via-primary/10 to-transparent" />
</div> </div>
</section>
{/* MERCHANT JOURNEY — self-contained cinematic showcase (auto-plays in {/* CARDS SECTION */}
place; no sticky pin, so no empty scroll-track). */} <div className="mx-auto w-full max-w-[2200px] px-6 lg:px-10 xl:px-12">
<section className="bg-white pb-20 pt-12 md:pb-28 md:pt-16">
<motion.div <motion.div
className="mx-auto max-w-3xl px-6 text-center" className="mt-10 text-center md:mt-14 xl:mt-10"
variants={fadeUp} variants={fadeUp}
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
viewport={viewport} viewport={viewport}
> >
<span className="inline-flex rounded-full bg-primary/8 px-4 py-2 text-xs font-black uppercase tracking-widest text-primary"> <span className="inline-flex rounded-full bg-primary/8 px-4 py-2 text-xs font-black uppercase tracking-widest text-primary">
Merchant Journey How It Works
</span> </span>
<h2 className="mt-5 font-display text-4xl font-black leading-tight tracking-tight text-brand-dark md:text-5xl lg:text-[54px]"> <h2 className="mt-5 font-display text-4xl font-black leading-tight tracking-tight text-brand-dark md:text-5xl lg:text-[54px]">
From your shelf to a growing business Build Your Neighbourhood Brand
</h2> </h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-relaxed text-on-surface-variant md:text-lg"> <p className="mx-auto mt-4 max-w-2xl text-base leading-relaxed text-on-surface-variant md:text-lg">
Eight steps, powered by AI. Join once, and Nearle turns your shelf into a Three simple milestones to scale your retail reach from offline streets straight into neighbourhood homes.
discoverable storefront while you stay at the centre of every order.
</p> </p>
</motion.div> </motion.div>
<MerchantJourney /> <motion.div
</section> className="mt-10 grid min-w-0 grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
</> variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
>
{STEPS.map((step) => (
<motion.article
key={step.num}
variants={fadeUp}
whileHover={{ y: -6, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
className="relative flex min-h-[380px] min-w-0 flex-col overflow-hidden rounded-3xl border border-primary/10 bg-white p-7 shadow-[0_18px_45px_rgba(22,0,29,0.08)] hover:shadow-[0_24px_60px_rgba(22,0,29,0.14)] hover:border-primary/20 transition-[box-shadow,border-color] duration-[250ms] md:min-h-[420px] lg:min-h-[460px] xl:min-h-[420px] lg:p-8"
>
<span className="absolute right-5 top-4 font-display text-6xl font-black leading-none text-primary/[0.07]">
{step.num}
</span>
<div className="relative z-10 flex h-13 w-13 items-center justify-center rounded-2xl bg-primary/7 text-primary">
<MaterialIcon icon={step.icon} size={28} />
</div>
<div className="relative z-10 mt-7 max-w-[22rem] lg:mt-8">
<p className="font-display text-2xl font-black text-brand-dark md:text-[28px]">
{step.title}
</p>
<p className="mt-3 text-sm leading-relaxed text-on-surface-variant md:text-base">
{step.desc}
</p>
</div>
<div className={`relative z-0 mt-auto w-full overflow-hidden rounded-[1.5rem] bg-brand-accent-light/60 ${step.visual === "phone" ? "h-64 lg:h-72" : "h-48 lg:h-52"}`}>
<StepVisual type={step.visual} />
</div>
</motion.article>
))}
</motion.div>
</div>
</section>
); );
} }

View File

@@ -1,7 +1,8 @@
"use client"; "use client";
import { useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { PhoneStage } from "@/components/home/voice/PhoneStage"; import { PhoneStage } from "@/components/home/voice/PhoneStage";
import { FloatingRequestCards } from "@/components/home/voice/FloatingRequestCards"; import { FloatingRequestCards } from "@/components/home/voice/FloatingRequestCards";
@@ -11,171 +12,183 @@ import { viewport } from "@/lib/animations";
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 EASE_OUT = [0.16, 1, 0.3, 1] as [number, number, number, number]; const EASE_OUT = [0.16, 1, 0.3, 1] as [number, number, number, number];
const FEATURE_CHIPS = [ const headStagger = {
{ emoji: "🎤", label: "Voice Shopping" },
{ emoji: "🧠", label: "Natural Language AI" },
{ emoji: "🏪", label: "Local Merchant Discovery" },
{ emoji: "⚡", label: "Real-Time Availability" },
];
const badgeVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
};
const lineStagger = {
hidden: {}, hidden: {},
visible: { transition: { staggerChildren: 0.14, delayChildren: 0.12 } }, visible: { transition: { staggerChildren: 0.14 } },
}; };
const lineReveal = { const lineReveal = {
hidden: { y: 48, opacity: 0, filter: "blur(8px)" }, hidden: { y: 44, opacity: 0, filter: "blur(10px)" },
visible: { y: 0, opacity: 1, filter: "blur(0px)", transition: { duration: 0.85, ease: EASE_OUT } }, visible: { y: 0, opacity: 1, filter: "blur(0px)", transition: { duration: 0.9, ease: EASE_OUT } },
}; };
const descVariant = { const descVariant = {
hidden: { opacity: 0, y: 22 }, hidden: { opacity: 0, y: 20, filter: "blur(8px)" },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.42, ease: EASE } }, visible: {
}; opacity: 1,
y: 0,
const chipStagger = { filter: "blur(0px)",
hidden: {}, transition: { duration: 0.95, delay: 0.34, ease: EASE_OUT },
visible: { transition: { staggerChildren: 0.07, delayChildren: 0.56 } }, },
};
const chipItem = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
const ctaVariant = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.86, ease: EASE } },
}; };
const phoneVariant = { const phoneVariant = {
hidden: { opacity: 0, y: 40, scale: 0.96 }, hidden: { opacity: 0, y: 56, scale: 0.955 },
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 1.05, delay: 0.15, ease: EASE_OUT } }, visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 1.15, delay: 0.46, ease: EASE_OUT },
},
}; };
// The homepage's product showcase: a live Nearle app running inside the phone, const captionVariant = {
// so a visitor can watch a spoken request turn into a delivered order without hidden: { opacity: 0, y: 14 },
// installing anything. The Hero above introduces Nearle — this shows the work. visible: { opacity: 1, y: 0, transition: { duration: 0.8, delay: 1.05, ease: EASE } },
};
const glowVariant = {
hidden: { opacity: 0, scale: 0.88 },
visible: { opacity: 1, scale: 1, transition: { duration: 1.7, ease: EASE } },
};
// The homepage's product moment: one large floating device, centred, running a
// real Nearle request end to end — spoken words, AI reasoning, a nearby
// merchant, the order, the rider, the doorstep. The copy above it is
// deliberately two lines; everything on this canvas exists to hold the eye on
// the phone.
export function VoiceCommerceExperience() { export function VoiceCommerceExperience() {
const sectionRef = useRef<HTMLElement>(null);
const reduced = useReducedMotion();
// Soft parallax: the device drifts against the page as the section passes,
// which reads as depth rather than as movement.
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const phoneY = useTransform(scrollYProgress, [0, 1], [34, -34]);
const auraY = useTransform(scrollYProgress, [0, 1], [-26, 26]);
return ( return (
// Explicit padding rather than the shared .section rhythm: the cards that
// float above and below the phone need more room than 88px to breathe.
<section <section
className="relative w-full overflow-hidden py-24 lg:py-32" ref={sectionRef}
style={{ id="nearle-in-action"
background: aria-labelledby="nearle-in-action-title"
"radial-gradient(60% 50% at 78% 32%, rgba(124,58,237,0.13), transparent 70%)," + className="relative w-full overflow-hidden border-b border-[#F1EAF8] bg-white pb-20 pt-24 lg:pb-24 lg:pt-28"
"radial-gradient(45% 42% at 12% 22%, rgba(168,85,247,0.10), transparent 70%)," +
"linear-gradient(180deg, #FDFBFF 0%, #F7F3FF 55%, #FAF7FF 100%)",
}}
> >
{/* Voice waves sit behind everything, centred on the phone side */} {/* ─── Ambient canvas ─── */}
<VoiceWaves className="left-auto right-0 z-0 hidden w-[62%] lg:block" /> <motion.div
<VoiceWaves className="z-0 lg:hidden" /> aria-hidden="true"
initial="hidden"
whileInView="visible"
viewport={viewport}
className="pointer-events-none absolute inset-0 z-0"
>
{/* the bloom the device stands in */}
<motion.div
variants={glowVariant}
style={reduced ? undefined : { y: auraY }}
className="absolute left-1/2 top-[46%] h-[720px] w-[min(92%,980px)] -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-[#A855F7]/[0.10] blur-[150px]"
/>
{/* two quiet mesh blobs, kept in the outer margins */}
<motion.div variants={glowVariant} className="absolute inset-0">
<div className="ai-orb-a absolute left-[3%] top-[30%] h-[340px] w-[340px] rounded-full bg-[#C084FC]/[0.09] blur-[120px]" />
<div className="ai-orb-b absolute right-[2%] top-[48%] h-[400px] w-[400px] rounded-full bg-[#F0ABFC]/[0.08] blur-[130px]" />
</motion.div>
</motion.div>
<div className="page-container relative z-10"> {/* Voice waves, centred on the device rather than pushed to one side */}
<div className="grid items-center gap-16 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] lg:gap-8 xl:gap-12"> <VoiceWaves className="z-0" />
{/* ─── LEFT: what it does ─── */}
<motion.div <div className="page-container relative z-10 flex flex-col items-center">
initial="hidden" {/* ─── Heading + one supporting line ─── */}
whileInView="visible" <motion.div
viewport={viewport} initial="hidden"
className="order-1 flex flex-col items-center text-center lg:items-start lg:text-left" whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[820px] text-center"
>
<motion.h2
id="nearle-in-action-title"
variants={headStagger}
className="font-display font-black tracking-[-0.048em] text-[#16001D]"
style={{ fontSize: "clamp(36px, 4.6vw, 62px)", lineHeight: 1.04 }}
> >
<motion.span <span className="block overflow-hidden pb-1">
variants={badgeVariant} <motion.span variants={lineReveal} className="block">
className="inline-flex w-fit items-center gap-1.5 rounded-full border border-[#7C3AED]/15 bg-white px-4 py-1.5 text-xs font-black uppercase tracking-widest text-[#6D28D9] shadow-[0_4px_18px_-6px_rgba(124,58,237,0.35)]" Experience Nearle
> </motion.span>
<MaterialIcon icon="graphic_eq" size={14} /> </span>
AI Voice Commerce <span className="block overflow-hidden pb-1">
</motion.span> <motion.span variants={lineReveal} className="block text-[#7C3AED]">
in Action
</motion.span>
</span>
</motion.h2>
<motion.h2 <motion.p
variants={lineStagger} variants={descVariant}
className="mt-6 font-display text-[32px] font-extrabold tracking-[-0.05em] text-[#16001D] sm:text-[42px] lg:text-[42px] xl:text-[52px]" className="mx-auto mt-6 max-w-[600px] text-base font-medium leading-[1.7] text-[#4A3F52] md:text-[19px]"
>
<div className="overflow-hidden pb-1">
<motion.div variants={lineReveal} className="leading-[0.98]">
Just speak.
</motion.div>
</div>
<div className="overflow-hidden pb-1">
{/* text-balance keeps the mobile wrap as "Nearle handles /
the rest." instead of orphaning "rest." on its own line */}
<motion.div variants={lineReveal} className="text-balance leading-[0.98]">
Nearle handles <span className="text-[#7C3AED]">the rest.</span>
</motion.div>
</div>
</motion.h2>
<motion.p
variants={descVariant}
className="mt-5 max-w-[520px] text-base font-medium leading-[1.6] text-[#4A3F52] md:text-lg"
>
Order products, discover nearby stores, and book trusted local services using
natural conversation.
</motion.p>
<motion.ul
variants={chipStagger}
className="mt-7 flex flex-wrap justify-center gap-2.5 lg:justify-start"
>
{FEATURE_CHIPS.map((chip) => (
<motion.li
key={chip.label}
variants={chipItem}
className="flex items-center gap-2 rounded-full border border-[#7C3AED]/12 bg-white/70 py-2 pl-2.5 pr-4 shadow-[0_6px_20px_-10px_rgba(76,20,140,0.35)] backdrop-blur-md"
>
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#7C3AED]/[0.08] text-[13px]">
{chip.emoji}
</span>
<span className="text-[13px] font-bold text-[#2C1A38]">{chip.label}</span>
</motion.li>
))}
</motion.ul>
<motion.div variants={ctaVariant} className="mt-8">
<Link
href="/platform"
className="group inline-flex items-center gap-2.5 rounded-full bg-[#6330D6] px-8 py-4 text-sm font-black uppercase tracking-wider text-white shadow-[0_14px_38px_-12px_rgba(99,48,214,0.75)] transition-all duration-200 hover:-translate-y-0.5 hover:bg-[#5225b8] hover:shadow-[0_18px_44px_-12px_rgba(99,48,214,0.85)] active:translate-y-0 active:scale-[0.98]"
>
<MaterialIcon icon="mic" size={18} />
Try Voice Commerce
<MaterialIcon
icon="arrow_forward"
size={17}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</motion.div>
{/* ─── RIGHT: the live phone ─── */}
<motion.div
variants={phoneVariant}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="relative order-2 flex justify-center lg:justify-end"
> >
{/* Width-driven at every breakpoint — the 476/941 aspect ratio Say what you need in your own words. Nearle finds it at a shop around the corner, places
derives the height, so the phone never collapses. */} the order, and tracks it to your door.
<div className="relative w-[min(72vw,300px)] lg:w-[min(38vh,400px)]"> </motion.p>
{/* soft purple glow anchoring the device */} </motion.div>
<div
aria-hidden="true" {/* ─── The device ─── */}
className="pointer-events-none absolute left-1/2 top-1/2 -z-10 h-[86%] w-[150%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#A855F7]/16 blur-[80px]" <motion.div
/> variants={phoneVariant}
<PhoneStage className="w-full" /> initial="hidden"
<FloatingRequestCards /> whileInView="visible"
</div> viewport={viewport}
</motion.div> style={reduced ? undefined : { y: phoneY }}
</div> className="relative mt-14 flex w-full justify-center lg:mt-16"
>
{/* Width-driven at every breakpoint — the 476/941 aspect ratio derives
the height, so the device never collapses. Height-relative on
desktop so the taller phone still fits a short laptop. */}
<div className="relative w-[min(80vw,352px)] lg:w-[min(46vh,470px)]">
{/* tight glow anchoring the device to the canvas */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 -z-10 h-[88%] w-[165%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#A855F7]/[0.14] blur-[110px]"
/>
<PhoneStage className="w-full" />
{/* centring the device frees both gutters, so every request card
now has room to drift without ever covering the screen */}
<FloatingRequestCards />
</div>
</motion.div>
{/* ─── One quiet line under the device ─── */}
<motion.div
variants={captionVariant}
initial="hidden"
whileInView="visible"
viewport={viewport}
// Below lg the request cards sit above and below the device instead
// of in the gutters, so the caption needs real clearance there.
className="mt-16 flex flex-col items-center gap-4 lg:mt-6"
>
<p className="flex items-center gap-2 text-[11px] font-black uppercase tracking-[0.18em] text-[#6D28D9]/70">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full rounded-full bg-[#7C3AED] opacity-60" />
</span>
Live AI Commerce demo
</p>
<Link
href="/platform"
className="group inline-flex items-center gap-2 text-sm font-bold text-[#4A3F52] transition-colors duration-200 hover:text-[#6330D6]"
>
See how the platform works
<MaterialIcon
icon="arrow_forward"
size={16}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</div> </div>
</section> </section>
); );

View File

@@ -1,46 +1,32 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion, useReducedMotion } from "framer-motion"; import { motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations"; import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
const CARDS = [ const CARDS = [
{ {
title: "Understands your intent", title: "Neighbourhood Trust",
desc: "Reads natural language and knows what you actually need.", desc: "Every order is fulfilled by a real local shop your street already knows — not an anonymous warehouse.",
icon: "psychology", icon: "handshake",
color: "bg-primary/5 text-primary border-primary/10", color: "bg-primary/5 text-primary border-primary/10",
}, },
{ {
title: "Finds matching products", title: "AI-Powered Catalog",
desc: "Maps your intent to real items on your stores shelves.", desc: "ProductLLM turns a shelf into a live, shoppable catalog in minutes — so any store can go online without the busywork.",
icon: "inventory_2", icon: "auto_awesome",
color: "bg-secondary/5 text-secondary border-secondary/10", color: "bg-secondary/5 text-secondary border-secondary/10",
}, },
{ {
title: "Recommends alternatives", title: "Offline-First Commerce",
desc: "Out of stock? It suggests the closest match in the store.", desc: "Billing and inventory keep working even when the network drops. Built for how neighbourhood stores really run.",
icon: "autorenew", icon: "wifi_off",
color: "bg-[#de96f9]/10 text-primary border-[#de96f9]/20", color: "bg-[#de96f9]/10 text-primary border-[#de96f9]/20",
}, },
{
title: "Checks availability",
desc: "Confirms whats in stock before you place the order.",
icon: "storefront",
color: "bg-secondary/5 text-secondary border-secondary/10",
},
{
title: "Helps you shop faster",
desc: "Fewer taps — everyday essentials in just seconds.",
icon: "bolt",
color: "bg-primary/5 text-primary border-primary/10",
},
]; ];
export function WhyAICommerceWins() { export function WhyAICommerceWins() {
const reduce = useReducedMotion();
return ( return (
<section className="section bg-white w-full overflow-hidden"> <section className="section bg-white w-full overflow-hidden">
<div className="page-container relative"> <div className="page-container relative">
@@ -52,43 +38,39 @@ export function WhyAICommerceWins() {
viewport={viewport} viewport={viewport}
> >
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/5 px-3.5 py-1.5 rounded-full"> <span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/5 px-3.5 py-1.5 rounded-full">
The Assistant AI Commerce
</span> </span>
<h2 className="text-4xl md:text-5xl lg:text-[54px] font-display font-black text-brand-dark mt-3 leading-tight tracking-tight"> <h2 className="text-4xl md:text-5xl lg:text-[54px] font-display font-black text-brand-dark mt-3 leading-tight tracking-tight">
What Nearle AI <span className="text-primary font-normal">Does</span> Why AI Commerce <span className="text-primary font-normal">Wins</span>
</h2> </h2>
<p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed"> <p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed">
The store stays the hero. Nearle AI is the quiet assistant that helps you Nearle isn&rsquo;t another delivery app. It&rsquo;s the AI layer that brings
find what you need inside it faster. neighbourhood stores online without replacing them.
</p> </p>
</motion.div> </motion.div>
<motion.div <motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 lg:gap-5" className="grid grid-cols-1 md:grid-cols-3 gap-5 lg:gap-7"
variants={staggerContainer} variants={staggerContainer}
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
viewport={viewport} viewport={viewport}
> >
{CARDS.map((card, i) => ( {CARDS.map((card) => (
<motion.div <motion.div
key={card.title} key={card.title}
variants={fadeIn} variants={fadeIn}
className="premium-card bg-white rounded-3xl p-5 md:p-6 border border-outline-variant/15 shadow-lg flex flex-col h-full" 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] } }} 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 } }} whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
> >
<motion.div <div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
className={`w-[52px] h-[52px] rounded-2xl flex items-center justify-center border mb-4 shrink-0 ${card.color}`} <MaterialIcon icon={card.icon} size={27} />
animate={reduce ? {} : { scale: [1, 1.08, 1] }} </div>
transition={{ repeat: Infinity, duration: 2.8, ease: "easeInOut", delay: i * 0.25 }} <p className="font-display text-xl font-black text-brand-dark mb-3">
>
<MaterialIcon icon={card.icon} size={25} />
</motion.div>
<p className="font-display text-lg font-black text-brand-dark mb-2 leading-tight">
{card.title} {card.title}
</p> </p>
<p className="text-sm text-on-surface-variant leading-relaxed"> <p className="text-sm md:text-base text-on-surface-variant leading-relaxed">
{card.desc} {card.desc}
</p> </p>
</motion.div> </motion.div>

View File

@@ -0,0 +1,174 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { Spotlight } from "@/components/ui/Spotlight";
import {
fadeUp,
slideInLeftSubtle,
slideInRight,
staggerContainer,
staggerCards,
viewport,
} from "@/lib/animations";
const TRADITIONAL = [
{ icon: "hub", text: "Platform owns discovery" },
{ icon: "visibility_off", text: "Merchant becomes invisible" },
{ icon: "search", text: "Endless searching" },
{ icon: "groups", text: "Customer belongs to platform" },
];
const NEARLE = [
{ icon: "psychology", text: "AI understands intent" },
{ icon: "storefront", text: "Merchant stays at the center" },
{ icon: "near_me", text: "Customers discover nearby stores naturally" },
{ icon: "handshake", text: "Technology empowers merchants instead of replacing them" },
];
export function WhyNearleDifferent() {
const reduce = useReducedMotion();
return (
<section
aria-labelledby="why-different-title"
className="section section-soft w-full overflow-hidden border-y border-[#6330D6]/10"
>
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto max-w-[760px] text-center"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1.5 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
The Difference
</motion.span>
<motion.h2
id="why-different-title"
variants={fadeUp}
className="mt-4 font-display font-black tracking-tight text-[#16001D]"
style={{ fontSize: "clamp(32px, 4vw, 54px)", lineHeight: 1.08 }}
>
Why Nearle Is Different
</motion.h2>
<motion.p
variants={fadeUp}
className="mx-auto mt-4 max-w-[600px] text-[17px] leading-[1.7] text-[#5E5866]"
>
Two ways to bring shopping online. One puts the platform in the middle the other keeps
the shop you already trust there.
</motion.p>
</motion.div>
<div className="mt-14 grid gap-5 lg:grid-cols-2 lg:gap-6">
{/* Traditional commerce */}
<motion.div
variants={slideInLeftSubtle}
initial="hidden"
whileInView="visible"
viewport={viewport}
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
>
<div className="flex h-full flex-col rounded-[32px] border border-[#16001D]/8 bg-white p-7 shadow-[0_10px_30px_rgba(22,0,29,0.05)] md:p-9">
<div className="flex items-center gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#F1EEF5] text-[#6D6172]">
<MaterialIcon icon="apartment" size={22} />
</span>
<div>
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#9A8FA6]">
Today
</p>
<h3 className="font-display text-xl font-black tracking-tight text-[#16001D]">
Traditional Commerce
</h3>
</div>
</div>
<motion.ul
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-7 space-y-3"
>
{TRADITIONAL.map((item) => (
<motion.li
key={item.text}
variants={fadeUp}
className="flex items-center gap-3 rounded-2xl border border-[#16001D]/6 bg-[#FAF9FB] p-3.5"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white text-[#9A8FA6] shadow-sm">
<MaterialIcon icon={item.icon} size={18} />
</span>
<span className="text-[15px] font-medium leading-snug text-[#5E5866]">
{item.text}
</span>
</motion.li>
))}
</motion.ul>
</div>
</motion.div>
{/* Nearle AI Commerce — highlighted */}
<motion.div
variants={slideInRight}
initial="hidden"
whileInView="visible"
viewport={viewport}
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
>
<Spotlight
className="flex h-full flex-col overflow-hidden rounded-[32px] border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-7 shadow-[0_16px_44px_-16px_rgba(99,48,214,0.28)] md:p-9"
color="rgba(168,85,247,0.14)"
size={420}
>
<div className="flex items-center gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#7C3AED]/10 text-[#7C3AED] ring-1 ring-[#7C3AED]/15">
<MaterialIcon icon="auto_awesome" size={22} />
</span>
<div>
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#7C3AED]">
Nearle
</p>
<h3 className="font-display text-xl font-black tracking-tight text-[#16001D]">
Nearle AI Commerce
</h3>
</div>
</div>
<motion.ul
variants={staggerCards}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mt-7 space-y-3"
>
{NEARLE.map((item) => (
<motion.li
key={item.text}
variants={fadeUp}
className="flex items-center gap-3 rounded-2xl border border-[#7C3AED]/12 bg-white/75 p-3.5 shadow-sm backdrop-blur-sm transition-colors duration-200 hover:border-[#7C3AED]/28"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#7C3AED]/10 text-[#7C3AED]">
<MaterialIcon icon={item.icon} size={18} />
</span>
<span className="text-[15px] font-bold leading-snug text-[#16001D]">
{item.text}
</span>
</motion.li>
))}
</motion.ul>
</Spotlight>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -1,314 +0,0 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { AIThinking } from "./AIThinking";
import {
AI_STEPS,
FULFILMENT_STEPS,
type JourneyStage,
type StoreJourney,
} from "@/hooks/useStoreJourney";
const screenEase = [0.22, 1, 0.36, 1] as [number, number, number, number];
// Slow, smooth screen changes (Apple/Linear-style) instead of snappy cuts.
const screenMotion = {
initial: { opacity: 0, y: 14, scale: 0.985 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -12, scale: 0.99 },
transition: { duration: 0.65, ease: screenEase },
};
// Which visual screen a stage maps to (enter-store + search share one screen so
// the transition into typing stays seamless; fulfilment + done share the flow).
function screenKey(stage: JourneyStage): string {
if (stage === "enter-store" || stage === "search") return "storefront";
if (stage === "done") return "fulfilment";
return stage;
}
// The device frame whose inner screen swaps as the journey advances.
export function StoreCanvas({ journey }: { journey: StoreJourney }) {
const {
stage,
stores,
store,
intent,
recommendation,
typed,
thinkingStep,
fulfilStep,
reduced,
selectStore,
} = journey;
return (
<div className="relative mx-auto flex w-full max-w-[320px] items-center justify-center">
{/* Ambient brand glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 scale-110 rounded-full bg-gradient-to-tr from-primary/20 to-[#a855f7]/20 blur-3xl"
/>
<motion.div
animate={reduced ? undefined : { y: [-6, 6, -6] }}
transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }}
className="relative z-10 aspect-[9/18.6] w-full select-none overflow-hidden rounded-[42px] border-[11px] border-brand-dark bg-brand-dark shadow-[0_28px_60px_rgba(22,0,29,0.22)]"
>
{/* Dynamic island */}
<div className="absolute left-1/2 top-3 z-30 flex h-5 w-24 -translate-x-1/2 items-center justify-end rounded-full bg-brand-dark px-3">
<span className="mr-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" />
<span className="h-1 w-1 rounded-full bg-blue-500/60" />
</div>
{/* Screen */}
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-[30px] bg-[#FBF8FE] text-brand-dark">
<AnimatePresence mode="wait">
{screenKey(stage) === "choose-store" && (
<motion.div key="choose-store" {...screenMotion} className="flex h-full flex-col">
<div className="bg-primary px-4 pb-4 pt-8 text-white">
<div className="flex items-center gap-1.5 text-[10px] font-bold opacity-90">
<MaterialIcon icon="location_on" size={12} className="shrink-0 text-white" />
<span className="truncate">R.S. Puram, Coimbatore</span>
</div>
<p className="mt-2 font-display text-base font-black tracking-tight">My Stores</p>
<p className="text-[10px] font-semibold opacity-80">Trusted shops you&rsquo;ve added</p>
</div>
<div className="flex-1 space-y-2 overflow-hidden px-3.5 pt-3.5">
{stores.map((s) => {
const active = s.id === store.id;
return (
<button
key={s.id}
type="button"
onClick={() => selectStore(s.id)}
className={`flex w-full items-center gap-2.5 rounded-2xl border bg-white px-3 py-2.5 text-left transition-all ${
active
? "border-primary/40 shadow-[0_0_0_3px_rgba(101,20,154,0.10)]"
: "border-primary/10"
}`}
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<MaterialIcon icon={s.icon} size={16} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-[12px] font-black text-brand-dark">
{s.name}
</span>
<span className="block truncate text-[9px] font-semibold text-on-surface-variant">
{s.area}
</span>
</span>
<MaterialIcon
icon={active ? "check_circle" : "chevron_right"}
size={16}
className={active ? "text-primary" : "text-on-surface-variant/50"}
/>
</button>
);
})}
</div>
</motion.div>
)}
{screenKey(stage) === "storefront" && (
<motion.div key="storefront" {...screenMotion} className="flex h-full flex-col">
<StoreHeader name={store.name} area={store.area} />
{/* In-store search */}
<div className="px-4 py-3">
<div className="flex items-center gap-2 rounded-full border border-primary/15 bg-white px-3.5 py-2 shadow-sm">
<MaterialIcon icon="search" size={14} className="shrink-0 text-primary/60" />
{typed ? (
<span className="truncate text-[11px] font-semibold text-brand-dark">{typed}</span>
) : (
<span className="truncate text-[11px] text-on-surface-variant/70">
Search inside this store
</span>
)}
{stage === "search" && !reduced && (
<motion.span
className="ml-0.5 h-3 w-[1.5px] bg-primary"
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 0.9, repeat: Infinity }}
/>
)}
</div>
</div>
{/* Categories */}
<div className="px-4">
<p className="mb-2 text-[9px] font-black uppercase tracking-widest text-on-surface-variant/70">
Shop by aisle
</p>
<div className="grid grid-cols-3 gap-2">
{STORE_CATEGORIES.map((c) => (
<div
key={c.name}
className="flex flex-col items-center gap-1 rounded-2xl border border-primary/8 bg-white px-1 py-2.5"
>
<span className="flex h-7 w-7 items-center justify-center rounded-xl bg-primary/8 text-primary">
<MaterialIcon icon={c.icon} size={15} />
</span>
<span className="text-[8px] font-bold text-brand-dark">{c.name}</span>
</div>
))}
</div>
</div>
</motion.div>
)}
{screenKey(stage) === "thinking" && (
<motion.div key="thinking" {...screenMotion} className="flex h-full flex-col">
<StoreHeader name={store.name} area={store.area} />
<div className="flex flex-1 flex-col justify-center px-4">
<div className="rounded-2xl border border-primary/10 bg-white px-3.5 py-4 shadow-sm">
<p className="mb-2.5 flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-primary">
<MaterialIcon icon="auto_awesome" size={11} />
Nearle AI
</p>
<AIThinking steps={AI_STEPS} activeStep={thinkingStep} compact />
</div>
</div>
</motion.div>
)}
{screenKey(stage) === "results" && recommendation && (
<motion.div key="results" {...screenMotion} className="flex h-full flex-col">
<StoreHeader name={store.name} area={store.area} />
<div className="flex-1 overflow-hidden px-4 pt-3">
<p className="mb-2 flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-primary">
<MaterialIcon icon="auto_awesome" size={11} />
Recommended · {recommendation.intent.bundleName}
</p>
<div className="space-y-1.5">
{recommendation.products.map((p, i) => (
<motion.div
key={p.id}
initial={reduced ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: reduced ? 0 : 0.15 + i * 0.22, duration: 0.5, ease: screenEase }}
className="flex items-center gap-2.5 rounded-xl border border-primary/8 bg-white px-2.5 py-2 shadow-sm"
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary/[0.06] text-base">
{p.emoji}
</span>
<span className="min-w-0 flex-1 truncate text-[11px] font-black text-brand-dark">
{p.name}
</span>
<span className="shrink-0 text-[11px] font-black text-primary">{p.price}</span>
</motion.div>
))}
</div>
<div className="mt-2.5 flex items-center gap-1.5 rounded-xl bg-primary/[0.06] px-2.5 py-2 text-[9px] font-bold text-primary">
<MaterialIcon icon="storefront" size={12} />
<span className="truncate">Available in {store.name}</span>
</div>
</div>
</motion.div>
)}
{screenKey(stage) === "cart" && recommendation && (
<motion.div key="cart" {...screenMotion} className="flex h-full flex-col">
<StoreHeader name={store.name} area={store.area} />
<div className="flex flex-1 flex-col justify-center px-4">
<div className="rounded-2xl border border-primary/10 bg-white p-4 shadow-sm">
<p className="font-display text-sm font-black text-brand-dark">
{recommendation.intent.bundleName}
</p>
<p className="text-[10px] font-semibold text-on-surface-variant">
{recommendation.itemCount} items · {store.name}
</p>
<div className="my-3 h-px w-full bg-primary/8" />
<div className="flex items-end justify-between">
<span className="text-[10px] font-bold text-on-surface-variant">Total</span>
<span className="font-display text-xl font-black text-brand-dark">
{recommendation.total}
</span>
</div>
<motion.div
whileTap={reduced ? undefined : { scale: 0.97 }}
className="mt-3 flex items-center justify-center gap-1.5 rounded-full bg-primary py-2.5 text-[11px] font-black text-white"
>
<MaterialIcon icon="bolt" size={13} />
Place Order
</motion.div>
</div>
</div>
</motion.div>
)}
{screenKey(stage) === "fulfilment" && (
<motion.div key="fulfilment" {...screenMotion} className="flex h-full flex-col">
<StoreHeader name={store.name} area={store.area} />
<div className="flex flex-1 flex-col justify-center px-4">
<p className="mb-3 flex items-center gap-1.5 text-[9px] font-black uppercase tracking-widest text-secondary">
<MaterialIcon icon="local_shipping" size={12} />
Order on the way
</p>
<div className="space-y-2.5">
{FULFILMENT_STEPS.map((step, i) => {
const done = i < fulfilStep;
const active = i === fulfilStep;
return (
<div
key={step.label}
className={`flex items-center gap-2.5 transition-opacity duration-300 ${
i <= fulfilStep ? "opacity-100" : "opacity-35"
}`}
>
<span
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full ${
done || active
? "bg-secondary/15 text-secondary"
: "bg-on-surface-variant/10 text-on-surface-variant"
}`}
>
{done ? (
<MaterialIcon icon="check" size={12} />
) : active && !reduced ? (
<motion.span
className="h-2 w-2 rounded-full bg-secondary"
animate={{ scale: [1, 1.5, 1], opacity: [1, 0.5, 1] }}
transition={{ duration: 0.8, repeat: Infinity, ease: "easeInOut" }}
/>
) : (
<MaterialIcon icon={step.icon} size={12} />
)}
</span>
<span className="text-[11px] font-bold text-brand-dark">{step.label}</span>
</div>
);
})}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
);
}
const STORE_CATEGORIES = [
{ name: "Groceries", icon: "shopping_basket" },
{ name: "Dairy", icon: "egg" },
{ name: "Snacks", icon: "cookie" },
{ name: "Household", icon: "cleaning_services" },
{ name: "Personal", icon: "self_care" },
{ name: "More", icon: "grid_view" },
];
function StoreHeader({ name, area }: { name: string; area: string }) {
return (
<div className="bg-primary px-4 pb-3 pt-8 text-white">
<div className="flex items-center gap-1.5 text-[10px] font-bold opacity-90">
<MaterialIcon icon="storefront" size={12} className="shrink-0 text-white" />
<span className="truncate">{area}</span>
</div>
<div className="mt-1.5 flex items-center justify-between">
<span className="truncate font-display text-base font-black tracking-tight">{name}</span>
<MaterialIcon icon="shopping_cart" size={15} />
</div>
</div>
);
}

View File

@@ -1,212 +0,0 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
import { StoreCanvas } from "./StoreCanvas";
import {
type JourneyStage,
STAGE_ORDER,
useStoreJourney,
} from "@/hooks/useStoreJourney";
// The seven progress beats (fulfilment + done collapse into one visible step).
const PROGRESS = STAGE_ORDER.filter((s) => s !== "done");
const CAPTIONS: Record<JourneyStage, { title: string; body: string }> = {
"choose-store": {
title: "Choose your store",
body: "Start with a neighbourhood shop you already know and trust — added once with a quick QR scan.",
},
"enter-store": {
title: "Step inside",
body: "Walk into your store digitally. The same shelves and the same shopkeeper — now searchable.",
},
search: {
title: "Say what you need",
body: "Search in plain language. No need to know exact brands or product names.",
},
thinking: {
title: "Nearle AI understands",
body: "AI reads your intent, finds matching products, and checks whats actually in stock.",
},
results: {
title: "Picks from your store",
body: "Every recommendation comes from the store you chose — never an anonymous warehouse.",
},
cart: {
title: "One tap to order",
body: "Build your basket and send it straight to the merchant — no middleman.",
},
fulfilment: {
title: "Merchant packs, Nearle delivers",
body: "Your shopkeeper packs the order and a Nearle rider brings it to your door.",
},
done: {
title: "Delivered by your neighbourhood",
body: "Your shopkeeper packed the order and a Nearle rider brought it home.",
},
};
export function StoreJourney() {
const journey = useStoreJourney();
const { stage, intent, intents, pickIntent, replay } = journey;
const caption = CAPTIONS[stage];
const activeIndex = stage === "done" ? PROGRESS.length - 1 : PROGRESS.indexOf(stage);
return (
<section className="section w-full overflow-hidden bg-white">
<div className="page-container">
{/* Header */}
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
onViewportEnter={() => journey.start()}
className="mx-auto max-w-[760px] text-center"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center gap-1.5 rounded-full bg-primary/5 px-3.5 py-1.5 text-xs font-black uppercase tracking-widest text-primary"
>
<MaterialIcon icon="auto_awesome" size={14} />
Live Demo
</motion.span>
<motion.h2
variants={fadeUp}
className="mt-3 font-display text-4xl font-black leading-tight tracking-tight text-brand-dark md:text-5xl lg:text-[54px]"
>
Your Neighbourhood Store, <span className="text-primary font-normal">Now Digital</span>
</motion.h2>
<motion.p
variants={fadeUp}
className="mx-auto mt-3 max-w-xl text-body-lg leading-relaxed text-on-surface-variant"
>
Browse products from your trusted nearby stores anytime. Nearle AI helps you discover
products faster without changing how your favourite store works.
</motion.p>
</motion.div>
{/* Interactive journey */}
<div className="mt-12 grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
{/* LEFT: caption + progress + intent chips */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="order-2 lg:order-1"
>
{/* Progress bar */}
<div className="mb-6 flex items-center gap-1.5" aria-hidden="true">
{PROGRESS.map((s, i) => (
<span
key={s}
className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${
i <= activeIndex ? "bg-primary" : "bg-primary/12"
}`}
/>
))}
</div>
{/* Stage caption */}
<div className="min-h-[120px]">
<p className="text-xs font-black uppercase tracking-widest text-primary/70">
Step {activeIndex + 1} of {PROGRESS.length}
</p>
<AnimatePresence mode="wait">
<motion.div
key={stage === "done" ? "fulfilment-caption" : stage}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
<h3 className="mt-2 font-display text-2xl font-black tracking-tight text-brand-dark md:text-[28px]">
{caption.title}
</h3>
<p className="mt-2 max-w-md text-base leading-relaxed text-on-surface-variant">
{caption.body}
</p>
</motion.div>
</AnimatePresence>
</div>
{/* Intent chips — drive the demo */}
<div className="mt-7">
<p className="mb-2.5 flex items-center gap-1.5 text-xs font-black uppercase tracking-widest text-on-surface-variant/70">
<MaterialIcon icon="bolt" size={14} />
Try a shopping intent
</p>
<div className="flex flex-wrap gap-2">
{intents.map((it) => {
const active = it.id === intent.id;
return (
<button
key={it.id}
type="button"
onClick={() => pickIntent(it.id)}
className={`rounded-full border px-3.5 py-1.5 text-[13px] font-bold transition-colors duration-200 ${
active
? "border-primary/40 bg-primary/10 text-primary"
: "border-primary/15 bg-primary/[0.04] text-primary hover:border-primary/30 hover:bg-primary/[0.08]"
}`}
>
{it.label}
</button>
);
})}
</div>
</div>
{/* Replay appears once the story completes */}
<div className="mt-6 h-9">
<AnimatePresence>
{stage === "done" && (
<motion.button
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
type="button"
onClick={replay}
className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-white px-4 py-2 text-sm font-black text-primary transition-colors hover:bg-primary/[0.06]"
>
<MaterialIcon icon="replay" size={18} />
Watch again
</motion.button>
)}
</AnimatePresence>
</div>
</motion.div>
{/* RIGHT: the store canvas */}
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="order-1 lg:order-2"
>
<StoreCanvas journey={journey} />
</motion.div>
</div>
{/* Closing product message */}
<motion.p
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="mx-auto mt-14 max-w-2xl text-center font-display text-lg font-black leading-relaxed text-brand-dark md:text-xl"
>
Your trusted neighbourhood store, now in your pocket.{" "}
<span className="text-primary font-normal">
Powered by AI, fulfilled by local merchants, delivered by Nearle.
</span>
</motion.p>
</div>
</section>
);
}

View File

@@ -65,41 +65,41 @@ export function MerchantJourney() {
</div> </div>
{/* Chapter rail — filling line, active beat highlighted, clickable */} {/* Chapter rail — filling line, active beat highlighted, clickable */}
<div className="mt-9 w-full max-w-2xl"> <div className="mt-10 w-full max-w-3xl lg:max-w-4xl px-2">
<div className="relative h-[3px] rounded-full bg-primary/12"> <div className="relative h-1 rounded-full bg-primary/12">
<motion.div <motion.div
className="absolute inset-y-0 left-0 rounded-full bg-primary" className="absolute inset-y-0 left-0 rounded-full bg-primary"
animate={{ width: `${fillPct}%` }} animate={{ width: `${fillPct}%` }}
transition={{ duration: reduced ? 0 : 0.5, ease: EASE }} transition={{ duration: reduced ? 0 : 0.4, ease: EASE }}
/> />
</div> </div>
<ol className="mt-3.5 flex justify-between"> <ol className="mt-4 flex justify-between gap-1 sm:gap-2">
{steps.map((s, i) => { {steps.map((s, i) => {
const active = i === index; const active = i === index;
const done = i < index; const done = i < index;
return ( return (
<li key={s.id}> <li key={s.id} className="flex-1 flex justify-center">
<button <button
type="button" type="button"
onClick={() => goTo(i)} onClick={() => goTo(i)}
aria-label={`Go to ${s.title}`} aria-label={`Go to ${s.title}`}
aria-current={active ? "step" : undefined} aria-current={active ? "step" : undefined}
className="group flex flex-col items-center gap-1.5" className="group flex flex-col items-center gap-2 p-1 focus:outline-none"
> >
<span <span
className={`flex h-7 w-7 items-center justify-center rounded-full border-2 transition-all duration-300 ${ className={`flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-full border-2 transition-all duration-300 ${
active active
? "scale-110 border-primary bg-primary text-white shadow-[0_6px_16px_rgba(101,20,154,0.3)]" ? "scale-110 border-primary bg-primary text-white shadow-[0_8px_20px_rgba(101,20,154,0.35)]"
: done : done
? "border-primary/60 bg-primary/10 text-primary" ? "border-primary/60 bg-primary/10 text-primary group-hover:border-primary group-hover:bg-primary/20"
: "border-primary/20 bg-white text-on-surface-variant/70 group-hover:border-primary/40" : "border-primary/20 bg-white text-on-surface-variant/70 group-hover:border-primary/50 group-hover:bg-primary/[0.04]"
}`} }`}
> >
<MaterialIcon icon={s.icon} size={14} /> <MaterialIcon icon={s.icon} size={22} />
</span> </span>
<span <span
className={`hidden text-[10px] font-black transition-colors duration-300 sm:block ${ className={`text-[10px] sm:text-xs font-bold transition-colors duration-300 text-center ${
active ? "text-brand-dark" : "text-on-surface-variant/45" active ? "text-brand-dark font-black" : "text-on-surface-variant/60 group-hover:text-primary"
}`} }`}
> >
{s.label} {s.label}
@@ -112,24 +112,24 @@ export function MerchantJourney() {
</div> </div>
{/* Controls — manual stepping + replay once the arc has played */} {/* Controls — manual stepping + replay once the arc has played */}
<div className="mt-7 flex items-center gap-2.5"> <div className="mt-8 flex items-center gap-3">
<button <button
type="button" type="button"
onClick={prev} onClick={prev}
disabled={index === 0} disabled={index === 0}
aria-label="Previous chapter" aria-label="Previous chapter"
className="flex h-10 w-10 items-center justify-center rounded-full border border-primary/15 text-primary transition-colors hover:bg-primary/[0.06] disabled:cursor-not-allowed disabled:opacity-30" className="flex h-11 w-11 items-center justify-center rounded-full border border-primary/20 text-primary transition-all hover:bg-primary/[0.08] hover:border-primary/40 disabled:cursor-not-allowed disabled:opacity-30"
> >
<MaterialIcon icon="arrow_back" size={18} /> <MaterialIcon icon="arrow_back" size={20} />
</button> </button>
<button <button
type="button" type="button"
onClick={next} onClick={next}
disabled={atEnd} disabled={atEnd}
aria-label="Next chapter" aria-label="Next chapter"
className="flex h-10 w-10 items-center justify-center rounded-full border border-primary/15 text-primary transition-colors hover:bg-primary/[0.06] disabled:cursor-not-allowed disabled:opacity-30" className="flex h-11 w-11 items-center justify-center rounded-full border border-primary/20 text-primary transition-all hover:bg-primary/[0.08] hover:border-primary/40 disabled:cursor-not-allowed disabled:opacity-30"
> >
<MaterialIcon icon="arrow_forward" size={18} /> <MaterialIcon icon="arrow_forward" size={20} />
</button> </button>
<AnimatePresence> <AnimatePresence>
@@ -140,9 +140,9 @@ export function MerchantJourney() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
type="button" type="button"
onClick={replay} onClick={replay}
className="ml-1 inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2.5 text-sm font-black text-white transition-colors hover:bg-primary-container" className="ml-2 inline-flex items-center gap-2 rounded-full bg-primary px-5 py-2.5 text-sm font-bold text-white shadow-md transition-all hover:bg-primary-container hover:shadow-lg"
> >
<MaterialIcon icon="replay" size={17} /> <MaterialIcon icon="replay" size={18} />
Replay Replay
</motion.button> </motion.button>
)} )}

View File

@@ -3,233 +3,322 @@
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import type { MerchantStep, MerchantStageId } from "@/hooks/useMerchantJourney"; import type { MerchantStep, MerchantStageId } from "@/hooks/useMerchantJourney";
import { useRealTimeClock } from "@/hooks/useRealTimeClock";
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 screen = { const screenVariant = {
initial: { opacity: 0, y: 12 }, initial: { opacity: 0, y: 6, scale: 0.98 },
animate: { opacity: 1, y: 0 }, animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -12 }, exit: { opacity: 0, y: -6, scale: 0.98 },
transition: { duration: 0.55, ease: EASE }, transition: { duration: 0.35, ease: EASE },
}; };
// The merchant-side companion device. Its screen mirrors the active stage, so // The merchant-side companion device. Its screen mirrors the active stage, so
// the illustration and the "real app" always tell the same beat of the story. // the illustration and the "real app" always tell the same beat of the story.
export function MerchantPhone({ step, reduced }: { step: MerchantStep; reduced: boolean }) { export function MerchantPhone({ step, reduced }: { step: MerchantStep; reduced: boolean }) {
const liveTime = useRealTimeClock();
return ( return (
<div className="relative mx-auto flex w-full max-w-[250px] items-center justify-center"> <div className="relative mx-auto flex w-full max-w-[260px] items-center justify-center">
{/* Ambient background glow */}
<div <div
aria-hidden="true" aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 scale-110 rounded-full bg-gradient-to-tr from-primary/20 to-[#a855f7]/20 blur-3xl" className="pointer-events-none absolute inset-0 -z-10 scale-125 rounded-full bg-gradient-to-tr from-primary/25 via-[#9333ea]/20 to-[#a855f7]/25 blur-3xl"
/> />
{/* Floating phone wrapper with gentle levitation */}
<motion.div <motion.div
animate={reduced ? undefined : { y: [-5, 5, -5] }} animate={reduced ? undefined : { y: [-6, 6, -6], rotate: [-0.5, 0.5, -0.5] }}
transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }} transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }}
className="relative z-10 aspect-[9/18.6] w-full select-none overflow-hidden rounded-[40px] border-[10px] border-brand-dark bg-brand-dark shadow-[0_28px_60px_rgba(22,0,29,0.22)]" className="relative z-10 aspect-[9/18.8] w-full select-none"
> >
<div className="absolute left-1/2 top-2.5 z-30 flex h-4.5 w-20 -translate-x-1/2 items-center justify-end rounded-full bg-brand-dark px-2.5"> {/* Physical Side Buttons */}
<span className="mr-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" /> {/* Left: Volume buttons */}
<span className="h-1 w-1 rounded-full bg-blue-500/60" /> <div className="absolute -left-[3px] top-[70px] h-6 w-[3px] rounded-l-sm bg-neutral-700 shadow-sm" />
</div> <div className="absolute -left-[3px] top-[102px] h-6 w-[3px] rounded-l-sm bg-neutral-700 shadow-sm" />
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-[28px] bg-[#FBF8FE] text-brand-dark"> {/* Right: Power button */}
<AnimatePresence mode="wait"> <div className="absolute -right-[3px] top-[84px] h-9 w-[3px] rounded-r-sm bg-neutral-700 shadow-sm" />
<motion.div key={step.id} {...screen} className="flex h-full flex-col">
{SCREENS[step.id]()} {/* Outer Phone Frame Shell - Sleek modern radius */}
</motion.div> <div className="relative h-full w-full rounded-[30px] bg-neutral-950 p-[6px] shadow-[0_24px_50px_rgba(22,0,29,0.25),0_8px_20px_rgba(0,0,0,0.2)] ring-1 ring-white/20">
</AnimatePresence>
{/* Inner Bezel Frame */}
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-[24px] bg-[#FBF8FE] border border-neutral-900">
{/* OS Status Bar Overlay: Time on Far Left, Cellular Signal Tower & Battery on Far Right */}
<div className="absolute inset-x-0 top-0 z-40 flex h-8 items-center justify-between px-4.5 text-white pointer-events-none select-none">
<span className="text-[10px] font-bold tracking-tight">{liveTime || "9:41"}</span>
<div className="flex items-center gap-1.5 text-white/95">
{/* Cellular Signal Tower Bars */}
<div className="flex items-end gap-[1.5px] h-2.5">
<span className="w-[2px] h-[3px] bg-white rounded-[0.5px]" />
<span className="w-[2px] h-[5px] bg-white rounded-[0.5px]" />
<span className="w-[2px] h-[7px] bg-white rounded-[0.5px]" />
<span className="w-[2px] h-[9px] bg-white rounded-[0.5px]" />
</div>
{/* Battery Icon */}
<div className="flex items-center">
<div className="h-2.5 w-4.5 rounded-[3px] border border-white p-[1px] flex items-center">
<div className="h-full w-3 rounded-[1.5px] bg-white" />
</div>
<div className="h-1 w-[1px] bg-white rounded-r-xs" />
</div>
</div>
</div>
{/* Dynamic Island - Centered between Time and Tower icons */}
<div className="absolute left-1/2 top-2 z-40 flex h-3.5 w-[64px] -translate-x-1/2 items-center justify-between rounded-full bg-black px-1.5 shadow-md pointer-events-none">
{/* Camera Lens */}
<div className="flex h-1.5 w-1.5 items-center justify-center rounded-full bg-neutral-900 ring-1 ring-neutral-800">
<span className="h-0.5 w-0.5 rounded-full bg-[#1e3a8a]/70" />
</div>
{/* Speaker Earpiece Slit & Live Dot */}
<div className="flex items-center gap-1">
<span className="h-0.5 w-1.5 rounded-full bg-neutral-800" />
<span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
</div>
</div>
{/* Glass Glare Overlay */}
<div className="pointer-events-none absolute inset-0 z-30 rounded-[24px] bg-gradient-to-tr from-transparent via-white/[0.06] to-transparent" />
{/* Phone Screen App Container */}
<div className="relative flex flex-1 flex-col overflow-hidden">
<AnimatePresence mode="wait">
<motion.div key={step.id} {...screenVariant} className="flex h-full flex-col">
{SCREENS[step.id]()}
</motion.div>
</AnimatePresence>
</div>
</div>
</div> </div>
</motion.div> </motion.div>
</div> </div>
); );
} }
function Header({ title, sub }: { title: string; sub?: string }) { function HomeBar() {
return ( return (
<div className="bg-primary px-3.5 pb-2.5 pt-7 text-white"> <div className="py-1 flex justify-center items-center shrink-0 bg-transparent">
<div className="flex items-center justify-between"> <div className="h-1 w-20 rounded-full bg-neutral-900/25" />
<span className="font-display text-[13px] font-black tracking-tight">nearle</span>
<span className="text-[8px] font-black uppercase tracking-widest opacity-80">Merchant</span>
</div>
<p className="mt-1.5 text-[11px] font-black">{title}</p>
{sub && <p className="text-[9px] font-semibold opacity-80">{sub}</p>}
</div> </div>
); );
} }
function Line({ w = "full", tone = "muted" }: { w?: string; tone?: "muted" | "primary" }) { function Header({ title, sub }: { title: string; sub?: string }) {
return ( return (
<span <div className="bg-primary px-3.5 pb-2.5 pt-8 text-white shadow-sm">
className={`block h-2 rounded-full ${tone === "primary" ? "bg-primary/70" : "bg-on-surface-variant/25"}`} <div className="flex items-center justify-between">
style={{ width: w }} <span className="font-display text-[12px] font-black tracking-tight flex items-center gap-1">
/> <span className="h-1.5 w-1.5 rounded-full bg-secondary" /> nearle
</span>
<span className="text-[7.5px] font-black uppercase tracking-widest bg-white/15 px-2 py-0.5 rounded-full border border-white/10">
Merchant
</span>
</div>
<p className="mt-1.5 text-[11px] font-black leading-snug">{title}</p>
{sub && <p className="text-[9px] font-semibold opacity-85">{sub}</p>}
</div>
); );
} }
const SCREENS: Record<MerchantStageId, () => React.ReactElement> = { const SCREENS: Record<MerchantStageId, () => React.ReactElement> = {
join: () => ( join: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Create your store" sub="Takes about 3 minutes" /> <Header title="Create your store" sub="Takes about 3 minutes" />
<div className="flex flex-1 flex-col gap-2.5 px-3.5 pt-3.5"> <div className="flex flex-1 flex-col gap-2 px-3.5 pt-2.5">
{["Owner name", "Shop name", "Neighbourhood"].map((f) => ( {[
<div key={f} className="rounded-xl border border-primary/12 bg-white px-3 py-2 shadow-sm"> { label: "Owner name", placeholder: "e.g. Rajesh Kumar" },
<p className="text-[8px] font-bold uppercase tracking-wide text-on-surface-variant/70">{f}</p> { label: "Shop name", placeholder: "e.g. Kumar Super Market" },
<Line w="70%" /> { label: "Neighbourhood", placeholder: "e.g. R.S. Puram" },
].map((f) => (
<div key={f.label} className="rounded-lg border border-primary/12 bg-white px-3 py-1.5 shadow-sm">
<p className="text-[7.5px] font-black uppercase tracking-wide text-primary/80">{f.label}</p>
<p className="text-[9.5px] font-semibold text-brand-dark mt-0.5">{f.placeholder}</p>
</div> </div>
))} ))}
<div className="mt-auto mb-3.5 flex items-center justify-center gap-1.5 rounded-full bg-primary py-2.5 text-[11px] font-black text-white"> <div className="mt-auto mb-1 flex items-center justify-center gap-1.5 rounded-lg bg-primary py-2 text-[10.5px] font-black text-white shadow-sm">
<MaterialIcon icon="handshake" size={13} /> Join Nearle <MaterialIcon icon="handshake" size={13} /> Join Nearle
</div> </div>
</div> </div>
</> <HomeBar />
</div>
), ),
digitize: () => ( digitize: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Store is live" /> <Header title="Store is live" sub="Searchable nearby" />
<div className="flex flex-1 flex-col items-center justify-center px-4 text-center"> <div className="flex flex-1 flex-col items-center justify-center px-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-secondary/12 text-secondary"> <div className="flex h-12 w-12 items-center justify-center rounded-full bg-secondary/15 text-secondary shadow-sm ring-4 ring-secondary/5">
<MaterialIcon icon="check_circle" size={40} fill /> <MaterialIcon icon="check_circle" size={30} fill />
</div> </div>
<p className="mt-3 text-[12px] font-black text-brand-dark">Kumar Super Market</p> <p className="mt-2.5 text-[12px] font-black text-brand-dark">Kumar Super Market</p>
<p className="mt-1 text-[9px] font-semibold text-on-surface-variant"> <p className="mt-1 text-[9px] font-medium text-on-surface-variant leading-tight max-w-[170px]">
Your digital storefront is now searchable nearby. Your storefront & inventory are now live on Nearle.
</p> </p>
<span className="mt-3 rounded-full bg-secondary/10 px-2.5 py-1 text-[8px] font-black uppercase tracking-widest text-secondary"> <span className="mt-3 inline-flex items-center gap-1 rounded-md bg-secondary/10 px-2.5 py-1 text-[8px] font-black uppercase tracking-widest text-secondary border border-secondary/20">
QR verified <MaterialIcon icon="qr_code_2" size={10} /> QR Verified
</span> </span>
</div> </div>
</> <HomeBar />
</div>
), ),
products: () => ( products: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Your catalogue" sub="18 items live" /> <Header title="Your catalogue" sub="18 items active" />
<div className="grid flex-1 grid-cols-2 gap-2 px-3.5 pt-3.5"> <div className="grid flex-1 grid-cols-2 gap-2 px-3 pt-2.5">
{[ {[
["🥛", "Aavin Milk"], { emoji: "🥛", name: "Aavin Milk", price: "₹28" },
["🍞", "Bread"], { emoji: "🍞", name: "Modern Bread", price: "₹40" },
["🧈", "Butter"], { emoji: "🧈", name: "Amul Butter", price: "₹58" },
["☕", "Coffee"], { emoji: "☕", name: "Nescafé Coffee", price: "₹120" },
].map(([e, n]) => ( ].map((item) => (
<div key={n} className="rounded-xl border border-primary/10 bg-white p-2 shadow-sm"> <div key={item.name} className="rounded-lg border border-primary/10 bg-white p-2 shadow-sm flex flex-col justify-between">
<div className="flex h-9 items-center justify-center rounded-lg bg-primary/[0.06] text-base">{e}</div> <div className="flex h-7 items-center justify-center rounded-md bg-primary/[0.06] text-sm">{item.emoji}</div>
<p className="mt-1.5 truncate text-[8px] font-black text-brand-dark">{n}</p> <div>
<Line w="60%" tone="primary" /> <p className="mt-1 truncate text-[8.5px] font-black text-brand-dark">{item.name}</p>
<p className="text-[8px] font-bold text-primary">{item.price}</p>
</div>
</div> </div>
))} ))}
</div> </div>
<div className="m-3.5 flex items-center justify-center gap-1 rounded-full border border-primary/20 py-2 text-[10px] font-black text-primary"> <div className="mx-3 my-1 flex items-center justify-center gap-1 rounded-lg border border-primary/20 bg-white py-1.5 text-[9.5px] font-black text-primary shadow-sm">
<MaterialIcon icon="add" size={13} /> Add products <MaterialIcon icon="add" size={12} /> Add products
</div> </div>
</> <HomeBar />
</div>
), ),
browse: () => ( browse: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Customers nearby" /> <Header title="Customers nearby" sub="Live customer searches" />
<div className="px-3.5 py-3"> <div className="px-3 pt-2 pb-1.5">
<div className="flex items-center gap-2 rounded-full border border-primary/15 bg-white px-3 py-2 shadow-sm"> <div className="flex items-center gap-1.5 rounded-lg border border-primary/15 bg-white px-2.5 py-1.5 shadow-sm">
<MaterialIcon icon="search" size={13} className="text-primary/60" /> <MaterialIcon icon="search" size={12} className="text-primary/70" />
<span className="text-[10px] font-semibold text-brand-dark">Need breakfast</span> <span className="text-[9.5px] font-bold text-brand-dark">Need breakfast items</span>
</div> </div>
</div> </div>
<div className="flex-1 space-y-2 px-3.5"> <div className="flex-1 space-y-1.5 px-3">
{["🥛 Aavin Milk", "🍞 Sandwich Bread", "🧈 Table Butter"].map((r) => ( {[
<div key={r} className="flex items-center justify-between rounded-xl border border-primary/8 bg-white px-2.5 py-2 shadow-sm"> { name: "Aavin Milk 500ml", dist: "300m" },
<span className="text-[10px] font-black text-brand-dark">{r}</span> { name: "Sandwich Bread", dist: "300m" },
<MaterialIcon icon="add_circle" size={15} className="text-primary" /> { name: "Table Butter 100g", dist: "300m" },
].map((r) => (
<div key={r.name} className="flex items-center justify-between rounded-lg border border-primary/8 bg-white px-2.5 py-1.5 shadow-sm">
<div>
<p className="text-[9px] font-black text-brand-dark">{r.name}</p>
<p className="text-[7.5px] font-semibold text-on-surface-variant/70">{r.dist} away</p>
</div>
<MaterialIcon icon="add_circle" size={14} className="text-primary" />
</div> </div>
))} ))}
</div> </div>
<p className="px-3.5 pb-3.5 pt-2 text-[8px] font-bold text-on-surface-variant">From your store · 700 m away</p> <p className="px-3 pt-1 text-[7.5px] font-bold text-on-surface-variant/70 text-center">3 shoppers viewing your store</p>
</> <HomeBar />
</div>
), ),
orders: () => ( orders: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Orders" sub="1 new" /> <Header title="New order received" sub="Order #1042 · Just now" />
<div className="px-3.5 pt-3"> <div className="px-3 pt-2">
<div className="rounded-2xl border border-primary/15 bg-white p-3 shadow-[0_10px_24px_rgba(101,20,154,0.12)]"> <div className="rounded-xl border border-primary/15 bg-white p-2 shadow-sm">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-primary"> <span className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-primary/10 text-primary">
<MaterialIcon icon="notifications_active" size={13} /> <MaterialIcon icon="notifications_active" size={11} />
</span> </span>
<span className="text-[10px] font-black text-brand-dark">New order · #1042</span> <span className="text-[9.5px] font-black text-brand-dark">Customer #1042</span>
<span className="ml-auto h-2 w-2 rounded-full bg-secondary" /> <span className="ml-auto h-1.5 w-1.5 rounded-full bg-secondary animate-ping" />
</div> </div>
<div className="mt-2.5 space-y-1.5"> <div className="mt-1.5 space-y-1">
{["Milk × 1", "Bread × 1", "Eggs × 1"].map((i) => ( {[
<div key={i} className="flex items-center justify-between text-[9px] font-semibold text-on-surface-variant"> { item: "Aavin Milk 500ml", qty: "× 1" },
<span>{i}</span> { item: "Sandwich Bread", qty: "× 1" },
<span className="text-primary"></span> { item: "Farm Eggs (6 pcs)", qty: "× 1" },
].map((i) => (
<div key={i.item} className="flex items-center justify-between text-[8.5px] font-semibold text-on-surface-variant">
<span className="truncate">{i.item}</span>
<span className="text-primary font-bold ml-1">{i.qty}</span>
</div> </div>
))} ))}
</div> </div>
<div className="mt-2.5 flex items-center justify-between border-t border-primary/10 pt-2"> <div className="mt-1.5 flex items-center justify-between border-t border-primary/10 pt-1">
<span className="text-[9px] font-bold text-on-surface-variant">Total</span> <span className="text-[8.5px] font-bold text-on-surface-variant">Total</span>
<span className="font-display text-[13px] font-black text-brand-dark">165</span> <span className="font-display text-[12px] font-black text-brand-dark">165</span>
</div> </div>
</div> </div>
</div> </div>
<div className="m-3.5 mt-auto flex items-center justify-center gap-1.5 rounded-full bg-primary py-2.5 text-[11px] font-black text-white"> <div className="mx-3 mt-auto mb-1 flex items-center justify-center gap-1.5 rounded-lg bg-primary py-2 text-[10.5px] font-black text-white shadow-sm">
Accept order Accept order
</div> </div>
</> <HomeBar />
</div>
), ),
packed: () => ( packed: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Pack order #1042" sub="Almost ready" /> <Header title="Pack order #1042" sub="2 of 3 items packed" />
<div className="flex-1 space-y-2 px-3.5 pt-3.5"> <div className="flex-1 space-y-1.5 px-3 pt-2.5">
{[["Aavin Milk", true], ["Sandwich Bread", true], ["Farm Eggs", false]].map(([n, done]) => ( {[
<div key={n as string} className="flex items-center gap-2.5 rounded-xl border border-primary/10 bg-white px-3 py-2 shadow-sm"> { name: "Aavin Milk 500ml", done: true },
{ name: "Sandwich Bread", done: true },
{ name: "Farm Eggs (6 pcs)", done: false },
].map((item) => (
<div key={item.name} className="flex items-center gap-2 rounded-lg border border-primary/10 bg-white px-2.5 py-1.5 shadow-sm">
<span <span
className={`flex h-5 w-5 items-center justify-center rounded-md ${done ? "bg-secondary/15 text-secondary" : "border border-on-surface-variant/30 text-transparent"}`} className={`flex h-4.5 w-4.5 items-center justify-center rounded-md ${item.done ? "bg-secondary/15 text-secondary" : "border border-on-surface-variant/30 text-transparent"}`}
> >
<MaterialIcon icon="check" size={12} /> <MaterialIcon icon="check" size={11} />
</span> </span>
<span className="text-[10px] font-black text-brand-dark">{n as string}</span> <span className="text-[9px] font-black text-brand-dark">{item.name}</span>
</div> </div>
))} ))}
</div> </div>
<div className="m-3.5 flex items-center justify-center gap-1.5 rounded-full bg-primary py-2.5 text-[11px] font-black text-white"> <div className="mx-3 mb-1 flex items-center justify-center gap-1.5 rounded-lg bg-primary py-2 text-[10.5px] font-black text-white shadow-sm">
<MaterialIcon icon="inventory_2" size={13} /> Mark packed <MaterialIcon icon="inventory_2" size={12} /> Mark packed
</div> </div>
</> <HomeBar />
</div>
), ),
rider: () => ( rider: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="Out for delivery" sub="Rider assigned" /> <Header title="Out for delivery" sub="Nearle Rider assigned" />
<div className="relative mx-3.5 mt-3 h-24 overflow-hidden rounded-2xl bg-[#EDE2F4]"> <div className="relative mx-3 mt-2 h-20 overflow-hidden rounded-xl bg-[#EDE2F4]">
<svg viewBox="0 0 200 96" className="h-full w-full"> <svg viewBox="0 0 200 96" className="h-full w-full">
<path d="M0 70 Q60 40 110 60 T200 40" fill="none" stroke="#65149A" strokeWidth="3" strokeDasharray="6 7" strokeLinecap="round" opacity="0.6" /> <path d="M0 70 Q60 40 110 60 T200 40" fill="none" stroke="#65149A" strokeWidth="3" strokeDasharray="6 7" strokeLinecap="round" opacity="0.6" />
<circle cx="18" cy="70" r="6" fill="#16A34A" /> <circle cx="18" cy="70" r="6" fill="#16A34A" />
<circle cx="184" cy="40" r="6" fill="#65149A" /> <circle cx="184" cy="40" r="6" fill="#65149A" />
</svg> </svg>
<span className="absolute left-1/2 top-1/2 flex h-9 w-9 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-white shadow-lg"> <span className="absolute left-1/2 top-1/2 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-white shadow-md">
<MaterialIcon icon="two_wheeler" size={18} /> <MaterialIcon icon="two_wheeler" size={15} />
</span> </span>
</div> </div>
<div className="mx-3.5 mt-3 rounded-xl border border-primary/10 bg-white px-3 py-2.5 shadow-sm"> <div className="mx-3 mt-1.5 rounded-lg border border-primary/10 bg-white px-2.5 py-1.5 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[10px] font-black text-brand-dark">Arriving in 8 min</span> <span className="text-[9px] font-black text-brand-dark">Arriving in 8 min</span>
<span className="rounded-full bg-secondary/12 px-2 py-0.5 text-[8px] font-black uppercase tracking-wide text-secondary">On the way</span> <span className="rounded-md bg-secondary/12 px-2 py-0.5 text-[7.5px] font-black uppercase tracking-wide text-secondary border border-secondary/20">On the way</span>
</div> </div>
</div> </div>
</> <HomeBar />
</div>
), ),
grows: () => ( grows: () => (
<> <div className="flex h-full flex-col bg-[#FBF8FE]">
<Header title="This week" sub="Revenue up 32%" /> <Header title="This week performance" sub="Revenue up 32% vs last week" />
<div className="px-3.5 pt-3"> <div className="px-3.5 pt-2.5">
<p className="font-display text-2xl font-black text-brand-dark">48,200</p> <p className="font-display text-xl font-black text-brand-dark">48,200</p>
<p className="flex items-center gap-1 text-[9px] font-black text-secondary"> <p className="flex items-center gap-1 text-[8.5px] font-black text-secondary">
<MaterialIcon icon="trending_up" size={12} /> +32% vs last week <MaterialIcon icon="trending_up" size={11} /> +32% vs last week
</p> </p>
</div> </div>
<div className="mt-3 flex flex-1 items-end gap-1.5 px-3.5 pb-4"> <div className="mt-2 flex flex-1 items-end gap-1.5 px-3.5 pb-1.5">
{[34, 46, 40, 58, 52, 70, 88].map((h, i) => ( {[34, 46, 40, 58, 52, 70, 88].map((h, i) => (
<span <span
key={i} key={i}
className={`flex-1 rounded-t-md ${i === 6 ? "bg-primary" : "bg-primary/25"}`} className={`flex-1 rounded-t-sm transition-all ${i === 6 ? "bg-primary shadow-sm" : "bg-primary/25"}`}
style={{ height: `${h}%` }} style={{ height: `${h}%` }}
/> />
))} ))}
</div> </div>
</> <HomeBar />
</div>
), ),
}; };

View File

@@ -5,45 +5,50 @@ import { motion, useReducedMotion } from "framer-motion";
// The kinds of things people actually say to Nearle — products *and* local // The kinds of things people actually say to Nearle — products *and* local
// services. // services.
// //
// Positions are percentages of the phone box, and every card is placed so it // Placement is anchored off the phone box's own edges rather than guessed
// never covers the live screen — the demo always stays readable: // percentages: `right-full` puts a card's right edge exactly on the phone's
// · xl+ : three cards drift in the wide gutter left of the phone, two sit // left edge, `left-full` the mirror. Plus a margin, that means a card can never
// clear above and below it. // cover the live screen at any width — the demo always stays readable.
// · below xl there is no gutter, so the visible cards sit above and below // · lg+ : three cards drift in the left gutter at staggered heights, two in
// the device instead of beside it. // the right gutter between them.
// · below lg there is no gutter, so the two shortest cards sit clear above
// and below the device instead of beside it.
//
// The longer labels are kept on the left, where the grid gap and the text
// column's own slack leave more room to bleed into than the page margin does.
const CARDS = [ const CARDS = [
{ {
emoji: "🥛", emoji: "🥛",
text: "Order two litres of milk", text: "Order two litres of milk",
pos: "left-[-4%] top-[-7%] xl:left-[-50%] xl:top-[12%]", pos: "left-[-4%] top-[-8%] lg:left-auto lg:right-full lg:top-[7%] lg:mr-4",
drift: { y: [-9, 9, -9], x: [0, 5, 0] }, drift: { y: [-9, 9, -9], x: [0, 5, 0], rotate: [-1, 1, -1] },
duration: 15, duration: 15,
delay: 0, delay: 0,
always: true, always: true,
}, },
{ {
emoji: "💊", emoji: "💊",
text: "Order medicine for headache", text: "Order medicine",
pos: "right-[-4%] bottom-[-6%] xl:right-[-6%] xl:bottom-[-7%]", pos: "right-[-4%] bottom-[-7%] lg:bottom-auto lg:right-auto lg:left-full lg:top-[62%] lg:ml-4",
drift: { y: [-10, 7, -10], x: [0, -5, 0] }, drift: { y: [-10, 7, -10], x: [0, -5, 0], rotate: [1, -1, 1] },
duration: 16, duration: 16,
delay: 2.1, delay: 2.1,
always: true, always: true,
}, },
{ {
emoji: "🍅", emoji: "🔧",
text: "Need ingredients for biryani", text: "Book an electrician",
pos: "right-[-6%] top-[-9%]", pos: "left-full top-[17%] ml-4",
drift: { y: [8, -8, 8], x: [0, -6, 0] }, drift: { y: [8, -8, 8], x: [0, -6, 0], rotate: [-1.2, 0.8, -1.2] },
duration: 18, duration: 18,
delay: 1.4, delay: 1.4,
always: false, always: false,
}, },
{ {
emoji: "🔧", emoji: "🍅",
text: "Book an electrician tomorrow", text: "Need ingredients for biryani",
pos: "left-[-56%] top-[45%]", pos: "right-full top-[42%] mr-4",
drift: { y: [7, -10, 7], x: [0, 6, 0] }, drift: { y: [7, -10, 7], x: [0, 6, 0], rotate: [0.9, -1.1, 0.9] },
duration: 20, duration: 20,
delay: 0.7, delay: 0.7,
always: false, always: false,
@@ -51,8 +56,8 @@ const CARDS = [
{ {
emoji: "🥣", emoji: "🥣",
text: "Need breakfast for four", text: "Need breakfast for four",
pos: "left-[-52%] top-[77%]", pos: "right-full top-[77%] mr-4",
drift: { y: [9, -7, 9], x: [0, 4, 0] }, drift: { y: [9, -7, 9], x: [0, 4, 0], rotate: [-0.8, 1.2, -0.8] },
duration: 19, duration: 19,
delay: 1, delay: 1,
always: false, always: false,
@@ -69,7 +74,7 @@ export function FloatingRequestCards() {
{CARDS.map((card, i) => ( {CARDS.map((card, i) => (
<motion.div <motion.div
key={card.text} key={card.text}
className={`absolute ${card.pos} ${card.always ? "" : "hidden xl:block"}`} className={`absolute ${card.pos} ${card.always ? "" : "hidden lg:block"}`}
initial={{ opacity: 0, y: 14, scale: 0.96 }} initial={{ opacity: 0, y: 14, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.7, delay: 0.9 + i * 0.12, ease: [0.22, 1, 0.36, 1] }} transition={{ duration: 0.7, delay: 0.9 + i * 0.12, ease: [0.22, 1, 0.36, 1] }}

View File

@@ -5,6 +5,7 @@ import Image from "next/image";
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { VoiceScreen, SCREEN_W, SCREEN_H } from "./VoiceScreen"; import { VoiceScreen, SCREEN_W, SCREEN_H } from "./VoiceScreen";
import { useVoiceJourney } from "@/hooks/useVoiceJourney"; import { useVoiceJourney } from "@/hooks/useVoiceJourney";
import { useRealTimeClock } from "@/hooks/useRealTimeClock";
// Geometry read straight off the provided SVG (viewBox 0 0 476 941): // Geometry read straight off the provided SVG (viewBox 0 0 476 941):
// screen rect x42 y8 w391 h826 rx57. The frame artwork never changes — only // screen rect x42 y8 w391 h826 rx57. The frame artwork never changes — only
@@ -21,6 +22,7 @@ const SCREEN = {
export function PhoneStage({ className = "" }: { className?: string }) { export function PhoneStage({ className = "" }: { className?: string }) {
const journey = useVoiceJourney(); const journey = useVoiceJourney();
const { start, stop, reduced } = journey; const { start, stop, reduced } = journey;
const liveTime = useRealTimeClock();
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const screenRef = useRef<HTMLDivElement>(null); const screenRef = useRef<HTMLDivElement>(null);
@@ -57,72 +59,91 @@ export function PhoneStage({ className = "" }: { className?: string }) {
animates box-shadow (which repaints every frame). */} animates box-shadow (which repaints every frame). */}
<motion.div <motion.div
aria-hidden="true" aria-hidden="true"
className="pointer-events-none absolute left-1/2 bottom-[3%] h-[9%] w-[78%] -translate-x-1/2 rounded-[50%] bg-[#3B0764]/25 blur-2xl" className="pointer-events-none absolute left-1/2 bottom-[3%] h-[9%] w-[82%] -translate-x-1/2 rounded-[50%] bg-[#3B0764]/25 blur-2xl"
animate={reduced ? undefined : { scaleX: [1, 0.9, 1], opacity: [0.5, 0.34, 0.5] }} // Shares the float's 7s period: the shadow tightens as the device
// lifts, which is what sells the float as real height.
animate={reduced ? undefined : { scaleX: [1, 0.88, 1], opacity: [0.5, 0.3, 0.5] }}
transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }} transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }}
/> />
{/* Float — vertical drift */} {/* Float — vertical drift */}
<motion.div <motion.div
animate={reduced ? undefined : { y: [-8, 8, -8] }} animate={reduced ? undefined : { y: [-10, 10, -10] }}
transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }} transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }}
style={{ transformStyle: "preserve-3d" }} style={{ transformStyle: "preserve-3d" }}
> >
{/* Sway — slow rotateY on a deliberately different period, so the two {/* Pitch — a very slow rotateX on a third, co-prime period (13s vs the
never sync into one obvious loop */} float's 7s and the sway's 11s), so the three never resolve into one
visible loop. */}
<motion.div <motion.div
className="relative w-full" animate={reduced ? undefined : { rotateX: [1.2, -1.2, 1.2] }}
style={{ aspectRatio: "476 / 941", transformStyle: "preserve-3d" }} transition={{ duration: 13, repeat: Infinity, ease: "easeInOut" }}
animate={reduced ? undefined : { rotateY: [-1.2, 1.2, -1.2] }} style={{ transformStyle: "preserve-3d" }}
transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
> >
{/* Layer 0 — the provided iPhone SVG, verbatim and permanent */} {/* Sway — the device rests at rotateY -4°, so its left edge sits
<Image slightly forward under the root's 1600px perspective. The sway
src="/images/hero/iphone-frame.svg" oscillates around that rest angle rather than around flat-on. */}
alt="" <motion.div
fill className="relative w-full"
unoptimized style={{ aspectRatio: "476 / 941", transformStyle: "preserve-3d" }}
priority animate={reduced ? { rotateY: -4 } : { rotateY: [-5.2, -2.8, -5.2] }}
aria-hidden="true" transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
className="select-none object-contain"
/>
{/* Layer 1 — the live app, clipped exactly to the screen rect */}
<div
ref={screenRef}
className="absolute overflow-hidden"
style={{
left: SCREEN.left,
top: SCREEN.top,
width: SCREEN.width,
height: SCREEN.height,
borderRadius: SCREEN.borderRadius,
}}
> >
{scale > 0 && ( {/* Layer 0 — the provided iPhone SVG, verbatim and permanent */}
<div <Image
style={{ src="/images/hero/iphone-frame.svg"
width: SCREEN_W, alt=""
height: SCREEN_H, fill
transform: `scale(${scale})`, unoptimized
transformOrigin: "top left", priority
}} aria-hidden="true"
> className="select-none object-contain"
<VoiceScreen journey={journey} /> />
</div>
)}
</div>
{/* Layer 2notch + status bar, extracted from the same SVG, so the {/* Layer 1the live app, clipped exactly to the screen rect */}
app sits *under* the notch exactly like real iOS */} <div
<Image ref={screenRef}
src="/images/hero/iphone-chrome.svg" className="absolute overflow-hidden"
alt="" style={{
fill left: SCREEN.left,
unoptimized top: SCREEN.top,
aria-hidden="true" width: SCREEN.width,
className="pointer-events-none select-none object-contain" height: SCREEN.height,
/> borderRadius: SCREEN.borderRadius,
}}
>
{scale > 0 && (
<div
style={{
width: SCREEN_W,
height: SCREEN_H,
transform: `scale(${scale})`,
transformOrigin: "top left",
}}
>
<VoiceScreen journey={journey} />
</div>
)}
</div>
{/* Layer 2 — notch + status bar, extracted from the same SVG, so the
app sits *under* the notch exactly like real iOS */}
<Image
src="/images/hero/iphone-chrome.svg"
alt=""
fill
unoptimized
aria-hidden="true"
className="pointer-events-none select-none object-contain"
/>
{/* Layer 3 — Real-Time Live Clock Overlay */}
<div
className="pointer-events-none absolute left-[18%] top-[2.85%] z-30 select-none text-[11px] font-black tracking-tight text-white sm:text-[12px]"
>
{liveTime || "9:41"}
</div>
</motion.div>
</motion.div> </motion.div>
</motion.div> </motion.div>
</div> </div>

View File

@@ -58,6 +58,7 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
const showRequest = const showRequest =
stage !== "launch" && !onHomeScreen && stage !== "listening" && stage !== "speaking"; stage !== "launch" && !onHomeScreen && stage !== "listening" && stage !== "speaking";
const fulfilmentSteps = isBooking ? SERVICE_FULFILMENT_STEPS : FULFILMENT_STEPS; const fulfilmentSteps = isBooking ? SERVICE_FULFILMENT_STEPS : FULFILMENT_STEPS;
const itemsSubtotal = request.items.reduce((sum, item) => sum + item.price, 0);
// On the final beat every fulfilment step is done — nothing left pulsing. // On the final beat every fulfilment step is done — nothing left pulsing.
const fulfilDone = stage === "delivered" ? fulfilmentSteps.length : fulfilStep; const fulfilDone = stage === "delivered" ? fulfilmentSteps.length : fulfilStep;
@@ -228,6 +229,20 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
</p> </p>
<AIThinking steps={AI_STEPS} activeStep={thinkingStep} tone="dark" /> <AIThinking steps={AI_STEPS} activeStep={thinkingStep} tone="dark" />
</div> </div>
{/* Shops resolving underneath the reasoning. Deliberately
contentless — they say "nearby stores are being checked"
without ever claiming a name, and they set up the merchant
card that lands on the next beat. */}
<p className="mb-3 mt-6 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-[0.18em] text-white/35">
<MaterialIcon icon="travel_explore" size={13} />
Scanning nearby
</p>
<div className="space-y-2.5">
{[0, 1, 2].map((i) => (
<MerchantSkeleton key={i} index={i} reduced={reduced} />
))}
</div>
</motion.div> </motion.div>
)} )}
@@ -235,7 +250,7 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
<motion.div <motion.div
key="merchant" key="merchant"
{...screenMotion} {...screenMotion}
className="absolute inset-0 flex h-full flex-col px-6 pt-4" className="absolute inset-0 flex h-full flex-col justify-center px-6 pb-8"
> >
<p className="mb-3 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-[0.18em] text-[#C9A2FF]"> <p className="mb-3 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-[0.18em] text-[#C9A2FF]">
<MaterialIcon icon="near_me" size={13} /> <MaterialIcon icon="near_me" size={13} />
@@ -274,6 +289,33 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
</span> </span>
</div> </div>
</motion.div> </motion.div>
{/* Why the AI settled on this one. Every figure is read
straight off the request data — nothing is invented. */}
<div className="mt-3 grid grid-cols-3 gap-2">
{[
{ icon: "near_me", label: "Nearest", value: request.merchant.distance },
{ icon: "star", label: "Rated", value: `${request.merchant.rating}` },
{
icon: "schedule",
label: isBooking ? "Slot" : "Delivery",
value: request.eta,
},
].map((fact) => (
<div
key={fact.label}
className="rounded-2xl border border-white/10 bg-white/[0.05] px-2 py-3 text-center"
>
<MaterialIcon icon={fact.icon} size={15} className="text-[#C9A2FF]" />
<p className="mt-1.5 truncate text-[11px] font-black leading-none">
{fact.value}
</p>
<p className="mt-1 text-[8.5px] font-bold uppercase tracking-wider text-white/40">
{fact.label}
</p>
</div>
))}
</div>
</motion.div> </motion.div>
)} )}
@@ -288,9 +330,22 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
{isBooking ? "Booking created" : "Order created"} {isBooking ? "Booking created" : "Order created"}
</p> </p>
<div className="flex items-center gap-2 text-[11px] font-bold text-white/70"> <div className="flex items-center gap-2.5 rounded-2xl border border-white/10 bg-white/[0.05] px-3 py-2.5">
<MaterialIcon icon={request.merchant.icon} size={13} /> <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[#A855F7]/20 text-[#C9A2FF]">
<span className="truncate">{request.merchant.name}</span> <MaterialIcon icon={request.merchant.icon} size={16} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-[12px] font-black leading-tight">
{request.merchant.name}
</span>
<span className="mt-0.5 block truncate text-[10px] font-semibold text-white/45">
{request.merchant.area} · {request.merchant.distance}
</span>
</span>
<span className="flex shrink-0 items-center gap-0.5 rounded-full bg-white/10 px-2 py-1 text-[9.5px] font-black text-white/80">
<MaterialIcon icon="star" size={10} fill />
{request.merchant.rating}
</span>
</div> </div>
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
@@ -324,8 +379,49 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
))} ))}
</div> </div>
{/* Where it lands. Matches the location already shown in the
app header, so the demo stays internally consistent.
Short orders are the ones that used to leave the screen
half empty; a four-item basket already fills it, and this
row would push the confirm button off the bottom. */}
{request.items.length < 3 && (
<div className="mt-3 flex shrink-0 items-center gap-2.5 rounded-2xl border border-white/10 bg-white/[0.05] px-3 py-2.5">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/[0.07] text-white/70">
<MaterialIcon icon={isBooking ? "home_work" : "location_on"} size={16} />
</span>
<span className="min-w-0 flex-1">
<span className="block text-[9px] font-bold uppercase tracking-wider text-white/40">
{isBooking ? "Service at" : "Deliver to"}
</span>
<span className="mt-0.5 block truncate text-[11.5px] font-black leading-tight">
Home · R.S. Puram
</span>
</span>
</div>
)}
{/* Itemised, so a one-item request still fills the bill card.
Items always sum to the request total — delivery is free,
it is the merchant's own shop round the corner. */}
<div className="mt-3 shrink-0 rounded-2xl border border-white/10 bg-white/[0.06] px-4 py-3"> <div className="mt-3 shrink-0 rounded-2xl border border-white/10 bg-white/[0.06] px-4 py-3">
<div className="flex items-end justify-between"> <div className="flex items-center justify-between text-[10.5px] font-bold text-white/45">
<span>
{request.items.length}{" "}
{isBooking
? request.items.length === 1
? "service"
: "services"
: request.items.length === 1
? "item"
: "items"}
</span>
<span>{itemsSubtotal}</span>
</div>
<div className="mt-1.5 flex items-center justify-between text-[10.5px] font-bold text-white/45">
<span>{isBooking ? "Booking fee" : "Delivery"}</span>
<span className="font-black text-emerald-300">Free</span>
</div>
<div className="mt-2.5 flex items-end justify-between border-t border-white/10 pt-2.5">
<span className="text-[10px] font-bold uppercase tracking-wider text-white/50"> <span className="text-[10px] font-bold uppercase tracking-wider text-white/50">
Total Total
</span> </span>
@@ -339,11 +435,13 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
</div> </div>
</div> </div>
{/* Confirm auto-taps at the end of this beat */} {/* Confirm auto-taps at the end of this beat. mt-auto pins it
to the foot of the screen the way a real checkout does, so
a short order never leaves a hole under the bill. */}
<motion.div <motion.div
animate={reduced ? undefined : { scale: [1, 1, 0.95, 1] }} animate={reduced ? undefined : { scale: [1, 1, 0.95, 1] }}
transition={{ duration: 2.6, times: [0, 0.76, 0.84, 0.92], ease: EASE }} transition={{ duration: 2.6, times: [0, 0.76, 0.84, 0.92], ease: EASE }}
className="mt-3 flex shrink-0 items-center justify-center gap-1.5 rounded-full bg-white py-3.5 text-[13px] font-black text-[#2A0846]" className="mb-1 mt-auto flex shrink-0 items-center justify-center gap-1.5 rounded-full bg-white py-3.5 text-[13px] font-black text-[#2A0846]"
> >
<MaterialIcon icon="bolt" size={15} /> <MaterialIcon icon="bolt" size={15} />
{request.cta} {request.cta}
@@ -467,6 +565,28 @@ export function VoiceScreen({ journey }: { journey: VoiceJourney }) {
); );
} }
// Placeholder shop rows that pulse while the AI searches. Intentionally blank —
// they read as "nearby stores are resolving" without ever asserting a name or a
// figure the demo hasn't earned yet.
function MerchantSkeleton({ index, reduced }: { index: number; reduced: boolean }) {
return (
<motion.div
className="flex items-center gap-3 rounded-2xl border border-white/[0.07] bg-white/[0.04] px-3 py-2.5"
animate={reduced ? undefined : { opacity: [0.4, 0.75, 0.4] }}
transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut", delay: index * 0.22 }}
>
<span className="h-9 w-9 shrink-0 rounded-xl bg-white/[0.07]" />
<span className="min-w-0 flex-1">
<span
className="block h-2.5 rounded-full bg-white/[0.09]"
style={{ width: `${72 - index * 14}%` }}
/>
<span className="mt-1.5 block h-2 w-[38%] rounded-full bg-white/[0.06]" />
</span>
</motion.div>
);
}
// The listening orb: a soft purple core with breathing rings. // The listening orb: a soft purple core with breathing rings.
function MicOrb({ reduced, active }: { reduced: boolean; active: boolean }) { function MicOrb({ reduced, active }: { reduced: boolean; active: boolean }) {
return ( return (

View File

@@ -7,10 +7,13 @@ import { motion, useReducedMotion } from "framer-motion";
const WAVE = const WAVE =
"M0 150 Q100 80 200 150 T400 150 T600 150 T800 150 T1000 150 T1200 150 T1400 150 T1600 150 T1800 150 T2000 150 T2200 150 T2400 150"; "M0 150 Q100 80 200 150 T400 150 T600 150 T800 150 T1000 150 T1200 150 T1400 150 T1600 150 T1800 150 T2000 150 T2200 150 T2400 150";
// Opacities are tuned for a pure white canvas. On the old lavender background
// these read as texture; on white anything stronger reads as clutter and pulls
// attention off the phone.
const LAYERS = [ const LAYERS = [
{ y: 0, scaleY: 1, opacity: 0.18, duration: 12, width: 2 }, { y: 0, scaleY: 1, opacity: 0.1, duration: 12, width: 2 },
{ y: 26, scaleY: 0.62, opacity: 0.13, duration: 16, width: 1.5 }, { y: 26, scaleY: 0.62, opacity: 0.07, duration: 16, width: 1.5 },
{ y: -22, scaleY: 1.35, opacity: 0.1, duration: 21, width: 1.5 }, { y: -22, scaleY: 1.35, opacity: 0.055, duration: 21, width: 1.5 },
]; ];
// Elegant voice waves behind the phone. Purely decorative, low opacity, and // Elegant voice waves behind the phone. Purely decorative, low opacity, and

View File

@@ -0,0 +1,264 @@
/* React Bits — BorderGlow (TS + Tailwind variant), vendored verbatim from
* https://reactbits.dev/components/border-glow
* Source: DavidHDev/react-bits · src/ts-tailwind/Components/BorderGlow/BorderGlow.tsx
*
* Kept unmodified so it can be re-synced from upstream. The intro sweep drives
* its own rAF loop from an effect, which trips the project's set-state-in-effect
* rule; disabled here rather than patching third-party code.
*/
/* eslint-disable react-hooks/set-state-in-effect */
import { useRef, useCallback, useState, useEffect, type ReactNode } from 'react';
interface BorderGlowProps {
children?: ReactNode;
className?: string;
edgeSensitivity?: number;
glowColor?: string;
backgroundColor?: string;
borderRadius?: number;
glowRadius?: number;
glowIntensity?: number;
coneSpread?: number;
animated?: boolean;
colors?: string[];
fillOpacity?: number;
}
function parseHSL(hslStr: string): { h: number; s: number; l: number } {
const match = hslStr.match(/([\d.]+)\s*([\d.]+)%?\s*([\d.]+)%?/);
if (!match) return { h: 40, s: 80, l: 80 };
return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };
}
function buildBoxShadow(glowColor: string, intensity: number): string {
const { h, s, l } = parseHSL(glowColor);
const base = `${h}deg ${s}% ${l}%`;
const layers: [number, number, number, number, number, boolean][] = [
[0, 0, 0, 1, 100, true], [0, 0, 1, 0, 60, true], [0, 0, 3, 0, 50, true],
[0, 0, 6, 0, 40, true], [0, 0, 15, 0, 30, true], [0, 0, 25, 2, 20, true],
[0, 0, 50, 2, 10, true],
[0, 0, 1, 0, 60, false], [0, 0, 3, 0, 50, false], [0, 0, 6, 0, 40, false],
[0, 0, 15, 0, 30, false], [0, 0, 25, 2, 20, false], [0, 0, 50, 2, 10, false],
];
return layers.map(([x, y, blur, spread, alpha, inset]) => {
const a = Math.min(alpha * intensity, 100);
return `${inset ? 'inset ' : ''}${x}px ${y}px ${blur}px ${spread}px hsl(${base} / ${a}%)`;
}).join(', ');
}
function easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }
function easeInCubic(x: number) { return x * x * x; }
interface AnimateOpts {
start?: number; end?: number; duration?: number; delay?: number;
ease?: (t: number) => number; onUpdate: (v: number) => void; onEnd?: () => void;
}
function animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: AnimateOpts) {
const t0 = performance.now() + delay;
function tick() {
const elapsed = performance.now() - t0;
const t = Math.min(elapsed / duration, 1);
onUpdate(start + (end - start) * ease(t));
if (t < 1) requestAnimationFrame(tick);
else if (onEnd) onEnd();
}
setTimeout(() => requestAnimationFrame(tick), delay);
}
const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];
const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];
function buildMeshGradients(colors: string[]): string[] {
const gradients: string[] = [];
for (let i = 0; i < 7; i++) {
const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
gradients.push(`radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`);
}
gradients.push(`linear-gradient(${colors[0]} 0 100%)`);
return gradients;
}
const BorderGlow: React.FC<BorderGlowProps> = ({
children,
className = '',
edgeSensitivity = 30,
glowColor = '40 80 80',
backgroundColor = '#120F17',
borderRadius = 28,
glowRadius = 40,
glowIntensity = 1.0,
coneSpread = 25,
animated = false,
colors = ['#c084fc', '#f472b6', '#38bdf8'],
fillOpacity = 0.5,
}) => {
const cardRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [cursorAngle, setCursorAngle] = useState(45);
const [edgeProximity, setEdgeProximity] = useState(0);
const [sweepActive, setSweepActive] = useState(false);
const getCenterOfElement = useCallback((el: HTMLElement) => {
const { width, height } = el.getBoundingClientRect();
return [width / 2, height / 2];
}, []);
const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
let kx = Infinity;
let ky = Infinity;
if (dx !== 0) kx = cx / Math.abs(dx);
if (dy !== 0) ky = cy / Math.abs(dy);
return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
}, [getCenterOfElement]);
const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
if (dx === 0 && dy === 0) return 0;
const radians = Math.atan2(dy, dx);
let degrees = radians * (180 / Math.PI) + 90;
if (degrees < 0) degrees += 360;
return degrees;
}, [getCenterOfElement]);
const handlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setEdgeProximity(getEdgeProximity(card, x, y));
setCursorAngle(getCursorAngle(card, x, y));
}, [getEdgeProximity, getCursorAngle]);
useEffect(() => {
if (!animated) return;
const angleStart = 110;
const angleEnd = 465;
setSweepActive(true);
setCursorAngle(angleStart);
animateValue({ duration: 500, onUpdate: v => setEdgeProximity(v / 100) });
animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => {
setCursorAngle((angleEnd - angleStart) * (v / 100) + angleStart);
}});
animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => {
setCursorAngle((angleEnd - angleStart) * (v / 100) + angleStart);
}});
animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,
onUpdate: v => setEdgeProximity(v / 100),
onEnd: () => setSweepActive(false),
});
}, [animated]);
const colorSensitivity = edgeSensitivity + 20;
const isVisible = isHovered || sweepActive;
const borderOpacity = isVisible
? Math.max(0, (edgeProximity * 100 - colorSensitivity) / (100 - colorSensitivity))
: 0;
const glowOpacity = isVisible
? Math.max(0, (edgeProximity * 100 - edgeSensitivity) / (100 - edgeSensitivity))
: 0;
const meshGradients = buildMeshGradients(colors);
const borderBg = meshGradients.map(g => `${g} border-box`);
const fillBg = meshGradients.map(g => `${g} padding-box`);
const angleDeg = `${cursorAngle.toFixed(3)}deg`;
return (
<div
ref={cardRef}
onPointerMove={handlePointerMove}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
className={`relative grid isolate border border-white/15 ${className}`}
style={{
background: backgroundColor,
borderRadius: `${borderRadius}px`,
transform: 'translate3d(0, 0, 0.01px)',
boxShadow: 'rgba(0,0,0,0.1) 0 1px 2px, rgba(0,0,0,0.1) 0 2px 4px, rgba(0,0,0,0.1) 0 4px 8px, rgba(0,0,0,0.1) 0 8px 16px, rgba(0,0,0,0.1) 0 16px 32px, rgba(0,0,0,0.1) 0 32px 64px',
}}
>
{/* mesh gradient border */}
<div
className="absolute inset-0 rounded-[inherit] -z-[1]"
style={{
border: '1px solid transparent',
background: [
`linear-gradient(${backgroundColor} 0 100%) padding-box`,
'linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box',
...borderBg,
].join(', '),
opacity: borderOpacity,
maskImage: `conic-gradient(from ${angleDeg} at center, black ${coneSpread}%, transparent ${coneSpread + 15}%, transparent ${100 - coneSpread - 15}%, black ${100 - coneSpread}%)`,
WebkitMaskImage: `conic-gradient(from ${angleDeg} at center, black ${coneSpread}%, transparent ${coneSpread + 15}%, transparent ${100 - coneSpread - 15}%, black ${100 - coneSpread}%)`,
transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out',
}}
/>
{/* mesh gradient fill near edges */}
<div
className="absolute inset-0 rounded-[inherit] -z-[1]"
style={{
border: '1px solid transparent',
background: fillBg.join(', '),
maskImage: [
'linear-gradient(to bottom, black, black)',
'radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%)',
'radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%)',
`conic-gradient(from ${angleDeg} at center, transparent 5%, black 15%, black 85%, transparent 95%)`,
].join(', '),
WebkitMaskImage: [
'linear-gradient(to bottom, black, black)',
'radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%)',
'radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%)',
'radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%)',
`conic-gradient(from ${angleDeg} at center, transparent 5%, black 15%, black 85%, transparent 95%)`,
].join(', '),
maskComposite: 'subtract, add, add, add, add, add',
WebkitMaskComposite: 'source-out, source-over, source-over, source-over, source-over, source-over',
opacity: borderOpacity * fillOpacity,
mixBlendMode: 'soft-light',
transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out',
} as React.CSSProperties}
/>
{/* outer glow */}
<span
className="absolute pointer-events-none z-[1] rounded-[inherit]"
style={{
inset: `${-glowRadius}px`,
maskImage: `conic-gradient(from ${angleDeg} at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%)`,
WebkitMaskImage: `conic-gradient(from ${angleDeg} at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%)`,
opacity: glowOpacity,
mixBlendMode: 'plus-lighter',
transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out',
} as React.CSSProperties}
>
<span
className="absolute rounded-[inherit]"
style={{
inset: `${glowRadius}px`,
boxShadow: buildBoxShadow(glowColor, glowIntensity),
}}
/>
</span>
<div className="flex flex-col relative overflow-auto z-[1]">
{children}
</div>
</div>
);
};
export default BorderGlow;

View File

@@ -0,0 +1,52 @@
"use client";
import { useRef, type PointerEvent, type ReactNode } from "react";
import { motion, useMotionValue, useSpring, useReducedMotion } from "framer-motion";
type MagneticButtonProps = {
children: ReactNode;
className?: string;
/** How far the content drifts toward the cursor, as a fraction of the offset. */
strength?: number;
};
/* React Bits — Magnetic Button, rebuilt on Framer Motion springs so it shares
* the easing feel of the rest of the Nearle motion system. Translation only, so
* it stays on the compositor; disabled under prefers-reduced-motion. */
export function MagneticButton({
children,
className = "",
strength = 0.3,
}: MagneticButtonProps) {
const reduce = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const springX = useSpring(x, { stiffness: 260, damping: 20, mass: 0.4 });
const springY = useSpring(y, { stiffness: 260, damping: 20, mass: 0.4 });
function handleMove(e: PointerEvent<HTMLSpanElement>) {
const el = ref.current;
if (!el || reduce) return;
const rect = el.getBoundingClientRect();
x.set((e.clientX - rect.left - rect.width / 2) * strength);
y.set((e.clientY - rect.top - rect.height / 2) * strength);
}
function reset() {
x.set(0);
y.set(0);
}
return (
<motion.span
ref={ref}
onPointerMove={handleMove}
onPointerLeave={reset}
style={reduce ? undefined : { x: springX, y: springY }}
className={`inline-flex ${className}`}
>
{children}
</motion.span>
);
}

View File

@@ -0,0 +1,52 @@
"use client";
import { useRef, type PointerEvent, type ReactNode } from "react";
import { useReducedMotion } from "framer-motion";
type SpotlightProps = {
children: ReactNode;
className?: string;
/** Diameter of the cursor wash, in px. */
size?: number;
color?: string;
};
/* React Bits — Spotlight. The pointer position is written straight to CSS
* custom properties, so the wash tracks at pointer rate without re-rendering
* React. The wrapper owns the card surface: it carries the background and
* radius, and the wash sits between that background and the content via a
* negative z-index inside the wrapper's own stacking context. */
export function Spotlight({
children,
className = "",
size = 360,
color = "rgba(168,85,247,0.16)",
}: SpotlightProps) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
function handleMove(e: PointerEvent<HTMLDivElement>) {
const el = ref.current;
if (!el || reduce) return;
const rect = el.getBoundingClientRect();
el.style.setProperty("--spot-x", `${e.clientX - rect.left}px`);
el.style.setProperty("--spot-y", `${e.clientY - rect.top}px`);
}
return (
<div
ref={ref}
onPointerMove={handleMove}
className={`group/spot relative isolate ${className}`}
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-[1] rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover/spot:opacity-100"
style={{
background: `radial-gradient(${size}px circle at var(--spot-x, 50%) var(--spot-y, 0px), ${color}, transparent 72%)`,
}}
/>
{children}
</div>
);
}

View File

@@ -65,7 +65,7 @@ export function WhyNearleHero() {
animate="visible" 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" 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? What is AI Commerce
</motion.span> </motion.span>
<motion.h1 <motion.h1

View File

@@ -1,7 +0,0 @@
[
{ "id": "groceries", "name": "Groceries", "icon": "shopping_basket" },
{ "id": "dairy", "name": "Dairy", "icon": "egg" },
{ "id": "snacks", "name": "Snacks", "icon": "cookie" },
{ "id": "household", "name": "Household", "icon": "cleaning_services" },
{ "id": "personalcare", "name": "Personal Care", "icon": "self_care" }
]

View File

@@ -1,34 +0,0 @@
[
{
"id": "breakfast",
"label": "Need breakfast",
"query": "Need breakfast",
"bundleName": "Breakfast Essentials",
"storeId": "kumar",
"productIds": ["milk", "bread", "butter", "eggs", "coffee"]
},
{
"id": "baby",
"label": "Need baby products",
"query": "Need baby products",
"bundleName": "Baby Care Bundle",
"storeId": "kumar",
"productIds": ["diapers", "wipes", "lotion", "powder"]
},
{
"id": "biryani",
"label": "Ingredients for biryani",
"query": "Ingredients for biryani",
"bundleName": "Biryani Ingredients",
"storeId": "kumar",
"productIds": ["basmati", "chickenmasala", "onions", "ghee", "curd"]
},
{
"id": "cleaning",
"label": "Need cleaning supplies",
"query": "Need cleaning supplies",
"bundleName": "Cleaning Kit",
"storeId": "kumar",
"productIds": ["floorcleaner", "dishwash", "detergent", "toiletcleaner"]
}
]

View File

@@ -1,23 +0,0 @@
[
{ "id": "milk", "name": "Aavin Milk", "emoji": "🥛", "price": 40, "unit": "500 ml", "categoryId": "dairy" },
{ "id": "bread", "name": "Sandwich Bread", "emoji": "🍞", "price": 45, "unit": "400 g", "categoryId": "groceries" },
{ "id": "butter", "name": "Table Butter", "emoji": "🧈", "price": 90, "unit": "100 g", "categoryId": "dairy" },
{ "id": "eggs", "name": "Farm Eggs", "emoji": "🥚", "price": 80, "unit": "6 pcs", "categoryId": "dairy" },
{ "id": "coffee", "name": "Filter Coffee", "emoji": "☕", "price": 165, "unit": "500 g", "categoryId": "groceries" },
{ "id": "diapers", "name": "Baby Diapers", "emoji": "🧷", "price": 399, "unit": "Pack of 30", "categoryId": "personalcare" },
{ "id": "wipes", "name": "Baby Wipes", "emoji": "🧻", "price": 99, "unit": "72 pcs", "categoryId": "personalcare" },
{ "id": "lotion", "name": "Baby Lotion", "emoji": "🍼", "price": 149, "unit": "200 ml", "categoryId": "personalcare" },
{ "id": "powder", "name": "Baby Powder", "emoji": "🧴", "price": 85, "unit": "200 g", "categoryId": "personalcare" },
{ "id": "basmati", "name": "Basmati Rice", "emoji": "🍚", "price": 128, "unit": "1 kg", "categoryId": "groceries" },
{ "id": "chickenmasala", "name": "Biryani Masala", "emoji": "🌶️", "price": 60, "unit": "50 g", "categoryId": "groceries" },
{ "id": "onions", "name": "Onions", "emoji": "🧅", "price": 40, "unit": "1 kg", "categoryId": "groceries" },
{ "id": "ghee", "name": "Pure Ghee", "emoji": "🫕", "price": 320, "unit": "500 ml", "categoryId": "dairy" },
{ "id": "curd", "name": "Fresh Curd", "emoji": "🥣", "price": 40, "unit": "400 g", "categoryId": "dairy" },
{ "id": "floorcleaner", "name": "Floor Cleaner", "emoji": "🧽", "price": 99, "unit": "1 L", "categoryId": "household" },
{ "id": "dishwash", "name": "Dishwash Gel", "emoji": "🧼", "price": 55, "unit": "750 ml", "categoryId": "household" },
{ "id": "detergent", "name": "Detergent Powder", "emoji": "🧺", "price": 199, "unit": "2 kg", "categoryId": "household" },
{ "id": "toiletcleaner", "name": "Toilet Cleaner", "emoji": "🚽", "price": 89, "unit": "500 ml", "categoryId": "household" }
]

View File

@@ -1,6 +0,0 @@
[
{ "id": "kumar", "name": "Kumar Super Market", "icon": "storefront", "area": "R.S. Puram" },
{ "id": "freshmart", "name": "Fresh Mart", "icon": "storefront", "area": "Gandhipuram" },
{ "id": "murugan", "name": "Sri Murugan Store", "icon": "storefront", "area": "Saibaba Colony" },
{ "id": "aavin", "name": "Aavin Booth", "icon": "storefront", "area": "Peelamedu" }
]

View File

@@ -116,9 +116,10 @@ export function useMerchantJourney(): MerchantJourney {
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [autoplaying, setAutoplaying] = useState(false); const [autoplaying, setAutoplaying] = useState(true);
const last = MERCHANT_STEPS.length - 1; const total = MERCHANT_STEPS.length;
const last = total - 1;
const timer = useRef<ReturnType<typeof setTimeout> | null>(null); const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const clear = useCallback(() => { const clear = useCallback(() => {
if (timer.current) clearTimeout(timer.current); if (timer.current) clearTimeout(timer.current);
@@ -126,50 +127,49 @@ export function useMerchantJourney(): MerchantJourney {
}, []); }, []);
useEffect(() => clear, [clear]); useEffect(() => clear, [clear]);
// Auto-advance one stage at a time; simply stop scheduling (don't loop) at the // Continuously advance to next slide every 3.2s
// end so the journey rests on the final stage. No synchronous setState here.
useEffect(() => { useEffect(() => {
if (!started || reduced || !autoplaying || index >= last) return; if (reduced || !autoplaying) return;
timer.current = setTimeout(() => setIndex((i) => Math.min(i + 1, last)), AUTOPLAY_MS); timer.current = setTimeout(() => {
setIndex((i) => (i + 1) % total);
}, AUTOPLAY_MS);
return clear; return clear;
}, [started, reduced, autoplaying, index, last, clear]); }, [reduced, autoplaying, index, total, clear]);
const start = useCallback(() => { const start = useCallback(() => {
setStarted((was) => { setStarted(true);
if (!was && !reduced) setAutoplaying(true); if (!reduced) setAutoplaying(true);
return true;
});
}, [reduced]); }, [reduced]);
const goTo = useCallback( const goTo = useCallback(
(i: number) => { (i: number) => {
clear(); clear();
setAutoplaying(false);
setStarted(true); setStarted(true);
setIndex(Math.max(0, Math.min(i, last))); setIndex(Math.max(0, Math.min(i, last)));
if (!reduced) setAutoplaying(true);
}, },
[clear, last] [clear, last, reduced]
); );
const next = useCallback(() => { const next = useCallback(() => {
clear(); clear();
setAutoplaying(false);
setStarted(true); setStarted(true);
setIndex((i) => Math.min(i + 1, last)); setIndex((i) => (i + 1) % total);
}, [clear, last]); if (!reduced) setAutoplaying(true);
}, [clear, total, reduced]);
const prev = useCallback(() => { const prev = useCallback(() => {
clear(); clear();
setAutoplaying(false);
setStarted(true); setStarted(true);
setIndex((i) => Math.max(i - 1, 0)); setIndex((i) => (i - 1 + total) % total);
}, [clear]); if (!reduced) setAutoplaying(true);
}, [clear, total, reduced]);
const replay = useCallback(() => { const replay = useCallback(() => {
clear(); clear();
setIndex(0); setIndex(0);
setStarted(true); setStarted(true);
setAutoplaying(!reduced); if (!reduced) setAutoplaying(true);
}, [clear, reduced]); }, [clear, reduced]);
return { return {

View File

@@ -0,0 +1,23 @@
"use client";
import { useState, useEffect } from "react";
export function useRealTimeClock(): string {
const [timeString, setTimeString] = useState<string>("");
useEffect(() => {
const updateClock = () => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const formattedMins = minutes < 10 ? `0${minutes}` : minutes;
setTimeString(`${hours}:${formattedMins}`);
};
updateClock();
const interval = setInterval(updateClock, 1000);
return () => clearInterval(interval);
}, []);
return timeString;
}

View File

@@ -1,244 +0,0 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useReducedMotion } from "framer-motion";
import {
getIntents,
getMyStores,
getRecommendation,
getStore,
} from "@/lib/experience/search";
import type { Intent, Recommendation, Store } from "@/types/experience";
// The seven-stage story a visitor watches (and can drive) to understand Nearle:
// choose a trusted store → walk in → search by intent → AI resolves it →
// see in-store recommendations → cart → merchant packs & rider delivers.
export type JourneyStage =
| "choose-store"
| "enter-store"
| "search"
| "thinking"
| "results"
| "cart"
| "fulfilment"
| "done";
export const STAGE_ORDER: JourneyStage[] = [
"choose-store",
"enter-store",
"search",
"thinking",
"results",
"cart",
"fulfilment",
"done",
];
export const AI_STEPS = [
"Understanding your shopping intent…",
"Finding matching products…",
"Checking availability…",
"Preparing recommendations…",
] as const;
export const FULFILMENT_STEPS = [
{ label: "Order sent", icon: "send" },
{ label: "Merchant receives order", icon: "storefront" },
{ label: "Products packed", icon: "inventory_2" },
{ label: "Nearle rider assigned", icon: "two_wheeler" },
{ label: "Delivered", icon: "check_circle" },
] as const;
// Pacing for the auto-play. Tuned to feel like a real person using the app —
// deliberate, with a beat of stillness between stages — not a slideshow.
// Full run lands around 1822s (varies with the typed query length).
const TIMING = {
chooseStoreHold: 950, // hold on the highlighted store
enterStoreHold: 1400, // fade into the store, then settle
typeCharMs: 95, // per-character typing cadence
afterTypeHold: 700, // pause after typing before AI thinks
thinkStepMs: 1000, // gap between each AI thought (~ChatGPT pace)
thinkFinalHold: 1200, // dwell on the last thought before results
resultsHold: 2900, // let products fade in one by one, then hold
cartHold: 2600, // read the basket + total before checkout
fulfilStepMs: 1050, // each merchant/rider beat stays readable
} as const;
const STORES = getMyStores();
const INTENTS = getIntents();
export type StoreJourney = {
stage: JourneyStage;
stores: Store[];
intents: Intent[];
store: Store;
intent: Intent;
recommendation: Recommendation | null;
typed: string;
thinkingStep: number;
fulfilStep: number;
reduced: boolean;
start: () => void;
selectStore: (id: string) => void;
pickIntent: (id: string) => void;
replay: () => void;
};
// One shared state machine drives the whole journey. Auto-advances via timers
// once started (on scroll-into-view), but store cards and intent chips can
// interrupt it at any point. Under reduced-motion it settles on the end state.
export function useStoreJourney(): StoreJourney {
const reduce = useReducedMotion() ?? false;
const [started, setStarted] = useState(false);
const [stage, setStage] = useState<JourneyStage>("choose-store");
const [storeId, setStoreId] = useState(STORES[0]?.id ?? "");
const [intentId, setIntentId] = useState(INTENTS[0]?.id ?? "");
const [typed, setTyped] = useState("");
const [thinkingStep, setThinkingStep] = useState(0);
const [fulfilStep, setFulfilStep] = useState(0);
const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
const intervals = useRef<ReturnType<typeof setInterval>[]>([]);
const clearAll = useCallback(() => {
timers.current.forEach(clearTimeout);
intervals.current.forEach(clearInterval);
timers.current = [];
intervals.current = [];
}, []);
useEffect(() => clearAll, [clearAll]);
const store = useMemo(
() => getStore(storeId) ?? STORES[0],
[storeId]
);
const intent = useMemo(
() => INTENTS.find((i) => i.id === intentId) ?? INTENTS[0],
[intentId]
);
const recommendation = useMemo<Recommendation | null>(() => {
const base = getRecommendation(intentId);
// Honour the store the visitor actually picked for the display.
return base ? { ...base, store } : null;
}, [intentId, store]);
// ── Auto-play orchestration (skipped under reduced motion) ──
useEffect(() => {
if (!started || reduce) return;
clearAll();
const push = (t: ReturnType<typeof setTimeout>) => timers.current.push(t);
switch (stage) {
case "choose-store":
// Let the highlighted store register before walking in.
push(setTimeout(() => setStage("enter-store"), TIMING.chooseStoreHold));
break;
case "enter-store":
// Smooth fade into the store, then a beat to take it in.
push(setTimeout(() => setStage("search"), TIMING.enterStoreHold));
break;
case "search": {
setTyped("");
const q = intent.query;
let i = 0;
const iv = setInterval(() => {
i += 1;
setTyped(q.slice(0, i));
if (i >= q.length) {
clearInterval(iv);
// Pause after typing before the AI starts thinking.
push(setTimeout(() => setStage("thinking"), TIMING.afterTypeHold));
}
}, TIMING.typeCharMs);
intervals.current.push(iv);
break;
}
case "thinking":
// Reveal each thought one at a time, ChatGPT-style (~4s total).
setThinkingStep(0);
for (let s = 1; s < AI_STEPS.length; s += 1) {
push(setTimeout(() => setThinkingStep(s), TIMING.thinkStepMs * s));
}
push(
setTimeout(
() => setStage("results"),
TIMING.thinkStepMs * (AI_STEPS.length - 1) + TIMING.thinkFinalHold
)
);
break;
case "results":
// Products fade in one by one (staggered in the canvas), then hold.
push(setTimeout(() => setStage("cart"), TIMING.resultsHold));
break;
case "cart":
push(setTimeout(() => setStage("fulfilment"), TIMING.cartHold));
break;
case "fulfilment":
// Each fulfilment beat stays visible long enough to read.
setFulfilStep(0);
for (let s = 1; s < FULFILMENT_STEPS.length; s += 1) {
push(setTimeout(() => setFulfilStep(s), TIMING.fulfilStepMs * s));
}
push(
setTimeout(
() => setStage("done"),
TIMING.fulfilStepMs * FULFILMENT_STEPS.length
)
);
break;
default:
break;
}
return clearAll;
}, [stage, started, reduce, intentId, intent.query, clearAll]);
// ── Reduced motion: settle directly on the completed end state ──
useEffect(() => {
if (!started || !reduce) return;
setTyped(intent.query);
setThinkingStep(AI_STEPS.length - 1);
setFulfilStep(FULFILMENT_STEPS.length - 1);
setStage("done");
}, [started, reduce, intentId, intent.query]);
const start = useCallback(() => setStarted(true), []);
const selectStore = useCallback((id: string) => {
setStarted(true);
setStoreId(id);
setStage((s) => (s === "choose-store" ? "enter-store" : s));
}, []);
const pickIntent = useCallback((id: string) => {
setStarted(true);
setIntentId(id);
setStage("search");
}, []);
const replay = useCallback(() => {
clearAll();
setTyped("");
setThinkingStep(0);
setFulfilStep(0);
setStage("choose-store");
setStarted(true);
}, [clearAll]);
return {
stage,
stores: STORES,
intents: INTENTS,
store,
intent,
recommendation,
typed,
thinkingStep,
fulfilStep,
reduced: reduce,
start,
selectStore,
pickIntent,
replay,
};
}

View File

@@ -1,59 +0,0 @@
// The ONLY module the store-journey UI imports for data.
//
// Everything here is powered by local dummy JSON today. To go live, swap each
// function body for a real API call and keep these exported signatures
// unchanged — the UI never needs to change.
import storesData from "@/data/experience/stores.json";
import categoriesData from "@/data/experience/categories.json";
import productsData from "@/data/experience/products.json";
import intentsData from "@/data/experience/intents.json";
import type { Category, Intent, Product, Recommendation, Store } from "@/types/experience";
const stores = storesData as Store[];
const categories = categoriesData as Category[];
const products = productsData as Product[];
const intents = intentsData as Intent[];
/** The stores a customer has already added — their "My Stores" list. */
export function getMyStores(): Store[] {
return stores;
}
export function getStore(id: string): Store | undefined {
return stores.find((s) => s.id === id);
}
/** The in-store shelves shown after entering a store. */
export function getCategories(): Category[] {
return categories;
}
/** The natural-language shopping intents offered as search chips. */
export function getIntents(): Intent[] {
return intents;
}
/**
* Resolve a shopping intent into a store-scoped bundle — the products Nearle AI
* recommends, joined with their store, plus the cart totals.
*/
export function getRecommendation(intentId: string): Recommendation | null {
const intent = intents.find((i) => i.id === intentId);
if (!intent) return null;
const store = getStore(intent.storeId);
if (!store) return null;
const items = intent.productIds
.map((pid) => products.find((p) => p.id === pid))
.filter((p): p is Product => Boolean(p));
return {
intent,
store,
products: items,
itemCount: items.length,
total: items.reduce((sum, p) => sum + p.price, 0),
};
}

View File

@@ -1,56 +1,11 @@
// Shared types for the "Digital Neighbourhood Store" homepage journey. // Shared types for the homepage voice commerce demo.
// These mirror the shape of the dummy JSON in src/data/experience so the files // These mirror the shape of the dummy JSON in src/data/experience so the files
// can later be replaced by real API responses without touching the UI. // can later be replaced by real API responses without touching the UI.
// A store the customer has already added (via QR onboarding) — their "My Stores". // ─── Voice commerce (homepage product demo) ───
export type Store = { // What Nearle AI resolves a spoken request into: the merchant is discovered
id: string; // *by the AI* after it understands the request — the customer never picks a
name: string; // store first.
icon: string; // Material Symbol name
area: string;
};
// A shelf/category inside a store.
export type Category = {
id: string;
name: string;
icon: string;
};
// A single product on a store's digital shelf.
export type Product = {
id: string;
name: string;
emoji: string;
price: number;
unit: string;
categoryId: string;
};
// A natural-language shopping intent ("Need breakfast") mapped to a bundle of
// products inside one store — what Nearle AI resolves a search into.
export type Intent = {
id: string;
label: string; // "Need breakfast"
query: string; // what gets typed into the search bar
bundleName: string; // "Breakfast Essentials"
storeId: string;
productIds: string[];
};
// An intent resolved against its store — the shape the journey UI renders.
export type Recommendation = {
intent: Intent;
store: Store;
products: Product[];
itemCount: number;
total: number;
};
// ─── Voice commerce (homepage hero demo) ───
// What Nearle AI resolves a spoken request into. Unlike the store journey above,
// the merchant is discovered *by the AI* after it understands the request —
// the customer never picks a store first.
// Nearle covers products and local services, not only grocery shopping. // Nearle covers products and local services, not only grocery shopping.
export type VoiceDomain = "shopping" | "services" | "healthcare"; export type VoiceDomain = "shopping" | "services" | "healthcare";