update why nearle page
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
|
||||
@@ -415,12 +416,29 @@ const CARDS = [
|
||||
export function BusinessFeaturesGrid() {
|
||||
return (
|
||||
<section
|
||||
className="bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
|
||||
className="relative bg-[#FAF7FD] border-b border-[#6330D6]/10 overflow-hidden"
|
||||
style={{ paddingTop: "clamp(44px, 4.5vw, 64px)", paddingBottom: "clamp(44px, 4.5vw, 64px)" }}
|
||||
>
|
||||
{/* Decorative mascot watermark — left side only, never crosses into the feature cards */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-y-0 left-0 z-0 flex items-center select-none"
|
||||
style={{ width: "clamp(360px, 34vw, 600px)" }}
|
||||
>
|
||||
<div className="relative h-[88%] w-full -translate-x-[2%]">
|
||||
<Image
|
||||
src="/superhero.png"
|
||||
alt=""
|
||||
fill
|
||||
sizes="600px"
|
||||
className="object-contain object-left opacity-[0.12] blur-[1px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Centered page container */}
|
||||
<div
|
||||
className="mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
|
||||
className="relative z-10 mx-auto w-[calc(100%-48px)] md:w-[calc(100%-64px)] xl:w-[calc(100%-96px)]"
|
||||
style={{ maxWidth: 1800 }}
|
||||
>
|
||||
{/* Main two-column grid */}
|
||||
@@ -439,7 +457,7 @@ export function BusinessFeaturesGrid() {
|
||||
{/* Badge + Heading */}
|
||||
<motion.div variants={fadeUp}>
|
||||
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
|
||||
Our features
|
||||
MerchantOS
|
||||
</span>
|
||||
<h2
|
||||
className="font-display font-black text-[#16001D] mt-4"
|
||||
|
||||
@@ -8,8 +8,8 @@ import { fadeUp, fadeIn, scaleIn, staggerContainer, viewport } from "@/lib/anima
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
title: "Digital Catalogue",
|
||||
desc: "Showcase your products online and keep your catalogue updated.",
|
||||
title: "AI Catalogue",
|
||||
desc: "Turn your shelf into a live online catalogue with ProductLLM — no manual data entry.",
|
||||
icon: "menu_book",
|
||||
visual: "/images/nearle-merchant/catalogue-ui.webp",
|
||||
alt: "Clean grocery catalogue interface preview",
|
||||
|
||||
@@ -82,19 +82,16 @@ export function StoreQRSection() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Download Nearle on the App Store"
|
||||
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
|
||||
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
|
||||
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
|
||||
</svg>
|
||||
<div className="text-left leading-tight">
|
||||
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
|
||||
App Store
|
||||
</div>
|
||||
<div className="text-sm font-black">
|
||||
Available on
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/apple-download.svg"
|
||||
alt="Download Nearle on the App Store"
|
||||
width={144}
|
||||
height={60}
|
||||
className="h-[60px] w-auto object-contain"
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -102,19 +99,15 @@ export function StoreQRSection() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Download Nearle on Google Play"
|
||||
className="h-[60px] border border-white/20 rounded-2xl bg-white/10 text-white/60 px-8 py-2 flex items-center gap-3.5 shrink-0 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
|
||||
className="h-[60px] flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-7 h-7 shrink-0 fill-current opacity-60" fill="currentColor">
|
||||
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
|
||||
</svg>
|
||||
<div className="text-left leading-tight">
|
||||
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
|
||||
Google Play
|
||||
</div>
|
||||
<div className="text-sm font-black">
|
||||
Available on
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/play-download.svg"
|
||||
alt="Download Nearle on Google Play"
|
||||
width={155}
|
||||
height={60}
|
||||
className="h-[60px] w-auto object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -264,19 +257,15 @@ export function StoreQRSection() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Download Nearle on the App Store"
|
||||
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
|
||||
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
|
||||
<path d="M17.537 12.625c-.023-2.949 2.398-4.37 2.506-4.44-1.364-2.008-3.48-2.282-4.227-2.307-1.79-.186-3.504 1.064-4.413 1.064-.918 0-2.323-1.04-3.827-1.01-1.963.028-3.788 1.146-4.796 2.91-2.06 3.587-.526 8.9 1.466 11.808.98 1.428 2.147 3.035 3.685 2.975 1.47-.059 2.03-.964 3.807-.964 1.767 0 2.266.964 3.818.932 1.58-.03 2.58-1.453 3.536-2.89 1.116-1.628 1.57-3.21 1.596-3.29-.035-.016-3.057-1.18-3.087-4.678l.036-.002zM14.87 4.058c.821-.99 1.375-2.361 1.223-3.733-1.184.05-2.618.784-3.47 1.77-.762.875-1.43 2.28-1.25 3.623 1.327.102 2.673-.668 3.497-1.66z" />
|
||||
</svg>
|
||||
<div className="text-left leading-tight">
|
||||
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
|
||||
App Store
|
||||
</div>
|
||||
<div className="text-sm font-black">
|
||||
Available on
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/apple-download.svg"
|
||||
alt="Download Nearle on the App Store"
|
||||
width={134}
|
||||
height={56}
|
||||
className="h-14 w-auto object-contain"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -284,19 +273,15 @@ export function StoreQRSection() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Download Nearle on Google Play"
|
||||
className="h-14 w-60 sm:w-auto border border-white/20 rounded-2xl bg-white/10 text-white/60 px-6 py-2 flex items-center justify-center gap-3 cursor-pointer select-none hover:bg-white/[0.16] transition-all duration-300 active:scale-[0.98]"
|
||||
className="h-14 flex items-center shrink-0 cursor-pointer select-none transition-all duration-300 hover:scale-[1.03] active:scale-[0.98]"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-6 h-6 shrink-0 fill-current opacity-60" fill="currentColor">
|
||||
<path d="M3.609 1.814L13.414 12 3.609 22.186c-.244-.178-.395-.466-.395-.786V2.6c0-.32.151-.608.395-.786zM14.244 12.83l2.705 2.688-4.073 2.276-4.073 2.276L14.244 12.83zm1-1.66l2.838-2.8 4.153 2.336c.464.26.464.688 0 .948l-4.153 2.336-2.838-2.82zM8.803 4.93l4.073 2.276 4.073 2.276-2.705 2.688L8.803 4.93z" />
|
||||
</svg>
|
||||
<div className="text-left leading-tight">
|
||||
<div className="text-[10px] font-bold tracking-wider uppercase opacity-60">
|
||||
Google Play
|
||||
</div>
|
||||
<div className="text-sm font-black">
|
||||
Available on
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/play-download.svg"
|
||||
alt="Download Nearle on Google Play"
|
||||
width={145}
|
||||
height={56}
|
||||
className="h-14 w-auto object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
102
src/components/home/WhyAICommerceWins.tsx
Normal file
102
src/components/home/WhyAICommerceWins.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
title: "Neighbourhood Trust",
|
||||
desc: "Every order is fulfilled by a real local shop your street already knows — not an anonymous warehouse.",
|
||||
icon: "handshake",
|
||||
color: "bg-primary/5 text-primary border-primary/10",
|
||||
},
|
||||
{
|
||||
title: "AI-Powered Catalog",
|
||||
desc: "ProductLLM turns a shelf into a live, shoppable catalog in minutes — so any store can go online without the busywork.",
|
||||
icon: "auto_awesome",
|
||||
color: "bg-secondary/5 text-secondary border-secondary/10",
|
||||
},
|
||||
{
|
||||
title: "Offline-First Commerce",
|
||||
desc: "Billing and inventory keep working even when the network drops. Built for how neighbourhood stores really run.",
|
||||
icon: "wifi_off",
|
||||
color: "bg-[#de96f9]/10 text-primary border-[#de96f9]/20",
|
||||
},
|
||||
];
|
||||
|
||||
export function WhyAICommerceWins() {
|
||||
return (
|
||||
<section className="section bg-white w-full overflow-hidden">
|
||||
<div className="page-container relative">
|
||||
<motion.div
|
||||
className="relative text-center mb-8 md:mb-10"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<span className="text-xs text-primary uppercase tracking-widest font-black bg-primary/5 px-3.5 py-1.5 rounded-full">
|
||||
AI Commerce
|
||||
</span>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-[54px] font-display font-black text-brand-dark mt-3 leading-tight tracking-tight">
|
||||
Why AI Commerce <span className="text-primary font-normal">Wins</span>
|
||||
</h2>
|
||||
<p className="text-body-lg text-on-surface-variant mt-2.5 max-w-xl mx-auto leading-relaxed">
|
||||
Nearle isn’t another delivery app. It’s the AI layer that brings
|
||||
neighbourhood stores online — without replacing them.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 lg:gap-7"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{CARDS.map((card) => (
|
||||
<motion.div
|
||||
key={card.title}
|
||||
variants={fadeIn}
|
||||
className="premium-card bg-white rounded-3xl p-6 md:p-7 border border-outline-variant/15 shadow-lg flex flex-col h-full"
|
||||
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
|
||||
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
|
||||
<MaterialIcon icon={card.icon} size={27} />
|
||||
</div>
|
||||
<p className="font-display text-xl font-black text-brand-dark mb-3">
|
||||
{card.title}
|
||||
</p>
|
||||
<p className="text-sm md:text-base text-on-surface-variant leading-relaxed">
|
||||
{card.desc}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="mt-9 flex justify-center"
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<Link
|
||||
href="/why-nearle"
|
||||
className="group inline-flex items-center gap-2 text-base font-black text-primary transition-colors hover:text-[#4C1D95]"
|
||||
>
|
||||
Learn Why Nearle
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
size={20}
|
||||
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
motion,
|
||||
@@ -14,11 +14,20 @@ import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const NAV_LINKS = [
|
||||
type NavChild = { label: string; href: string };
|
||||
type NavLink = { label: string; href: string; children?: NavChild[] };
|
||||
|
||||
const SOLUTIONS_CHILDREN: NavChild[] = [
|
||||
{ label: "For Business", href: "/businesses" },
|
||||
{ label: "For People", href: "/people" },
|
||||
{ label: "For D2C Brands", href: "/brands" },
|
||||
];
|
||||
|
||||
const NAV_LINKS: NavLink[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Nearle for Business", href: "/businesses" },
|
||||
{ label: "Nearle for People", href: "/people" },
|
||||
{ label: "Nearle for D2C Brands", href: "/brands" },
|
||||
{ label: "Why Nearle", href: "/why-nearle" },
|
||||
{ label: "Platform", href: "/platform" },
|
||||
{ label: "Solutions", href: "/businesses", children: SOLUTIONS_CHILDREN },
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
];
|
||||
@@ -47,11 +56,44 @@ const mobileItemVariants = {
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
|
||||
};
|
||||
|
||||
const dropdownVariants = {
|
||||
hidden: { opacity: 0, y: 8, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0.22, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: 6,
|
||||
scale: 0.98,
|
||||
transition: { duration: 0.15, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
const mobileSubmenuVariants = {
|
||||
hidden: { opacity: 0, height: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
transition: { duration: 0.26, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transition: { duration: 0.18, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
export function PremiumNavbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [mobileSolutionsOpen, setMobileSolutionsOpen] = useState(false);
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { scrollY } = useScroll();
|
||||
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (y) => {
|
||||
setScrolled(y > 20);
|
||||
@@ -62,6 +104,28 @@ export function PremiumNavbar() {
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const isGroupActive = (link: NavLink) => {
|
||||
if (link.children) {
|
||||
return link.children.some((c) => isActiveLink(c.href));
|
||||
}
|
||||
return isActiveLink(link.href);
|
||||
};
|
||||
|
||||
const openMenu = (label: string) => {
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
setOpenDropdown(label);
|
||||
};
|
||||
|
||||
const scheduleClose = () => {
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
closeTimer.current = setTimeout(() => setOpenDropdown(null), 120);
|
||||
};
|
||||
|
||||
const closeMobile = () => {
|
||||
setMobileOpen(false);
|
||||
setMobileSolutionsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
|
||||
@@ -91,9 +155,82 @@ export function PremiumNavbar() {
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-10">
|
||||
<nav className="hidden flex-1 items-center justify-center gap-4 lg:flex min-[1440px]:gap-9">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = isActiveLink(link.href);
|
||||
const isActive = isGroupActive(link);
|
||||
|
||||
if (link.children) {
|
||||
const isOpen = openDropdown === link.label;
|
||||
return (
|
||||
<div
|
||||
key={link.label}
|
||||
className="relative py-2"
|
||||
onMouseEnter={() => openMenu(link.label)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
|
||||
isActive ? "text-white" : "text-white/70 hover:text-white"
|
||||
}`}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => setOpenDropdown(isOpen ? null : link.label)}
|
||||
onFocus={() => openMenu(link.label)}
|
||||
>
|
||||
{link.label}
|
||||
<motion.span
|
||||
className="flex"
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
>
|
||||
<MaterialIcon icon="expand_more" size={16} />
|
||||
</motion.span>
|
||||
</button>
|
||||
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="activeNavUnderline"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
variants={dropdownVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="absolute left-1/2 top-full z-50 mt-2 w-56 -translate-x-1/2 overflow-hidden rounded-2xl border border-white/12 bg-[#30226F]/98 p-1.5 shadow-[0_18px_45px_rgba(20,0,30,0.45)] backdrop-blur-xl"
|
||||
role="menu"
|
||||
>
|
||||
{link.children.map((child) => {
|
||||
const childActive = isActiveLink(child.href);
|
||||
return (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
role="menuitem"
|
||||
onClick={() => setOpenDropdown(null)}
|
||||
className={`block rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
|
||||
childActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/75 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={link.label}
|
||||
@@ -164,12 +301,72 @@ export function PremiumNavbar() {
|
||||
animate="visible"
|
||||
>
|
||||
{NAV_LINKS.map((link) => {
|
||||
if (link.children) {
|
||||
const groupActive = isGroupActive(link);
|
||||
return (
|
||||
<motion.div key={link.label} variants={mobileItemVariants}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileSolutionsOpen((v) => !v)}
|
||||
aria-expanded={mobileSolutionsOpen}
|
||||
className={`flex w-full items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
groupActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
<motion.span
|
||||
className="flex"
|
||||
animate={{ rotate: mobileSolutionsOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
>
|
||||
<MaterialIcon icon="expand_more" size={20} />
|
||||
</motion.span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{mobileSolutionsOpen && (
|
||||
<motion.div
|
||||
variants={mobileSubmenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-1 flex flex-col gap-1 pl-3">
|
||||
{link.children.map((child) => {
|
||||
const childActive = isActiveLink(child.href);
|
||||
return (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
onClick={closeMobile}
|
||||
className={`flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-bold transition-all duration-200 ${
|
||||
childActive
|
||||
? "bg-white/12 text-white"
|
||||
: "text-white/60 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<span className="h-1 w-1 rounded-full bg-white/40" />
|
||||
{child.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = isActiveLink(link.href);
|
||||
return (
|
||||
<motion.div key={link.label} variants={mobileItemVariants}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
onClick={closeMobile}
|
||||
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-white/15 text-white"
|
||||
|
||||
78
src/components/platform/ConsumerShowcase.tsx
Normal file
78
src/components/platform/ConsumerShowcase.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { PhoneMockup } from "@/components/platform/DeviceShowcase";
|
||||
|
||||
/**
|
||||
* ConsumerOS showcase — a premium, sequential product demo.
|
||||
*
|
||||
* Only ONE illustration is ever visible at a time:
|
||||
* 1. The ConsumerOS phone mockup, held completely static for PHONE_DURATION.
|
||||
* 2. A premium fade + horizontal-slide transition (TRANSITION_DURATION).
|
||||
* 3. The Food.svg artwork, held static so its OWN internal SVG animation can
|
||||
* play through uninterrupted. The wrapper never floats or scales.
|
||||
* 4. The same transition back to the phone. Repeats infinitely.
|
||||
*
|
||||
* The PhoneMockup artwork and the Food.svg artwork are used untouched.
|
||||
*/
|
||||
|
||||
// Per-illustration display durations (configurable).
|
||||
const PHONE_DURATION = 6000; // ConsumerOS held static
|
||||
// Food.svg's internal SMIL animation is a continuous loop of dur="13.467s"
|
||||
// (repeatCount="indefinite"). We hold it for exactly one full loop so the art
|
||||
// returns to its opening frame before we transition — never cut mid-animation.
|
||||
const FOOD_DURATION = 13467;
|
||||
const TRANSITION_DURATION = 700; // premium fade + slide, 600–800ms
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
function FoodArt() {
|
||||
return (
|
||||
// Re-mounts on each cycle (keyed AnimatePresence) so the SVG's internal
|
||||
// animation restarts and plays from the beginning every time it appears.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src="/Food.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none h-auto w-full max-w-[240px] select-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PhoneArt() {
|
||||
return (
|
||||
<div className="w-full max-w-[220px]">
|
||||
<PhoneMockup />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConsumerShowcase() {
|
||||
const reduce = useReducedMotion();
|
||||
const [index, setIndex] = useState(0); // 0 = ConsumerOS phone, 1 = Food.svg
|
||||
|
||||
useEffect(() => {
|
||||
if (reduce) return; // Reduced motion: hold the phone, never cycle.
|
||||
const delay = index === 0 ? PHONE_DURATION : FOOD_DURATION;
|
||||
const id = setTimeout(() => setIndex((i) => (i === 0 ? 1 : 0)), delay);
|
||||
return () => clearTimeout(id);
|
||||
}, [index, reduce]);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[300px] w-full items-center justify-center overflow-hidden sm:h-[320px] md:h-[360px] lg:h-[400px]">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: 28 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -28 }}
|
||||
transition={{ duration: TRANSITION_DURATION / 1000, ease: EASE }}
|
||||
className="flex w-full items-center justify-center"
|
||||
>
|
||||
{index === 0 ? <PhoneArt /> : <FoodArt />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
297
src/components/platform/DeviceShowcase.tsx
Normal file
297
src/components/platform/DeviceShowcase.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
|
||||
/**
|
||||
* Large animated device illustrations used to replace the flat feature grids.
|
||||
* - DashboardMockup → MerchantOS (a live store dashboard)
|
||||
* - PhoneMockup → ConsumerOS (the neighbourhood shopping app)
|
||||
*
|
||||
* Purely decorative. GPU-friendly (opacity / transform / pathLength only)
|
||||
* and fully reduced-motion aware.
|
||||
*/
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const rise = {
|
||||
hidden: { opacity: 0, y: 16 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
|
||||
};
|
||||
|
||||
const pop = {
|
||||
hidden: { opacity: 0, scale: 0.9 },
|
||||
visible: { opacity: 1, scale: 1, transition: { duration: 0.5, ease: EASE } },
|
||||
};
|
||||
|
||||
const draw = {
|
||||
hidden: { pathLength: 0, opacity: 0 },
|
||||
visible: { pathLength: 1, opacity: 1, transition: { duration: 1.5, ease: EASE } },
|
||||
};
|
||||
|
||||
const growBar = {
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 0.7, ease: EASE } },
|
||||
};
|
||||
|
||||
const svgContainer = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } },
|
||||
};
|
||||
|
||||
// ─── MerchantOS — store dashboard ────────────────────────────────────────────
|
||||
|
||||
export function DashboardMockup() {
|
||||
const reduce = useReducedMotion();
|
||||
const floatA = reduce ? {} : { y: [0, -7, 0] };
|
||||
const floatB = reduce ? {} : { y: [0, 6, 0] };
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
viewBox="0 0 440 336"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
role="img"
|
||||
aria-label="MerchantOS store dashboard preview"
|
||||
variants={svgContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.35 }}
|
||||
>
|
||||
{/* Soft ground shadow */}
|
||||
<ellipse cx="220" cy="312" rx="150" ry="14" fill="#6330D6" opacity="0.08" />
|
||||
|
||||
{/* Main dashboard window */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="40" y="30" width="360" height="256" rx="22" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
|
||||
{/* Top chrome */}
|
||||
<circle cx="66" cy="54" r="4" fill="#E6D9F7" />
|
||||
<circle cx="80" cy="54" r="4" fill="#E6D9F7" />
|
||||
<circle cx="94" cy="54" r="4" fill="#E6D9F7" />
|
||||
<rect x="150" y="48" width="140" height="13" rx="6.5" fill="#F3E8F6" />
|
||||
<line x1="40" y1="72" x2="400" y2="72" stroke="#F0E9F8" strokeWidth="1.5" />
|
||||
</motion.g>
|
||||
|
||||
{/* Sidebar */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="52" y="82" width="58" height="192" rx="14" fill="#FAF7FD" />
|
||||
<rect x="64" y="98" width="34" height="8" rx="4" fill="#6330D6" />
|
||||
<rect x="64" y="120" width="30" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="138" width="34" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="156" width="26" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="174" width="32" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
</motion.g>
|
||||
|
||||
{/* KPI chips */}
|
||||
<motion.g variants={pop}>
|
||||
<rect x="124" y="84" width="80" height="46" rx="12" fill="#F5F1FB" />
|
||||
<rect x="136" y="96" width="30" height="7" rx="3.5" fill="#B9A6E0" />
|
||||
<rect x="136" y="110" width="46" height="10" rx="5" fill="#6330D6" />
|
||||
</motion.g>
|
||||
<motion.g variants={pop}>
|
||||
<rect x="212" y="84" width="80" height="46" rx="12" fill="#ECFBF1" />
|
||||
<rect x="224" y="96" width="30" height="7" rx="3.5" fill="#86E0A6" />
|
||||
<rect x="224" y="110" width="40" height="10" rx="5" fill="#16A34A" />
|
||||
</motion.g>
|
||||
<motion.g variants={pop}>
|
||||
<rect x="300" y="84" width="80" height="46" rx="12" fill="#FFF7E8" />
|
||||
<rect x="312" y="96" width="30" height="7" rx="3.5" fill="#F1C876" />
|
||||
<rect x="312" y="110" width="34" height="10" rx="5" fill="#D97706" />
|
||||
</motion.g>
|
||||
|
||||
{/* Chart panel */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="124" y="142" width="168" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
|
||||
<rect x="138" y="156" width="44" height="8" rx="4" fill="#C9B8EC" />
|
||||
{/* gridlines */}
|
||||
<line x1="138" y1="196" x2="278" y2="196" stroke="#EFE7FB" strokeWidth="1" />
|
||||
<line x1="138" y1="222" x2="278" y2="222" stroke="#EFE7FB" strokeWidth="1" />
|
||||
{/* Area + animated line */}
|
||||
<motion.path
|
||||
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176 L278 248 L140 248 Z"
|
||||
fill="url(#dashArea)"
|
||||
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 1, delay: 0.5 } } }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176"
|
||||
fill="none"
|
||||
stroke="#6330D6"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
variants={draw}
|
||||
/>
|
||||
<motion.circle cx="278" cy="176" r="4" fill="#6330D6" stroke="#fff" strokeWidth="1.5" variants={pop} />
|
||||
</motion.g>
|
||||
|
||||
{/* Mini bar chart */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="300" y="142" width="80" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
|
||||
{[
|
||||
{ x: 314, h: 34, c: "#DDD0F4" },
|
||||
{ x: 332, h: 52, c: "#B99BEC" },
|
||||
{ x: 350, h: 44, c: "#9B78E4" },
|
||||
{ x: 368, h: 66, c: "#6330D6" },
|
||||
].map((b) => (
|
||||
<motion.rect
|
||||
key={b.x}
|
||||
x={b.x}
|
||||
y={250 - b.h}
|
||||
width="11"
|
||||
height={b.h}
|
||||
rx="4"
|
||||
fill={b.c}
|
||||
variants={growBar}
|
||||
style={{ transformBox: "fill-box", transformOrigin: "bottom" }}
|
||||
/>
|
||||
))}
|
||||
</motion.g>
|
||||
|
||||
{/* Floating "new order" card */}
|
||||
<motion.g animate={floatA} transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}>
|
||||
<motion.g variants={pop}>
|
||||
<rect x="316" y="24" width="104" height="42" rx="13" fill="#16001D" />
|
||||
<circle cx="336" cy="45" r="10" fill="#7C3AED" />
|
||||
<path d="M332 45 l3 3 l6 -6.5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<rect x="352" y="38" width="52" height="6" rx="3" fill="#ffffff" opacity="0.9" />
|
||||
<rect x="352" y="49" width="34" height="5" rx="2.5" fill="#C9A2FF" />
|
||||
</motion.g>
|
||||
</motion.g>
|
||||
|
||||
{/* Floating live-stock pill */}
|
||||
<motion.g animate={floatB} transition={{ repeat: Infinity, duration: 4.2, ease: "easeInOut" }}>
|
||||
<motion.g variants={pop}>
|
||||
<rect x="22" y="196" width="96" height="38" rx="12" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
|
||||
<motion.circle
|
||||
cx="42" cy="215" r="6" fill="#22C55E"
|
||||
animate={reduce ? {} : { opacity: [1, 0.4, 1], scale: [1, 1.15, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
|
||||
/>
|
||||
<rect x="56" y="208" width="48" height="6" rx="3" fill="#16001D" opacity="0.75" />
|
||||
<rect x="56" y="219" width="30" height="5" rx="2.5" fill="#B9A6E0" />
|
||||
</motion.g>
|
||||
</motion.g>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="dashArea" x1="0" y1="176" x2="0" y2="248" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6330D6" stopOpacity="0.22" />
|
||||
<stop offset="1" stopColor="#6330D6" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── ConsumerOS — neighbourhood shopping app ─────────────────────────────────
|
||||
|
||||
export function PhoneMockup() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
viewBox="0 0 360 336"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
role="img"
|
||||
aria-label="ConsumerOS neighbourhood shopping app preview"
|
||||
variants={svgContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.35 }}
|
||||
>
|
||||
<ellipse cx="180" cy="316" rx="120" ry="13" fill="#6330D6" opacity="0.08" />
|
||||
|
||||
{/* Phone body */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="118" y="14" width="150" height="306" rx="34" fill="#16001D" />
|
||||
<rect x="126" y="22" width="134" height="290" rx="28" fill="#FBF9FE" />
|
||||
{/* notch */}
|
||||
<rect x="168" y="30" width="50" height="10" rx="5" fill="#16001D" />
|
||||
</motion.g>
|
||||
|
||||
{/* Search bar */}
|
||||
<motion.g variants={pop}>
|
||||
<rect x="140" y="52" width="106" height="26" rx="13" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.4" />
|
||||
<circle cx="156" cy="65" r="5" fill="none" stroke="#6330D6" strokeWidth="1.8" />
|
||||
<line x1="160" y1="69" x2="164" y2="73" stroke="#6330D6" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<rect x="170" y="62" width="60" height="6" rx="3" fill="#D9CBEF" />
|
||||
</motion.g>
|
||||
|
||||
{/* Map region with roads */}
|
||||
<motion.g variants={rise}>
|
||||
<rect x="140" y="86" width="106" height="88" rx="14" fill="#EEF4FF" />
|
||||
<path d="M140 128 H246" stroke="#FFFFFF" strokeWidth="5" />
|
||||
<path d="M193 86 V174" stroke="#FFFFFF" strokeWidth="5" />
|
||||
<path d="M140 104 Q168 104 168 128" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
|
||||
<path d="M218 128 Q218 152 246 156" stroke="#FFFFFF" strokeWidth="3.5" fill="none" />
|
||||
{/* animated route */}
|
||||
<motion.path
|
||||
d="M162 158 L162 128 L193 128 L193 108 L224 108"
|
||||
fill="none"
|
||||
stroke="#6330D6"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="4 5"
|
||||
variants={draw}
|
||||
/>
|
||||
</motion.g>
|
||||
|
||||
{/* Floating map pins */}
|
||||
<motion.g
|
||||
animate={reduce ? {} : { y: [0, -6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
|
||||
>
|
||||
<motion.g variants={pop}>
|
||||
<path d="M162 150 C157 150 153 146 153 141 C153 135 162 126 162 126 C162 126 171 135 171 141 C171 146 167 150 162 150 Z" fill="#6330D6" />
|
||||
<circle cx="162" cy="140" r="3.4" fill="#fff" />
|
||||
</motion.g>
|
||||
</motion.g>
|
||||
<motion.g
|
||||
animate={reduce ? {} : { y: [0, -6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.9, ease: "easeInOut", delay: 0.4 }}
|
||||
>
|
||||
<motion.g variants={pop}>
|
||||
<path d="M224 122 C219 122 215 118 215 113 C215 107 224 98 224 98 C224 98 233 107 233 113 C233 118 229 122 224 122 Z" fill="#22C55E" />
|
||||
<circle cx="224" cy="112" r="3.4" fill="#fff" />
|
||||
</motion.g>
|
||||
</motion.g>
|
||||
|
||||
{/* Product cards */}
|
||||
{[0, 1].map((i) => (
|
||||
<motion.g key={i} variants={pop}>
|
||||
<rect x="140" y={184 + i * 40} width="106" height="34" rx="11" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.3" />
|
||||
<rect x="148" y={190 + i * 40} width="22" height="22" rx="7" fill={i === 0 ? "#EDE9FE" : "#DCFCE7"} />
|
||||
<rect x="178" y={192 + i * 40} width="42" height="6" rx="3" fill="#16001D" opacity="0.72" />
|
||||
<rect x="178" y={202 + i * 40} width="28" height="5" rx="2.5" fill="#C1B2E4" />
|
||||
<circle cx="234" cy={201 + i * 40} r="8" fill={i === 0 ? "#6330D6" : "#22C55E"} />
|
||||
<path d={`M230 ${201 + i * 40} h8 M234 ${197 + i * 40} v8`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</motion.g>
|
||||
))}
|
||||
|
||||
{/* Bottom nav */}
|
||||
<motion.g variants={pop}>
|
||||
<rect x="140" y="272" width="106" height="30" rx="12" fill="#FFFFFF" stroke="#F0E9F8" strokeWidth="1.2" />
|
||||
<circle cx="160" cy="287" r="4.5" fill="#6330D6" />
|
||||
<circle cx="184" cy="287" r="4" fill="#D9CBEF" />
|
||||
<circle cx="208" cy="287" r="4" fill="#D9CBEF" />
|
||||
<circle cx="230" cy="287" r="4" fill="#D9CBEF" />
|
||||
</motion.g>
|
||||
|
||||
{/* Floating rewards chip */}
|
||||
<motion.g
|
||||
animate={reduce ? {} : { y: [0, 7, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
>
|
||||
<motion.g variants={pop}>
|
||||
<rect x="250" y="196" width="96" height="40" rx="13" fill="#16001D" />
|
||||
<circle cx="270" cy="216" r="10" fill="#F59E0B" />
|
||||
<path d="M270 211 l1.6 3.3 l3.6 0.5 l-2.6 2.5 l0.6 3.6 l-3.2 -1.7 l-3.2 1.7 l0.6 -3.6 l-2.6 -2.5 l3.6 -0.5 Z" fill="#fff" />
|
||||
<rect x="286" y="209" width="50" height="6" rx="3" fill="#ffffff" opacity="0.9" />
|
||||
<rect x="286" y="220" width="32" height="5" rx="2.5" fill="#F5C97B" />
|
||||
</motion.g>
|
||||
</motion.g>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
156
src/components/platform/PlatformArchitecture.tsx
Normal file
156
src/components/platform/PlatformArchitecture.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, scaleInSpring, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
|
||||
|
||||
const LAYERS = [
|
||||
{
|
||||
name: "MerchantOS",
|
||||
tag: "For the store",
|
||||
icon: "storefront",
|
||||
desc: "Billing, inventory, catalog, and orders — the operating system a neighborhood shop runs on.",
|
||||
accent: "from-[#6330D6] to-[#7C3AED]",
|
||||
},
|
||||
{
|
||||
name: "ProductLLM",
|
||||
tag: "The intelligence",
|
||||
icon: "auto_awesome",
|
||||
desc: "Nearle's product AI. It reads a shelf and turns it into an accurate, live digital catalog.",
|
||||
accent: "from-[#8B5CF6] to-[#A855F7]",
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
name: "ConsumerOS",
|
||||
tag: "For the shopper",
|
||||
icon: "smartphone",
|
||||
desc: "Discovery, search, ordering, and rewards — how the neighborhood finds and shops nearby stores.",
|
||||
accent: "from-[#6330D6] to-[#4C1D95]",
|
||||
},
|
||||
];
|
||||
|
||||
/** A gradient link with a pulse of light traveling between two layers. */
|
||||
function FlowConnector() {
|
||||
const reduce = useReducedMotion();
|
||||
return (
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
aria-hidden="true"
|
||||
className="flex shrink-0 items-center justify-center py-1 lg:px-2"
|
||||
>
|
||||
{/* mobile — vertical */}
|
||||
<div className="relative h-9 w-0.5 lg:hidden">
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-b from-[#6330D6]/45 to-[#A855F7]/45" />
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
className="absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
|
||||
animate={{ y: [0, 30], opacity: [0, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* desktop — horizontal */}
|
||||
<div className="relative hidden h-0.5 w-12 lg:block">
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#6330D6]/45 to-[#A855F7]/45" />
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
className="absolute left-0 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.6)]"
|
||||
animate={{ x: [0, 42], opacity: [0, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.7, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformArchitecture() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
Platform Architecture
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
One connected stack
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-[#5E5866] mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
Every order flows through the same three layers — from the shopkeeper’s
|
||||
counter to the shopper’s phone.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="mt-12 flex flex-col items-stretch gap-2 lg:flex-row lg:items-center lg:gap-2"
|
||||
>
|
||||
{LAYERS.map((layer, i) => (
|
||||
<div key={layer.name} className="contents">
|
||||
<motion.div
|
||||
variants={scaleInSpring}
|
||||
className={`premium-card group relative flex-1 overflow-hidden rounded-[28px] border p-7 md:p-8 ${
|
||||
layer.highlight
|
||||
? "border-[#6330D6]/25 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_40px_rgba(99,48,214,0.14)]"
|
||||
: "border-[#6330D6]/10 bg-white shadow-[0_18px_45px_rgba(22,0,29,0.07)]"
|
||||
}`}
|
||||
>
|
||||
{/* animated aura on the intelligence layer */}
|
||||
{layer.highlight && !reduce && (
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full"
|
||||
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.20), transparent 70%)" }}
|
||||
animate={{ scale: [1, 1.18, 1], opacity: [0.6, 0.9, 0.6] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className={`relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br ${layer.accent} text-white shadow-[0_10px_24px_rgba(99,48,214,0.28)]`}
|
||||
animate={reduce ? {} : { y: [0, -6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.4 + i * 0.4, ease: "easeInOut" }}
|
||||
>
|
||||
<MaterialIcon icon={layer.icon} size={28} />
|
||||
</motion.div>
|
||||
<p className="relative mt-5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
|
||||
{layer.tag}
|
||||
</p>
|
||||
<p className="relative mt-1 font-display font-black text-[#16001D] text-2xl leading-tight">
|
||||
{layer.name}
|
||||
</p>
|
||||
<p className="relative mt-3 text-[15px] leading-relaxed text-[#5E5866]">
|
||||
{layer.desc}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{i < LAYERS.length - 1 && <FlowConnector />}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
134
src/components/platform/PlatformHero.tsx
Normal file
134
src/components/platform/PlatformHero.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { HeroLogo } from "@/components/heroes/HeroLogo";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const badgeVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
|
||||
};
|
||||
const headingVariant = {
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
|
||||
};
|
||||
const descVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
|
||||
};
|
||||
const pillsVariant = {
|
||||
hidden: { opacity: 0, y: 16 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
|
||||
};
|
||||
|
||||
const PILLARS = ["MerchantOS", "ProductLLM", "ConsumerOS"];
|
||||
|
||||
export function PlatformHero() {
|
||||
const shouldReduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="w-full flex flex-col select-none bg-white">
|
||||
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
|
||||
{/* Ambient background image */}
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
initial={shouldReduce ? undefined : { opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1.4, ease: "easeOut" }}
|
||||
>
|
||||
<Image
|
||||
src="/platform.png"
|
||||
alt=""
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
priority
|
||||
fetchPriority="high"
|
||||
/>
|
||||
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
|
||||
</motion.div>
|
||||
|
||||
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
|
||||
<div className="relative z-20 flex w-full max-w-[900px] flex-col items-center text-center px-2 md:px-8">
|
||||
<HeroLogo />
|
||||
|
||||
<motion.span
|
||||
variants={badgeVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
|
||||
>
|
||||
The Nearle Platform
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={headingVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
|
||||
>
|
||||
The AI Infrastructure
|
||||
<br />
|
||||
Behind Every
|
||||
<br />
|
||||
<span className="text-[#C9A2FF]">Neighborhood Store.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={descVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
|
||||
>
|
||||
Three systems, one platform. MerchantOS runs the store, ProductLLM
|
||||
understands the products, and ConsumerOS brings the neighborhood to
|
||||
shop.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={pillsVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="mt-9 flex flex-wrap items-center justify-center gap-3"
|
||||
>
|
||||
{PILLARS.map((p) => (
|
||||
<span
|
||||
key={p}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-4 py-2 text-sm font-black text-white backdrop-blur-sm"
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#C9A2FF]" />
|
||||
{p}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={pillsVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="mt-8"
|
||||
>
|
||||
<Link
|
||||
href="/businesses"
|
||||
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
|
||||
>
|
||||
Bring Your Store Online
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
size={20}
|
||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
153
src/components/platform/ProductFeatureGrid.tsx
Normal file
153
src/components/platform/ProductFeatureGrid.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { DashboardMockup } from "@/components/platform/DeviceShowcase";
|
||||
import { ConsumerShowcase } from "@/components/platform/ConsumerShowcase";
|
||||
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
|
||||
|
||||
export type ProductFeature = {
|
||||
icon: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
type ProductFeatureGridProps = {
|
||||
id?: string;
|
||||
eyebrow: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
features: ProductFeature[];
|
||||
variant?: "light" | "lavender";
|
||||
footnote?: string;
|
||||
/** Which animated device illustration to pair with the feature list. */
|
||||
showcase?: "dashboard" | "phone";
|
||||
};
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const rowVariant = {
|
||||
hidden: { opacity: 0, y: 18 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
|
||||
};
|
||||
|
||||
export function ProductFeatureGrid({
|
||||
id,
|
||||
eyebrow,
|
||||
name,
|
||||
tagline,
|
||||
features,
|
||||
variant = "light",
|
||||
footnote,
|
||||
showcase = "dashboard",
|
||||
}: ProductFeatureGridProps) {
|
||||
const bg = variant === "lavender" ? "section-soft" : "bg-white";
|
||||
// Alternate the media side to give the two sections opposite rhythm.
|
||||
const mediaRight = variant === "light";
|
||||
const isPhone = showcase === "phone";
|
||||
|
||||
const reduce = useReducedMotion();
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ["start end", "end start"],
|
||||
});
|
||||
const mediaY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [42, -42]);
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
ref={sectionRef}
|
||||
className={`section w-full border-b border-[#6330D6]/10 overflow-hidden ${bg}`}
|
||||
>
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="max-w-[760px]"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
{eyebrow}
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
{name}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-[#5E5866] mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
{tagline}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-12 grid items-center gap-10 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)] lg:gap-16">
|
||||
{/* ── Animated device illustration (parallax) ── */}
|
||||
<motion.div
|
||||
style={{ y: mediaY }}
|
||||
className={`relative mx-auto w-full ${isPhone ? "max-w-[600px]" : "max-w-[440px]"} ${mediaRight ? "lg:order-2" : "lg:order-1"}`}
|
||||
>
|
||||
{/* ambient glow */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 -z-10 blur-2xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)",
|
||||
}}
|
||||
/>
|
||||
{isPhone ? <ConsumerShowcase /> : <DashboardMockup />}
|
||||
</motion.div>
|
||||
|
||||
{/* ── Editorial feature list (was a 4-col card grid) ── */}
|
||||
<motion.ul
|
||||
variants={staggerCards}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport20}
|
||||
className={`relative ${mediaRight ? "lg:order-1" : "lg:order-2"}`}
|
||||
>
|
||||
{/* connection line threading the icons */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-[23px] top-6 bottom-6 w-px bg-gradient-to-b from-[#6330D6]/25 via-[#6330D6]/12 to-transparent"
|
||||
/>
|
||||
{features.map((f) => (
|
||||
<motion.li
|
||||
key={f.title}
|
||||
variants={rowVariant}
|
||||
className="group relative flex items-start gap-4 rounded-2xl px-3 py-3.5 transition-colors duration-200 hover:bg-[#6330D6]/[0.04]"
|
||||
>
|
||||
<span className="relative z-10 mt-0.5 flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-xl bg-[#6330D6]/[0.08] text-[#6330D6] transition-transform duration-200 group-hover:scale-110 group-hover:bg-[#6330D6] group-hover:text-white">
|
||||
<MaterialIcon icon={f.icon} size={20} />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block font-display font-black text-[#16001D] text-[17px] leading-tight">
|
||||
{f.title}
|
||||
</span>
|
||||
<span className="mt-1 block text-[14px] leading-relaxed text-[#5E5866]">
|
||||
{f.desc}
|
||||
</span>
|
||||
</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</motion.ul>
|
||||
</div>
|
||||
|
||||
{footnote && (
|
||||
<p className="mt-8 text-sm font-bold text-[#6330D6]">{footnote}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
322
src/components/platform/ProductLLMWorkflow.tsx
Normal file
322
src/components/platform/ProductLLMWorkflow.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const STEPS = [
|
||||
{ icon: "photo_camera", title: "Shelf Scan", desc: "A single photo of the shelf starts everything." },
|
||||
{ icon: "barcode_reader", title: "Barcode Detection", desc: "Every barcode on the shelf is found and read." },
|
||||
{ icon: "visibility", title: "Product Recognition", desc: "Each item is matched to a known product." },
|
||||
{ icon: "category", title: "Category", desc: "Products are sorted into the right aisles." },
|
||||
{ icon: "nutrition", title: "Nutrition", desc: "Nutritional facts are pulled in automatically." },
|
||||
{ icon: "science", title: "Ingredients", desc: "Ingredient lists are attached to each product." },
|
||||
{ icon: "sell", title: "Pricing", desc: "Prices are set and kept in sync with the store." },
|
||||
{ icon: "inventory_2", title: "Inventory", desc: "Live stock counts flow into the catalog." },
|
||||
{ icon: "check_circle", title: "Ready", desc: "A complete, shoppable catalog — in minutes." },
|
||||
];
|
||||
|
||||
const connectorVariant = {
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 1.1, ease: EASE } },
|
||||
};
|
||||
|
||||
const stepVariant = {
|
||||
hidden: { opacity: 0, x: -20 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: EASE } },
|
||||
};
|
||||
|
||||
/** A shelf photo being scanned and turned into live catalog cards. */
|
||||
function ShelfScanSVG() {
|
||||
const reduce = useReducedMotion();
|
||||
const detect = {
|
||||
hidden: { opacity: 0, scale: 0.7 },
|
||||
visible: (i: number) => ({
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { delay: 0.5 + i * 0.18, duration: 0.4, ease: EASE },
|
||||
}),
|
||||
};
|
||||
const cardIn = {
|
||||
hidden: { opacity: 0, x: 14 },
|
||||
visible: (i: number) => ({
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { delay: 1.1 + i * 0.16, duration: 0.5, ease: EASE },
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
viewBox="0 0 460 300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
role="img"
|
||||
aria-label="ProductLLM scanning a shelf photo into a digital catalog"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.35 }}
|
||||
>
|
||||
{/* ── Photo of the shelf ── */}
|
||||
<motion.g variants={fadeUp}>
|
||||
<rect x="20" y="40" width="210" height="220" rx="18" fill="#160022" stroke="#3B2357" strokeWidth="1.5" />
|
||||
<rect x="34" y="54" width="182" height="14" rx="7" fill="#2A1740" />
|
||||
<circle cx="45" cy="61" r="3" fill="#C9A2FF" />
|
||||
{/* shelves */}
|
||||
{[96, 150, 204].map((y) => (
|
||||
<rect key={y} x="34" y={y} width="182" height="4" rx="2" fill="#3B2357" />
|
||||
))}
|
||||
{/* products on shelves */}
|
||||
{[
|
||||
{ x: 44, y: 74, w: 20, h: 22, c: "#7C3AED" },
|
||||
{ x: 70, y: 70, w: 16, h: 26, c: "#A855F7" },
|
||||
{ x: 92, y: 78, w: 22, h: 18, c: "#6330D6" },
|
||||
{ x: 120, y: 72, w: 18, h: 24, c: "#8B5CF6" },
|
||||
{ x: 146, y: 76, w: 24, h: 20, c: "#7C3AED" },
|
||||
{ x: 178, y: 70, w: 18, h: 26, c: "#A855F7" },
|
||||
{ x: 46, y: 128, w: 18, h: 22, c: "#6330D6" },
|
||||
{ x: 72, y: 124, w: 22, h: 26, c: "#8B5CF6" },
|
||||
{ x: 102, y: 130, w: 18, h: 20, c: "#A855F7" },
|
||||
{ x: 128, y: 122, w: 20, h: 28, c: "#7C3AED" },
|
||||
{ x: 156, y: 126, w: 22, h: 24, c: "#6330D6" },
|
||||
{ x: 184, y: 128, w: 16, h: 22, c: "#8B5CF6" },
|
||||
{ x: 50, y: 182, w: 22, h: 22, c: "#8B5CF6" },
|
||||
{ x: 80, y: 178, w: 18, h: 26, c: "#7C3AED" },
|
||||
{ x: 106, y: 184, w: 22, h: 20, c: "#6330D6" },
|
||||
{ x: 138, y: 180, w: 18, h: 24, c: "#A855F7" },
|
||||
{ x: 166, y: 184, w: 24, h: 20, c: "#7C3AED" },
|
||||
].map((p, i) => (
|
||||
<rect key={i} x={p.x} y={p.y} width={p.w} height={p.h} rx="3" fill={p.c} opacity="0.9" />
|
||||
))}
|
||||
|
||||
{/* detection boxes */}
|
||||
{[
|
||||
{ x: 42, y: 68, w: 46, h: 30 },
|
||||
{ x: 118, y: 70, w: 54, h: 28 },
|
||||
{ x: 70, y: 120, w: 50, h: 32 },
|
||||
{ x: 154, y: 122, w: 46, h: 30 },
|
||||
].map((b, i) => (
|
||||
<motion.rect
|
||||
key={i}
|
||||
x={b.x}
|
||||
y={b.y}
|
||||
width={b.w}
|
||||
height={b.h}
|
||||
rx="4"
|
||||
fill="none"
|
||||
stroke="#4ADE80"
|
||||
strokeWidth="1.6"
|
||||
custom={i}
|
||||
variants={detect}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* scanning line */}
|
||||
{!reduce && (
|
||||
<motion.rect
|
||||
x="30"
|
||||
y="52"
|
||||
width="190"
|
||||
height="3"
|
||||
rx="1.5"
|
||||
fill="#C9A2FF"
|
||||
style={{ filter: "drop-shadow(0 0 6px rgba(201,162,255,0.9))" }}
|
||||
animate={{ y: [56, 244, 56] }}
|
||||
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
</motion.g>
|
||||
|
||||
{/* ── Flow arrow ── */}
|
||||
<motion.g
|
||||
variants={fadeUp}
|
||||
animate={reduce ? {} : { x: [0, 5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut" }}
|
||||
>
|
||||
<path d="M244 150 h28" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="3 4" />
|
||||
<path d="M270 143 l9 7 l-9 7" stroke="#C9A2FF" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</motion.g>
|
||||
|
||||
{/* ── Resulting catalog cards ── */}
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.g key={i} custom={i} variants={cardIn}>
|
||||
<rect x="292" y={58 + i * 62} width="150" height="52" rx="12" fill="#FFFFFF" />
|
||||
<rect x="304" y={70 + i * 62} width="30" height="28" rx="7" fill={["#EDE9FE", "#DCFCE7", "#FFF3D6"][i]} />
|
||||
<rect x="344" y={72 + i * 62} width="76" height="7" rx="3.5" fill="#16001D" opacity="0.8" />
|
||||
<rect x="344" y={84 + i * 62} width="48" height="6" rx="3" fill="#B9A6E0" />
|
||||
<circle cx="428" cy={76 + i * 62} r="7" fill="#22C55E" />
|
||||
<path d={`M425 ${76 + i * 62} l2 2 l4 -4.5`} stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</motion.g>
|
||||
))}
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductLLMWorkflow() {
|
||||
const reduce = useReducedMotion();
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ["start end", "end start"],
|
||||
});
|
||||
const heroY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [56, -56]);
|
||||
const illustrationY = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [40, -40]);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="productllm"
|
||||
ref={sectionRef}
|
||||
className="section w-full border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] overflow-hidden"
|
||||
>
|
||||
<div className="page-container">
|
||||
<div className="grid items-center gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:gap-14">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="max-w-[620px]"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.06] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
|
||||
>
|
||||
<MaterialIcon icon="auto_awesome" size={14} />
|
||||
The Differentiator
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-white mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
ProductLLM turns a shelf into a store.
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-white/70 mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
The hardest part of going online is the catalog. ProductLLM does it
|
||||
from a single photo — recognizing every product, then enriching it
|
||||
with category, nutrition, ingredients, price, and stock.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* animated scan illustration */}
|
||||
<div className="w-full">
|
||||
<ShelfScanSVG />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow timeline (left) supported by branded illustration (right) */}
|
||||
<div className="mt-14 grid items-center gap-12 lg:grid-cols-[minmax(0,58fr)_minmax(0,42fr)] lg:gap-16">
|
||||
<div className="relative w-full max-w-3xl">
|
||||
{/* Animated vertical connector */}
|
||||
<motion.div
|
||||
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#C9A2FF]/60 via-[#8B5CF6]/40 to-[#22C55E]/50 hidden sm:block"
|
||||
variants={connectorVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.05 }}
|
||||
/>
|
||||
{/* Pulse of light travelling down the pipeline */}
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute left-[23px] md:left-[27px] top-8 hidden h-3 w-3 rounded-full bg-[#C9A2FF] shadow-[0_0_14px_4px_rgba(201,162,255,0.6)] sm:block"
|
||||
animate={{ y: [0, 380], opacity: [0, 1, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.09, delayChildren: 0.1 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.05 }}
|
||||
>
|
||||
{STEPS.map((step, i) => {
|
||||
const isLast = i === STEPS.length - 1;
|
||||
return (
|
||||
<motion.div
|
||||
key={step.title}
|
||||
className="relative flex gap-5 sm:gap-7"
|
||||
variants={stepVariant}
|
||||
>
|
||||
<motion.div
|
||||
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full border shadow-[0_8px_20px_rgba(20,0,30,0.4)] ${
|
||||
isLast
|
||||
? "border-[#22C55E]/40 bg-[#22C55E]/15 text-[#4ADE80]"
|
||||
: "border-white/12 bg-white/[0.07] text-[#C9A2FF]"
|
||||
}`}
|
||||
animate={isLast && !reduce ? { scale: [1, 1.06, 1] } : {}}
|
||||
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
|
||||
>
|
||||
<MaterialIcon icon={step.icon} size={26} />
|
||||
</motion.div>
|
||||
<div
|
||||
className={`flex-1 rounded-[22px] border p-5 md:p-6 ${
|
||||
isLast
|
||||
? "border-[#22C55E]/25 bg-[#22C55E]/[0.08]"
|
||||
: "border-white/10 bg-white/[0.04]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-black tracking-widest text-white/40">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<p className="font-display font-black text-white text-lg leading-tight">
|
||||
{step.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-white/65">
|
||||
{step.desc}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Branded illustration filling the right side */}
|
||||
<motion.div
|
||||
style={{ y: illustrationY }}
|
||||
className="relative flex items-center justify-center justify-self-center lg:justify-self-end"
|
||||
>
|
||||
{/* soft purple radial glow */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 h-[75%] w-[75%] -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(139,92,246,0.38) 0%, rgba(124,58,237,0.18) 45%, transparent 72%)",
|
||||
}}
|
||||
/>
|
||||
{/* subtle floating illustration */}
|
||||
<motion.div
|
||||
animate={reduce ? {} : { y: [0, -14, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 7, ease: "easeInOut" }}
|
||||
className="relative"
|
||||
>
|
||||
<Image
|
||||
src="/superhero.png"
|
||||
alt="ProductLLM building a live catalog"
|
||||
width={1341}
|
||||
height={1533}
|
||||
sizes="(max-width: 640px) 300px, (max-width: 1024px) 380px, 520px"
|
||||
className="h-auto w-[300px] sm:w-[320px] md:w-[380px] lg:w-full lg:max-w-[520px]"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
192
src/components/ui/AICommerceComparison.tsx
Normal file
192
src/components/ui/AICommerceComparison.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import {
|
||||
fadeUp,
|
||||
slideInLeftSubtle,
|
||||
slideInRight,
|
||||
staggerContainer,
|
||||
staggerCards,
|
||||
viewport,
|
||||
viewport20,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const rowContainer = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.06 } },
|
||||
};
|
||||
|
||||
type Row = {
|
||||
topic: string;
|
||||
icon: string;
|
||||
quick: string;
|
||||
ai: string;
|
||||
};
|
||||
|
||||
const ROWS: Row[] = [
|
||||
{
|
||||
topic: "Fulfillment",
|
||||
icon: "local_shipping",
|
||||
quick: "Central dark stores hold the stock and ship it out.",
|
||||
ai: "The neighborhood store you already trust fulfills the order.",
|
||||
},
|
||||
{
|
||||
topic: "Store Ownership",
|
||||
icon: "storefront",
|
||||
quick: "Inventory and brand belong to the platform.",
|
||||
ai: "The merchant keeps their shop, stock, and customers.",
|
||||
},
|
||||
{
|
||||
topic: "Trust",
|
||||
icon: "verified_user",
|
||||
quick: "A logo and a delivery promise.",
|
||||
ai: "A shopkeeper the neighborhood has known for years.",
|
||||
},
|
||||
{
|
||||
topic: "AI Catalog",
|
||||
icon: "auto_awesome",
|
||||
quick: "Catalogs are built and maintained by hand.",
|
||||
ai: "ProductLLM turns a shelf into a live digital catalog.",
|
||||
},
|
||||
{
|
||||
topic: "Sustainability",
|
||||
icon: "eco",
|
||||
quick: "New warehouses, new fleets, new footprint.",
|
||||
ai: "Runs on stores and trips that already exist.",
|
||||
},
|
||||
{
|
||||
topic: "Offline Support",
|
||||
icon: "wifi_off",
|
||||
quick: "Everything depends on being online.",
|
||||
ai: "Billing and inventory keep working offline.",
|
||||
},
|
||||
{
|
||||
topic: "Customer Relationship",
|
||||
icon: "favorite",
|
||||
quick: "Owned by the platform, not the store.",
|
||||
ai: "Stays between the shopper and their local store.",
|
||||
},
|
||||
];
|
||||
|
||||
export function AICommerceComparison() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section section-soft w-full border-y border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[760px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
The Difference
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
AI Commerce vs Quick Commerce
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-[#5E5866] mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
Two ways to bring local shopping online. One builds around the
|
||||
neighborhood store — the other around a warehouse.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Column headers (desktop) */}
|
||||
<div className="mt-12 hidden md:grid grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] gap-5 px-1">
|
||||
<div className="pl-2">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-[#5E5866]">
|
||||
Quick Commerce
|
||||
</span>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-[#6330D6]">
|
||||
AI Commerce · Nearle
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerCards}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport20}
|
||||
className="mt-3 flex flex-col gap-4"
|
||||
>
|
||||
{ROWS.map((row) => (
|
||||
<motion.div
|
||||
key={row.topic}
|
||||
variants={rowContainer}
|
||||
className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] md:items-stretch"
|
||||
>
|
||||
{/* Quick commerce side — slides in from the left */}
|
||||
<motion.div
|
||||
variants={slideInLeftSubtle}
|
||||
whileHover={reduce ? undefined : { y: -3 }}
|
||||
transition={{ duration: 0.25, ease: EASE }}
|
||||
className="relative rounded-[24px] border border-[#6330D6]/10 bg-white p-6 shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#F3F3F6] text-[#6D6172]">
|
||||
<MaterialIcon icon={row.icon} size={20} />
|
||||
</div>
|
||||
<p className="font-display font-black text-[#16001D] text-base md:hidden">
|
||||
{row.topic}
|
||||
</p>
|
||||
<span className="ml-auto text-[10px] font-black uppercase tracking-widest text-[#6D6172] md:hidden">
|
||||
Quick
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[15px] leading-relaxed text-[#5E5866]">
|
||||
{row.quick}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* AI commerce side — slides in from the right */}
|
||||
<motion.div
|
||||
variants={slideInRight}
|
||||
whileHover={reduce ? undefined : { y: -5 }}
|
||||
transition={{ duration: 0.25, ease: EASE }}
|
||||
className="group relative overflow-hidden rounded-[24px] border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-6 shadow-[0_4px_28px_rgba(99,48,214,0.10)] transition-shadow duration-300 hover:shadow-[0_14px_40px_rgba(99,48,214,0.18)]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#6330D6]/10 text-[#6330D6]">
|
||||
<MaterialIcon icon={row.icon} size={20} />
|
||||
</div>
|
||||
<p className="font-display font-black text-[#16001D] text-base">
|
||||
{row.topic}
|
||||
</p>
|
||||
<motion.span
|
||||
className="ml-auto flex h-6 w-6 items-center justify-center rounded-full bg-[#DCFCE7] text-[#16A34A]"
|
||||
animate={reduce ? {} : { scale: [1, 1.12, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 2.4, ease: "easeInOut" }}
|
||||
>
|
||||
<MaterialIcon icon="check" size={14} />
|
||||
</motion.span>
|
||||
</div>
|
||||
<p className="mt-3 text-[15px] leading-relaxed text-[#3F3A47]">
|
||||
{row.ai}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
src/components/ui/PullQuote.tsx
Normal file
63
src/components/ui/PullQuote.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { fadeUp, staggerSlow, viewport } from "@/lib/animations";
|
||||
|
||||
type PullQuoteProps = {
|
||||
lines: string[];
|
||||
attribution?: string;
|
||||
};
|
||||
|
||||
export function PullQuote({ lines, attribution }: PullQuoteProps) {
|
||||
return (
|
||||
<section className="bg-white px-2 py-2 md:px-3 md:py-3 lg:px-4 lg:py-4">
|
||||
<motion.div
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="relative overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] px-6 py-16 text-center md:px-12 md:py-20"
|
||||
>
|
||||
{/* Ambient glow */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 30% 20%, rgba(124,58,237,0.28), transparent 45%), radial-gradient(circle at 75% 80%, rgba(168,85,247,0.18), transparent 45%)",
|
||||
}}
|
||||
/>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
aria-hidden="true"
|
||||
className="relative block font-display text-6xl font-black leading-none text-[#C9A2FF]/40"
|
||||
>
|
||||
“
|
||||
</motion.span>
|
||||
<motion.blockquote
|
||||
variants={fadeUp}
|
||||
className="relative mx-auto max-w-[860px] font-display font-black text-white tracking-tight"
|
||||
style={{ fontSize: "clamp(26px, 3.4vw, 46px)", lineHeight: 1.18 }}
|
||||
>
|
||||
{lines.map((line, i) => (
|
||||
<span key={i} className="block">
|
||||
{i === lines.length - 1 ? (
|
||||
<span className="text-[#C9A2FF]">{line}</span>
|
||||
) : (
|
||||
line
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</motion.blockquote>
|
||||
{attribution && (
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="relative mt-6 text-sm font-black uppercase tracking-widest text-white/50"
|
||||
>
|
||||
{attribution}
|
||||
</motion.p>
|
||||
)}
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
121
src/components/why-nearle/AICommerceCTA.tsx
Normal file
121
src/components/why-nearle/AICommerceCTA.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
const VP = { once: true, amount: 0.3 } as const;
|
||||
|
||||
const stagger = { hidden: {}, visible: { transition: { staggerChildren: 0.12 } } };
|
||||
const reveal = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
};
|
||||
|
||||
const VALUES = [
|
||||
{ icon: "auto_awesome", label: "AI-built catalog" },
|
||||
{ icon: "qr_code_2", label: "QR ordering" },
|
||||
{ icon: "point_of_sale", label: "Free POS" },
|
||||
];
|
||||
|
||||
export function AICommerceCTA() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="bg-white px-2 pt-2 pb-2 md:px-3 md:pt-3 md:pb-3 lg:px-4 lg:pt-4 lg:pb-4 2xl:px-6 2xl:pb-5">
|
||||
<motion.div
|
||||
variants={stagger}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={VP}
|
||||
className="relative w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] text-white"
|
||||
>
|
||||
{/* Ambient animated glows */}
|
||||
{!reduce && (
|
||||
<>
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -left-24 -top-24 h-72 w-72 rounded-full blur-3xl"
|
||||
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.45), rgba(124,58,237,0) 70%)" }}
|
||||
animate={{ opacity: [0.4, 0.75, 0.4], scale: [1, 1.15, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 5, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -right-24 -bottom-24 h-72 w-72 rounded-full blur-3xl"
|
||||
style={{ background: "radial-gradient(circle, rgba(201,162,255,0.35), rgba(201,162,255,0) 70%)" }}
|
||||
animate={{ opacity: [0.35, 0.65, 0.35], scale: [1.1, 1, 1.1] }}
|
||||
transition={{ repeat: Infinity, duration: 6, ease: "easeInOut" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="relative z-10 px-6 py-16 text-center md:px-10 md:py-20 lg:px-14">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<motion.span
|
||||
variants={reveal}
|
||||
className="mb-6 inline-flex items-center gap-2 rounded-full border border-white/15 px-3 py-1 text-xs font-black uppercase tracking-widest text-white/70"
|
||||
>
|
||||
<MaterialIcon icon="hub" size={14} />
|
||||
Join the ecosystem
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
variants={reveal}
|
||||
className="font-display font-black tracking-tight leading-[1.08]"
|
||||
style={{ fontSize: "clamp(32px, 4vw, 56px)" }}
|
||||
>
|
||||
Bring your store into
|
||||
<br />
|
||||
the age of <span className="text-[#C9A2FF]">AI Commerce</span>
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
variants={reveal}
|
||||
className="mx-auto mt-5 max-w-lg text-base leading-relaxed text-white/70 md:text-lg"
|
||||
>
|
||||
Keep the trust, the customers and the corner you’ve built.
|
||||
Nearle adds the technology — for free.
|
||||
</motion.p>
|
||||
|
||||
{/* Value chips */}
|
||||
<motion.div variants={reveal} className="mt-8 flex flex-wrap justify-center gap-2.5">
|
||||
{VALUES.map((v) => (
|
||||
<span
|
||||
key={v.label}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-bold text-white/85"
|
||||
>
|
||||
<MaterialIcon icon={v.icon} size={16} className="text-[#C9A2FF]" />
|
||||
{v.label}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={reveal} className="mt-10 flex flex-wrap justify-center gap-3.5">
|
||||
<Link
|
||||
href="/businesses"
|
||||
className="group flex items-center gap-2 rounded-xl bg-white px-8 py-3.5 text-sm font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
|
||||
>
|
||||
<MaterialIcon icon="storefront" size={18} />
|
||||
<span>Start Your Store</span>
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
size={18}
|
||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href="/platform"
|
||||
className="flex items-center gap-2 rounded-xl border border-white/20 bg-white/10 px-8 py-3.5 text-sm font-black text-white transition-colors duration-200 hover:bg-white/15 active:scale-95"
|
||||
>
|
||||
<MaterialIcon icon="hub" size={18} />
|
||||
<span>Explore the Platform</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
195
src/components/why-nearle/AICommerceExplainer.tsx
Normal file
195
src/components/why-nearle/AICommerceExplainer.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { cardReveal, fadeUp, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const HAVE = [
|
||||
{ img: "/trust.svg", label: "Trust", desc: "Built over years, face to face." },
|
||||
{ img: "/customers.svg", label: "Customers", desc: "Regulars who already walk in." },
|
||||
{ img: "/location.svg", label: "Location", desc: "Minutes from every doorstep." },
|
||||
];
|
||||
|
||||
const ADD = [
|
||||
{ icon: "auto_awesome", label: "AI", desc: "ProductLLM builds the catalog." },
|
||||
{ icon: "storefront", label: "Digital Storefront", desc: "Their shop, now online." },
|
||||
{ icon: "inventory_2", label: "Inventory", desc: "Live stock, always accurate." },
|
||||
{ icon: "shopping_bag", label: "Ordering", desc: "Pickup and delivery, built in." },
|
||||
];
|
||||
|
||||
export function AICommerceExplainer() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[760px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
The Idea
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
AI Commerce doesn’t replace the neighborhood store. It upgrades it.
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-[#5E5866] mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
Local shops already own the hardest part of retail. Nearle simply
|
||||
adds the technology layer on top.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative mt-12 grid gap-6 lg:grid-cols-2 lg:items-stretch">
|
||||
{/* Center merge badge — trust + technology */}
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
initial={{ opacity: 0, scale: 0.6 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] lg:flex"
|
||||
>
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
|
||||
animate={{ scale: [1, 1.5], opacity: [0.5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
<MaterialIcon icon="add" size={26} />
|
||||
</motion.div>
|
||||
|
||||
{/* Already have */}
|
||||
<motion.div
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="rounded-[32px] border border-[#6330D6]/10 bg-[#FAF7FD] p-7 md:p-9 shadow-[0_18px_45px_rgba(22,0,29,0.06)]"
|
||||
>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-xs font-black uppercase tracking-widest text-[#5E5866]"
|
||||
>
|
||||
Stores already have
|
||||
</motion.p>
|
||||
<div className="mt-5 flex flex-col gap-4">
|
||||
{HAVE.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
variants={cardReveal}
|
||||
whileHover={reduce ? undefined : { y: -6 }}
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
className="group flex min-h-[220px] flex-col gap-3 rounded-3xl bg-white px-6 py-5 md:min-h-0 md:px-7 md:py-6 border border-[#6330D6]/[0.08] shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_22px_50px_rgba(99,48,214,0.14)] md:flex-row md:items-center md:gap-6"
|
||||
>
|
||||
{/* Left — heading + supporting copy (48%) */}
|
||||
<div className="min-w-0 md:basis-[48%]">
|
||||
<p className="font-display font-black text-[#16001D] leading-[1.05] tracking-tight" style={{ fontSize: "clamp(24px, 2.1vw, 30px)" }}>
|
||||
{item.label}
|
||||
</p>
|
||||
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
{/* Right — animated illustration (52%, floating + gentle rotation) */}
|
||||
<div className="relative flex w-full items-center justify-center md:basis-[52%]">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 m-auto rounded-full opacity-70 blur-2xl transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
width: "58%",
|
||||
height: "58%",
|
||||
background:
|
||||
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
|
||||
}}
|
||||
/>
|
||||
<motion.img
|
||||
src={item.img}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="relative h-auto"
|
||||
style={{ width: "clamp(148px, 34vw, 168px)" }}
|
||||
animate={reduce ? {} : { y: [0, -9, 0], rotate: [-1.5, 1.5, -1.5] }}
|
||||
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Nearle adds */}
|
||||
<motion.div
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="flex flex-col rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-7 md:p-9 shadow-[0_20px_50px_rgba(20,0,30,0.28)]"
|
||||
>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
|
||||
>
|
||||
Nearle adds
|
||||
</motion.p>
|
||||
<div className="mt-6 grid flex-1 gap-4 sm:grid-cols-2 sm:auto-rows-fr">
|
||||
{ADD.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
variants={cardReveal}
|
||||
whileHover={reduce ? undefined : { y: -6 }}
|
||||
transition={{ duration: 0.28, ease: EASE }}
|
||||
className="group relative flex min-h-[196px] flex-col items-start overflow-hidden rounded-3xl bg-white/[0.05] p-6 border border-white/10 transition-colors duration-300 hover:border-[#C9A2FF]/45 hover:bg-white/[0.09]"
|
||||
>
|
||||
{/* Illustration — glowing icon medallion (top of the stack) */}
|
||||
<div className="relative mb-5">
|
||||
{/* Stronger purple radial glow behind the illustration */}
|
||||
{!reduce && (
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -inset-5 rounded-full blur-2xl"
|
||||
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.55), rgba(124,58,237,0) 70%)" }}
|
||||
animate={{ opacity: [0.5, 0.9, 0.5], scale: [1, 1.14, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 3.6 + i * 0.4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
<motion.div
|
||||
className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-[#C9A2FF]/35 bg-gradient-to-br from-[#7C3AED]/45 to-[#4C1D95]/25 text-[#EBDBFF] shadow-[0_12px_32px_rgba(124,58,237,0.5)] transition-colors duration-300 group-hover:border-[#C9A2FF]/60"
|
||||
animate={reduce ? {} : { y: [0, -6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 5 + i * 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<MaterialIcon icon={item.icon} size={30} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<p className="relative z-10 font-display font-black text-white text-xl leading-tight">
|
||||
{item.label}
|
||||
</p>
|
||||
{/* Description */}
|
||||
<p className="relative z-10 mt-1.5 text-sm leading-relaxed text-white/65">{item.desc}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
196
src/components/why-nearle/BeforeAfterFlow.tsx
Normal file
196
src/components/why-nearle/BeforeAfterFlow.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const TRADITIONAL = [
|
||||
{ icon: "edit_note", label: "Manual Catalog" },
|
||||
{ icon: "store", label: "Offline Store" },
|
||||
{ icon: "directions_walk", label: "Walk-in Customers" },
|
||||
{ icon: "receipt_long", label: "Manual Billing" },
|
||||
];
|
||||
|
||||
const NEARLE = [
|
||||
{ icon: "auto_awesome", label: "ProductLLM" },
|
||||
{ icon: "storefront", label: "Digital Storefront" },
|
||||
{ icon: "shopping_bag", label: "Omnichannel Ordering" },
|
||||
{ icon: "travel_explore", label: "AI Powered Discovery" },
|
||||
];
|
||||
|
||||
const nodeVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
|
||||
};
|
||||
|
||||
const connectorVariant = {
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 0.5, ease: EASE } },
|
||||
};
|
||||
|
||||
function FlowColumn({
|
||||
eyebrow,
|
||||
items,
|
||||
tone,
|
||||
reduce,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
items: { icon: string; label: string }[];
|
||||
tone: "muted" | "brand";
|
||||
reduce: boolean | null;
|
||||
}) {
|
||||
const isBrand = tone === "brand";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
className={`relative flex flex-col items-center rounded-[32px] border p-8 md:p-10 ${
|
||||
isBrand
|
||||
? "border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
|
||||
: "border-[#6330D6]/10 bg-[#FAF7FD] shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
|
||||
}`}
|
||||
>
|
||||
<motion.p
|
||||
variants={nodeVariant}
|
||||
className={`text-xs font-black uppercase tracking-widest ${
|
||||
isBrand ? "text-[#C9A2FF]" : "text-[#5E5866]"
|
||||
}`}
|
||||
>
|
||||
{eyebrow}
|
||||
</motion.p>
|
||||
|
||||
<div className="mt-8 flex w-full max-w-[300px] flex-col items-center">
|
||||
{items.map((item, i) => (
|
||||
<div key={item.label} className="flex w-full flex-col items-center">
|
||||
<motion.div
|
||||
variants={nodeVariant}
|
||||
whileHover={reduce ? undefined : { y: -4, scale: 1.02 }}
|
||||
transition={{ duration: 0.25, ease: EASE }}
|
||||
className={`flex w-full items-center gap-4 rounded-2xl border px-5 py-4 ${
|
||||
isBrand
|
||||
? "border-white/10 bg-white/[0.06] hover:border-[#C9A2FF]/40 hover:bg-white/[0.1]"
|
||||
: "border-[#6330D6]/10 bg-white shadow-[0_8px_22px_rgba(22,0,29,0.04)]"
|
||||
} transition-colors duration-300`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${
|
||||
isBrand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.08] text-[#6330D6]"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={item.icon} size={22} />
|
||||
</div>
|
||||
<p
|
||||
className={`font-display font-black leading-tight ${
|
||||
isBrand ? "text-white" : "text-[#16001D]"
|
||||
} text-[15px] md:text-base`}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Animated connector + travelling pulse */}
|
||||
{i < items.length - 1 && (
|
||||
<div className="relative my-1.5 h-8 w-0.5">
|
||||
<motion.span
|
||||
variants={connectorVariant}
|
||||
className={`absolute inset-0 origin-top rounded-full ${
|
||||
isBrand ? "bg-[#C9A2FF]/40" : "bg-[#6330D6]/25"
|
||||
}`}
|
||||
/>
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className={`absolute left-1/2 top-0 h-2 w-2 -translate-x-1/2 rounded-full ${
|
||||
isBrand
|
||||
? "bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.6)]"
|
||||
: "bg-[#7C3AED] shadow-[0_0_10px_2px_rgba(124,58,237,0.5)]"
|
||||
}`}
|
||||
animate={{ y: [0, 26], opacity: [0, 1, 0] }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration: 1.8,
|
||||
ease: "easeInOut",
|
||||
delay: i * 0.3,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BeforeAfterFlow() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
Before vs After
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
The same store, reimagined
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative mt-14 grid gap-6 md:grid-cols-2 md:items-start md:gap-8">
|
||||
<FlowColumn
|
||||
eyebrow="Traditional Commerce"
|
||||
items={TRADITIONAL}
|
||||
tone="muted"
|
||||
reduce={reduce}
|
||||
/>
|
||||
|
||||
{/* Center VS badge (desktop) */}
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
initial={{ opacity: 0, scale: 0.6 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5, delay: 0.25, ease: EASE }}
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 z-20 hidden h-14 w-14 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-[#6330D6]/15 bg-white text-xs font-black uppercase tracking-widest text-[#6330D6] shadow-[0_10px_30px_rgba(99,48,214,0.22)] md:flex"
|
||||
>
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
|
||||
animate={{ scale: [1, 1.45], opacity: [0.5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2.2, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
VS
|
||||
</motion.div>
|
||||
|
||||
<FlowColumn
|
||||
eyebrow="Nearle AI Commerce"
|
||||
items={NEARLE}
|
||||
tone="brand"
|
||||
reduce={reduce}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
156
src/components/why-nearle/CommerceEvolution.tsx
Normal file
156
src/components/why-nearle/CommerceEvolution.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const STAGES = [
|
||||
{
|
||||
era: "The corner shop",
|
||||
title: "Traditional Retail",
|
||||
desc: "Trust and relationships, built one street at a time.",
|
||||
icon: "storefront",
|
||||
},
|
||||
{
|
||||
era: "The catalog goes online",
|
||||
title: "E-Commerce",
|
||||
desc: "Endless shelves, but the neighborhood disappears.",
|
||||
icon: "shopping_cart",
|
||||
},
|
||||
{
|
||||
era: "Speed at all costs",
|
||||
title: "Quick Commerce",
|
||||
desc: "Minutes-fast delivery, powered by dark stores.",
|
||||
icon: "bolt",
|
||||
},
|
||||
{
|
||||
era: "The store, upgraded",
|
||||
title: "AI Commerce",
|
||||
desc: "Local stores digitized by AI — trust and technology together.",
|
||||
icon: "auto_awesome",
|
||||
highlight: true,
|
||||
},
|
||||
];
|
||||
|
||||
const connectorVariant = {
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 1, ease: EASE } },
|
||||
};
|
||||
|
||||
export function CommerceEvolution() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
The Evolution
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
How commerce got here
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative mt-12 max-w-2xl mx-auto">
|
||||
{/* Vertical connector */}
|
||||
<motion.div
|
||||
className="absolute left-[26px] md:left-[30px] top-8 bottom-8 w-0.5 origin-top bg-gradient-to-b from-[#6330D6]/30 via-[#6330D6]/25 to-[#6330D6]/40 hidden sm:block"
|
||||
variants={connectorVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
/>
|
||||
{/* Pulse travelling down the timeline */}
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute left-[23px] md:left-[27px] top-8 hidden h-2.5 w-2.5 rounded-full bg-[#7C3AED] shadow-[0_0_12px_3px_rgba(124,58,237,0.5)] sm:block"
|
||||
animate={{ y: [0, 300], opacity: [0, 1, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="space-y-5"
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12, delayChildren: 0.1 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
>
|
||||
{STAGES.map((stage, i) => (
|
||||
<motion.div
|
||||
key={stage.title}
|
||||
className="relative flex gap-5 sm:gap-7"
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: -24 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className={`relative z-10 flex h-[52px] w-[52px] md:h-[60px] md:w-[60px] shrink-0 items-center justify-center rounded-full shadow-[0_8px_20px_rgba(99,48,214,0.25)] ${
|
||||
stage.highlight
|
||||
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white"
|
||||
: "bg-[#F3E8F6] text-[#6330D6]"
|
||||
}`}
|
||||
animate={reduce ? {} : { y: [0, -5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.2 + i * 0.35, ease: "easeInOut" }}
|
||||
>
|
||||
{/* pulsing ring on the current era */}
|
||||
{stage.highlight && !reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 rounded-full border-2 border-[#7C3AED]"
|
||||
animate={{ scale: [1, 1.4], opacity: [0.6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
<MaterialIcon icon={stage.icon} size={26} />
|
||||
</motion.div>
|
||||
<div
|
||||
className={`flex-1 rounded-[24px] border p-5 md:p-6 transition-shadow ${
|
||||
stage.highlight
|
||||
? "border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#faf5ff_55%,#f1e8fb_100%)] shadow-[0_12px_35px_rgba(99,48,214,0.12)]"
|
||||
: "border-[#6330D6]/10 bg-white shadow-[0_10px_30px_rgba(22,0,29,0.05)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="font-display font-black text-[#16001D] text-lg md:text-xl leading-tight">
|
||||
{stage.title}
|
||||
</p>
|
||||
{stage.highlight && (
|
||||
<span className="rounded-full bg-[#6330D6] px-2.5 py-0.5 text-[10px] font-black uppercase tracking-wide text-white">
|
||||
Now
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs font-black uppercase tracking-widest text-[#6330D6]/60">
|
||||
{stage.era}
|
||||
</p>
|
||||
<p className="mt-2 text-[15px] leading-relaxed text-[#5E5866]">
|
||||
{stage.desc}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
149
src/components/why-nearle/EcosystemDiagram.tsx
Normal file
149
src/components/why-nearle/EcosystemDiagram.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const NODES = [
|
||||
{ icon: "store", label: "Merchant", sub: "The local shop" },
|
||||
{ icon: "point_of_sale", label: "MerchantOS", sub: "Billing & stock" },
|
||||
{ icon: "auto_awesome", label: "ProductLLM", sub: "AI catalog engine", hub: true },
|
||||
{ icon: "smartphone", label: "ConsumerOS", sub: "Shopping app" },
|
||||
{ icon: "mood", label: "Customer", sub: "The neighbour" },
|
||||
];
|
||||
|
||||
const nodeVariant = {
|
||||
hidden: { opacity: 0, scale: 0.85, y: 16 },
|
||||
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
};
|
||||
|
||||
function Connector({ reduce, delay }: { reduce: boolean | null; delay: number }) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { duration: 0.4, ease: EASE } },
|
||||
}}
|
||||
className="relative my-1 h-9 w-0.5 overflow-hidden rounded-full bg-white/10 md:mx-2 md:my-0 md:mt-9 md:h-0.5 md:w-auto md:flex-1"
|
||||
>
|
||||
{!reduce && (
|
||||
<>
|
||||
{/* Vertical pulse (mobile) */}
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute left-1/2 top-0 h-3 w-1 -translate-x-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:hidden"
|
||||
animate={{ y: [-4, 36], opacity: [0, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.8, ease: "easeInOut", delay }}
|
||||
/>
|
||||
{/* Horizontal pulse (desktop) */}
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute left-0 top-1/2 hidden h-1 w-4 -translate-y-1/2 rounded-full bg-[#C9A2FF] shadow-[0_0_10px_2px_rgba(201,162,255,0.7)] md:block"
|
||||
animate={{ x: ["-10%", "460%"], opacity: [0, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut", delay }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EcosystemDiagram() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
|
||||
>
|
||||
The Ecosystem
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-white mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
One connected loop, powered by AI
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-white/70 mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
Every part of Nearle talks to the next — from the merchant’s
|
||||
shelf to the customer’s doorstep.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="mt-16 flex flex-col items-center md:flex-row md:items-start md:justify-between"
|
||||
>
|
||||
{NODES.map((node, i) => (
|
||||
<Fragment key={node.label}>
|
||||
<motion.div
|
||||
variants={nodeVariant}
|
||||
whileHover={reduce ? undefined : { y: -6 }}
|
||||
transition={{ duration: 0.25, ease: EASE }}
|
||||
className="flex shrink-0 flex-col items-center text-center"
|
||||
>
|
||||
<div className="relative">
|
||||
{/* Glow */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`pointer-events-none absolute inset-0 rounded-full blur-2xl ${
|
||||
node.hub ? "opacity-90" : "opacity-50"
|
||||
}`}
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)",
|
||||
}}
|
||||
/>
|
||||
{/* Pulsing ring on the hub */}
|
||||
{node.hub && !reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
|
||||
animate={{ scale: [1, 1.35], opacity: [0.6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`relative flex items-center justify-center rounded-full ${
|
||||
node.hub
|
||||
? "h-[84px] w-[84px] bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_16px_40px_rgba(124,58,237,0.5)]"
|
||||
: "h-[72px] w-[72px] bg-white/[0.08] text-[#C9A2FF] border border-white/15 shadow-[0_12px_30px_rgba(0,0,0,0.35)]"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={node.icon} size={node.hub ? 36 : 30} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 font-display font-black text-white text-base md:text-lg leading-tight">
|
||||
{node.label}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-white/50">{node.sub}</p>
|
||||
</motion.div>
|
||||
|
||||
{i < NODES.length - 1 && <Connector reduce={reduce} delay={i * 0.25} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
153
src/components/why-nearle/FutureVision.tsx
Normal file
153
src/components/why-nearle/FutureVision.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const MILESTONES = [
|
||||
{
|
||||
phase: "Today",
|
||||
title: "AI Commerce, live",
|
||||
desc: "Digital catalogs, QR ordering and live inventory.",
|
||||
icon: "bolt",
|
||||
now: true,
|
||||
},
|
||||
{
|
||||
phase: "Next",
|
||||
title: "Digitizing Stores",
|
||||
desc: "Every neighbourhood shop, online in minutes.",
|
||||
icon: "storefront",
|
||||
},
|
||||
{
|
||||
phase: "Soon",
|
||||
title: "Connected Neighborhoods",
|
||||
desc: "Stores, shoppers and riders in one network.",
|
||||
icon: "hub",
|
||||
},
|
||||
{
|
||||
phase: "The Horizon",
|
||||
title: "Future of AI Commerce",
|
||||
desc: "Local commerce that runs itself.",
|
||||
icon: "rocket_launch",
|
||||
},
|
||||
];
|
||||
|
||||
const nodeVariant = {
|
||||
hidden: { opacity: 0, y: 22 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
};
|
||||
|
||||
export function FutureVision() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section w-full overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-white/10 px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#C9A2FF]"
|
||||
>
|
||||
The Road Ahead
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-white mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
Where AI Commerce is headed
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.16 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.25 }}
|
||||
className="relative mt-16"
|
||||
>
|
||||
{/* Rails */}
|
||||
<div className="absolute left-[8%] right-[8%] top-[26px] hidden h-0.5 bg-white/10 md:block" />
|
||||
<motion.div
|
||||
className="absolute left-[8%] top-[26px] hidden h-0.5 origin-left bg-gradient-to-r from-[#C9A2FF] via-[#7C3AED] to-[#6330D6] md:block"
|
||||
style={{ width: "84%" }}
|
||||
variants={{ hidden: { scaleX: 0 }, visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } } }}
|
||||
/>
|
||||
{/* travelling glow (desktop) */}
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute top-[22px] left-[8%] hidden h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_14px_4px_rgba(201,162,255,0.7)] md:block"
|
||||
animate={{ left: ["8%", "92%"], opacity: [0, 1, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
{/* vertical rail (mobile) */}
|
||||
<div className="absolute left-[26px] top-2 bottom-2 w-0.5 -translate-x-1/2 bg-white/10 md:hidden" />
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-4 md:gap-6">
|
||||
{MILESTONES.map((m, i) => (
|
||||
<motion.div
|
||||
key={m.title}
|
||||
variants={nodeVariant}
|
||||
className="relative flex items-start gap-5 md:flex-col md:items-center md:text-center"
|
||||
>
|
||||
{/* node dot */}
|
||||
<div className="relative z-10 shrink-0">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`pointer-events-none absolute inset-0 rounded-full blur-lg ${
|
||||
m.now ? "opacity-90" : "opacity-40"
|
||||
}`}
|
||||
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.7), rgba(124,58,237,0) 70%)" }}
|
||||
/>
|
||||
{m.now && !reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 rounded-full border-2 border-[#C9A2FF]"
|
||||
animate={{ scale: [1, 1.5], opacity: [0.6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`relative flex h-[52px] w-[52px] items-center justify-center rounded-full ${
|
||||
m.now
|
||||
? "bg-gradient-to-br from-[#7C3AED] to-[#6330D6] text-white shadow-[0_14px_34px_rgba(124,58,237,0.5)]"
|
||||
: "border border-white/15 bg-white/[0.08] text-[#C9A2FF]"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={m.icon} size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:mt-5">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-black uppercase tracking-widest ${
|
||||
m.now ? "bg-[#6330D6] text-white" : "bg-white/10 text-[#C9A2FF]"
|
||||
}`}
|
||||
>
|
||||
{m.phase}
|
||||
</span>
|
||||
<p className="mt-2 font-display font-black text-white text-lg leading-tight">
|
||||
{m.title}
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-relaxed text-white/55 md:max-w-[220px] md:mx-auto">
|
||||
{m.desc}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
102
src/components/why-nearle/NeighborhoodStores.tsx
Normal file
102
src/components/why-nearle/NeighborhoodStores.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { cardReveal, fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
img: "/trust.svg",
|
||||
label: "Trust",
|
||||
desc: "A face the street has known for years.",
|
||||
},
|
||||
{
|
||||
img: "/customers.svg",
|
||||
label: "Community",
|
||||
desc: "Regulars who shop where they belong.",
|
||||
},
|
||||
{
|
||||
img: "/location.svg",
|
||||
label: "Convenience",
|
||||
desc: "Always minutes from the doorstep.",
|
||||
},
|
||||
];
|
||||
|
||||
export function NeighborhoodStores() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
Why It Works
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
Why neighborhood stores matter
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.14 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="mt-14 grid gap-6 md:grid-cols-3 md:items-stretch"
|
||||
>
|
||||
{CARDS.map((card, i) => (
|
||||
<motion.div
|
||||
key={card.label}
|
||||
variants={cardReveal}
|
||||
whileHover={reduce ? undefined : { y: -8 }}
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
className="group relative flex flex-col items-center overflow-hidden rounded-[32px] border border-[#6330D6]/[0.08] bg-[linear-gradient(180deg,#ffffff_0%,#faf7fd_100%)] px-8 pt-12 pb-9 text-center shadow-[0_14px_40px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_26px_60px_rgba(99,48,214,0.14)]"
|
||||
>
|
||||
{/* Soft glow behind the illustration */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-1/2 top-14 h-48 w-48 -translate-x-1/2 rounded-full opacity-60 blur-3xl transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(124,58,237,0.16), rgba(124,58,237,0) 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Large floating illustration — the visual focus */}
|
||||
<motion.img
|
||||
src={card.img}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="relative h-auto"
|
||||
style={{ width: "clamp(190px, 22vw, 240px)" }}
|
||||
animate={reduce ? {} : { y: [0, -10, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 4 + i * 0.6, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
<p className="relative mt-8 font-display font-black text-[#16001D] leading-tight tracking-tight" style={{ fontSize: "clamp(24px, 2.2vw, 30px)" }}>
|
||||
{card.label}
|
||||
</p>
|
||||
<p className="relative mt-2 max-w-[240px] text-[15px] leading-relaxed text-[#5E5866]">
|
||||
{card.desc}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
186
src/components/why-nearle/ProductLLMCompare.tsx
Normal file
186
src/components/why-nearle/ProductLLMCompare.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const MANUAL_POINTS = [
|
||||
{ icon: "keyboard", label: "Typed in by hand" },
|
||||
{ icon: "error_outline", label: "Missing details & typos" },
|
||||
{ icon: "hourglass_empty", label: "Goes stale quickly" },
|
||||
];
|
||||
|
||||
const AI_POINTS = [
|
||||
{ icon: "photo_camera", label: "Recognised from a photo" },
|
||||
{ icon: "verified", label: "Full, structured attributes" },
|
||||
{ icon: "sync", label: "Always kept in sync" },
|
||||
];
|
||||
|
||||
const panelReveal = {
|
||||
hidden: { opacity: 0, y: 28 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE } },
|
||||
};
|
||||
|
||||
function Bar({
|
||||
value,
|
||||
brand,
|
||||
}: {
|
||||
value: number; // 0–100 target width
|
||||
brand: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`mt-6 h-2.5 w-full overflow-hidden rounded-full ${
|
||||
brand ? "bg-white/10" : "bg-[#6330D6]/10"
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className={`h-full rounded-full ${
|
||||
brand
|
||||
? "bg-gradient-to-r from-[#C9A2FF] to-[#7C3AED]"
|
||||
: "bg-gradient-to-r from-[#B7ADCB] to-[#8A7FA6]"
|
||||
}`}
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: `${value}%` }}
|
||||
viewport={{ once: true, amount: 0.6 }}
|
||||
transition={{ duration: 1.2, ease: EASE, delay: 0.2 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Point({
|
||||
icon,
|
||||
label,
|
||||
brand,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
brand: boolean;
|
||||
}) {
|
||||
return (
|
||||
<motion.li
|
||||
variants={fadeUp}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<span
|
||||
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
|
||||
brand ? "bg-white/10 text-[#C9A2FF]" : "bg-[#6330D6]/[0.07] text-[#8A7FA6]"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={icon} size={18} />
|
||||
</span>
|
||||
<span className={`text-[15px] font-medium ${brand ? "text-white/85" : "text-[#5E5866]"}`}>
|
||||
{label}
|
||||
</span>
|
||||
</motion.li>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductLLMCompare() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section section-soft w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
Why ProductLLM
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
Catalogs built in minutes, not days
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-14 grid gap-6 md:grid-cols-2 md:items-stretch">
|
||||
{/* Manual */}
|
||||
<motion.div
|
||||
variants={panelReveal}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="flex flex-col rounded-[32px] border border-[#6330D6]/10 bg-white p-8 md:p-10 shadow-[0_14px_40px_rgba(22,0,29,0.05)]"
|
||||
>
|
||||
<p className="text-xs font-black uppercase tracking-widest text-[#8A7FA6]">
|
||||
Traditional catalog creation
|
||||
</p>
|
||||
<div className="mt-5 flex items-end gap-2">
|
||||
<span className="font-display font-black leading-none text-[#16001D]" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
|
||||
~6
|
||||
</span>
|
||||
<span className="mb-2 text-lg font-bold text-[#5E5866]">hrs / store</span>
|
||||
</div>
|
||||
<Bar value={100} brand={false} />
|
||||
<motion.ul
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="mt-8 flex flex-col gap-4"
|
||||
>
|
||||
{MANUAL_POINTS.map((p) => (
|
||||
<Point key={p.label} icon={p.icon} label={p.label} brand={false} />
|
||||
))}
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
|
||||
{/* ProductLLM */}
|
||||
<motion.div
|
||||
variants={panelReveal}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="relative flex flex-col overflow-hidden rounded-[32px] border border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] p-8 md:p-10 shadow-[0_24px_60px_rgba(20,0,30,0.32)]"
|
||||
>
|
||||
{/* soft glow */}
|
||||
{!reduce && (
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -right-10 -top-10 h-48 w-48 rounded-full blur-3xl"
|
||||
style={{ background: "radial-gradient(circle, rgba(124,58,237,0.5), rgba(124,58,237,0) 70%)" }}
|
||||
animate={{ opacity: [0.5, 0.85, 0.5], scale: [1, 1.12, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
<p className="relative text-xs font-black uppercase tracking-widest text-[#C9A2FF]">
|
||||
AI-powered with ProductLLM
|
||||
</p>
|
||||
<div className="relative mt-5 flex items-end gap-2">
|
||||
<span className="font-display font-black leading-none text-white" style={{ fontSize: "clamp(44px, 6vw, 68px)" }}>
|
||||
~2
|
||||
</span>
|
||||
<span className="mb-2 text-lg font-bold text-white/70">min / store</span>
|
||||
</div>
|
||||
<Bar value={14} brand />
|
||||
<motion.ul
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="relative mt-8 flex flex-col gap-4"
|
||||
>
|
||||
{AI_POINTS.map((p) => (
|
||||
<Point key={p.label} icon={p.icon} label={p.label} brand />
|
||||
))}
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
189
src/components/why-nearle/ProductLLMPipeline.tsx
Normal file
189
src/components/why-nearle/ProductLLMPipeline.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const STAGES = [
|
||||
{ icon: "photo_camera", label: "Shelf Scan" },
|
||||
{ icon: "barcode_scanner", label: "Barcode Detection" },
|
||||
{ icon: "center_focus_strong", label: "Recognition" },
|
||||
{ icon: "category", label: "Category" },
|
||||
{ icon: "nutrition", label: "Nutrition" },
|
||||
{ icon: "science", label: "Ingredients" },
|
||||
{ icon: "sell", label: "Pricing" },
|
||||
{ icon: "inventory_2", label: "Inventory" },
|
||||
{ icon: "check_circle", label: "Ready", done: true },
|
||||
];
|
||||
|
||||
const nodeVariant = {
|
||||
hidden: { opacity: 0, scale: 0.7, y: 12 },
|
||||
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
|
||||
};
|
||||
|
||||
function StageNode({
|
||||
stage,
|
||||
index,
|
||||
reduce,
|
||||
size = "md",
|
||||
}: {
|
||||
stage: (typeof STAGES)[number];
|
||||
index: number;
|
||||
reduce: boolean | null;
|
||||
size?: "md" | "sm";
|
||||
}) {
|
||||
const dim = size === "md" ? "h-[68px] w-[68px]" : "h-14 w-14";
|
||||
return (
|
||||
<motion.div variants={nodeVariant} className="relative">
|
||||
{/* light-up glow */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`pointer-events-none absolute inset-0 rounded-2xl blur-xl ${
|
||||
stage.done ? "opacity-90" : "opacity-50"
|
||||
}`}
|
||||
style={{
|
||||
background: stage.done
|
||||
? "radial-gradient(circle, rgba(34,197,94,0.5), rgba(34,197,94,0) 70%)"
|
||||
: "radial-gradient(circle, rgba(124,58,237,0.35), rgba(124,58,237,0) 70%)",
|
||||
}}
|
||||
/>
|
||||
{stage.done && !reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 rounded-2xl border-2 border-[#22C55E]"
|
||||
animate={{ scale: [1, 1.25], opacity: [0.6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.9, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`relative flex ${dim} items-center justify-center rounded-2xl border ${
|
||||
stage.done
|
||||
? "border-transparent bg-gradient-to-br from-[#22C55E] to-[#16A34A] text-white shadow-[0_14px_34px_rgba(34,197,94,0.4)]"
|
||||
: "border-[#6330D6]/12 bg-white text-[#6330D6] shadow-[0_10px_26px_rgba(99,48,214,0.12)]"
|
||||
}`}
|
||||
>
|
||||
<MaterialIcon icon={stage.icon} size={size === "md" ? 30 : 26} />
|
||||
<span
|
||||
className={`absolute -top-2 -right-2 flex h-6 w-6 items-center justify-center rounded-full text-[11px] font-black ${
|
||||
stage.done ? "bg-white text-[#16A34A]" : "bg-[#6330D6] text-white"
|
||||
} shadow-md`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductLLMPipeline() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
|
||||
<div className="page-container">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
className="text-center max-w-[720px] mx-auto"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
|
||||
>
|
||||
ProductLLM
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
variants={fadeUp}
|
||||
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
|
||||
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
|
||||
>
|
||||
From a shelf photo to a live catalog
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-[#5E5866] mt-4"
|
||||
style={{ fontSize: 18, lineHeight: 1.65 }}
|
||||
>
|
||||
One scan runs the full pipeline — no manual data entry, no
|
||||
spreadsheets.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Desktop: horizontal processing rail ── */}
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="relative mt-16 hidden lg:block"
|
||||
>
|
||||
{/* base rail */}
|
||||
<div className="absolute left-[5%] right-[5%] top-[34px] h-1 rounded-full bg-[#6330D6]/10" />
|
||||
{/* filling gradient */}
|
||||
<motion.div
|
||||
className="absolute left-[5%] top-[34px] h-1 origin-left rounded-full bg-gradient-to-r from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
|
||||
style={{ width: "90%" }}
|
||||
variants={{
|
||||
hidden: { scaleX: 0 },
|
||||
visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } },
|
||||
}}
|
||||
/>
|
||||
{/* travelling data pulse */}
|
||||
{!reduce && (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute top-[30px] left-[5%] h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_12px_3px_rgba(124,58,237,0.7)]"
|
||||
animate={{ left: ["5%", "95%"], opacity: [0, 1, 1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative grid grid-cols-9">
|
||||
{STAGES.map((stage, i) => (
|
||||
<div key={stage.label} className="flex flex-col items-center px-1 text-center">
|
||||
<StageNode stage={stage} index={i} reduce={reduce} />
|
||||
<p className="mt-4 text-[13px] font-bold leading-tight text-[#16001D]">
|
||||
{stage.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Mobile / tablet: vertical pipeline ── */}
|
||||
<motion.div
|
||||
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1 } } }}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
className="relative mx-auto mt-12 max-w-md lg:hidden"
|
||||
>
|
||||
{/* vertical rail */}
|
||||
<div className="absolute left-7 top-8 bottom-8 w-1 -translate-x-1/2 rounded-full bg-[#6330D6]/10" />
|
||||
<motion.div
|
||||
className="absolute left-7 top-8 w-1 -translate-x-1/2 origin-top rounded-full bg-gradient-to-b from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
|
||||
style={{ bottom: "2rem" }}
|
||||
variants={{
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 1.6, ease: EASE } },
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
{STAGES.map((stage, i) => (
|
||||
<div key={stage.label} className="flex items-center gap-5">
|
||||
<StageNode stage={stage} index={i} reduce={reduce} size="sm" />
|
||||
<p className="font-display font-black text-[#16001D] text-base leading-tight">
|
||||
{stage.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
119
src/components/why-nearle/WhyNearleHero.tsx
Normal file
119
src/components/why-nearle/WhyNearleHero.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { HeroLogo } from "@/components/heroes/HeroLogo";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const badgeVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 0.9, ease: EASE } },
|
||||
};
|
||||
|
||||
const headingVariant = {
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 1.0, ease: EASE } },
|
||||
};
|
||||
|
||||
const descVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, delay: 1.1, ease: EASE } },
|
||||
};
|
||||
|
||||
const ctaVariant = {
|
||||
hidden: { opacity: 0, y: 16 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: 1.2, ease: EASE } },
|
||||
};
|
||||
|
||||
export function WhyNearleHero() {
|
||||
const shouldReduce = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="w-full flex flex-col select-none bg-white">
|
||||
<div className="relative overflow-hidden border-b border-white/10 bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D]">
|
||||
{/* Ambient background image */}
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
initial={shouldReduce ? undefined : { opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1.4, ease: "easeOut" }}
|
||||
>
|
||||
<Image
|
||||
src="/why-nearle.png"
|
||||
alt=""
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
priority
|
||||
fetchPriority="high"
|
||||
/>
|
||||
{/* Dark gradient overlay to make text pop while keeping the image vibrant */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#1A0322]/80 via-black/45 to-[#0A000D]/90" />
|
||||
</motion.div>
|
||||
|
||||
<div className="page-container relative z-10 flex min-h-[520px] items-center justify-center px-6 pt-20 pb-14 sm:px-8 sm:pt-24 md:min-h-[76vh] md:py-16">
|
||||
<div className="relative z-20 flex w-full max-w-[880px] flex-col items-center text-center px-2 md:px-8">
|
||||
<HeroLogo />
|
||||
|
||||
<motion.span
|
||||
variants={badgeVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest bg-white text-[#6330D6] shadow-md uppercase mb-6 w-fit"
|
||||
>
|
||||
What is AI Commerce?
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={headingVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[52px] xl:text-[60px] font-extrabold text-white leading-[1.08] tracking-tight [text-shadow:0_4px_32px_rgba(0,0,0,0.5)]"
|
||||
>
|
||||
AI Commerce Starts
|
||||
<br />
|
||||
Where <span className="text-[#C9A2FF]">Quick Commerce</span>
|
||||
<br />
|
||||
Stops.
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={descVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="max-w-[660px] text-lg md:text-xl text-white/90 font-medium mt-6 leading-[1.6]"
|
||||
>
|
||||
Neighborhood stores already have the trust, the customers, and the
|
||||
location. Nearle adds the AI, the digital storefront, and the
|
||||
ordering — so local commerce finally scales without losing what
|
||||
makes it local.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={ctaVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="mt-9"
|
||||
>
|
||||
<Link
|
||||
href="/platform"
|
||||
className="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 text-base font-black text-[#16001D] shadow-[0_16px_30px_rgba(0,0,0,0.35)] transition-transform duration-200 hover:scale-[1.03] active:scale-95"
|
||||
>
|
||||
Learn About Our Platform
|
||||
<MaterialIcon
|
||||
icon="arrow_forward"
|
||||
size={20}
|
||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user