update home page cards design changes
This commit is contained in:
324
src/components/home/CommerceCompareSlider.tsx
Normal file
324
src/components/home/CommerceCompareSlider.tsx
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from "react";
|
||||||
|
import { motion, useInView, useReducedMotion } from "framer-motion";
|
||||||
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||||
|
|
||||||
|
// Divider position, in percent of card width. The top (Nearle) layer is clipped
|
||||||
|
// from the left by this value, so 0 = all Nearle, 100 = all Traditional.
|
||||||
|
const REST_POSITION = 50;
|
||||||
|
|
||||||
|
// One-shot hint played on scroll-in: sweep out to 20, across to 80, settle back
|
||||||
|
// at centre. Purely to teach the interaction — cancelled the moment the user
|
||||||
|
// touches the card.
|
||||||
|
const HINT_STOPS = [20, 80, REST_POSITION];
|
||||||
|
const HINT_LEG_MS = 820;
|
||||||
|
const HINT_DELAY_MS = 480;
|
||||||
|
|
||||||
|
const TRADITIONAL = [
|
||||||
|
{ icon: "hub", text: "Platform owns discovery" },
|
||||||
|
{ icon: "visibility_off", text: "Merchant becomes invisible" },
|
||||||
|
{ icon: "search", text: "Endless searching" },
|
||||||
|
{ icon: "groups", text: "Customer belongs to platform" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const NEARLE = [
|
||||||
|
{ icon: "chat_bubble", text: "Just ask in your own words" },
|
||||||
|
{ icon: "storefront", text: "The shop you trust stays at the centre" },
|
||||||
|
{ icon: "near_me", text: "You discover the shops right around you" },
|
||||||
|
{ icon: "handshake", text: "Local shops grow — they never get replaced" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const clamp = (n: number) => Math.min(100, Math.max(0, n));
|
||||||
|
const easeInOut = (t: number) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
|
||||||
|
|
||||||
|
type PanelProps = {
|
||||||
|
/** Renders the panel in normal flow, invisibly, purely to give the card its height. */
|
||||||
|
ghost?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two layers sit on top of each other, so a horizontal wipe only reads as a
|
||||||
|
// swap if each side keeps its content on its own side of the divider:
|
||||||
|
// Traditional hugs the left edge, Nearle mirrors to the right. Rows never wrap —
|
||||||
|
// the divider, not a line break, is what truncates them.
|
||||||
|
const ROW_BASE = "flex items-center gap-3 overflow-hidden rounded-2xl p-3 sm:p-3.5";
|
||||||
|
const TEXT_BASE = "min-w-0 flex-1 truncate text-[13px] font-medium leading-snug sm:text-[15px]";
|
||||||
|
|
||||||
|
function TraditionalPanel({ ghost }: PanelProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col p-5 sm:p-8 md:p-10 ${
|
||||||
|
ghost ? "invisible" : "absolute inset-0 bg-[#F2F5F8]"
|
||||||
|
}`}
|
||||||
|
aria-hidden={ghost}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="flex h-9 w-9 shrink-0 sm:h-11 sm:w-11 items-center justify-center rounded-2xl bg-white text-[#7A8B9A] shadow-sm">
|
||||||
|
<MaterialIcon icon="apartment" size={22} />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#94A3B1]">Today</p>
|
||||||
|
<h3 className="font-display text-[15px] font-black tracking-tight sm:text-xl text-[#0A2540]/70 ">
|
||||||
|
Traditional Commerce
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="mt-6 space-y-2.5 sm:mt-7 sm:space-y-3">
|
||||||
|
{TRADITIONAL.map((item) => (
|
||||||
|
<li key={item.text} className={`${ROW_BASE} border border-[#0A2540]/6 bg-white/70`}>
|
||||||
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#E7ECF1] text-[#8296A6]">
|
||||||
|
<MaterialIcon icon={item.icon} size={18} />
|
||||||
|
</span>
|
||||||
|
<span className={`${TEXT_BASE} text-[#5A6B7B]`}>{item.text}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NearlePanel({ ghost }: PanelProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col p-5 sm:p-8 md:p-10 ${
|
||||||
|
ghost
|
||||||
|
? "invisible"
|
||||||
|
: "absolute inset-0 bg-[linear-gradient(135deg,#FFFFFF_0%,#F3FDFC_52%,#DFF7F5_100%)]"
|
||||||
|
}`}
|
||||||
|
aria-hidden={ghost}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row-reverse items-center gap-3">
|
||||||
|
<span className="flex h-9 w-9 shrink-0 sm:h-11 sm:w-11 items-center justify-center rounded-2xl bg-[#00D4C8]/12 text-[#00A79E] ring-1 ring-[#00D4C8]/25">
|
||||||
|
<MaterialIcon icon="auto_awesome" size={22} />
|
||||||
|
</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#00A79E]">Nearle</p>
|
||||||
|
<h3 className="font-display text-[15px] font-black tracking-tight sm:text-xl text-[#0A2540]">
|
||||||
|
Nearle AI Commerce
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="mt-6 space-y-2.5 sm:mt-7 sm:space-y-3">
|
||||||
|
{NEARLE.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.text}
|
||||||
|
className={`${ROW_BASE} flex-row-reverse border border-[#00D4C8]/20 bg-white/80 shadow-[0_2px_10px_rgba(10,37,64,0.04)] backdrop-blur-sm`}
|
||||||
|
>
|
||||||
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#00D4C8]/12 text-[#00A79E]">
|
||||||
|
<MaterialIcon icon={item.icon} size={18} />
|
||||||
|
</span>
|
||||||
|
<span className={`${TEXT_BASE} text-right font-bold text-[#0A2540]`}>{item.text}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommerceCompareSlider() {
|
||||||
|
const reduce = useReducedMotion();
|
||||||
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inView = useInView(wrapRef, { once: true, margin: "-60px" });
|
||||||
|
|
||||||
|
const [position, setPosition] = useState(REST_POSITION);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
// Hint bookkeeping: a rAF id to cancel, and a latch so it never replays.
|
||||||
|
const hintFrame = useRef<number | null>(null);
|
||||||
|
const hintTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const hintDone = useRef(false);
|
||||||
|
|
||||||
|
const stopHint = useCallback(() => {
|
||||||
|
hintDone.current = true;
|
||||||
|
if (hintFrame.current !== null) cancelAnimationFrame(hintFrame.current);
|
||||||
|
if (hintTimer.current !== null) clearTimeout(hintTimer.current);
|
||||||
|
hintFrame.current = null;
|
||||||
|
hintTimer.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setFromClientX = useCallback((clientX: number) => {
|
||||||
|
const rect = cardRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect || rect.width === 0) return;
|
||||||
|
setPosition(clamp(((clientX - rect.left) / rect.width) * 100));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── One-shot hint ──────────────────────────────────────────────────────
|
||||||
|
// Hand-rolled rAF tween rather than Framer Motion: this component reserves
|
||||||
|
// Framer Motion for the entrance only, and the divider is driven by the same
|
||||||
|
// numeric state the pointer handlers write to.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inView || reduce || hintDone.current) return;
|
||||||
|
|
||||||
|
let leg = 0;
|
||||||
|
let from = REST_POSITION;
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
const step = (now: number) => {
|
||||||
|
if (!start) start = now;
|
||||||
|
const t = Math.min(1, (now - start) / HINT_LEG_MS);
|
||||||
|
const to = HINT_STOPS[leg];
|
||||||
|
setPosition(from + (to - from) * easeInOut(t));
|
||||||
|
|
||||||
|
if (t < 1) {
|
||||||
|
hintFrame.current = requestAnimationFrame(step);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
leg += 1;
|
||||||
|
if (leg >= HINT_STOPS.length) {
|
||||||
|
stopHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
from = to;
|
||||||
|
start = 0;
|
||||||
|
hintFrame.current = requestAnimationFrame(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
hintTimer.current = setTimeout(() => {
|
||||||
|
hintFrame.current = requestAnimationFrame(step);
|
||||||
|
}, HINT_DELAY_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (hintFrame.current !== null) cancelAnimationFrame(hintFrame.current);
|
||||||
|
if (hintTimer.current !== null) clearTimeout(hintTimer.current);
|
||||||
|
};
|
||||||
|
}, [inView, reduce, stopHint]);
|
||||||
|
|
||||||
|
// ── Mouse ──────────────────────────────────────────────────────────────
|
||||||
|
const onMouseDown = (e: ReactMouseEvent) => {
|
||||||
|
stopHint();
|
||||||
|
setDragging(true);
|
||||||
|
setFromClientX(e.clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const onMove = (e: MouseEvent) => setFromClientX(e.clientX);
|
||||||
|
const onUp = () => setDragging(false);
|
||||||
|
window.addEventListener("mousemove", onMove);
|
||||||
|
window.addEventListener("mouseup", onUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", onMove);
|
||||||
|
window.removeEventListener("mouseup", onUp);
|
||||||
|
};
|
||||||
|
}, [dragging, setFromClientX]);
|
||||||
|
|
||||||
|
// ── Touch ──────────────────────────────────────────────────────────────
|
||||||
|
// touch-action: pan-y keeps vertical page scrolling native; horizontal
|
||||||
|
// movement over the card is ours.
|
||||||
|
const onTouchStart = (e: ReactTouchEvent) => {
|
||||||
|
stopHint();
|
||||||
|
setDragging(true);
|
||||||
|
setFromClientX(e.touches[0].clientX);
|
||||||
|
};
|
||||||
|
const onTouchMove = (e: ReactTouchEvent) => setFromClientX(e.touches[0].clientX);
|
||||||
|
const onTouchEnd = () => setDragging(false);
|
||||||
|
|
||||||
|
// ── Keyboard ───────────────────────────────────────────────────────────
|
||||||
|
const onKeyDown = (e: ReactKeyboardEvent) => {
|
||||||
|
const step = e.shiftKey ? 10 : 4;
|
||||||
|
if (e.key === "ArrowLeft") {
|
||||||
|
stopHint();
|
||||||
|
setPosition((p) => clamp(p - step));
|
||||||
|
} else if (e.key === "ArrowRight") {
|
||||||
|
stopHint();
|
||||||
|
setPosition((p) => clamp(p + step));
|
||||||
|
} else if (e.key === "Home") {
|
||||||
|
stopHint();
|
||||||
|
setPosition(0);
|
||||||
|
} else if (e.key === "End") {
|
||||||
|
stopHint();
|
||||||
|
setPosition(100);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={wrapRef}
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
animate={inView ? { opacity: 1, y: 0 } : undefined}
|
||||||
|
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="mt-14"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={cardRef}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchCancel={onTouchEnd}
|
||||||
|
className="relative min-h-[320px] select-none overflow-hidden rounded-[32px] border border-[#0A2540]/8 bg-white shadow-[0_10px_30px_rgba(10,37,64,0.06)] md:min-h-[400px]"
|
||||||
|
style={{ touchAction: "pan-y", cursor: dragging ? "grabbing" : "ew-resize" }}
|
||||||
|
>
|
||||||
|
{/* Invisible in-flow copy: gives the card a real height so neither
|
||||||
|
absolutely-positioned layer can ever be clipped on small screens. */}
|
||||||
|
<NearlePanel ghost />
|
||||||
|
|
||||||
|
{/* Bottom layer — the world as it works today. */}
|
||||||
|
<TraditionalPanel />
|
||||||
|
|
||||||
|
{/* Top layer — clipped from the left by the divider. */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
clipPath: `inset(0 0 0 ${position}%)`,
|
||||||
|
willChange: "clip-path",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NearlePanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Seam: an opaque strip under the divider so the two halves never read
|
||||||
|
as one run-on sentence when the rows meet mid-word. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-y-0 w-[11px] -translate-x-1/2 bg-white/80 backdrop-blur-[2px] sm:w-[7px]"
|
||||||
|
style={{ left: `${position}%` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Divider line */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-y-0 w-[3px] -translate-x-1/2 bg-[linear-gradient(180deg,#00D4C8_0%,#FF6B35_100%)] shadow-[0_0_18px_rgba(0,212,200,0.45)]"
|
||||||
|
style={{ left: `${position}%` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Handle */}
|
||||||
|
<div
|
||||||
|
role="slider"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Compare traditional commerce with Nearle AI commerce"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-valuenow={Math.round(100 - position)}
|
||||||
|
aria-valuetext={`${Math.round(100 - position)}% Nearle AI Commerce`}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
className="absolute top-1/2 flex h-12 w-12 items-center justify-center rounded-full border border-[#0A2540]/8 bg-white text-[#FF6B35] shadow-[0_6px_20px_rgba(10,37,64,0.18)] outline-none transition-transform duration-200 focus-visible:ring-2 focus-visible:ring-[#00D4C8] focus-visible:ring-offset-2"
|
||||||
|
style={{
|
||||||
|
left: `${position}%`,
|
||||||
|
cursor: dragging ? "grabbing" : "grab",
|
||||||
|
transform: `translate(-50%, -50%) scale(${dragging ? 1.08 : 1})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="swap_horiz" size={24} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={inView ? { opacity: 1 } : undefined}
|
||||||
|
transition={{ duration: 0.5, delay: 0.35, ease: "easeOut" }}
|
||||||
|
className="mt-4 flex items-center justify-center gap-1.5 text-[13px] font-medium text-[#7A8B9A]"
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="swap_horiz" size={16} />
|
||||||
|
Drag to compare
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
src/components/home/CommerceFlipCard.tsx
Normal file
117
src/components/home/CommerceFlipCard.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { motion, useInView } from "framer-motion";
|
||||||
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||||
|
|
||||||
|
interface DifferencePillar {
|
||||||
|
number: string;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
traditional: string;
|
||||||
|
nearle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PILLARS: DifferencePillar[] = [
|
||||||
|
{
|
||||||
|
number: "01",
|
||||||
|
icon: "search",
|
||||||
|
title: "Ask in Natural Words",
|
||||||
|
description: "Type or speak like you're talking to a neighbour — 'Who has fresh sourdough near me?' to see live local store stock.",
|
||||||
|
traditional: "Paid ads & dark warehouses",
|
||||||
|
nearle: "Real local shop inventory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "02",
|
||||||
|
icon: "storefront",
|
||||||
|
title: "Your Store at the Centre",
|
||||||
|
description: "Your favourite neighbourhood grocery, bakery, or chemist gets its own digital storefront and QR ordering.",
|
||||||
|
traditional: "Faceless dark warehouses",
|
||||||
|
nearle: "The shop you already trust",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "03",
|
||||||
|
icon: "sell",
|
||||||
|
title: "Direct Store Prices",
|
||||||
|
description: "Order directly from your local merchant with zero middleman app commission markups.",
|
||||||
|
traditional: "25–30% extra app markup",
|
||||||
|
nearle: "Direct prices & 1-tap reorders",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "04",
|
||||||
|
icon: "favorite",
|
||||||
|
title: "Local Stores Grow",
|
||||||
|
description: "Every purchase keeps local store owners thriving, preserving neighbourhood jobs and community culture.",
|
||||||
|
traditional: "Money leaves neighbourhood",
|
||||||
|
nearle: "100% supports local stores",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const EASE = [0.22, 1, 0.36, 1] as const;
|
||||||
|
|
||||||
|
export function CommerceFlipCard() {
|
||||||
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inView = useInView(wrapRef, { once: true, margin: "-50px" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={wrapRef}
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={inView ? { opacity: 1, y: 0 } : undefined}
|
||||||
|
transition={{ duration: 0.5, ease: EASE }}
|
||||||
|
className="mt-10 lg:mt-12 w-full max-w-[1240px] mx-auto"
|
||||||
|
>
|
||||||
|
{/* ─── 4-Card Grid matching Nearle Identity ─── */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-6 items-stretch">
|
||||||
|
{PILLARS.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.number}
|
||||||
|
className="group relative flex flex-col justify-between rounded-[28px] border border-[rgba(99,48,214,0.12)] bg-white p-6 md:p-7 shadow-[0_16px_40px_rgba(22,0,29,0.06)] hover:shadow-[0_24px_55px_rgba(99,48,214,0.14)] hover:-translate-y-1 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{/* Top Row: Icon + Number */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#6330D6]/8 text-[#6330D6] group-hover:bg-[#6330D6] group-hover:text-white transition-colors duration-300 shadow-xs">
|
||||||
|
<MaterialIcon icon={item.icon} size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="font-display text-4xl font-black text-[#6330D6]/15 group-hover:text-[#6330D6]/25 transition-colors select-none">
|
||||||
|
{item.number}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Description */}
|
||||||
|
<h3 className="font-display text-lg lg:text-xl font-black text-[#16001D] leading-tight">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2.5 text-[13px] leading-relaxed text-[#5E5866] font-medium">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Comparison Pill */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-[#6330D6]/10 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-[11px] font-semibold text-slate-500">
|
||||||
|
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-rose-100 text-rose-600 font-bold text-[9px]">
|
||||||
|
✕
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{item.traditional}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-[11px] font-bold text-[#008B82]">
|
||||||
|
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#00D4C8]/25 text-[#007D76] font-bold text-[9px]">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{item.nearle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,29 +2,8 @@
|
|||||||
|
|
||||||
import { motion, useReducedMotion } from "framer-motion";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||||
import { Spotlight } from "@/components/ui/Spotlight";
|
import { CommerceFlipCard } from "@/components/home/CommerceFlipCard";
|
||||||
import {
|
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||||
fadeUp,
|
|
||||||
slideInLeftSubtle,
|
|
||||||
slideInRight,
|
|
||||||
staggerContainer,
|
|
||||||
staggerCards,
|
|
||||||
viewport,
|
|
||||||
} from "@/lib/animations";
|
|
||||||
|
|
||||||
const TRADITIONAL = [
|
|
||||||
{ icon: "hub", text: "Platform owns discovery" },
|
|
||||||
{ icon: "visibility_off", text: "Merchant becomes invisible" },
|
|
||||||
{ icon: "search", text: "Endless searching" },
|
|
||||||
{ icon: "groups", text: "Customer belongs to platform" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const NEARLE = [
|
|
||||||
{ icon: "psychology", text: "Just ask in your own words" },
|
|
||||||
{ icon: "storefront", text: "The shop you trust stays at the centre" },
|
|
||||||
{ icon: "near_me", text: "You discover the shops right around you" },
|
|
||||||
{ icon: "handshake", text: "Local shops grow — they never get replaced" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// The gentle S-curve the transformation flow travels. Stretched full-bleed via
|
// The gentle S-curve the transformation flow travels. Stretched full-bleed via
|
||||||
// preserveAspectRatio="none", so exact geometry matters less than the direction:
|
// preserveAspectRatio="none", so exact geometry matters less than the direction:
|
||||||
@@ -321,132 +300,13 @@ export function WhyNearleDifferent() {
|
|||||||
</motion.h2>
|
</motion.h2>
|
||||||
<motion.p
|
<motion.p
|
||||||
variants={fadeUp}
|
variants={fadeUp}
|
||||||
className="mx-auto mt-4 max-w-[600px] text-[17px] leading-[1.7] text-[#5E5866]"
|
className="mx-auto mt-3 max-w-[560px] text-[15px] sm:text-[17px] leading-[1.6] text-[#5E5866]"
|
||||||
>
|
>
|
||||||
Two ways to bring shopping online. One puts the platform in the middle — the other keeps
|
One puts big platforms in the middle — the other keeps the shop you already trust there.
|
||||||
the shop you already trust there.
|
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Sequential reveal: heading (above) → left card → right card, staggered
|
<CommerceFlipCard />
|
||||||
by the parent; rows stagger within each card. */}
|
|
||||||
<motion.div
|
|
||||||
variants={staggerContainer}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={viewport}
|
|
||||||
className="mt-14 grid gap-5 lg:grid-cols-2 lg:gap-6"
|
|
||||||
>
|
|
||||||
{/* Traditional commerce */}
|
|
||||||
<motion.div
|
|
||||||
variants={slideInLeftSubtle}
|
|
||||||
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
|
|
||||||
transition={{ type: "spring", stiffness: 260, damping: 22 }}
|
|
||||||
>
|
|
||||||
<div className="flex h-full flex-col rounded-[32px] border border-[#16001D]/8 bg-white p-7 shadow-[0_10px_30px_rgba(22,0,29,0.05)] transition-shadow duration-300 hover:shadow-[0_18px_44px_rgba(22,0,29,0.09)] md:p-9">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#F1EEF5] text-[#6D6172]">
|
|
||||||
<MaterialIcon icon="apartment" size={22} />
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#9A8FA6]">
|
|
||||||
Today
|
|
||||||
</p>
|
|
||||||
<h3 className="font-display text-xl font-black tracking-tight text-[#16001D]">
|
|
||||||
Traditional Commerce
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.ul
|
|
||||||
variants={staggerCards}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={viewport}
|
|
||||||
className="mt-7 space-y-3"
|
|
||||||
>
|
|
||||||
{TRADITIONAL.map((item) => (
|
|
||||||
<motion.li
|
|
||||||
key={item.text}
|
|
||||||
variants={fadeUp}
|
|
||||||
className="flex items-center gap-3 rounded-2xl border border-[#16001D]/6 bg-[#FAF9FB] p-3.5"
|
|
||||||
>
|
|
||||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white text-[#9A8FA6] shadow-sm">
|
|
||||||
<MaterialIcon icon={item.icon} size={18} />
|
|
||||||
</span>
|
|
||||||
<span className="text-[15px] font-medium leading-snug text-[#5E5866]">
|
|
||||||
{item.text}
|
|
||||||
</span>
|
|
||||||
</motion.li>
|
|
||||||
))}
|
|
||||||
</motion.ul>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Nearle AI Commerce — highlighted */}
|
|
||||||
<motion.div
|
|
||||||
variants={slideInRight}
|
|
||||||
whileHover={reduce ? undefined : { y: -6, scale: 1.02 }}
|
|
||||||
transition={{ type: "spring", stiffness: 260, damping: 22 }}
|
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
{/* soft border glow behind the highlighted card */}
|
|
||||||
<motion.div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute -inset-1 -z-10 rounded-[36px] bg-[#7C3AED]/10 blur-2xl"
|
|
||||||
animate={reduce ? undefined : { opacity: [0.4, 0.75, 0.4] }}
|
|
||||||
transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
/>
|
|
||||||
<Spotlight
|
|
||||||
className="flex h-full flex-col overflow-hidden rounded-[32px] border border-[#6330D6]/20 bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_55%,#f1e8fb_100%)] p-7 shadow-[0_16px_44px_-16px_rgba(99,48,214,0.28)] md:p-9"
|
|
||||||
color="rgba(168,85,247,0.14)"
|
|
||||||
size={420}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#7C3AED]/10 text-[#7C3AED] ring-1 ring-[#7C3AED]/15">
|
|
||||||
<motion.span
|
|
||||||
className="inline-flex"
|
|
||||||
animate={reduce ? undefined : { scale: [1, 1.12, 1], opacity: [0.85, 1, 0.85] }}
|
|
||||||
transition={{ duration: 2.6, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<MaterialIcon icon="auto_awesome" size={22} />
|
|
||||||
</motion.span>
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-[#7C3AED]">
|
|
||||||
Nearle
|
|
||||||
</p>
|
|
||||||
<h3 className="font-display text-xl font-black tracking-tight text-[#16001D]">
|
|
||||||
Nearle AI Commerce
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.ul
|
|
||||||
variants={staggerCards}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={viewport}
|
|
||||||
className="mt-7 space-y-3"
|
|
||||||
>
|
|
||||||
{NEARLE.map((item) => (
|
|
||||||
<motion.li
|
|
||||||
key={item.text}
|
|
||||||
variants={fadeUp}
|
|
||||||
className="flex items-center gap-3 rounded-2xl border border-[#7C3AED]/12 bg-white/75 p-3.5 shadow-sm backdrop-blur-sm transition-colors duration-200 hover:border-[#7C3AED]/28"
|
|
||||||
>
|
|
||||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#7C3AED]/10 text-[#7C3AED]">
|
|
||||||
<MaterialIcon icon={item.icon} size={18} />
|
|
||||||
</span>
|
|
||||||
<span className="text-[15px] font-bold leading-snug text-[#16001D]">
|
|
||||||
{item.text}
|
|
||||||
</span>
|
|
||||||
</motion.li>
|
|
||||||
))}
|
|
||||||
</motion.ul>
|
|
||||||
</Spotlight>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user