update why nearle page
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user