update merchantos svg

This commit is contained in:
2026-07-21 13:15:34 +05:30
parent c5631a6011
commit 4e0facc9af
2 changed files with 469 additions and 3 deletions

View File

@@ -0,0 +1,467 @@
"use client";
import { useEffect, useState } from "react";
import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
type ProductAsset = {
emoji: string;
name: string;
xStart: string; // Tailwind coordinate
yStart: string; // Tailwind coordinate
floatDelay: number;
floatDuration: number;
};
// Define products that float and participate in AI automation
const PRODUCT_ASSETS: ProductAsset[] = [
{ emoji: "🍎", name: "Apple", xStart: "left-[5%]", yStart: "top-[10%]", floatDelay: 0, floatDuration: 4 },
{ emoji: "🥛", name: "Milk", xStart: "right-[5%]", yStart: "top-[15%]", floatDelay: 0.5, floatDuration: 4.5 },
{ emoji: "🍞", name: "Bread", xStart: "left-[0%]", yStart: "top-[45%]", floatDelay: 0.8, floatDuration: 4.8 },
{ emoji: "🥬", name: "Spinach", xStart: "right-[2%]", yStart: "top-[50%]", floatDelay: 1.2, floatDuration: 5.2 },
{ emoji: "🏷️", name: "Price Tag", xStart: "left-[8%]", yStart: "bottom-[12%]", floatDelay: 1.5, floatDuration: 4.2 },
{ emoji: "📲", name: "QR Code", xStart: "right-[8%]", yStart: "bottom-[15%]", floatDelay: 2, floatDuration: 4.6 },
];
export function AnimatedDashboard() {
const reduce = useReducedMotion();
// Animation states
const [animatingIndex, setAnimatingIndex] = useState(0);
const [stage, setStage] = useState<"idle" | "flying" | "scanning" | "updating" | "notifying">("idle");
const [itemsCount, setItemsCount] = useState(142);
const [ordersCount, setOrdersCount] = useState(8);
useEffect(() => {
if (reduce) return;
const cycle = setInterval(() => {
// 1. Start Fly-in phase
setStage("flying");
// 2. Scan phase: Product reaches center, trigger laser scan
const scanTimer = setTimeout(() => {
setStage("scanning");
}, 900);
// 3. Update dashboard values (counter, green success pulse)
const updateTimer = setTimeout(() => {
setStage("updating");
// Update specific counters based on what product is processed
if (animatingIndex === 0) {
setItemsCount((c) => c + 1);
} else if (animatingIndex === 1) {
setOrdersCount((c) => c + 1);
}
}, 1400);
// 4. Micro-notification appears
const notifyTimer = setTimeout(() => {
setStage("notifying");
}, 1800);
// 5. Clear notification, return to idle
const idleTimer = setTimeout(() => {
setStage("idle");
setAnimatingIndex((prev) => (prev + 1) % 4); // Cycle through the first 4 products
}, 5200);
return () => {
clearTimeout(scanTimer);
clearTimeout(updateTimer);
clearTimeout(notifyTimer);
clearTimeout(idleTimer);
};
}, 8000); // Complete animation cycle every 8 seconds
return () => clearInterval(cycle);
}, [animatingIndex, reduce]);
// Floating variants for the idle products surrounding the dashboard
const floatVariant = (delay: number, duration: number) => ({
animate: {
y: [0, -10, 0],
rotate: [-3, 3, -3],
scale: [1, 1.02, 1],
transition: {
duration,
repeat: Infinity,
ease: "easeInOut",
delay,
},
},
});
// Curved flight coordinates relative to the absolute dashboard layout
// Product index maps to original coordinates:
// index 0 (Apple): left-5%, top-10% (x: 5%, y: 10%) -> center (x: 50%, y: 45%)
const flightPaths = [
{ x: ["5%", "25%", "50%"], y: ["10%", "20%", "45%"] }, // Apple
{ x: ["95%", "75%", "50%"], y: ["15%", "25%", "45%"] }, // Milk
{ x: ["0%", "20%", "50%"], y: ["45%", "40%", "45%"] }, // Bread
{ x: ["98%", "78%", "50%"], y: ["50%", "45%", "45%"] }, // Spinach
];
return (
<div className="relative w-full aspect-[440/336] flex items-center justify-center p-8 sm:p-12 overflow-visible select-none">
{/* ── Background Glow ── */}
<div className="absolute inset-0 -z-20 bg-gradient-to-tr from-purple-500/10 to-[#6330D6]/10 blur-3xl rounded-full pointer-events-none scale-90" />
{/* ── 1. Floating Product Assets (Around Dashboard) ── */}
{PRODUCT_ASSETS.map((product, idx) => {
// Hide specific products on tablet/mobile screens
let responsiveClass = "absolute z-20 ";
if (idx === 2 || idx === 3) {
responsiveClass += "hidden lg:block"; // Only desktop
} else if (idx === 0 || idx === 1 || idx === 4 || idx === 5) {
responsiveClass += "hidden md:block"; // Desktop & Tablet
}
const isCurrentlyAnimating = stage !== "idle" && animatingIndex === idx;
return (
<motion.div
key={product.name}
className={`${responsiveClass} ${product.xStart} ${product.yStart}`}
variants={floatVariant(product.floatDelay, product.floatDuration)}
animate={isCurrentlyAnimating ? "exit" : "animate"}
style={{ opacity: isCurrentlyAnimating ? 0.15 : 1 }}
>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-purple-500/10 bg-white/75 backdrop-blur-md shadow-[0_4px_16px_rgba(99,48,214,0.04)] text-lg">
{product.emoji}
</div>
</motion.div>
);
})}
{/* ── 2. Flying Product Scan Asset ── */}
<AnimatePresence>
{stage === "flying" && animatingIndex < 4 && (
<motion.div
initial={{
x: flightPaths[animatingIndex].x[0],
y: flightPaths[animatingIndex].y[0],
opacity: 1,
scale: 1,
}}
animate={{
x: flightPaths[animatingIndex].x,
y: flightPaths[animatingIndex].y,
opacity: [1, 1, 0.2],
scale: [1, 0.7, 0.4],
}}
transition={{
duration: 0.9,
ease: [0.25, 0.1, 0.25, 1],
}}
className="absolute z-30 pointer-events-none flex h-11 w-11 items-center justify-center rounded-2xl border-2 border-[#7C3AED] bg-white shadow-[0_0_15px_#7C3AED] text-lg"
>
{PRODUCT_ASSETS[animatingIndex].emoji}
</motion.div>
)}
</AnimatePresence>
{/* ── 3. AI Processing Particle Trails ── */}
{stage === "flying" && animatingIndex < 4 && (
<>
{[0, 1, 2].map((particleIdx) => (
<motion.div
key={particleIdx}
initial={{
x: flightPaths[animatingIndex].x[0],
y: flightPaths[animatingIndex].y[0],
opacity: 0,
scale: 0.8,
}}
animate={{
x: flightPaths[animatingIndex].x,
y: flightPaths[animatingIndex].y,
opacity: [0, 0.7, 0],
scale: [0.6, 0.3, 0],
}}
transition={{
duration: 0.9,
delay: 0.08 * (particleIdx + 1),
ease: "easeOut",
}}
className="absolute z-28 w-1.5 h-1.5 rounded-full bg-[#7C3AED] shadow-[0_0_8px_#7C3AED]"
/>
))}
</>
)}
{/* ── 4. Slide-in Micro-Notifications ── */}
<AnimatePresence>
{stage === "notifying" && (
<motion.div
initial={{ y: -25, opacity: 0, x: "-50%" }}
animate={{ y: 0, opacity: 1, x: "-50%" }}
exit={{ y: -20, opacity: 0, x: "-50%" }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
className="absolute top-[22%] left-1/2 z-40 bg-neutral-950 text-white rounded-full px-4 py-1.5 shadow-lg border border-purple-500/20 flex items-center gap-2 whitespace-nowrap"
>
<span className="text-emerald-400 text-xs font-bold"></span>
<span className="text-[10px] sm:text-[11px] font-bold tracking-tight">
{animatingIndex === 0 && "Apple Catalog Listing Synced"}
{animatingIndex === 1 && "Milk Delivery Stock Updated"}
{animatingIndex === 2 && "Bread Pricing Configuration Live"}
{animatingIndex === 3 && "Spinach Category Catalog Indexed"}
</span>
</motion.div>
)}
</AnimatePresence>
{/* ── 5. Main Dashboard Canvas ── */}
<div className="relative w-full h-full flex items-center justify-center">
{/* Ambient Success Pulse Border */}
<motion.div
animate={
stage === "updating"
? {
borderColor: ["rgba(99,48,214,0.1)", "rgba(34,197,94,0.6)", "rgba(99,48,214,0.1)"],
boxShadow: [
"0 0 0 rgba(34,197,94,0)",
"0 0 20px rgba(34,197,94,0.2)",
"0 0 0 rgba(34,197,94,0)",
],
}
: {}
}
transition={{ duration: 0.8 }}
className="absolute inset-[30px] rounded-[22px] border-[1.5px] border-[#EDE9FE]/0 pointer-events-none z-10"
/>
<motion.svg
viewBox="0 0 440 336"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto relative z-0"
>
{/* Ground shadow */}
<ellipse cx="220" cy="312" rx="150" ry="14" fill="#6330D6" opacity="0.08" />
{/* Main dashboard window */}
<g>
<rect x="40" y="30" width="360" height="256" rx="22" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
{/* Top window bar 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" />
</g>
{/* Sidebar navigation */}
<g>
<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" />
{/* QR Ordering live badge */}
<g>
<motion.rect
x="64"
y="210"
width="34"
height="12"
rx="4"
fill="#6330D6"
animate={{
fill: stage === "updating" && animatingIndex === 3 ? "#22C55E" : "#6330D6",
opacity: stage === "updating" && animatingIndex === 3 ? 0.25 : 0.15,
}}
transition={{ duration: 0.3 }}
/>
<motion.text
x="68"
y="218"
fontSize="5"
fontWeight="bold"
animate={{
fill: stage === "updating" && animatingIndex === 3 ? "#16A34A" : "#6330D6",
}}
transition={{ duration: 0.3 }}
>
QR ACTIVE
</motion.text>
</g>
{/* Loyalty active badge */}
<g>
<rect x="64" y="226" width="34" height="12" rx="4" fill="#6330D6" opacity="0.1" />
<text x="69" y="234" fill="#6330D6" fontSize="5" fontWeight="bold">LOYALTY</text>
</g>
</g>
{/* ── KPI Chips ── */}
{/* KPI 1: Inventory Items */}
<g>
<rect x="124" y="84" width="80" height="46" rx="12" fill="#F5F1FB" />
<text x="136" y="97" fill="#8B7FB5" fontSize="7" fontWeight="bold">ITEMS</text>
{/* Animate number change */}
<text x="136" y="118" fill="#16001D" fontSize="13" fontWeight="900" className="font-display">
{itemsCount}
</text>
{/* Micro pulse indicator */}
<motion.circle
cx="188"
cy="95"
r="3.5"
fill="#6330D6"
initial={{ scale: 0.8 }}
animate={stage === "updating" && animatingIndex === 0 ? { scale: [1, 1.8, 1], opacity: [0.8, 0, 0.8] } : {}}
transition={{ duration: 0.6 }}
/>
</g>
{/* KPI 2: Sales Counter */}
<g>
<rect x="212" y="84" width="80" height="46" rx="12" fill="#ECFBF1" />
<text x="224" y="97" fill="#008037" fontSize="7" fontWeight="bold">SALES</text>
<text x="224" y="118" fill="#007239" fontSize="13" fontWeight="900" className="font-display">
18,240
</text>
</g>
{/* KPI 3: Orders Counter */}
<g>
<rect x="300" y="84" width="80" height="46" rx="12" fill="#FFF7E8" />
<text x="312" y="97" fill="#C28C00" fontSize="7" fontWeight="bold">ORDERS</text>
{/* Animate orders increase */}
<text x="312" y="118" fill="#D97706" fontSize="13" fontWeight="900" className="font-display">
{ordersCount}
</text>
<motion.circle
cx="364"
cy="95"
r="3"
fill="#D97706"
initial={{ scale: 0.8 }}
animate={stage === "updating" && animatingIndex === 1 ? { scale: [1, 1.8, 1], opacity: [0.8, 0, 0.8] } : {}}
transition={{ duration: 0.6 }}
/>
</g>
{/* ── Chart Panel (Middle Area) ── */}
<g>
<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" />
{/* Dynamic line graph */}
<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"
animate={
stage === "updating" && animatingIndex === 2
? {
stroke: ["#6330D6", "#10B981", "#6330D6"],
strokeWidth: [2.5, 3.5, 2.5],
}
: {}
}
transition={{ duration: 0.8 }}
/>
{/* Pulse node */}
<motion.circle
cx="278"
cy="176"
r="4"
fill="#6330D6"
stroke="#fff"
strokeWidth="1.5"
animate={stage === "updating" && animatingIndex === 2 ? { scale: [1, 1.6, 1] } : {}}
/>
</g>
{/* ── Mini Bar Chart ── */}
<g>
<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, bIdx) => {
// If index 2 product (price/pricing) updates, scale the heights briefly
const isTargetBar = bIdx === 2 || bIdx === 3;
const multiplier = stage === "updating" && animatingIndex === 2 && isTargetBar ? 1.25 : 1;
const barHeight = b.h * multiplier;
return (
<motion.rect
key={b.x}
x={b.x}
y={250 - barHeight}
width="11"
height={barHeight}
rx="4"
fill={b.c}
transition={{ type: "spring", stiffness: 100, damping: 12 }}
style={{ transformBox: "fill-box", transformOrigin: "bottom" }}
/>
);
})}
</g>
{/* ── Floating order card top right (floating) ── */}
<motion.g
animate={{ y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}
>
<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>
{/* ── Floating live stock status bottom left (floating) ── */}
<motion.g
animate={{ y: [0, 6, 0] }}
transition={{ repeat: Infinity, duration: 4.2, ease: "easeInOut" }}
>
<rect x="22" y="196" width="96" height="38" rx="12" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
<circle cx="42" cy="215" r="6" fill="#22C55E" />
<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>
{/* ── Purple AI Laser Scanning Beam ── */}
{stage === "scanning" && (
<motion.line
x1="45"
y1="34"
x2="395"
y2="34"
stroke="#7C3AED"
strokeWidth="2.5"
initial={{ y: 0, opacity: 0 }}
animate={{
y: [0, 240, 0],
opacity: [0, 0.85, 0.85, 0],
}}
transition={{ duration: 0.95, ease: "easeInOut" }}
/>
)}
</motion.svg>
</div>
</div>
);
}

View File

@@ -3,8 +3,7 @@
import { useRef } from "react"; import { useRef } from "react";
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion"; import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { DashboardMockup } from "@/components/platform/DeviceShowcase"; import { AnimatedDashboard } from "@/components/platform/AnimatedDashboard";
import { ConsumerShowcase } from "@/components/platform/ConsumerShowcase";
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations"; import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
export type ProductFeature = { export type ProductFeature = {
@@ -137,7 +136,7 @@ export function ProductFeatureGrid({
"radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)", "radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)",
}} }}
/> />
{isPhone ? <ConsumerShowcase /> : <DashboardMockup />} <AnimatedDashboard />
</motion.div> </motion.div>
</div> </div>